Repository: mlange-42/arche
Pull request #4 (2023-02-05 21:44:56)

Implement basic archetypes
* Implement basic archetype
* Remove ByteStorage (slower than ReflectStorage, not GC-safe)
* Implement iteration benchmarks compared to https://github.com/marioolofo/go-gameengine-ecs


Patch:
diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml
index 12529f9..8358123 100644
--- a/.github/workflows/benchmarks.yml
+++ b/.github/workflows/benchmarks.yml
@@ -18,3 +18,4 @@ jobs:
       - name: Benchmarks
         run: |
           go test -benchmem -run=^$ -bench ^.*$ github.com/mlange-42/arche/ecs
+          go test -benchmem -run=^$ -bench ^.*$ github.com/mlange-42/arche/benchmark
diff --git a/benchmark/components.go b/benchmark/components.go
new file mode 100644
index 0000000..a04bfc5
--- /dev/null
+++ b/benchmark/components.go
@@ -0,0 +1,10 @@
+package benchmark
+
+type position struct {
+	X int
+	Y int
+}
+
+type rotation struct {
+	Angle int
+}
diff --git a/ecs/archetype.go b/ecs/archetype.go
new file mode 100644
index 0000000..8be2dcd
--- /dev/null
+++ b/ecs/archetype.go
@@ -0,0 +1,73 @@
+package ecs
+
+import (
+	"unsafe"
+)
+
+// Archetype is the interface for archetypes
+type Archetype interface {
+	GetEntity(index int) Entity
+	Get(index int, id ID) unsafe.Pointer
+	Add(entity Entity, components ...Component)
+	Remove(index int)
+}
+
+// NewArchetype creates a new Archetype for the given components
+// Component arguments must be sorted by ID!
+func NewArchetype(components ...Component) Archetype {
+	return newArchetype(components...)
+}
+
+type archetype struct {
+	mask       Mask
+	indices    []ID
+	entities   Storage
+	components []Storage
+}
+
+func newArchetype(components ...Component) *archetype {
+	var mask Mask
+	indices := make([]ID, len(components))
+	comps := make([]Storage, MaskTotalBits)
+	for i, c := range components {
+		mask.Set(c.ID, true)
+		indices[i] = c.ID
+		comps[c.ID] = NewReflectStorage(c.Component, 32)
+	}
+
+	return &archetype{
+		mask:       mask,
+		indices:    indices,
+		entities:   NewReflectStorage(Entity{}, 32),
+		components: comps,
+	}
+}
+
+// GetEntity returns the entity at the given index
+func (a *archetype) GetEntity(index int) Entity {
+	return *(*Entity)(a.entities.Get(uint32(index)))
+}
+
+// GetEntity returns the component with the given ID at the given index
+func (a *archetype) Get(index int, id ID) unsafe.Pointer {
+	return a.components[id].Get(uint32(index))
+}
+
+// Add adds an entity with components to the archetype
+func (a *archetype) Add(entity Entity, components ...Component) {
+	if len(components) != len(a.indices) {
+		panic("Invalid number of components")
+	}
+	a.entities.Add(&entity)
+	for _, c := range components {
+		a.components[c.ID].Add(c.Component)
+	}
+}
+
+// Remove removes an entity from the archetype
+func (a *archetype) Remove(index int) {
+	a.entities.Remove(uint32(index))
+	for _, c := range a.indices {
+		a.components[c].Remove(uint32(index))
+	}
+}
diff --git a/ecs/bitmask.go b/ecs/bitmask.go
new file mode 100644
index 0000000..7b69258
--- /dev/null
+++ b/ecs/bitmask.go
@@ -0,0 +1,58 @@
+package ecs
+
+// Mask is a bitmask
+type Mask uint64
+
+// MaskTotalBits is the size of Mask in bits
+const MaskTotalBits = 64
+
+var nibbleToBitsSet = [16]uint{0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4}
+
+// NewMask creates a new bitmask from a list of IDs
+// If any ID is bigger or equal MaskTotalBits, it'll not be added to the mask
+// Implementation taken from https://github.com/marioolofo/go-gameengine-ecs
+func NewMask(ids ...ID) Mask {
+	var mask Mask
+	for _, id := range ids {
+		mask.Set(id, true)
+	}
+	return mask
+}
+
+// Get reports if bit index defined by ID is true or false
+// The return will be always false for bit >= MaskTotalBits
+func (e Mask) Get(bit ID) bool {
+	mask := Mask(1 << bit)
+	return e&mask == mask
+}
+
+// Set sets the state of bit index to true or false
+// This function has no effect for bit >= MaskTotalBits
+func (e *Mask) Set(bit ID, value bool) {
+	if value {
+		*e |= Mask(1 << bit)
+	} else {
+		*e &= Mask(^(1 << bit))
+	}
+}
+
+// Reset change the state of all bits to false
+func (e *Mask) Reset() {
+	*e = 0
+}
+
+// Contains reports if other mask is a subset of this mask
+func (e Mask) Contains(other Mask) bool {
+	return e&other == other
+}
+
+// TotalBitsSet returns how many bits are set in this mask
+func (e Mask) TotalBitsSet() uint {
+	var count uint
+
+	for e != 0 {
+		count += nibbleToBitsSet[e&0xf]
+		e >>= 4
+	}
+	return count
+}
diff --git a/ecs/entity.go b/ecs/entity.go
index 6c6c94f..1352395 100644
--- a/ecs/entity.go
+++ b/ecs/entity.go
@@ -3,10 +3,10 @@ package ecs
 // Entity identifier
 // TODO: Store ID and generation in a single uint64?
 type Entity struct {
-	id  uint32
+	id  ID
 	gen uint16
 }
 
 func newEntity(id int) Entity {
-	return Entity{uint32(id), 0}
+	return Entity{ID(id), 0}
 }
diff --git a/ecs/entity_pool.go b/ecs/entity_pool.go
index 0819372..d342250 100644
--- a/ecs/entity_pool.go
+++ b/ecs/entity_pool.go
@@ -33,7 +33,7 @@ func newImplicitListEntityPool() *implicitListEntityPool {
 // Implements https://skypjack.github.io/2019-05-06-ecs-baf-part-3/
 type implicitListEntityPool struct {
 	entities  []Entity
-	next      uint32
+	next      ID
 	available uint32
 }
 
diff --git a/ecs/storage.go b/ecs/storage.go
index 85adfb9..1cdb416 100644
--- a/ecs/storage.go
+++ b/ecs/storage.go
@@ -14,83 +14,6 @@ type Storage interface {
 	Len() uint32
 }
 
-// ByteStorage stores components in a byte slice
-// It does not work with components that contain references or pointers,
-// because the referenced memory will be garbage-collected.
-type ByteStorage struct {
-	data              []byte
-	itemSize          uintptr
-	len               uint32
-	capacityIncrement uint32
-}
-
-// NewByteStorage creates a new ByteStorage
-func NewByteStorage(obj interface{}, increment int) *ByteStorage {
-	tp := reflect.TypeOf(obj)
-	size := tp.Size()
-
-	return &ByteStorage{
-		data:              make([]byte, increment*int(size)),
-		itemSize:          size,
-		len:               0,
-		capacityIncrement: uint32(increment),
-	}
-}
-
-// Get retrieves an unsafe pointer to an element
-func (s *ByteStorage) Get(index uint32) unsafe.Pointer {
-	base := unsafe.Pointer(&s.data[0])
-
-	offset := uintptr(index) * s.itemSize
-	return unsafe.Add(base, offset)
-}
-
-// Add adds an element to the end of the storage
-func (s *ByteStorage) Add(value interface{}) (index uint32) {
-	// TODO this allocates a new slice and should be improved
-	if uint32(len(s.data)) < (s.len+1)*uint32(s.itemSize) {
-		old := s.data
-		s.data = make([]byte, (s.len+s.capacityIncrement)*uint32(s.itemSize))
-		copy(s.data, old)
-	}
-	s.len++
-	s.set(s.len-1, value)
-	return s.len - 1
-}
-
-// Remove swap-removes an element
-func (s *ByteStorage) Remove(index uint32) {
-	o := s.len - 1
-	n := index
-	s.copy(o, n)
-	s.len--
-	// TODO shrink the underlying data arrays
-}
-
-// Len returns the number of items in the storage
-func (s *ByteStorage) Len() uint32 {
-	return s.len
-}
-
-func (s *ByteStorage) set(index uint32, value interface{}) {
-	dst := s.Get(index)
-	src := unsafe.Pointer(reflect.ValueOf(value).Pointer())
-	for i := uintptr(0); i < s.itemSize; i++ {
-		*(*byte)(dst) = *(*byte)(src)
-		dst = unsafe.Add(dst, 1)
-		src = unsafe.Add(src, 1)
-	}
-}
-
-func (s *ByteStorage) copy(from, to uint32) {
-	if from == to {
-		return
-	}
-	f := uintptr(from) * s.itemSize
-	t := uintptr(to) * s.itemSize
-	copy(s.data[t:t+s.itemSize], s.data[f:f+s.itemSize])
-}
-
 // ReflectStorage is a storage implementation that works with reflection
 type ReflectStorage struct {
 	buffer            reflect.Value
diff --git a/ecs/types.go b/ecs/types.go
new file mode 100644
index 0000000..a5334f3
--- /dev/null
+++ b/ecs/types.go
@@ -0,0 +1,10 @@
+package ecs
+
+// ID defines the format for the components identifier
+type ID uint32
+
+// Component is a component ID with a reference struct
+type Component struct {
+	ID
+	Component interface{}
+}
diff --git a/ecs/world.go b/ecs/world.go
new file mode 100644
index 0000000..47091cc
--- /dev/null
+++ b/ecs/world.go
@@ -0,0 +1,26 @@
+package ecs
+
+// World is the interface for the ECS world
+type World interface {
+	NewEntity() Entity
+	RemEntity(entity Entity)
+}
+
+// NewWorld creates a new World
+func NewWorld() World {
+	return &world{
+		entityPool: NewEntityPool(),
+	}
+}
+
+type world struct {
+	entityPool EntityPool
+}
+
+func (w *world) NewEntity() Entity {
+	return w.entityPool.Get()
+}
+
+func (w *world) RemEntity(entity Entity) {
+	w.entityPool.Recycle(entity)
+}
diff --git a/go.mod b/go.mod
index 6ef6626..daa4898 100644
--- a/go.mod
+++ b/go.mod
@@ -2,7 +2,10 @@ module github.com/mlange-42/arche
 
 go 1.20
 
-require github.com/stretchr/testify v1.8.1
+require (
+	github.com/marioolofo/go-gameengine-ecs v0.0.0-20230205000850-cead1cc4bf6a
+	github.com/stretchr/testify v1.8.1
+)
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
diff --git a/go.sum b/go.sum
index 2ec90f7..e617318 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,8 @@
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/marioolofo/go-gameengine-ecs v0.0.0-20230205000850-cead1cc4bf6a h1:jekwEG0D06sirA4kYHVfEbikefM7pvcnqCxtj/cnwmU=
+github.com/marioolofo/go-gameengine-ecs v0.0.0-20230205000850-cead1cc4bf6a/go.mod h1:YptmFsn359Z4KR6GK4xoFb74auGWY4uvnlpjGNOTtEg=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=


Test patch:
diff --git a/benchmark/arche_test.go b/benchmark/arche_test.go
new file mode 100644
index 0000000..b2a62b1
--- /dev/null
+++ b/benchmark/arche_test.go
@@ -0,0 +1,33 @@
+package benchmark
+
+import (
+	"testing"
+
+	"github.com/mlange-42/arche/ecs"
+)
+
+func BenchmarkArche(b *testing.B) {
+	world := ecs.NewWorld()
+
+	comps := []ecs.Component{
+		{ID: 0, Component: position{}},
+		{ID: 1, Component: rotation{}},
+	}
+
+	arch := ecs.NewArchetype(comps...)
+
+	for i := 0; i < 1000; i++ {
+		arch.Add(
+			world.NewEntity(),
+			ecs.Component{ID: 0, Component: &position{1, 2}},
+			ecs.Component{ID: 1, Component: &rotation{3}},
+		)
+	}
+
+	for i := 0; i < b.N; i++ {
+		for j := 0; j < 1000; j++ {
+			pos := (*position)(arch.Get(i, ecs.ID(0)))
+			_ = pos
+		}
+	}
+}
diff --git a/benchmark/geecs_test.go b/benchmark/geecs_test.go
new file mode 100644
index 0000000..a54ab9e
--- /dev/null
+++ b/benchmark/geecs_test.go
@@ -0,0 +1,29 @@
+package benchmark
+
+import (
+	"testing"
+
+	ecs "github.com/marioolofo/go-gameengine-ecs"
+)
+
+func BenchmarkGameEngineEcs(b *testing.B) {
+	comps := []ecs.ComponentConfig{
+		{ID: 0, Component: position{}},
+		{ID: 1, Component: rotation{}},
+	}
+	world := ecs.NewWorld(comps...)
+
+	for i := 0; i < 1000; i++ {
+		entity := world.NewEntity()
+		world.Assign(entity, 0, 1)
+	}
+
+	filter := world.NewFilter(0, 1)
+
+	for i := 0; i < b.N; i++ {
+		for _, e := range filter.Entities() {
+			pos := (*position)(world.Component(e, 0))
+			_ = pos
+		}
+	}
+}
diff --git a/ecs/archetype_test.go b/ecs/archetype_test.go
new file mode 100644
index 0000000..a2e368d
--- /dev/null
+++ b/ecs/archetype_test.go
@@ -0,0 +1,109 @@
+package ecs
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+type Position struct {
+	X int
+	Y int
+}
+
+type Rotation struct {
+	Angle int
+}
+
+func TestArchetype(t *testing.T) {
+	comps := []Component{
+		{ID: 0, Component: Position{}},
+		{ID: 1, Component: Rotation{}},
+	}
+
+	arch := newArchetype(comps...)
+
+	arch.Add(
+		newEntity(0),
+		Component{ID: 0, Component: &Position{1, 2}},
+		Component{ID: 1, Component: &Rotation{3}},
+	)
+
+	arch.Add(
+		newEntity(1),
+		Component{ID: 0, Component: &Position{4, 5}},
+		Component{ID: 1, Component: &Rotation{6}},
+	)
+
+	assert.Equal(t, 2, int(arch.entities.Len()))
+	assert.Equal(t, 2, int(arch.components[0].Len()))
+	assert.Equal(t, 2, int(arch.components[1].Len()))
+
+	e0 := arch.GetEntity(0)
+	e1 := arch.GetEntity(1)
+	assert.Equal(t, Entity{0, 0}, e0)
+	assert.Equal(t, Entity{1, 0}, e1)
+
+	pos0 := (*Position)(arch.Get(0, ID(0)))
+	rot0 := (*Rotation)(arch.Get(0, ID(1)))
+	pos1 := (*Position)(arch.Get(1, ID(0)))
+	rot1 := (*Rotation)(arch.Get(1, ID(1)))
+
+	assert.Equal(t, 1, pos0.X)
+	assert.Equal(t, 2, pos0.Y)
+	assert.Equal(t, 3, rot0.Angle)
+	assert.Equal(t, 4, pos1.X)
+	assert.Equal(t, 5, pos1.Y)
+	assert.Equal(t, 6, rot1.Angle)
+
+	arch.Remove(0)
+	assert.Equal(t, 1, int(arch.entities.Len()))
+	assert.Equal(t, 1, int(arch.components[0].Len()))
+	assert.Equal(t, 1, int(arch.components[1].Len()))
+
+	pos0 = (*Position)(arch.Get(0, ID(0)))
+	rot0 = (*Rotation)(arch.Get(0, ID(1)))
+	assert.Equal(t, 4, pos0.X)
+	assert.Equal(t, 5, pos0.Y)
+	assert.Equal(t, 6, rot0.Angle)
+
+	assert.Panics(t, func() {
+		arch.Add(
+			newEntity(1),
+			Component{ID: 0, Component: &Position{4, 5}},
+		)
+	})
+}
+
+func TestNewArchetype(t *testing.T) {
+	comps := []Component{
+		{ID: 0, Component: Position{}},
+		{ID: 1, Component: Rotation{}},
+	}
+
+	_ = NewArchetype(comps...)
+}
+
+func BenchmarkArchetypeAccess(b *testing.B) {
+	comps := []Component{
+		{ID: 0, Component: Position{}},
+		{ID: 1, Component: Rotation{}},
+	}
+
+	arch := newArchetype(comps...)
+
+	for i := 0; i < 1000; i++ {
+		arch.Add(
+			newEntity(i),
+			Component{ID: 0, Component: &Position{1, 2}},
+			Component{ID: 1, Component: &Rotation{3}},
+		)
+	}
+
+	for i := 0; i < b.N; i++ {
+		for j := 0; j < 1000; j++ {
+			pos := (*Position)(arch.Get(i, ID(0)))
+			_ = pos
+		}
+	}
+}
diff --git a/ecs/bitmask_test.go b/ecs/bitmask_test.go
new file mode 100644
index 0000000..042321d
--- /dev/null
+++ b/ecs/bitmask_test.go
@@ -0,0 +1,36 @@
+package ecs
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestBitMask(t *testing.T) {
+	mask := NewMask(ID(1), ID(2), ID(13), ID(27))
+
+	assert.Equal(t, uint(4), mask.TotalBitsSet())
+
+	assert.True(t, mask.Get(1))
+	assert.True(t, mask.Get(2))
+	assert.True(t, mask.Get(13))
+	assert.True(t, mask.Get(27))
+
+	assert.False(t, mask.Get(0))
+	assert.False(t, mask.Get(3))
+
+	mask.Set(ID(0), true)
+	mask.Set(ID(1), false)
+
+	assert.True(t, mask.Get(0))
+	assert.False(t, mask.Get(1))
+
+	other1 := NewMask(ID(1), ID(2), ID(32))
+	other2 := NewMask(ID(0), ID(2))
+
+	assert.False(t, mask.Contains(other1))
+	assert.True(t, mask.Contains(other2))
+
+	mask.Reset()
+	assert.Equal(t, uint(0), mask.TotalBitsSet())
+}
diff --git a/ecs/storage_test.go b/ecs/storage_test.go
index bd8ef97..6bdd683 100644
--- a/ecs/storage_test.go
+++ b/ecs/storage_test.go
@@ -17,40 +17,6 @@ type simpleStruct struct {
 	Index int
 }
 
-func TestByteStorageItemSize(t *testing.T) {
-	obj1 := struct{}{}
-	obj2 := struct{ bool }{true}
-	obj3 := struct{ int8 }{0}
-	obj4 := struct{ int16 }{0}
-	obj5 := struct{ int32 }{0}
-	obj6 := struct{ int64 }{0}
-	obj7 := struct {
-		int64
-		bool
-	}{0, true}
-
-	s1 := NewByteStorage(obj1, 32)
-	s2 := NewByteStorage(obj2, 32)
-	s3 := NewByteStorage(obj3, 32)
-	s4 := NewByteStorage(obj4, 32)
-	s5 := NewByteStorage(obj5, 32)
-	s6 := NewByteStorage(obj6, 32)
-	s7 := NewByteStorage(obj7, 32)
-
-	assert.Equal(
-		t,
-		[]uintptr{0, 1, 1, 2, 4, 8, 16},
-		[]uintptr{s1.itemSize, s2.itemSize, s3.itemSize, s4.itemSize, s5.itemSize, s6.itemSize, s7.itemSize},
-		"Unexpected struct size",
-	)
-}
-
-func TestByteStorageAddGet(t *testing.T) {
-	obj1 := testStruct{}
-	s := NewByteStorage(obj1, 32)
-	storageAddGet(t, s)
-}
-
 func TestReflectStorageAddGet(t *testing.T) {
 	obj1 := testStruct{}
 	s := NewReflectStorage(obj1, 32)
@@ -80,13 +46,6 @@ func storageAddGet(t *testing.T, s Storage) {
 	assert.Equal(t, []testStruct{{}, {1, 1001, true, false}}, ToSlice[testStruct](s), "Wrong extracted struct slice")
 }
 
-func TestByteStorageRemove(t *testing.T) {
-	ref := simpleStruct{}
-	s := NewByteStorage(ref, 32)
-
-	storageRemove(t, s)
-}
-
 func TestReflectStorageRemove(t *testing.T) {
 	ref := simpleStruct{}
 	s := NewReflectStorage(ref, 32)
@@ -111,33 +70,6 @@ func storageRemove(t *testing.T, s Storage) {
 	assert.Equal(t, []simpleStruct{{0}, {3}, {2}}, ToSlice[simpleStruct](s), "Wrong slice after remove")
 }
 
-func TestByteStorageDataSize(t *testing.T) {
-	ref := simpleStruct{}
-	s := NewByteStorage(ref, 1)
-
-	size := int(s.itemSize)
-
-	for i := 0; i < 5; i++ {
-		obj := simpleStruct{i}
-		s.Add(&obj)
-	}
-
-	assert.Equal(t, 5*size, len(s.data))
-
-	s.Remove(0)
-	assert.Equal(t, 5*size, len(s.data))
-	s.Remove(0)
-	assert.Equal(t, 5*size, len(s.data))
-
-	s.Add(&simpleStruct{})
-	assert.Equal(t, 5*size, len(s.data))
-	s.Add(&simpleStruct{})
-	assert.Equal(t, 5*size, len(s.data))
-
-	s.Add(&simpleStruct{})
-	assert.Equal(t, 6*size, len(s.data))
-}
-
 func TestReflectStorageDataSize(t *testing.T) {
 	ref := simpleStruct{}
 	s := NewReflectStorage(ref, 1)
@@ -163,30 +95,10 @@ func TestReflectStorageDataSize(t *testing.T) {
 	assert.Equal(t, 6, int(s.cap))
 }
 
-func TestNewByteStorage(t *testing.T) {
-	_ = NewByteStorage(simpleStruct{}, 32)
-}
-
 func TestNewReflectStorage(t *testing.T) {
 	_ = NewReflectStorage(simpleStruct{}, 32)
 }
 
-func BenchmarkIterByteStorage(b *testing.B) {
-	ref := testStruct{}
-	s := NewByteStorage(ref, 128)
-	for i := 0; i < 1000; i++ {
-		s.Add(&testStruct{})
-	}
-	assert.Equal(b, 1000, int(s.Len()))
-
-	for i := 0; i < b.N; i++ {
-		for j := 0; j < int(s.Len()); j++ {
-			a := (*testStruct)(s.Get(uint32(j)))
-			_ = a
-		}
-	}
-}
-
 func BenchmarkIterReflectStorage(b *testing.B) {
 	ref := testStruct{}
 	s := NewReflectStorage(ref, 128)
@@ -233,19 +145,6 @@ func BenchmarkIterSliceInterface(b *testing.B) {
 	}
 }
 
-func BenchmarkAddByteStorage(b *testing.B) {
-	ref := testStruct{}
-	for i := 0; i < b.N; i++ {
-		b.StopTimer()
-		s := NewByteStorage(ref, 1024)
-		b.StartTimer()
-
-		for i := 0; i < 1000; i++ {
-			s.Add(&ref)
-		}
-	}
-}
-
 func BenchmarkAddReflectStorage(b *testing.B) {
 	ref := testStruct{}
 	for i := 0; i < b.N; i++ {
@@ -293,22 +192,6 @@ func BenchmarkAddSliceInterface(b *testing.B) {
 	}
 }
 
-func BenchmarkRemoveByteStorage(b *testing.B) {
-	ref := testStruct{}
-	for i := 0; i < b.N; i++ {
-		b.StopTimer()
-		s := NewByteStorage(ref, 1024)
-		for i := 0; i < 1000; i++ {
-			s.Add(&ref)
-		}
-		b.StartTimer()
-
-		for i := 0; i < 1000; i++ {
-			s.Remove(0)
-		}
-	}
-}
-
 func BenchmarkRemoveReflectStorage(b *testing.B) {
 	ref := testStruct{}
 	for i := 0; i < b.N; i++ {
diff --git a/ecs/workd_test.go b/ecs/workd_test.go
new file mode 100644
index 0000000..0e81e30
--- /dev/null
+++ b/ecs/workd_test.go
@@ -0,0 +1,18 @@
+package ecs
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestWorldEntites(t *testing.T) {
+	w := NewWorld()
+
+	assert.Equal(t, Entity{0, 0}, w.NewEntity())
+	assert.Equal(t, Entity{1, 0}, w.NewEntity())
+	assert.Equal(t, Entity{2, 0}, w.NewEntity())
+
+	w.RemEntity(Entity{1, 0})
+	assert.Equal(t, Entity{1, 1}, w.NewEntity())
+}