From d2f7db9d0d1bcd91d6eb0d14664263f9cd661dc5 Mon Sep 17 00:00:00 2001 From: Tnze Date: Mon, 23 May 2022 10:04:01 +0800 Subject: [PATCH] simple ecs system --- server/dimension/loadmanager.go | 102 --------------------------- server/dimension/loadmanager_test.go | 7 -- server/dimension/world.go | 32 +++++++++ server/entities.go | 1 - server/internal/ecs/bitest.go | 39 ++++++++++ server/internal/ecs/ecs_test.go | 27 +++++++ server/internal/ecs/storage.go | 34 +++++++++ server/internal/ecs/system.go | 49 +++++++++++++ server/internal/ecs/world.go | 58 +++++++++++++++ 9 files changed, 239 insertions(+), 110 deletions(-) delete mode 100644 server/dimension/loadmanager.go delete mode 100644 server/dimension/loadmanager_test.go create mode 100644 server/dimension/world.go delete mode 100644 server/entities.go create mode 100644 server/internal/ecs/bitest.go create mode 100644 server/internal/ecs/ecs_test.go create mode 100644 server/internal/ecs/storage.go create mode 100644 server/internal/ecs/system.go create mode 100644 server/internal/ecs/world.go diff --git a/server/dimension/loadmanager.go b/server/dimension/loadmanager.go deleted file mode 100644 index a51a6c3..0000000 --- a/server/dimension/loadmanager.go +++ /dev/null @@ -1,102 +0,0 @@ -package dimension - -import ( - "container/list" - - "github.com/Tnze/go-mc/data/packetid" - "github.com/Tnze/go-mc/level" - pk "github.com/Tnze/go-mc/net/packet" - "github.com/Tnze/go-mc/server" - "github.com/Tnze/go-mc/server/clientinfo" - "github.com/Tnze/go-mc/server/internal/bvh" -) - -type vec2d = bvh.Vec2[float64] -type sphere2d = bvh.Sphere[float64, vec2d] - -type manager struct { - storage - activeChunks *list.List - chunkElement map[level.ChunkPos]*list.Element - players bvh.Tree[float64, sphere2d, *server.Player] - clients *clientinfo.ClientInformation -} - -type chunkHandler struct { - level.ChunkPos - *level.Chunk - // records all players loaded this chunk - players map[*server.Player]struct{} -} - -func (m *manager) refresh() error { - all := func(b sphere2d) bool { return true } - m.players.Find(all, func(n *bvh.Node[float64, sphere2d, *server.Player]) bool { - p := n.Value - v := m.clients.Players[p.UUID].ViewDistance - for i := 1 - v; i < v; i++ { - for j := 1 - v; j < v; j++ { - pos := level.ChunkPos{ - X: int(n.Box.Center[0]) >> 4, - Z: int(n.Box.Center[1]) >> 4, - } - point := vec2d{float64(pos.X + i), float64(pos.Z + j)} - if _, exist := m.chunkElement[pos]; !exist && n.Box.WithIn(point) { - c, err := m.GetChunk(pos) - if err != nil { - return false - } - m.chunkElement[pos] = m.activeChunks.PushBack(c) - } - } - } - return true - }) - for e := m.activeChunks.Front(); e != nil; { - ch := e.Value.(*chunkHandler) - point := vec2d{float64(ch.X), float64(ch.Z)} - filter := bvh.TouchPoint[vec2d, sphere2d](point) - newPlayers := make(map[*server.Player]struct{}) - m.players.Find(filter, func(n *bvh.Node[float64, sphere2d, *server.Player]) bool { - p := n.Value - if _, ok := ch.players[p]; ok { - delete(ch.players, p) - } else { - playerLoadChunk(p, ch) - } - newPlayers[p] = struct{}{} - return true - }) - for p := range ch.players { - playerUnloadChunk(p, ch) - } - if len(newPlayers) > 0 { - ch.players = newPlayers - e = e.Next() - } else { - // no player around this chunk, unload it - if err := m.PutChunk(ch.ChunkPos, ch.Chunk); err != nil { - return err - } - next := e.Next() - m.activeChunks.Remove(e) - delete(m.chunkElement, ch.ChunkPos) - e = next - } - } - return nil -} - -func playerLoadChunk(p *server.Player, c *chunkHandler) { - p.WritePacket(server.Packet758(pk.Marshal( - packetid.ClientboundLevelChunkWithLight, - c.ChunkPos, c.Chunk, - ))) -} - -func playerUnloadChunk(p *server.Player, c *chunkHandler) { - p.WritePacket(server.Packet758(pk.Marshal( - packetid.ClientboundForgetLevelChunk, - c.ChunkPos, - ))) -} diff --git a/server/dimension/loadmanager_test.go b/server/dimension/loadmanager_test.go deleted file mode 100644 index 258223d..0000000 --- a/server/dimension/loadmanager_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package dimension - -import "testing" - -func Test_manager(t *testing.T) { - -} diff --git a/server/dimension/world.go b/server/dimension/world.go new file mode 100644 index 0000000..b780da4 --- /dev/null +++ b/server/dimension/world.go @@ -0,0 +1,32 @@ +package dimension + +import ( + "github.com/Tnze/go-mc/server" +) + +type World struct { +} + +func (w *World) Init(g *server.Game) { + //TODO implement me + panic("implement me") +} + +func (w *World) Info() server.LevelInfo { + //TODO implement me + panic("implement me") +} + +func (w *World) PlayerJoin(p *server.Player) { + //TODO implement me + panic("implement me") +} + +func (w *World) PlayerQuit(p *server.Player) { + //TODO implement me + panic("implement me") +} + +func (w *World) register() { + +} diff --git a/server/entities.go b/server/entities.go deleted file mode 100644 index abb4e43..0000000 --- a/server/entities.go +++ /dev/null @@ -1 +0,0 @@ -package server diff --git a/server/internal/ecs/bitest.go b/server/internal/ecs/bitest.go new file mode 100644 index 0000000..edda8d9 --- /dev/null +++ b/server/internal/ecs/bitest.go @@ -0,0 +1,39 @@ +package ecs + +type BitSet struct { + // TODO: this is not a bitset, I'm just testing + values map[Index]struct{} +} + +func (b BitSet) Set(i Index) (old bool) { + _, old = b.values[i] + b.values[i] = struct{}{} + return +} + +func (b BitSet) Unset(i Index) (old bool) { + _, old = b.values[i] + delete(b.values, i) + return +} + +func (b BitSet) Contains(i Index) bool { + _, contains := b.values[i] + return contains +} + +func (b BitSet) And(other BitSet) (result BitSet) { + result = BitSet{values: make(map[Index]struct{})} + for i := range b.values { + if other.Contains(i) { + result.values[i] = struct{}{} + } + } + return result +} + +func (b BitSet) Range(f func(eid Index)) { + for i := range b.values { + f(i) + } +} diff --git a/server/internal/ecs/ecs_test.go b/server/internal/ecs/ecs_test.go new file mode 100644 index 0000000..1744c11 --- /dev/null +++ b/server/internal/ecs/ecs_test.go @@ -0,0 +1,27 @@ +package ecs + +import "testing" + +func Test_common(t *testing.T) { + // W + w := NewWorld() + // C + type pos [2]int + type vel [2]int + Register(w, pos{}) + Register(w, vel{}) + // E + w.CreateEntity(pos{0, 0}) + w.CreateEntity(vel{1, 2}) + w.CreateEntity(pos{1, 2}, vel{2, 0}) + // S + s1 := FuncSystem(func(p pos) { + t.Log("system 1", p) + }) + s2 := FuncSystem(func(p pos, v vel) { + t.Log("system 2", p, v) + }) + // Run + s1.Update(w) + s2.Update(w) +} diff --git a/server/internal/ecs/storage.go b/server/internal/ecs/storage.go new file mode 100644 index 0000000..dbfc7a3 --- /dev/null +++ b/server/internal/ecs/storage.go @@ -0,0 +1,34 @@ +package ecs + +type Index uint32 + +type Storage interface { + Get(eid Index) any + Insert(eid Index, v any) + Remove(eid Index) any + BitSet() BitSet +} + +type HashMapStorage[T any] struct { + keys BitSet + values map[Index]T +} + +func NewHashMapStorage[T any]() *HashMapStorage[T] { + return &HashMapStorage[T]{ + keys: BitSet{values: make(map[Index]struct{})}, + values: make(map[Index]T), + } +} +func (h *HashMapStorage[T]) Get(eid Index) any { return h.values[eid] } +func (h *HashMapStorage[T]) Insert(eid Index, v any) { + h.keys.Set(eid) + h.values[eid] = v.(T) +} +func (h *HashMapStorage[T]) Remove(eid Index) any { + h.keys.Unset(eid) + v := h.values[eid] + delete(h.values, eid) + return v +} +func (h *HashMapStorage[T]) BitSet() BitSet { return h.keys } diff --git a/server/internal/ecs/system.go b/server/internal/ecs/system.go new file mode 100644 index 0000000..e0f9b66 --- /dev/null +++ b/server/internal/ecs/system.go @@ -0,0 +1,49 @@ +package ecs + +import "reflect" + +type System interface { + Update(w *World) +} + +type funcsystem struct { + update reflect.Value + args func(w *World) []Storage +} + +func FuncSystem(F any) System { + f := reflect.ValueOf(F) + in := f.Type().NumIn() + argTypes := make([]reflect.Type, in) + for i := 0; i < in; i++ { + argTypes[i] = f.Type().In(i) + } + return &funcsystem{ + update: f, + args: func(w *World) (args []Storage) { + args = make([]Storage, in) + for i := 0; i < in; i++ { + args[i] = w.GetResourceRaw(argTypes[i]).(Storage) + } + return + }, + } +} + +func (f *funcsystem) Update(w *World) { + storages := f.args(w) + if len(storages) == 0 { + return + } + eids := storages[0].BitSet() + for _, v := range storages[1:] { + eids = eids.And(v.BitSet()) + } + args := make([]reflect.Value, len(storages)) + eids.Range(func(eid Index) { + for i := range args { + args[i] = reflect.ValueOf(storages[i].Get(eid)) + } + f.update.Call(args) + }) +} diff --git a/server/internal/ecs/world.go b/server/internal/ecs/world.go new file mode 100644 index 0000000..5acbf10 --- /dev/null +++ b/server/internal/ecs/world.go @@ -0,0 +1,58 @@ +package ecs + +import ( + "reflect" + "sync/atomic" +) + +type World struct { + resources map[reflect.Type]any + maxEID Index +} + +func NewWorld() *World { + return &World{resources: make(map[reflect.Type]any)} +} + +func (w *World) Insert(resource any) { + t := reflect.ValueOf(resource).Type() + w.resources[t] = resource +} + +func (w *World) Remove(resource any) any { + t := reflect.ValueOf(resource).Type() + resource = w.resources[t] + delete(w.resources, t) + return resource +} + +func (w *World) GetResource(resource any) any { + if resource == nil { + return nil + } + t := reflect.ValueOf(resource).Type() + v, _ := w.resources[t] + return v +} + +func (w *World) GetResourceRaw(t reflect.Type) any { + v, _ := w.resources[t] + return v +} + +func Register[T any](w *World, component T) { + t := reflect.TypeOf(component) + s := NewHashMapStorage[T]() + w.resources[t] = s +} + +func (w *World) CreateEntity(components ...any) (i Index) { + i = Index(atomic.AddUint32((*uint32)(&w.maxEID), 1)) + for _, c := range components { + v := reflect.ValueOf(c) + t := v.Type() + storage := w.resources[t].(Storage) + storage.Insert(w.maxEID, c) + } + return +}