-
Notifications
You must be signed in to change notification settings - Fork 0
/
mecs.py
463 lines (345 loc) · 19.9 KB
/
mecs.py
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
"""An implementation of the Entity Component System (ECS) paradigm."""
from itertools import repeat as _repeat
__version__ = '1.2.1'
class CommandBuffer():
"""A buffer that stores commands and plays them back later.
*New in version 1.1.*
"""
def __init__(self, scene):
"""Associate the buffer with the provided scene."""
self.scene = scene
self.commands = []
self.lasteid = 0
self.eidmap = {}
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.flush()
def new(self, *comps):
"""Returns an entity id that is only valid to use with the current buffer. If one or more components are supplied to the method, these will be added to the new entity.
*New in version 1.2.*
"""
self.lasteid -= 1
self.commands.append((self.scene.new, (self.lasteid, *comps,)))
return self.lasteid
def add(self, eid, *comps):
"""Add a component to an entity. The component will not be added immediately, but when the buffer is flushed. In particular, exceptions do not occur when calling this method, but only when the buffer is flushed.
*Changed in version 1.2:* Added support for multiple components.
*Deprecated since version 1.2:* Use *set()* instead.
"""
self.commands.append((self.scene.add, (eid, *comps)))
def set(self, eid, *comps):
"""Set components of an entity. The componentes will not be set immediately, but when the buffer is flushed. In particular, exception do not ossur when calling this method, but only when the buffer if flushed.
*New in version 1.2.*
"""
self.commands.append((self.scene.set, (eid, *comps)))
def remove(self, eid, *comptypes):
"""Remove a component from an entity. The component will not be removed immediately, but when the buffer is flushed. In particular, exceptions do not occur when calling this method, but only when the buffer is flushed.
*Changed in version 1.2:* Added support for multiple component types.
"""
self.commands.append((self.scene.remove, (eid, *comptypes)))
def free(self, eid):
"""Remove all components of an entity. The components will not be removed immediately, but when the buffer if flushed. In particular, exceptions do not occur when calling this method, but only when the buffer is flushed."""
self.commands.append((self.scene.free, (eid,)))
def flush(self):
"""Flush the buffer. This will apply all commands that have been previously stored in the buffer to its associated scene. If any arguments in these commands are faulty, exceptions may arrise."""
for cmd, args in self.commands:
if cmd == self.scene.new:
eid, *comps = args
realeid = self.scene.new(*comps)
self.eidmap[eid] = realeid
else:
eid, *other = args
if eid < 0: eid = self.eidmap[eid]
cmd(eid, *other)
self.commands.clear()
class Scene():
"""A scene of entities that allows for efficient component management."""
def __init__(self):
self.entitymap = {} # {eid: (archetype, index)}
self.archetypemap = {} # {component type: set(archetype)}
self.chunkmap = {} # {archetype: ([eid], {component type: [component]})}
self.lasteid = -1 # the last valid entity id
def _removeEntity(self, eid):
"""Internal method to remove an entity. The entity id must be valid and in entitymap, i.e. the entity must have at least one component."""
archetype, index = self.entitymap[eid]
eidlist, comptypemap = self.chunkmap[archetype]
# remove the entity by swapping it with another entity, or ...
if len(eidlist) > 1:
swapid = eidlist[-1]
if swapid != eid: # do not replace with self
self.entitymap[swapid] = (archetype, index)
eidlist[index] = swapid
for complist in comptypemap.values():
swapcomp = complist[-1]
complist[index] = swapcomp
# remove swaped entity
eidlist.pop()
for complist in comptypemap.values():
complist.pop()
else: # ... if the archetype container will be empty after this, remove it
for ct in archetype:
self.archetypemap[ct].remove(archetype)
if not self.archetypemap[ct]:
del self.archetypemap[ct]
del self.chunkmap[archetype]
del self.entitymap[eid]
def _addEntity(self, eid, compdict):
"""Internal method to add an entity. The entity id must be valid and the component list must be non-empty. Also, there must be a maximum of one component of each type."""
archetype = frozenset(compdict.keys())
if archetype in self.chunkmap: # collect unique instance from cache, if possible
archetype = next(iter(x for x in self.chunkmap if x == archetype))
# if there is no container for the new archetype, create one
if archetype not in self.chunkmap:
# add to chunkmap
self.chunkmap[archetype] = ([], {ct: [] for ct in archetype})
# add to archetypemap
for ct in archetype:
if ct not in self.archetypemap:
self.archetypemap[ct] = set()
self.archetypemap[ct].add(archetype)
# add the entity and components to the archetype container
eidlist, comptypemap = self.chunkmap[archetype]
eidlist.append(eid)
for ct, c in compdict.items():
comptypemap[ct].append(c)
# make reference to entity in entitymap
index = len(eidlist) - 1
self.entitymap[eid] = (archetype, index)
def buffer(self):
"""Return a new command buffer that is associated to this scene.
*New in version 1.1.*
*Deprecated since version 1.2:* Use *CommandBuffer(scene)* instead.
"""
return CommandBuffer(self)
def new(self, *comps):
"""Returns a valid and previously unused entity id. If one or more components are supplied to the method, these will be added to the new entity. Raises *ValueError* if trying to add duplicate component types.
*Changed in version 1.2:* Added the optional *comps* parameter.
"""
# increment valid entity id
self.lasteid += 1
# add components
if comps:
compdict = {type(c): c for c in comps}
# raise ValueError on trying to add duplicate component types
if len(compdict) < len(comps):
comptypes = [type(comp) for comp in comps]
raise ValueError(f"adding duplicate component type(s): {', '.join(str(ct) for ct in comptypes if comptypes.count(ct) > 1)}")
self._addEntity(self.lasteid, compdict)
return self.lasteid
def free(self, eid):
"""Remove all components of an entity. The entity id will not be invalidated by this operation. Returns a list of the components. Raises *KeyError* if the entity id is not valid."""
# raise KeyError on invalid entity id
if eid < 0 or eid > self.lasteid:
raise KeyError(f"invalid entity id: {eid}")
# unpack entity
try:
archetype, index = self.entitymap[eid]
_, comptypemap = self.chunkmap[archetype]
except KeyError: # eid not in self.entitymap
return []
# collect the components and remove the entity
components = [comptypemap[comptype][index] for comptype in comptypemap]
self._removeEntity(eid)
return components
def components(self, eid):
"""Returns a tuple of all components of an entity. Raises *KeyError* if the entity id is not valid."""
# raise KeyError on invalid entity id
if eid < 0 or eid > self.lasteid:
raise KeyError(f"invalid entity id: {eid}")
# unpack entity
try:
archetype, index = self.entitymap[eid]
_, comptypemap = self.chunkmap[archetype]
except KeyError: # eid not in self.entitymap
return ()
return tuple(comptypemap[comptype][index] for comptype in comptypemap)
def archetype(self, eid):
"""Returns the archetype of an entity. Raises *KeyError* if the entity id is not valid."""
# raise KeyError on invalid entity id
if eid < 0 or eid > self.lasteid:
raise KeyError(f"invalid entity id: {eid}")
# unpack entity
try:
archetype, _ = self.entitymap[eid]
except KeyError: # eid not in self.entitymap
return ()
return tuple(archetype)
def add(self, eid, *comps):
"""Add components to an entity. Returns the component(s) as a list if two or more components are given, or a single component instance if only one component is given. Raises *KeyError* if the entity id is not valid or *ValueError* if the entity would have one or more components of the same type after this operation or no components are supplied to the method.
*Changed in version 1.2:* Added support for multiple components.
*Deprecated since version 1.2:* Use *set()* instead.
"""
# raise KeyError on invalid entity id
if eid < 0 or eid > self.lasteid:
raise KeyError(f"invalid entity id: {eid}")
# raise ValueError if no component are given
if not comps:
raise ValueError("missing input")
# raise ValueError if trying to add duplicate component types
if len(set(type(comp) for comp in comps)) < len(comps):
comptypes = [type(comp) for comp in comps]
raise ValueError(f"adding duplicate component type(s): {', '.join(str(ct) for ct in comptypes if comptypes.count(ct) > 1)}")
complist = list(comps)
if eid in self.entitymap:
archetype, index = self.entitymap[eid]
_, comptypemap = self.chunkmap[archetype]
# raise ValueError if trying to add component types that are already present
if any(type(comp) in comptypemap for comp in comps):
raise ValueError(f"component type(s) already present: {', '.join(str(type(comp)) for comp in comps if type(comp) in comptypemap)}")
# collect old components and remove the entity
complist.extend(comptypemap[comptype][index] for comptype in comptypemap)
self._removeEntity(eid)
compdict = {type(c): c for c in complist}
self._addEntity(eid, compdict)
if len(comps) == 1:
return comps[0]
else:
return list(comps)
def set(self, eid, *comps):
"""Set components of an entity. Raises *KeyError* if the entity id is not valid or *ValueError* if trying to set two or more components of the same type simultaneously.
*New in version 1.2.*
"""
# raise KeyError on invalid entity id
if eid < 0 or eid > self.lasteid:
raise KeyError(f"invalid entity id: {eid}")
# skip if no components are given
if not comps:
return
# sort components by type
compdict = {type(comp): comp for comp in comps}
# raise ValueError if trying to set duplicate component types
if len(compdict) < len(comps):
comptypes = list(compdict.keys())
raise ValueError(f"duplicate component type(s): {', '.join(str(ct) for ct in comptypes if comptypes.count(ct) > 1)}")
# Modify entity if already presend, else ...
if eid in self.entitymap:
archetype, index = self.entitymap[eid]
_, comptypemap = self.chunkmap[archetype]
oldcompdict = {ct: comptypemap[ct][index] for ct in comptypemap}
# If possible update components directly, else ...
if compdict.keys() <= oldcompdict.keys():
for ct, c in compdict.items():
comptypemap[ct][index] = c
else: # ... move entity in into another chunk.
newcompdict = {**oldcompdict, **compdict}
self._removeEntity(eid)
self._addEntity(eid, newcompdict)
else: # ... add entity.
self._addEntity(eid, compdict)
def has(self, eid, *comptypes):
"""Return *True* if the entity has a component of each of the given types, *False* otherwise. Raises *KeyError* if the entity id is not valid or *ValueError* if no component type is supplied to the method.
*Changed in version 1.2:* Added support for multiple component types.
"""
# raise KeyError on invalid entity id
if eid < 0 or eid > self.lasteid:
raise KeyError(f"invalid entity id: {eid}")
# raise ValueError if no component types are given
if not comptypes:
raise ValueError("missing input")
# unpack entity
try:
archetype, _ = self.entitymap[eid]
_, comptypemap = self.chunkmap[archetype]
except KeyError: # eid not in self.entitymap
return False
return all(ct in comptypemap for ct in comptypes)
def collect(self, eid, *comptypes):
"""Collect multiple components of an entity. Returns a list of the components. Raises *KeyError* if the entity id is not valid or *ValueError* if a component of any of the requested types is missing.
*New in version 1.2.*
"""
# raise KeyError on invalid entity id
if eid < 0 or eid > self.lasteid:
raise KeyError(f"invalid entity id: {eid}")
# return empty list if no components are requested
if not comptypes:
return []
# unpack entity
try:
archetype, index = self.entitymap[eid]
_, comptypemap = self.chunkmap[archetype]
except KeyError: # eid not in self.entitymap
raise ValueError(f"missing component type(s): {', '.join(str(ct) for ct in comptypes)}")
# collect and return components
try:
return [comptypemap[ct][index] for ct in comptypes]
except KeyError: # ct not in comptypemap
raise ValueError(f"missing component type(s): {', '.join(str(ct) for ct in comptypes if ct not in comptypemap)}")
def get(self, eid, comptype):
"""Get one component of an entity. Returns the component. Raises *KeyError* if the entity id is not valid or *ValueError* if the entity does not have a component of the requested type."""
# raise KeyError on invalid entity id
if eid < 0 or eid > self.lasteid:
raise KeyError(f"invalid entity id: {eid}")
# unpack entity
try:
archetype, index = self.entitymap[eid]
_, comptypemap = self.chunkmap[archetype]
except KeyError: # eid not in self.entitymap
raise ValueError(f"missing component type: {str(comptype)}")
# collect and return component
try:
return comptypemap[comptype][index]
except KeyError: # comptype not in comptypemap
raise ValueError(f"missing component type: {str(comptype)}")
def remove(self, eid, *comptypes):
"""Remove components from an entity. Returns a list of the components if two or more component types are given, or a single component instance if only one component type is given. Raises *KeyError* if the entity id is not valid or *ValueError* if the entity does not have a component of any of the given types or if no component types are supplied to the method.
*Changed in version 1.2:* Added support for multiple component types.
"""
# raise KeyError on invalid entity id
if eid < 0 or eid > self.lasteid:
raise KeyError(f"invalid entity id: {eid}")
# raise ValueError if no component types are given
if not comptypes:
raise ValueError("missing input")
# unpack entity
try:
archetype, index = self.entitymap[eid]
_, comptypemap = self.chunkmap[archetype]
except KeyError: # eid not in self.entitymap
raise ValueError(f"missing component type(s): {', '.join(str(ct) for ct in comptypes)}")
# raise ValueError if the entity does not have the requested component types
if not all(ct in comptypemap for ct in comptypes):
raise ValueError(f"missing component type(s): {', '.join(str(ct) for ct in comptypes if ct not in comptypemap)}")
# collect components that will remain on the entity and the ones to be removed
compdict = {ct: comptypemap[ct][index] for ct in comptypemap if ct not in comptypes}
removed = list(comptypemap[ct][index] for ct in comptypes)
# remove the entity and add it back if there are remaining components
self._removeEntity(eid)
if compdict:
self._addEntity(eid, compdict)
if len(removed) == 1:
return removed[0]
else:
return removed
def start(self, *systems, **kwargs):
"""Initialize the scene. All systems must implement an `onStart(scene, **kwargs)` method where this scene instance will be passed as the first argument and the `kwargs` of this method will also be passed on. The systems will be called in the same order they are supplied to this method."""
for system in systems:
system.onStart(self, **kwargs)
def update(self, *systems, **kwargs):
"""Update the scene. All systems must implement an `onUpdate(scene, **kwargs)` method where this scene instance will be passed as the first argument and the `kwargs` of this method will also be passed on. The systems will be called in the same order they are supplied to this method."""
for system in systems:
system.onUpdate(self, **kwargs)
def stop(self, *systems, **kwargs):
"""Clean up the scene. All systems must implement an 'onStop(scene, **kwargs)' method where this scene instance will be passed as the first argument and the `kwargs` of this method will also be passed on. The systems will be called in the same order they are supplied to this method."""
for system in systems:
system.onStop(self, **kwargs)
def select(self, *comptypes, exclude=None):
"""Iterate over entity ids and their corresponding components. Yields tuples of the form `(eid, (compA, compB, ...))` where `compA`, `compB`, ... are of the given component types and belong to the entity with entity id eid. If no component types are given, iterate over all entities. If *exclude* is not *None*, entities with component types listed in *exclude* will not be considered. Raises *ValueError* if *exclude* contains component types that are also explicitly included."""
# raise ValueError if trying to exclude component types that are also included
if exclude and any(ct in exclude for ct in comptypes):
raise ValueError(f"excluding explicitely included component types: {', '.join(str(x) for x in set(comptypes).intersection(exclude))}")
# collect archetypes that should be included and archetypes that should be excluded
incarchetypes = set.intersection(*[self.archetypemap.get(ct, set()) for ct in comptypes]) if comptypes else set(self.chunkmap.keys())
excarchetypes = set.union(*[self.archetypemap.get(ct, set()) for ct in exclude]) if exclude else set()
# iterate over all included archetype that are not excluded
# the iteration is reversed, because this will yield better performance when calling e.g. scene.remove() on the result.
archetypes = incarchetypes - excarchetypes
if comptypes:
for archetype in archetypes:
eidlist, comptypemap = self.chunkmap[archetype]
complists = [reversed(comptypemap[ct]) for ct in comptypes]
yield from zip(reversed(eidlist), zip(*complists))
else:
for archetype in archetypes:
eidlist, _ = self.chunkmap[archetype]
yield from zip(reversed(eidlist), _repeat(()))