diff --git a/entity/base_comp.go b/entity/base_comp.go index 770906e..f47cd34 100755 --- a/entity/base_comp.go +++ b/entity/base_comp.go @@ -1,19 +1,18 @@ package entity -import "github.com/bloeys/nmage/assert" +import "github.com/bloeys/nmage/registry" var _ Comp = &BaseComp{} type BaseComp struct { - Entity *BaseEntity + Handle registry.Handle } func (b BaseComp) baseComp() { } -func (b *BaseComp) Init(parent *BaseEntity) { - assert.T(parent != nil, "Component was initialized with a nil parent. That is not allowed.") - b.Entity = parent +func (b *BaseComp) Init(parentHandle registry.Handle) { + b.Handle = parentHandle } func (b BaseComp) Name() string { diff --git a/entity/comp.go b/entity/comp.go index efce0ff..5b92fbf 100755 --- a/entity/comp.go +++ b/entity/comp.go @@ -1,6 +1,9 @@ package entity -import "github.com/bloeys/nmage/assert" +import ( + "github.com/bloeys/nmage/assert" + "github.com/bloeys/nmage/registry" +) type Comp interface { // This ensures that implementors of the Comp interface @@ -8,7 +11,7 @@ type Comp interface { baseComp() Name() string - Init(parent *BaseEntity) + Init(parentHandle registry.Handle) Update() Destroy() } @@ -21,12 +24,12 @@ type CompContainer struct { Comps []Comp } -func AddComp[T Comp](e *BaseEntity, cc *CompContainer, c T) { +func AddComp[T Comp](entityHandle registry.Handle, cc *CompContainer, c T) { - assert.T(!HasComp[T](cc), "Entity with id '%v' already has component of type '%T'", e.ID, c) + assert.T(!HasComp[T](cc), "Entity with id '%v' already has component of type '%T'", entityHandle, c) cc.Comps = append(cc.Comps, c) - c.Init(e) + c.Init(entityHandle) } func HasComp[T Comp](e *CompContainer) bool { diff --git a/entity/entity.go b/entity/entity.go index e021351..08d9ddf 100755 --- a/entity/entity.go +++ b/entity/entity.go @@ -1,54 +1,7 @@ package entity -type EntityFlag byte - -const ( - EntityFlag_None EntityFlag = 0 - EntityFlag_Alive EntityFlag = 1 << (iota - 1) -) - -const ( - GenerationShiftBits = 64 - 8 - FlagsShiftBits = 64 - 16 - IndexBitMask = 0x00_00_FFFF_FFFF_FFFF -) +import "github.com/bloeys/nmage/registry" type Entity interface { - baseEntity() - GetHandle() EntityHandle -} - -type EntityHandle uint64 - -var _ Entity = &BaseEntity{} - -type BaseEntity struct { - // Byte 1: Generation; Byte 2: Flags; Bytes 3-8: Index - ID EntityHandle -} - -func (be BaseEntity) baseEntity() {} - -func (be BaseEntity) GetHandle() EntityHandle { - return be.ID -} - -func (e *BaseEntity) HasFlag(ef EntityFlag) bool { - return GetFlags(e.ID)&ef > 0 -} - -func GetGeneration(id EntityHandle) byte { - return byte(id >> GenerationShiftBits) -} - -func GetFlags(id EntityHandle) EntityFlag { - return EntityFlag(id >> FlagsShiftBits) -} - -func GetIndex(id EntityHandle) uint64 { - return uint64(id & IndexBitMask) -} - -func NewEntityId(generation byte, flags EntityFlag, index uint64) EntityHandle { - return EntityHandle(index | (uint64(generation) << GenerationShiftBits) | (uint64(flags) << FlagsShiftBits)) + GetHandle() registry.Handle } diff --git a/entity/registry.go b/entity/registry.go deleted file mode 100755 index fe0186b..0000000 --- a/entity/registry.go +++ /dev/null @@ -1,104 +0,0 @@ -package entity - -import ( - "github.com/bloeys/nmage/assert" -) - -var ( - // The number of slots required to be in the free list before the free list - // is used for creating new entries - FreeListUsageThreshold uint32 = 20 -) - -type freeListitem struct { - EntityIndex uint64 - nextFree *freeListitem -} - -type Registry struct { - EntityCount uint64 - Entities []BaseEntity - - FreeList *freeListitem - FreeListSize uint32 -} - -func (r *Registry) NewEntity() *BaseEntity { - - assert.T(r.EntityCount < uint64(len(r.Entities)), "Can not add more entities to registry because it is full") - - entityToUseIndex := uint64(0) - var entityToUse *BaseEntity = nil - - if r.FreeList != nil && r.FreeListSize > FreeListUsageThreshold { - - entityToUseIndex = r.FreeList.EntityIndex - entityToUse = &r.Entities[entityToUseIndex] - r.FreeList = r.FreeList.nextFree - r.FreeListSize-- - } else { - - for i := 0; i < len(r.Entities); i++ { - - e := &r.Entities[i] - if e.HasFlag(EntityFlag_Alive) { - continue - } - - entityToUse = e - entityToUseIndex = uint64(i) - break - } - } - - if entityToUse == nil { - panic("failed to create new entity because we did not find a free spot in the registry. Why did the assert not go off?") - } - - r.EntityCount++ - entityToUse.ID = NewEntityId(GetGeneration(entityToUse.ID)+1, EntityFlag_Alive, entityToUseIndex) - assert.T(entityToUse.ID != 0, "Entity ID must not be zero") - return entityToUse -} - -func (r *Registry) GetEntity(id EntityHandle) *BaseEntity { - - index := GetIndex(id) - gen := GetGeneration(id) - - e := &r.Entities[index] - eGen := GetGeneration(e.ID) - - if gen != eGen { - return nil - } - - return e -} - -// FreeEntity resets the entity flags then adds this entity to the free list -func (r *Registry) FreeEntity(id EntityHandle) { - - e := r.GetEntity(id) - if e == nil { - return - } - - r.EntityCount-- - eIndex := GetIndex(e.ID) - - e.ID = NewEntityId(GetGeneration(e.ID), EntityFlag_None, eIndex) - - r.FreeList = &freeListitem{ - EntityIndex: eIndex, - nextFree: r.FreeList, - } - r.FreeListSize++ -} - -func NewRegistry(size uint32) *Registry { - assert.T(size > 0, "Registry size must be more than zero") - return &Registry{ - Entities: make([]BaseEntity, size), - } -} diff --git a/level/level.go b/level/level.go index 3f492c4..527b45c 100755 --- a/level/level.go +++ b/level/level.go @@ -2,19 +2,16 @@ package level import ( "github.com/bloeys/nmage/assert" - "github.com/bloeys/nmage/entity" ) type Level struct { - *entity.Registry Name string } -func NewLevel(name string, maxEntities uint32) *Level { +func NewLevel(name string) *Level { assert.T(name != "", "Level name can not be empty") return &Level{ - Name: name, - Registry: entity.NewRegistry(maxEntities), + Name: name, } } diff --git a/main.go b/main.go index 93b9189..4c83799 100755 --- a/main.go +++ b/main.go @@ -11,10 +11,10 @@ import ( "github.com/bloeys/nmage/engine" "github.com/bloeys/nmage/entity" "github.com/bloeys/nmage/input" - "github.com/bloeys/nmage/level" "github.com/bloeys/nmage/logging" "github.com/bloeys/nmage/materials" "github.com/bloeys/nmage/meshes" + "github.com/bloeys/nmage/registry" "github.com/bloeys/nmage/renderer/rend3dgl" "github.com/bloeys/nmage/timing" nmageimgui "github.com/bloeys/nmage/ui/imgui" @@ -87,14 +87,17 @@ func (t *TransformComp) Name() string { func Test() { - lvl := level.NewLevel("test level", 1000) - e1 := lvl.Registry.NewEntity() + // lvl := level.NewLevel("test level") + testRegistry := registry.NewRegistry[int](100) + + e1, e1Handle := testRegistry.New() e1CompContainer := entity.NewCompContainer() + fmt.Printf("Entity 1: %+v; Handle: %+v; Index: %+v; Gen: %+v; Flags: %+v\n", e1, e1Handle, e1Handle.Index(), e1Handle.Generation(), e1Handle.Flags()) trComp := entity.GetComp[*TransformComp](&e1CompContainer) fmt.Println("Get comp before adding any:", trComp) - entity.AddComp(e1, &e1CompContainer, &TransformComp{ + entity.AddComp(e1Handle, &e1CompContainer, &TransformComp{ Pos: gglm.NewVec3(0, 0, 0), Rot: gglm.NewQuatEulerXYZ(0, 0, 0), Scale: gglm.NewVec3(0, 0, 0), @@ -102,10 +105,21 @@ func Test() { trComp = entity.GetComp[*TransformComp](&e1CompContainer) fmt.Println("Get transform comp:", trComp) - fmt.Printf("Entity: %+v\n", e1) - fmt.Printf("Entity: %+v\n", lvl.Registry.NewEntity()) - fmt.Printf("Entity: %+v\n", lvl.Registry.NewEntity()) - fmt.Printf("Entity: %+v\n", lvl.Registry.NewEntity()) + e2, e2Handle := testRegistry.New() + e3, e3Handle := testRegistry.New() + e4, e4Handle := testRegistry.New() + fmt.Printf("Entity 2: %+v; Handle: %+v; Index: %+v; Gen: %+v; Flags: %+v\n", e2, e2Handle, e2Handle.Index(), e2Handle.Generation(), e2Handle.Flags()) + fmt.Printf("Entity 3: %+v; Handle: %+v; Index: %+v; Gen: %+v; Flags: %+v\n", e3, e3Handle, e3Handle.Index(), e3Handle.Generation(), e3Handle.Flags()) + fmt.Printf("Entity 4: %+v; Handle: %+v; Index: %+v; Gen: %+v; Flags: %+v\n", e4, e4Handle, e4Handle.Index(), e4Handle.Generation(), e4Handle.Flags()) + + *e2 = 1000 + fmt.Printf("Entity 2 value after registry get: %+v\n", *testRegistry.Get(e2Handle)) + + testRegistry.Free(e2Handle) + fmt.Printf("Entity 2 value after free: %+v\n", testRegistry.Get(e2Handle)) + + e5, e5Handle := testRegistry.New() + fmt.Printf("Entity 5: %+v; Handle: %+v; Index: %+v; Gen: %+v; Flags: %+v\n", e5, e5Handle, e5Handle.Index(), e5Handle.Generation(), e5Handle.Flags()) } func main() { @@ -301,8 +315,6 @@ func (g *OurGame) Update() { g.Win.SDLWin.SetTitle(fmt.Sprint("nMage (", timing.GetAvgFPS(), " fps)")) } -var testString string - func (g *OurGame) updateCameraLookAround() { mouseX, mouseY := input.GetMouseMotion() diff --git a/registry/registry.go b/registry/registry.go new file mode 100755 index 0000000..10ea534 --- /dev/null +++ b/registry/registry.go @@ -0,0 +1,123 @@ +package registry + +import ( + "math" + + "github.com/bloeys/nmage/assert" +) + +type freeListitem struct { + ItemIndex uint64 + nextFree *freeListitem +} + +// Registry is a storage data structure that can efficiently create/get/free items using generational indices. +// Each item stored in the registry is associated with a 'handle' object that is used to get and free objects +// +// The registry 'owns' all items it stores and returns pointers to items in its array. All items are allocated upfront. +// +// It is NOT safe to concurrently create or free items. However, it is SAFE to concurrently get items +type Registry[T any] struct { + ItemCount uint64 + Handles []Handle + Items []T + + FreeList *freeListitem + FreeListSize uint32 + + // The number of slots required to be in the free list before the free list + // is used for creating new entries + FreeListUsageThreshold uint32 +} + +func (r *Registry[T]) New() (*T, Handle) { + + assert.T(r.ItemCount < uint64(len(r.Handles)), "Can not add more entities to registry because it is full") + + var index uint64 = math.MaxUint64 + + // Find index to use for the new item + if r.FreeList != nil && r.FreeListSize > r.FreeListUsageThreshold { + + index = r.FreeList.ItemIndex + + r.FreeList = r.FreeList.nextFree + r.FreeListSize-- + } else { + + for i := 0; i < len(r.Handles); i++ { + + handle := r.Handles[i] + + if handle.HasFlag(HandleFlag_Alive) { + continue + } + + index = uint64(i) + break + } + } + + if index == math.MaxUint64 { + panic("failed to create new entity because we did not find a free spot in the registry. Why did the item count assert not go off?") + } + + var newItem T + newHandle := NewHandle(r.Handles[index].Generation()+1, HandleFlag_Alive, index) + assert.T(newHandle != 0, "Entity handle must not be zero") + + r.ItemCount++ + r.Handles[index] = newHandle + r.Items[index] = newItem + + // It is very important we return directly from the items array, because if we return + // a pointer to newItem, and T is a value not a pointer, then newItem and what's stored in items will be different + return &r.Items[index], newHandle +} + +func (r *Registry[T]) Get(id Handle) *T { + + index := id.Index() + assert.T(index < uint64(len(r.Handles)), "Failed to get entity because of invalid entity handle. Handle index is %d while registry only has %d slots. Handle: %+v", index, r.ItemCount, id) + + handle := r.Handles[index] + if handle.Generation() != id.Generation() || !handle.HasFlag(HandleFlag_Alive) { + return nil + } + + item := &r.Items[index] + return item +} + +// Free resets the entity flags then adds this entity to the free list +func (r *Registry[T]) Free(id Handle) { + + index := id.Index() + assert.T(index < uint64(len(r.Handles)), "Failed to free entity because of invalid entity handle. Handle index is %d while registry only has %d slots. Handle: %+v", index, r.ItemCount, id) + + // Nothing to do if already free + handle := r.Handles[index] + if handle.Generation() != id.Generation() || !handle.HasFlag(HandleFlag_Alive) { + return + } + + // Generation is incremented on aquire, so here we just reset flags + r.ItemCount-- + r.Handles[index] = NewHandle(id.Generation(), HandleFlag_None, index) + + // Add to free list + r.FreeList = &freeListitem{ + ItemIndex: index, + nextFree: r.FreeList, + } + r.FreeListSize++ +} + +func NewRegistry[T any](size uint32) *Registry[T] { + assert.T(size > 0, "Registry size must be more than zero") + return &Registry[T]{ + Handles: make([]Handle, size), + Items: make([]T, size), + FreeListUsageThreshold: 30, + } +} diff --git a/registry/registry_handle.go b/registry/registry_handle.go new file mode 100755 index 0000000..a67cb79 --- /dev/null +++ b/registry/registry_handle.go @@ -0,0 +1,37 @@ +package registry + +type HandleFlag byte + +const ( + HandleFlag_None HandleFlag = 0 + HandleFlag_Alive HandleFlag = 1 << (iota - 1) +) + +const ( + GenerationShiftBits = 64 - 8 + FlagsShiftBits = 64 - 16 + IndexBitMask = 0x00_00_FFFF_FFFF_FFFF +) + +// Byte 1: Generation; Byte 2: Flags; Bytes 3-8: Index +type Handle uint64 + +func (h Handle) HasFlag(ef HandleFlag) bool { + return h.Flags()&ef > 0 +} + +func (h Handle) Generation() byte { + return byte(h >> GenerationShiftBits) +} + +func (h Handle) Flags() HandleFlag { + return HandleFlag(h >> FlagsShiftBits) +} + +func (h Handle) Index() uint64 { + return uint64(h & IndexBitMask) +} + +func NewHandle(generation byte, flags HandleFlag, index uint64) Handle { + return Handle(index | (uint64(generation) << GenerationShiftBits) | (uint64(flags) << FlagsShiftBits)) +}