Skip to content

Commit dd39196

Browse files
authored
Implement tables, masks and registry (#6)
1 parent 75c73ff commit dd39196

18 files changed

+500
-10
lines changed

ecs/archetype.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
package ecs
2+
3+
type archetype struct {
4+
tables []*table
5+
hasRelation bool
6+
}

ecs/archetype_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package ecs
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestArchetype(t *testing.T) {
10+
arch := archetype{}
11+
assert.False(t, arch.hasRelation)
12+
assert.Equal(t, 0, len(arch.tables))
13+
}

ecs/column.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ func newColumn(tp reflect.Type, capacity int) column {
1818
size, align := tp.Size(), uintptr(tp.Align())
1919
size = (size + (align - 1)) / align * align
2020

21+
// TODO: should be use a slice instead of an array here?
2122
data := reflect.New(reflect.ArrayOf(capacity, tp)).Elem()
2223
pointer := data.Addr().UnsafePointer()
2324

ecs/column_bench_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package ecs
2+
3+
import (
4+
"math/rand/v2"
5+
"reflect"
6+
"testing"
7+
"unsafe"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
type columnInterface interface {
13+
Add(any)
14+
Get(uint32) unsafe.Pointer
15+
}
16+
17+
type sliceColumn[T any] struct {
18+
data []T
19+
pointer unsafe.Pointer
20+
itemSize uintptr
21+
}
22+
23+
func newSliceColumn[T any]() *sliceColumn[T] {
24+
return &sliceColumn[T]{
25+
itemSize: sizeOf(typeOf[T]()),
26+
}
27+
}
28+
29+
func (c *sliceColumn[T]) Get(index uint32) unsafe.Pointer {
30+
return unsafe.Add(c.pointer, int(index)*int(c.itemSize))
31+
}
32+
33+
func (c *sliceColumn[T]) Add(value any) {
34+
c.data = append(c.data, value.(T))
35+
c.pointer = unsafe.Pointer(&c.data[0])
36+
}
37+
38+
func BenchmarkColumnGet(b *testing.B) {
39+
n := 1000
40+
posType := reflect.TypeOf(Position{})
41+
column := newColumn(posType, 1000)
42+
43+
indices := make([]uint32, n)
44+
for i := 0; i < n; i++ {
45+
column.Add(unsafe.Pointer(&Position{1, 2}))
46+
indices[i] = uint32(i)
47+
}
48+
rand.Shuffle(n, func(i, j int) {
49+
indices[i], indices[j] = indices[j], indices[i]
50+
})
51+
52+
var ptr unsafe.Pointer
53+
for b.Loop() {
54+
ptr = column.Get(500)
55+
}
56+
57+
assert.NotNil(b, ptr)
58+
}
59+
60+
func BenchmarkSliceColumnGet(b *testing.B) {
61+
n := 1000
62+
var column columnInterface = newSliceColumn[Position]()
63+
64+
indices := make([]uint32, n)
65+
for i := 0; i < n; i++ {
66+
column.Add(Position{1, 2})
67+
indices[i] = uint32(i)
68+
}
69+
rand.Shuffle(n, func(i, j int) {
70+
indices[i], indices[j] = indices[j], indices[i]
71+
})
72+
73+
var ptr unsafe.Pointer
74+
for b.Loop() {
75+
ptr = column.Get(500)
76+
}
77+
78+
assert.NotNil(b, ptr)
79+
}

ecs/component.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import (
66

77
// ID is the component identifier.
88
type ID struct {
9-
id uint32
9+
id uint8
1010
}
1111

12-
func id(id uint32) ID {
13-
return ID{id}
12+
func id(id int) ID {
13+
return ID{uint8(id)}
1414
}
1515

1616
//type componentInfo struct {

ecs/component_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ func TestIDsSearch(t *testing.T) {
1616
n := 100
1717
arr := make([]ID, n)
1818
for i := range n {
19-
arr[i] = id(uint32(i + 5))
19+
arr[i] = id(i + 5)
2020
}
2121
idsSorted := newIDs(arr...)
2222

2323
tests := []struct {
24-
search uint32
24+
search int
2525
index int
2626
found bool
2727
}{
@@ -41,10 +41,10 @@ func TestIDsSearch(t *testing.T) {
4141
func benchmarkIDsSearch(b *testing.B, n int) {
4242
arr := make([]ID, n)
4343
for i := range n {
44-
arr[i] = id(uint32(i))
44+
arr[i] = id(i)
4545
}
4646
idsSorted := newIDs(arr...)
47-
searchFor := id(uint32(float32(n) * 0.6))
47+
searchFor := id(int(float32(n) * 0.6))
4848

4949
for b.Loop() {
5050
_, _ = idsSorted.Search(searchFor)
@@ -54,10 +54,10 @@ func benchmarkIDsSearch(b *testing.B, n int) {
5454
func benchmarkIDsSearchLinear(b *testing.B, n int) {
5555
arr := make([]ID, n)
5656
for i := range n {
57-
arr[i] = id(uint32(i))
57+
arr[i] = id(i)
5858
}
5959
idsSorted := newIDs(arr...)
60-
searchFor := id(uint32(float32(n) * 0.5))
60+
searchFor := id(int(float32(n) * 0.5))
6161

6262
for b.Loop() {
6363
_, _ = idsSorted.SearchLinear(searchFor)
@@ -69,7 +69,7 @@ func benchmarkIDsContains(b *testing.B, k, n int) {
6969

7070
allIDs := make([]ID, n)
7171
for i := range n {
72-
allIDs[i] = id(uint32(i))
72+
allIDs[i] = id(i)
7373
}
7474
archIDs := newIDs(allIDs...)
7575

ecs/entity.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package ecs
22

33
type entityID uint32
44

5+
var entityType = typeOf[Entity]()
6+
57
// Entity identifier.
68
type Entity struct {
79
id entityID // Entity ID
@@ -11,3 +13,8 @@ type Entity struct {
1113
func newEntity(id entityID) Entity {
1214
return Entity{id, 0}
1315
}
16+
17+
type entityIndex struct {
18+
table int
19+
row int
20+
}

ecs/entity_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,9 @@ func TestEntity(t *testing.T) {
1111
assert.EqualValues(t, 100, e.id)
1212
assert.EqualValues(t, 0, e.gen)
1313
}
14+
15+
func TestEntityIndex(t *testing.T) {
16+
index := entityIndex{}
17+
assert.Equal(t, 0, index.table)
18+
assert.Equal(t, 0, index.row)
19+
}

ecs/functions.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package ecs
2+
3+
import "reflect"
4+
5+
// ComponentID returns the [ID] for a component type via generics.
6+
// Registers the type if it is not already registered.
7+
//
8+
// The number of unique component types per [World] is limited to 256 ([MaskTotalBits]).
9+
//
10+
// Panics if called on a locked world and the type is not registered yet.
11+
//
12+
// Note that type aliases are not considered separate component types.
13+
// Type re-definitions, however, are separate types.
14+
//
15+
// ⚠️ Warning: Using IDs that are outside of the range of registered IDs anywhere in [World] or other places will result in undefined behavior!
16+
func ComponentID[T any](w *World) ID {
17+
tp := reflect.TypeOf((*T)(nil)).Elem()
18+
19+
id, _ := w.registry.ComponentID(tp)
20+
//if newID {
21+
// TODO: check lock and unroll
22+
// if w.IsLocked() {
23+
// w.registry.unregisterLastComponent()
24+
// panic("attempt to register a new component in a locked world")
25+
// }
26+
//}
27+
return id
28+
}

ecs/mask.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package ecs
2+
3+
import (
4+
"math/bits"
5+
)
6+
7+
// MaskTotalBits is the size of a [Mask] in bits.
8+
// It is the maximum number of component types that may exist in any [World].
9+
//
10+
// Use build tag `tiny` to reduce all masks to 64 bits.
11+
const MaskTotalBits = 256
12+
13+
// Mask is a 256 bit bitmask.
14+
// It is also a [Filter] for including certain components.
15+
//
16+
// Use [All] to create a mask for a list of component IDs.
17+
// A mask can be further specified using [Mask.Without] or [Mask.Exclusive].
18+
//
19+
// Use build tag `tiny` to reduce all masks to 64 bits.
20+
type Mask struct {
21+
bits [4]uint64 // 4x 64 bits of the mask
22+
}
23+
24+
// Matches the mask as filter against another mask.
25+
func (b Mask) Matches(bits *Mask) bool {
26+
return bits.Contains(&b)
27+
}
28+
29+
// All creates a new Mask from a list of IDs.
30+
// Matches all entities that have the respective components, and potentially further components.
31+
//
32+
// See also [Mask.Without] and [Mask.Exclusive]
33+
func All(ids ...ID) Mask {
34+
var mask Mask
35+
for _, id := range ids {
36+
mask.Set(id, true)
37+
}
38+
return mask
39+
}
40+
41+
// Get reports whether the bit at the given index [ID] is set.
42+
func (b *Mask) Get(bit ID) bool {
43+
idx := bit.id / 64
44+
offset := bit.id - (64 * idx)
45+
mask := uint64(1 << offset)
46+
return b.bits[idx]&mask == mask
47+
}
48+
49+
// Set sets the state of the bit at the given index.
50+
func (b *Mask) Set(bit ID, value bool) {
51+
idx := bit.id / 64
52+
offset := bit.id - (64 * idx)
53+
if value {
54+
b.bits[idx] |= (1 << offset)
55+
} else {
56+
b.bits[idx] &= ^(1 << offset)
57+
}
58+
}
59+
60+
// Not returns the inversion of this mask.
61+
func (b *Mask) Not() Mask {
62+
return Mask{
63+
bits: [4]uint64{^b.bits[0], ^b.bits[1], ^b.bits[2], ^b.bits[3]},
64+
}
65+
}
66+
67+
// IsZero returns whether no bits are set in the mask.
68+
func (b *Mask) IsZero() bool {
69+
return b.bits[0] == 0 && b.bits[1] == 0 && b.bits[2] == 0 && b.bits[3] == 0
70+
}
71+
72+
// Reset the mask setting all bits to false.
73+
func (b *Mask) Reset() {
74+
b.bits = [4]uint64{0, 0, 0, 0}
75+
}
76+
77+
// Contains reports if the other mask is a subset of this mask.
78+
func (b *Mask) Contains(other *Mask) bool {
79+
return b.bits[0]&other.bits[0] == other.bits[0] &&
80+
b.bits[1]&other.bits[1] == other.bits[1] &&
81+
b.bits[2]&other.bits[2] == other.bits[2] &&
82+
b.bits[3]&other.bits[3] == other.bits[3]
83+
}
84+
85+
// ContainsAny reports if any bit of the other mask is in this mask.
86+
func (b *Mask) ContainsAny(other *Mask) bool {
87+
return b.bits[0]&other.bits[0] != 0 ||
88+
b.bits[1]&other.bits[1] != 0 ||
89+
b.bits[2]&other.bits[2] != 0 ||
90+
b.bits[3]&other.bits[3] != 0
91+
}
92+
93+
// And returns the bitwise AND of two masks.
94+
func (b *Mask) And(other *Mask) Mask {
95+
return Mask{
96+
bits: [4]uint64{
97+
b.bits[0] & other.bits[0],
98+
b.bits[1] & other.bits[1],
99+
b.bits[2] & other.bits[2],
100+
b.bits[3] & other.bits[3],
101+
},
102+
}
103+
}
104+
105+
// Or returns the bitwise OR of two masks.
106+
func (b *Mask) Or(other *Mask) Mask {
107+
return Mask{
108+
bits: [4]uint64{
109+
b.bits[0] | other.bits[0],
110+
b.bits[1] | other.bits[1],
111+
b.bits[2] | other.bits[2],
112+
b.bits[3] | other.bits[3],
113+
},
114+
}
115+
}
116+
117+
// Xor returns the bitwise XOR of two masks.
118+
func (b *Mask) Xor(other *Mask) Mask {
119+
return Mask{
120+
bits: [4]uint64{
121+
b.bits[0] ^ other.bits[0],
122+
b.bits[1] ^ other.bits[1],
123+
b.bits[2] ^ other.bits[2],
124+
b.bits[3] ^ other.bits[3],
125+
},
126+
}
127+
}
128+
129+
// TotalBitsSet returns how many bits are set in this mask.
130+
func (b *Mask) TotalBitsSet() int {
131+
return bits.OnesCount64(b.bits[0]) + bits.OnesCount64(b.bits[1]) + bits.OnesCount64(b.bits[2]) + bits.OnesCount64(b.bits[3])
132+
}
133+
134+
// Equals returns whether two masks are equal.
135+
func (b *Mask) Equals(other *Mask) bool {
136+
return b.bits == other.bits
137+
}

0 commit comments

Comments
 (0)