-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathentities.nim
180 lines (137 loc) · 7.07 KB
/
entities.nim
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
## This module contains ECS (Entity-Component-System) implementation.
import tables
export tables
import ./logging
# uint doesn't check boundaries, thus int; set[int32] won't compile, thus int16
type Entity* = int16 ## Entity is just an int which may have components of any type. Zero entity is reserved as "not initialized". Please be careful with threading: all threads share the same entities registry, so if you create an entity in one thread, it will be visible in another thread. However, components aren't shared between threads, so you can't create a component in one thread and access it in another thread (which is not recommended anyway).
var entities: set[Entity]
# ---- Entity ----
proc isInitialized*(self: Entity): bool =
## Checks whether entity was initialized using `newEntity()`.
self != 0
proc newEntity*(): Entity =
## Return new Entity or raise error if limit exceeded
result = low(Entity)
while result in entities or not result.isInitialized:
result += 1 # TODO: pretty dumb, use random or `lastUsedID` instead
entities.incl(result) # add entity to global entities registry
debug "created new entity", result
iterator iterEntities*(): Entity =
for entity in entities:
yield entity
proc getNumEntities*(): int =
entities.len
# ---- Components ----
# Here goes a hacky code.
# Entity may have as much components as desired. For each type (component) there is a separate table ``Table[Entity, <component type>]``. This table is created automatically by compiler using {.global.} pragma.
# However, we want to delete all components when Entity is deleted. Since components tables are created automatically, we don't have a list of tables to delete from. The only way to know which tables require deletion of components is to create a component destructor for that specific table when it is initialised, and add each destructor to a sequence.
# When Entity is deleted, call all destructors from that sequence.
# The idea is quite simple, but ``{.global.}`` variables have a bit complicated behaviour. In order to make it working we need to make destructors sequence ``{.global.}`` too.
var seenTables {.threadvar.}: seq[pointer]
var destructors {.threadvar.}: seq[proc(entity: Entity)]
proc getComponents*(T: typedesc): var Table[Entity, T] =
## Returns a table of components of specific type ``T`` (``Table[Entity, T]``)
var table {.global, threadvar.}: Table[Entity, T] # https://github.com/nim-lang/Nim/issues/17552
# That's what all nim is about: there is some feature like {.global.} which is documented
# and should work, but after some version update you find out it doesn't; after some googling
# you find some post on nim forum which says that this feature works but only for primitive types,
# and for complex types it works ONLY if you split declaration and initialization. Previously
# you had some additional logic executed only once during initialization, but now you can't do
# it. You start googling how to create your subtype with custom initialization and guess what?
# You cannot! Cause in nim there are no "constructors", everyone can initialize whatever he wants
# in whatever way he wants. So you end up having a global variable of "seen", or "initialized" tables,
# so that when you get a new one, you execute that "only once" logic. And you pray, you pray that
# it will work at least a year until Araq decides to change something again, I dunno, implement
# an 11th garbage collecting algo or some another cool feature like
# "proc ~(*)@$@ from nullptr by nilvar as ptrref {.fuckoff:[@wat].}". I just wanna write code
# and be sure that the code does what the documentation says! But that bug is 2yo now.
let tableAddr = addr(table)
if tableAddr notin seenTables:
seenTables.add(tableAddr)
destructors.add(
proc(entity: Entity) = entity.del(T)
)
return table
proc delete*(entity: Entity) =
## Delete the Entity and all its components. Each component will be deleted as well.
for destructor in destructors:
destructor(entity)
if entity notin entities:
warn "deleting non-existent entity", entity
entities.excl(entity) # will not alert if entity does not exist
proc flush*() =
## Removes all entities
for entity in entities:
entity.delete()
# ---- CRUD for components ----
template has*(entity: Entity, T: typedesc): bool =
## Checks whether ``entity`` has component of type ``T``
assert entity.isInitialized, "Entity is not initialized, possibly forgot to call `newEntity()`"
getComponents(T).hasKey(entity)
template del*(entity: Entity, T: typedesc) =
## Deletes component ``T`` from ``entity``, or does nothing if ``entity`` doesn't have such a component
assert entity.isInitialized, "Entity is not initialized, possibly forgot to call `newEntity()`"
getComponents(T).del(entity)
template `[]`*(entity: Entity, T: typedesc): var typed =
## Returns ``T`` component for ``entity``. Make sure the component exists before retrieving it.
assert entity.isInitialized, "Entity is not initialized, possibly forgot to call `newEntity()`"
tables.`[]`(getComponents(T), entity)
template `[]=`*(entity: Entity, T: typedesc, value: T) =
## Attaches new component ``T`` to an ``entity``. Previous component (if exists) will be deleted.
assert entity.isInitialized, "Entity is not initialized, possibly forgot to call `newEntity()`"
entity.del(T)
tables.`[]=`(getComponents(T), entity, value) # N.B. non-ref ``value`` is copied!
when isMainModule:
import unittest
import c4/threads
type
PhysicsComponent = object
value: int
suite "Entities tests":
test "Undefined entity":
expect AssertionDefect:
var entity: Entity
entity[PhysicsComponent] = PhysicsComponent(value: 5)
test "Basic usage":
let
entity1 = newEntity()
entity2 = newEntity()
entity1[PhysicsComponent] = PhysicsComponent(value: 3)
let componentTableAddr1 = getComponents(PhysicsComponent).addr
entity2[PhysicsComponent] = PhysicsComponent(value: 10)
let componentTableAddr2 = getComponents(PhysicsComponent).addr
check:
entity1[PhysicsComponent].value == 3
componentTableAddr1 == componentTableAddr2
entity1.del(PhysicsComponent)
check:
not entity1.has(PhysicsComponent)
entity1.delete()
check:
entity1 notin entities
entity2.del(PhysicsComponent)
entity2.delete()
test "Auto-destruction of components":
let entity = newEntity()
entity[PhysicsComponent] = PhysicsComponent()
entity.delete()
check:
not entity.has(PhysicsComponent)
test "Multithreading support":
discard newEntity()
spawnThread ThreadID(1):
let entity2 = newEntity()
entity2[PhysicsComponent] = PhysicsComponent(value: 5)
check:
getNumEntities() == 2
joinActiveThreads()
spawnThread ThreadID(2):
let entity3 = newEntity()
entity3[int] = 3
let entity4 = newEntity()
entity4[string] = "name"
check:
getNumEntities() == 4
joinActiveThreads()
check:
getNumEntities() == 4