tcod-ecs 5.0.0__tar.gz → 5.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tcod_ecs-5.0.0 → tcod_ecs-5.2.0}/LICENSE +1 -1
- {tcod_ecs-5.0.0 → tcod_ecs-5.2.0}/PKG-INFO +82 -61
- {tcod_ecs-5.0.0 → tcod_ecs-5.2.0}/README.md +81 -60
- {tcod_ecs-5.0.0 → tcod_ecs-5.2.0}/pyproject.toml +15 -44
- {tcod_ecs-5.0.0 → tcod_ecs-5.2.0}/tcod/ecs/__init__.py +4 -1
- {tcod_ecs-5.0.0 → tcod_ecs-5.2.0}/tcod/ecs/_converter.py +2 -2
- {tcod_ecs-5.0.0 → tcod_ecs-5.2.0}/tcod/ecs/_version.py +2 -2
- {tcod_ecs-5.0.0 → tcod_ecs-5.2.0}/tcod/ecs/callbacks.py +4 -3
- {tcod_ecs-5.0.0 → tcod_ecs-5.2.0}/tcod/ecs/constants.py +3 -0
- {tcod_ecs-5.0.0 → tcod_ecs-5.2.0}/tcod/ecs/entity.py +163 -143
- {tcod_ecs-5.0.0 → tcod_ecs-5.2.0}/tcod/ecs/query.py +120 -102
- tcod_ecs-5.0.0/tcod/ecs/world.py → tcod_ecs-5.2.0/tcod/ecs/registry.py +30 -27
- {tcod_ecs-5.0.0 → tcod_ecs-5.2.0}/tcod/ecs/typing.py +5 -4
- tcod_ecs-5.2.0/tcod/ecs/world.py +8 -0
- {tcod_ecs-5.0.0 → tcod_ecs-5.2.0}/tcod/ecs/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tcod-ecs
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.2.0
|
|
4
4
|
Summary: A type-hinted Entity Component System based on Python dictionaries and sets.
|
|
5
5
|
Author-email: Kyle Benesch <4b796c65+github@gmail.com>
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -45,7 +45,7 @@ The following features are currently implemented:
|
|
|
45
45
|
- Entities can have one instance of a type, or multiple instances of a type using a hashable tag to differentiate them.
|
|
46
46
|
- Entity relationships are supported, either as many-to-many or many-to-one relationships.
|
|
47
47
|
- ECS Queries can be made to fetch entities having a combination of components/tags/relations or excluding such.
|
|
48
|
-
- The ECS
|
|
48
|
+
- The ECS Registry object can be serialized with Python's pickle module for easy storage.
|
|
49
49
|
|
|
50
50
|
A lightweight version which implements only the entity-component framework exists called [tcod-ec](https://pypi.org/project/tcod-ec/).
|
|
51
51
|
`tcod-ec` was geared towards a dynamic-typed-dict style of syntax and is missing a lot of important features such as queries and named components.
|
|
@@ -53,6 +53,7 @@ A lightweight version which implements only the entity-component framework exist
|
|
|
53
53
|
# Installation
|
|
54
54
|
|
|
55
55
|
Use pip to install this library:
|
|
56
|
+
|
|
56
57
|
```
|
|
57
58
|
pip install tcod-ecs
|
|
58
59
|
```
|
|
@@ -62,57 +63,59 @@ Remove or update `tcod` to fix this issue.
|
|
|
62
63
|
|
|
63
64
|
# Examples
|
|
64
65
|
|
|
65
|
-
##
|
|
66
|
+
## Registry
|
|
67
|
+
|
|
68
|
+
The ECS Registry is used to create and store entities and their components.
|
|
66
69
|
|
|
67
70
|
```py
|
|
68
71
|
>>> import tcod.ecs
|
|
69
|
-
>>>
|
|
72
|
+
>>> registry = tcod.ecs.Registry() # New empty registry
|
|
70
73
|
|
|
71
74
|
```
|
|
72
75
|
|
|
73
76
|
## Entity
|
|
74
77
|
|
|
75
|
-
Each Entity is identified by its unique id (`uid`) which can be any hashable object combined with the `
|
|
76
|
-
New unique entities can be created with `
|
|
77
|
-
An entity always knows about its assigned
|
|
78
|
-
|
|
78
|
+
Each Entity is identified by its unique id (`uid`) which can be any hashable object combined with the `registry` it belongs.
|
|
79
|
+
New unique entities can be created with `Registry.new_entity` which uses a new `object()` as the `uid`, this guarantees uniqueness which is not always desireable.
|
|
80
|
+
An entity always knows about its assigned registry, which can be accessed with the `Entity.registry` property from any Entity instance.
|
|
81
|
+
Registries only know about their entities once the entity is assigned a name, component, tag, or relation.
|
|
79
82
|
|
|
80
83
|
```py
|
|
81
|
-
>>> entity =
|
|
84
|
+
>>> entity = registry.new_entity() # Creates a unique entity using `object()` as the uid
|
|
82
85
|
>>> entity
|
|
83
86
|
<Entity(uid=object at ...)>
|
|
84
|
-
>>> entity.
|
|
87
|
+
>>> entity.registry is registry # Registry can always be accessed from their entity
|
|
85
88
|
True
|
|
86
|
-
>>>
|
|
89
|
+
>>> registry[entity.uid] is entity # Entities with the same registry/uid are compared using `is`
|
|
87
90
|
True
|
|
88
91
|
|
|
89
92
|
# Reference an entity with the given uid, can be any hashable object:
|
|
90
|
-
>>> entity =
|
|
93
|
+
>>> entity = registry["MyEntity"]
|
|
91
94
|
>>> entity
|
|
92
95
|
<Entity(uid='MyEntity')>
|
|
93
|
-
>>>
|
|
96
|
+
>>> registry["MyEntity"] is entity # Matching entities ALWAYS share a single identity
|
|
94
97
|
True
|
|
95
98
|
|
|
96
99
|
```
|
|
97
100
|
|
|
98
|
-
Use `
|
|
99
|
-
`
|
|
101
|
+
Use `Registry.new_entity` to create unique entities and use `Registry[x]` to reference a global entity or relation with an id.
|
|
102
|
+
`registry[None]` is recommend for use as a global entity when you want to store components in the registry itself.
|
|
100
103
|
|
|
101
|
-
Do not save the `uid`'s of entities to be used later with `
|
|
104
|
+
Do not save the `uid`'s of entities to be used later with `registry[uid]`, this process is slower than holding onto the Entity instance.
|
|
102
105
|
|
|
103
106
|
## Serialization
|
|
104
107
|
|
|
105
|
-
|
|
108
|
+
Registries are normal Python objects and can be pickled as long as all stored components are pickleable.
|
|
106
109
|
|
|
107
110
|
```py
|
|
108
111
|
>>> import pickle
|
|
109
|
-
>>> pickled_data: bytes = pickle.dumps(
|
|
110
|
-
>>>
|
|
112
|
+
>>> pickled_data: bytes = pickle.dumps(registry)
|
|
113
|
+
>>> registry = pickle.loads(pickled_data)
|
|
111
114
|
|
|
112
115
|
```
|
|
113
116
|
|
|
114
117
|
Stability is a priority but changes may still break older saves.
|
|
115
|
-
Backwards compatibility is not a priority, pickled
|
|
118
|
+
Backwards compatibility is not a priority, pickled registries should not be unpickled with an older version of the library.
|
|
116
119
|
This project follows [Semantic Versioning](https://semver.org/), major version increments will break the API, the save format or both, minor version increments may break backwards compatibility.
|
|
117
120
|
Check the [changelog](https://github.com/HexDecimal/python-tcod-ecs/blob/main/CHANGELOG.md) to be aware of format changes and breaks.
|
|
118
121
|
There should always be a transition period before a format break, so keeping up with the latest version is a good idea.
|
|
@@ -126,7 +129,7 @@ The types used can be custom classes or standard Python types.
|
|
|
126
129
|
|
|
127
130
|
```py
|
|
128
131
|
>>> import attrs
|
|
129
|
-
>>> entity =
|
|
132
|
+
>>> entity = registry.new_entity()
|
|
130
133
|
>>> entity.components[int] = 42
|
|
131
134
|
>>> entity.components[int]
|
|
132
135
|
42
|
|
@@ -152,20 +155,20 @@ Vector2(x=1, y=2)
|
|
|
152
155
|
>>> entity.components[Vector2]
|
|
153
156
|
Vector2(x=0, y=0)
|
|
154
157
|
|
|
155
|
-
# Queries can be made on all entities of a
|
|
156
|
-
>>> for e in
|
|
158
|
+
# Queries can be made on all entities of a registry with matching components
|
|
159
|
+
>>> for e in registry.Q.all_of(components=[Vector2]):
|
|
157
160
|
... e.components[Vector2].x += 10
|
|
158
161
|
>>> entity.components[Vector2]
|
|
159
162
|
Vector2(x=10, y=0)
|
|
160
163
|
|
|
161
164
|
# You can match components and iterate over them at the same time. This can be combined with the above
|
|
162
|
-
>>> for pos, i in
|
|
165
|
+
>>> for pos, i in registry.Q[Vector2, int]:
|
|
163
166
|
... print((pos, i))
|
|
164
167
|
(Vector2(x=10, y=0), 11)
|
|
165
168
|
|
|
166
169
|
# You can include `Entity` to iterate over entities with their components
|
|
167
170
|
# This always iterates over the entity itself instead of an Entity component
|
|
168
|
-
>>> for e, pos, i in
|
|
171
|
+
>>> for e, pos, i in registry.Q[tcod.ecs.Entity, Vector2, int]:
|
|
169
172
|
... print((e, pos, i))
|
|
170
173
|
(<Entity...>, Vector2(x=10, y=0), 11)
|
|
171
174
|
|
|
@@ -180,7 +183,7 @@ The syntax `[type]` and `[(name, type)]` can be used interchangeably in all plac
|
|
|
180
183
|
Queries on components access named components with the same syntax and must use names explicitly.
|
|
181
184
|
|
|
182
185
|
```py
|
|
183
|
-
>>> entity =
|
|
186
|
+
>>> entity = registry.new_entity()
|
|
184
187
|
>>> entity.components[Vector2] = Vector2(0, 0)
|
|
185
188
|
>>> entity.components[("velocity", Vector2)] = Vector2(1, 1)
|
|
186
189
|
>>> entity.components[("velocity", Vector2)]
|
|
@@ -203,11 +206,11 @@ Vector2(x=1, y=1)
|
|
|
203
206
|
'empty'
|
|
204
207
|
|
|
205
208
|
# Queries can be made on all named components with the same syntax as normal ones
|
|
206
|
-
>>> for e in
|
|
209
|
+
>>> for e in registry.Q.all_of(components=[("hp", int), ("max_hp", int)]):
|
|
207
210
|
... e.components[("hp", int)] = e.components[("max_hp", int)]
|
|
208
211
|
>>> entity.components[("hp", int)]
|
|
209
212
|
12
|
|
210
|
-
>>> for e, pos, delta in
|
|
213
|
+
>>> for e, pos, delta in registry.Q[tcod.ecs.Entity, Vector2, ("velocity", Vector2)]:
|
|
211
214
|
... e.components[Vector2] = Vector2(pos.x + delta.x, pos.y + delta.y)
|
|
212
215
|
>>> entity.components[Vector2]
|
|
213
216
|
Vector2(x=1, y=1)
|
|
@@ -220,67 +223,85 @@ Tags are hashable objects stored in the set-like `Entity.tags`.
|
|
|
220
223
|
These are useful as flags or to group entities together.
|
|
221
224
|
|
|
222
225
|
```py
|
|
223
|
-
>>> entity =
|
|
226
|
+
>>> entity = registry.new_entity()
|
|
224
227
|
>>> entity.tags.add("player") # Works well for groups
|
|
225
228
|
>>> "player" in entity.tags
|
|
226
229
|
True
|
|
227
230
|
>>> entity.tags.add(("eats", "fruit"))
|
|
228
231
|
>>> entity.tags.add(("eats", "meat"))
|
|
229
|
-
>>> set(
|
|
232
|
+
>>> set(registry.Q.all_of(tags=["player"])) == {entity}
|
|
230
233
|
True
|
|
231
234
|
|
|
232
235
|
```
|
|
233
236
|
|
|
234
237
|
## Relations
|
|
235
238
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
Use `
|
|
239
|
+
Entity relations are unidirectional from an origin entity to possibly multiple target entities.
|
|
240
|
+
|
|
241
|
+
- Use `origin.relation_tag[tag] = target` to associate an origins tag exclusively with the target entity.
|
|
242
|
+
This uses standard assignment and is useful for tags which would not make sense with multiple targets.
|
|
243
|
+
Reading `origin.relation_tag[tag]` returns a single target while enforcing the invariant of only having one target.
|
|
244
|
+
- Use `origin.relation_tags_many[tag].add(target)` to associate a tag with multiple targets.
|
|
245
|
+
This supports `set`-like syntax such as adding or removing multiple targets at once.
|
|
246
|
+
This allows for many-to-many relations.
|
|
247
|
+
- Use `origin.relation_components[component_key][target] = component` to associate a target entity with a component.
|
|
248
|
+
This allows storing data along with a relation.
|
|
249
|
+
This supports `dict`-like syntax.
|
|
250
|
+
The `component_key` can be queried like a normal tag.
|
|
251
|
+
|
|
252
|
+
### Relation queries
|
|
253
|
+
|
|
254
|
+
Relations are queried with `registry.Q.all_of(relations=[...])`.
|
|
255
|
+
This expects 2-item or 3-item tuples following these rules:
|
|
239
256
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
257
|
+
- Use `(tag, target)` to match the origin entities with the relation `tag` to `target`.
|
|
258
|
+
- If `tag` is a component key then component relations are also matched.
|
|
259
|
+
This means you should be careful with tags which look like component keys.
|
|
260
|
+
- `target` can be a specific entity. This means only entities relating to that specific entity will be matched.
|
|
261
|
+
- `target` can be query itself. This means only entities relating to a match from the sub-query are matched.
|
|
262
|
+
- `target` can be `...` which means an entity with a relation to any entity is matched.
|
|
263
|
+
- To reverse the direction use a 3-item tuple `(origin, tag, None)`. `origin` can be anything a `target` could be.
|
|
264
|
+
|
|
265
|
+
Relations using sub-queries may be chained together.
|
|
266
|
+
See [Sander Mertens - Why it is time to start thinking of games as databases](https://ajmmertens.medium.com/why-it-is-time-to-start-thinking-of-games-as-databases-e7971da33ac3) to understand the repercussion of this.
|
|
267
|
+
|
|
268
|
+
You can use the following table to help with constructing relation queries:
|
|
269
|
+
|
|
270
|
+
| Matches | Syntax |
|
|
271
|
+
| ------------------------------------------------------------------- | :--------------------------------------: |
|
|
272
|
+
| Origins with a relation `tag` to `target_entity` | `(tag, target_entity)` |
|
|
273
|
+
| Origins with a relation `tag` to any target entity | `(tag, ...)` (Literal dot-dot-dot) |
|
|
274
|
+
| Origins with a relation `tag` to any targets matching a sub-query | `(tag, registry.Q.all_of(...))` |
|
|
275
|
+
| Targets of the relation `tag` from `origin_entity` | `(origin_entity, tag, None)` |
|
|
276
|
+
| Targets of the relation `tag` from any origin entity | `(..., tag, None)` (Literal dot-dot-dot) |
|
|
277
|
+
| Targets of the relation `tag` from any origins matching a sub-query | `(registry.Q.all_of(...), tag, None)` |
|
|
243
278
|
|
|
244
279
|
```py
|
|
245
280
|
>>> @attrs.define
|
|
246
281
|
... class OrbitOf: # OrbitOf component
|
|
247
282
|
... dist: int
|
|
248
283
|
>>> LandedOn = "LandedOn" # LandedOn tag
|
|
249
|
-
>>> star =
|
|
250
|
-
>>> planet =
|
|
251
|
-
>>> moon =
|
|
252
|
-
>>> ship =
|
|
253
|
-
>>> player =
|
|
254
|
-
>>> moon_rock =
|
|
284
|
+
>>> star = registry.new_entity()
|
|
285
|
+
>>> planet = registry.new_entity()
|
|
286
|
+
>>> moon = registry.new_entity()
|
|
287
|
+
>>> ship = registry.new_entity()
|
|
288
|
+
>>> player = registry.new_entity()
|
|
289
|
+
>>> moon_rock = registry.new_entity()
|
|
255
290
|
>>> planet.relation_components[OrbitOf][star] = OrbitOf(dist=1000)
|
|
256
291
|
>>> moon.relation_components[OrbitOf][planet] = OrbitOf(dist=10)
|
|
257
292
|
>>> ship.relation_tag[LandedOn] = moon
|
|
258
293
|
>>> moon_rock.relation_tag[LandedOn] = moon
|
|
259
294
|
>>> player.relation_tag[LandedOn] = moon_rock
|
|
260
|
-
>>> set(
|
|
295
|
+
>>> set(registry.Q.all_of(relations=[(OrbitOf, planet)])) == {moon}
|
|
261
296
|
True
|
|
262
|
-
>>> set(
|
|
297
|
+
>>> set(registry.Q.all_of(relations=[(OrbitOf, ...)])) == {planet, moon} # Get objects in an orbit
|
|
263
298
|
True
|
|
264
|
-
>>> set(
|
|
299
|
+
>>> set(registry.Q.all_of(relations=[(..., OrbitOf, None)])) == {star, planet} # Get objects being orbited
|
|
265
300
|
True
|
|
266
|
-
>>> set(
|
|
301
|
+
>>> set(registry.Q.all_of(relations=[(LandedOn, ...)])) == {ship, moon_rock, player}
|
|
267
302
|
True
|
|
268
|
-
>>> set(
|
|
303
|
+
>>> set(registry.Q.all_of(relations=[(LandedOn, ...)]).none_of(relations=[(LandedOn, moon)])) == {player}
|
|
269
304
|
True
|
|
270
305
|
|
|
271
306
|
```
|
|
272
307
|
|
|
273
|
-
### Relation queries
|
|
274
|
-
|
|
275
|
-
You can use the following table to help with constructing relation queries.
|
|
276
|
-
`tag` is a component key if you are querying for a component relation.
|
|
277
|
-
|
|
278
|
-
| Includes | Syntax |
|
|
279
|
-
| --------------------------------------------------------------------- | :----: |
|
|
280
|
-
| Entities with a relation tag to the given target | `(tag, target_entity)` |
|
|
281
|
-
| Entities with a relation tag to any target | `(tag, ...)` (Literal dot-dot-dot) |
|
|
282
|
-
| Entities with a relation tag to the targets in the given query | `(tag, world.Q.all_of(...))` |
|
|
283
|
-
| The target entities of a relation of a given entity | `(origin_entity, tag, None)` |
|
|
284
|
-
| The target entities of any entity with the given relation tag | `(..., tag, None)` (Literal dot-dot-dot) |
|
|
285
|
-
| The target entities of the queried entities with the given relation | `(tag, world.Q.all_of(...))` |
|
|
286
|
-
|
|
@@ -16,7 +16,7 @@ The following features are currently implemented:
|
|
|
16
16
|
- Entities can have one instance of a type, or multiple instances of a type using a hashable tag to differentiate them.
|
|
17
17
|
- Entity relationships are supported, either as many-to-many or many-to-one relationships.
|
|
18
18
|
- ECS Queries can be made to fetch entities having a combination of components/tags/relations or excluding such.
|
|
19
|
-
- The ECS
|
|
19
|
+
- The ECS Registry object can be serialized with Python's pickle module for easy storage.
|
|
20
20
|
|
|
21
21
|
A lightweight version which implements only the entity-component framework exists called [tcod-ec](https://pypi.org/project/tcod-ec/).
|
|
22
22
|
`tcod-ec` was geared towards a dynamic-typed-dict style of syntax and is missing a lot of important features such as queries and named components.
|
|
@@ -24,6 +24,7 @@ A lightweight version which implements only the entity-component framework exist
|
|
|
24
24
|
# Installation
|
|
25
25
|
|
|
26
26
|
Use pip to install this library:
|
|
27
|
+
|
|
27
28
|
```
|
|
28
29
|
pip install tcod-ecs
|
|
29
30
|
```
|
|
@@ -33,57 +34,59 @@ Remove or update `tcod` to fix this issue.
|
|
|
33
34
|
|
|
34
35
|
# Examples
|
|
35
36
|
|
|
36
|
-
##
|
|
37
|
+
## Registry
|
|
38
|
+
|
|
39
|
+
The ECS Registry is used to create and store entities and their components.
|
|
37
40
|
|
|
38
41
|
```py
|
|
39
42
|
>>> import tcod.ecs
|
|
40
|
-
>>>
|
|
43
|
+
>>> registry = tcod.ecs.Registry() # New empty registry
|
|
41
44
|
|
|
42
45
|
```
|
|
43
46
|
|
|
44
47
|
## Entity
|
|
45
48
|
|
|
46
|
-
Each Entity is identified by its unique id (`uid`) which can be any hashable object combined with the `
|
|
47
|
-
New unique entities can be created with `
|
|
48
|
-
An entity always knows about its assigned
|
|
49
|
-
|
|
49
|
+
Each Entity is identified by its unique id (`uid`) which can be any hashable object combined with the `registry` it belongs.
|
|
50
|
+
New unique entities can be created with `Registry.new_entity` which uses a new `object()` as the `uid`, this guarantees uniqueness which is not always desireable.
|
|
51
|
+
An entity always knows about its assigned registry, which can be accessed with the `Entity.registry` property from any Entity instance.
|
|
52
|
+
Registries only know about their entities once the entity is assigned a name, component, tag, or relation.
|
|
50
53
|
|
|
51
54
|
```py
|
|
52
|
-
>>> entity =
|
|
55
|
+
>>> entity = registry.new_entity() # Creates a unique entity using `object()` as the uid
|
|
53
56
|
>>> entity
|
|
54
57
|
<Entity(uid=object at ...)>
|
|
55
|
-
>>> entity.
|
|
58
|
+
>>> entity.registry is registry # Registry can always be accessed from their entity
|
|
56
59
|
True
|
|
57
|
-
>>>
|
|
60
|
+
>>> registry[entity.uid] is entity # Entities with the same registry/uid are compared using `is`
|
|
58
61
|
True
|
|
59
62
|
|
|
60
63
|
# Reference an entity with the given uid, can be any hashable object:
|
|
61
|
-
>>> entity =
|
|
64
|
+
>>> entity = registry["MyEntity"]
|
|
62
65
|
>>> entity
|
|
63
66
|
<Entity(uid='MyEntity')>
|
|
64
|
-
>>>
|
|
67
|
+
>>> registry["MyEntity"] is entity # Matching entities ALWAYS share a single identity
|
|
65
68
|
True
|
|
66
69
|
|
|
67
70
|
```
|
|
68
71
|
|
|
69
|
-
Use `
|
|
70
|
-
`
|
|
72
|
+
Use `Registry.new_entity` to create unique entities and use `Registry[x]` to reference a global entity or relation with an id.
|
|
73
|
+
`registry[None]` is recommend for use as a global entity when you want to store components in the registry itself.
|
|
71
74
|
|
|
72
|
-
Do not save the `uid`'s of entities to be used later with `
|
|
75
|
+
Do not save the `uid`'s of entities to be used later with `registry[uid]`, this process is slower than holding onto the Entity instance.
|
|
73
76
|
|
|
74
77
|
## Serialization
|
|
75
78
|
|
|
76
|
-
|
|
79
|
+
Registries are normal Python objects and can be pickled as long as all stored components are pickleable.
|
|
77
80
|
|
|
78
81
|
```py
|
|
79
82
|
>>> import pickle
|
|
80
|
-
>>> pickled_data: bytes = pickle.dumps(
|
|
81
|
-
>>>
|
|
83
|
+
>>> pickled_data: bytes = pickle.dumps(registry)
|
|
84
|
+
>>> registry = pickle.loads(pickled_data)
|
|
82
85
|
|
|
83
86
|
```
|
|
84
87
|
|
|
85
88
|
Stability is a priority but changes may still break older saves.
|
|
86
|
-
Backwards compatibility is not a priority, pickled
|
|
89
|
+
Backwards compatibility is not a priority, pickled registries should not be unpickled with an older version of the library.
|
|
87
90
|
This project follows [Semantic Versioning](https://semver.org/), major version increments will break the API, the save format or both, minor version increments may break backwards compatibility.
|
|
88
91
|
Check the [changelog](https://github.com/HexDecimal/python-tcod-ecs/blob/main/CHANGELOG.md) to be aware of format changes and breaks.
|
|
89
92
|
There should always be a transition period before a format break, so keeping up with the latest version is a good idea.
|
|
@@ -97,7 +100,7 @@ The types used can be custom classes or standard Python types.
|
|
|
97
100
|
|
|
98
101
|
```py
|
|
99
102
|
>>> import attrs
|
|
100
|
-
>>> entity =
|
|
103
|
+
>>> entity = registry.new_entity()
|
|
101
104
|
>>> entity.components[int] = 42
|
|
102
105
|
>>> entity.components[int]
|
|
103
106
|
42
|
|
@@ -123,20 +126,20 @@ Vector2(x=1, y=2)
|
|
|
123
126
|
>>> entity.components[Vector2]
|
|
124
127
|
Vector2(x=0, y=0)
|
|
125
128
|
|
|
126
|
-
# Queries can be made on all entities of a
|
|
127
|
-
>>> for e in
|
|
129
|
+
# Queries can be made on all entities of a registry with matching components
|
|
130
|
+
>>> for e in registry.Q.all_of(components=[Vector2]):
|
|
128
131
|
... e.components[Vector2].x += 10
|
|
129
132
|
>>> entity.components[Vector2]
|
|
130
133
|
Vector2(x=10, y=0)
|
|
131
134
|
|
|
132
135
|
# You can match components and iterate over them at the same time. This can be combined with the above
|
|
133
|
-
>>> for pos, i in
|
|
136
|
+
>>> for pos, i in registry.Q[Vector2, int]:
|
|
134
137
|
... print((pos, i))
|
|
135
138
|
(Vector2(x=10, y=0), 11)
|
|
136
139
|
|
|
137
140
|
# You can include `Entity` to iterate over entities with their components
|
|
138
141
|
# This always iterates over the entity itself instead of an Entity component
|
|
139
|
-
>>> for e, pos, i in
|
|
142
|
+
>>> for e, pos, i in registry.Q[tcod.ecs.Entity, Vector2, int]:
|
|
140
143
|
... print((e, pos, i))
|
|
141
144
|
(<Entity...>, Vector2(x=10, y=0), 11)
|
|
142
145
|
|
|
@@ -151,7 +154,7 @@ The syntax `[type]` and `[(name, type)]` can be used interchangeably in all plac
|
|
|
151
154
|
Queries on components access named components with the same syntax and must use names explicitly.
|
|
152
155
|
|
|
153
156
|
```py
|
|
154
|
-
>>> entity =
|
|
157
|
+
>>> entity = registry.new_entity()
|
|
155
158
|
>>> entity.components[Vector2] = Vector2(0, 0)
|
|
156
159
|
>>> entity.components[("velocity", Vector2)] = Vector2(1, 1)
|
|
157
160
|
>>> entity.components[("velocity", Vector2)]
|
|
@@ -174,11 +177,11 @@ Vector2(x=1, y=1)
|
|
|
174
177
|
'empty'
|
|
175
178
|
|
|
176
179
|
# Queries can be made on all named components with the same syntax as normal ones
|
|
177
|
-
>>> for e in
|
|
180
|
+
>>> for e in registry.Q.all_of(components=[("hp", int), ("max_hp", int)]):
|
|
178
181
|
... e.components[("hp", int)] = e.components[("max_hp", int)]
|
|
179
182
|
>>> entity.components[("hp", int)]
|
|
180
183
|
12
|
|
181
|
-
>>> for e, pos, delta in
|
|
184
|
+
>>> for e, pos, delta in registry.Q[tcod.ecs.Entity, Vector2, ("velocity", Vector2)]:
|
|
182
185
|
... e.components[Vector2] = Vector2(pos.x + delta.x, pos.y + delta.y)
|
|
183
186
|
>>> entity.components[Vector2]
|
|
184
187
|
Vector2(x=1, y=1)
|
|
@@ -191,66 +194,84 @@ Tags are hashable objects stored in the set-like `Entity.tags`.
|
|
|
191
194
|
These are useful as flags or to group entities together.
|
|
192
195
|
|
|
193
196
|
```py
|
|
194
|
-
>>> entity =
|
|
197
|
+
>>> entity = registry.new_entity()
|
|
195
198
|
>>> entity.tags.add("player") # Works well for groups
|
|
196
199
|
>>> "player" in entity.tags
|
|
197
200
|
True
|
|
198
201
|
>>> entity.tags.add(("eats", "fruit"))
|
|
199
202
|
>>> entity.tags.add(("eats", "meat"))
|
|
200
|
-
>>> set(
|
|
203
|
+
>>> set(registry.Q.all_of(tags=["player"])) == {entity}
|
|
201
204
|
True
|
|
202
205
|
|
|
203
206
|
```
|
|
204
207
|
|
|
205
208
|
## Relations
|
|
206
209
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
Use `
|
|
210
|
+
Entity relations are unidirectional from an origin entity to possibly multiple target entities.
|
|
211
|
+
|
|
212
|
+
- Use `origin.relation_tag[tag] = target` to associate an origins tag exclusively with the target entity.
|
|
213
|
+
This uses standard assignment and is useful for tags which would not make sense with multiple targets.
|
|
214
|
+
Reading `origin.relation_tag[tag]` returns a single target while enforcing the invariant of only having one target.
|
|
215
|
+
- Use `origin.relation_tags_many[tag].add(target)` to associate a tag with multiple targets.
|
|
216
|
+
This supports `set`-like syntax such as adding or removing multiple targets at once.
|
|
217
|
+
This allows for many-to-many relations.
|
|
218
|
+
- Use `origin.relation_components[component_key][target] = component` to associate a target entity with a component.
|
|
219
|
+
This allows storing data along with a relation.
|
|
220
|
+
This supports `dict`-like syntax.
|
|
221
|
+
The `component_key` can be queried like a normal tag.
|
|
222
|
+
|
|
223
|
+
### Relation queries
|
|
224
|
+
|
|
225
|
+
Relations are queried with `registry.Q.all_of(relations=[...])`.
|
|
226
|
+
This expects 2-item or 3-item tuples following these rules:
|
|
210
227
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
228
|
+
- Use `(tag, target)` to match the origin entities with the relation `tag` to `target`.
|
|
229
|
+
- If `tag` is a component key then component relations are also matched.
|
|
230
|
+
This means you should be careful with tags which look like component keys.
|
|
231
|
+
- `target` can be a specific entity. This means only entities relating to that specific entity will be matched.
|
|
232
|
+
- `target` can be query itself. This means only entities relating to a match from the sub-query are matched.
|
|
233
|
+
- `target` can be `...` which means an entity with a relation to any entity is matched.
|
|
234
|
+
- To reverse the direction use a 3-item tuple `(origin, tag, None)`. `origin` can be anything a `target` could be.
|
|
235
|
+
|
|
236
|
+
Relations using sub-queries may be chained together.
|
|
237
|
+
See [Sander Mertens - Why it is time to start thinking of games as databases](https://ajmmertens.medium.com/why-it-is-time-to-start-thinking-of-games-as-databases-e7971da33ac3) to understand the repercussion of this.
|
|
238
|
+
|
|
239
|
+
You can use the following table to help with constructing relation queries:
|
|
240
|
+
|
|
241
|
+
| Matches | Syntax |
|
|
242
|
+
| ------------------------------------------------------------------- | :--------------------------------------: |
|
|
243
|
+
| Origins with a relation `tag` to `target_entity` | `(tag, target_entity)` |
|
|
244
|
+
| Origins with a relation `tag` to any target entity | `(tag, ...)` (Literal dot-dot-dot) |
|
|
245
|
+
| Origins with a relation `tag` to any targets matching a sub-query | `(tag, registry.Q.all_of(...))` |
|
|
246
|
+
| Targets of the relation `tag` from `origin_entity` | `(origin_entity, tag, None)` |
|
|
247
|
+
| Targets of the relation `tag` from any origin entity | `(..., tag, None)` (Literal dot-dot-dot) |
|
|
248
|
+
| Targets of the relation `tag` from any origins matching a sub-query | `(registry.Q.all_of(...), tag, None)` |
|
|
214
249
|
|
|
215
250
|
```py
|
|
216
251
|
>>> @attrs.define
|
|
217
252
|
... class OrbitOf: # OrbitOf component
|
|
218
253
|
... dist: int
|
|
219
254
|
>>> LandedOn = "LandedOn" # LandedOn tag
|
|
220
|
-
>>> star =
|
|
221
|
-
>>> planet =
|
|
222
|
-
>>> moon =
|
|
223
|
-
>>> ship =
|
|
224
|
-
>>> player =
|
|
225
|
-
>>> moon_rock =
|
|
255
|
+
>>> star = registry.new_entity()
|
|
256
|
+
>>> planet = registry.new_entity()
|
|
257
|
+
>>> moon = registry.new_entity()
|
|
258
|
+
>>> ship = registry.new_entity()
|
|
259
|
+
>>> player = registry.new_entity()
|
|
260
|
+
>>> moon_rock = registry.new_entity()
|
|
226
261
|
>>> planet.relation_components[OrbitOf][star] = OrbitOf(dist=1000)
|
|
227
262
|
>>> moon.relation_components[OrbitOf][planet] = OrbitOf(dist=10)
|
|
228
263
|
>>> ship.relation_tag[LandedOn] = moon
|
|
229
264
|
>>> moon_rock.relation_tag[LandedOn] = moon
|
|
230
265
|
>>> player.relation_tag[LandedOn] = moon_rock
|
|
231
|
-
>>> set(
|
|
266
|
+
>>> set(registry.Q.all_of(relations=[(OrbitOf, planet)])) == {moon}
|
|
232
267
|
True
|
|
233
|
-
>>> set(
|
|
268
|
+
>>> set(registry.Q.all_of(relations=[(OrbitOf, ...)])) == {planet, moon} # Get objects in an orbit
|
|
234
269
|
True
|
|
235
|
-
>>> set(
|
|
270
|
+
>>> set(registry.Q.all_of(relations=[(..., OrbitOf, None)])) == {star, planet} # Get objects being orbited
|
|
236
271
|
True
|
|
237
|
-
>>> set(
|
|
272
|
+
>>> set(registry.Q.all_of(relations=[(LandedOn, ...)])) == {ship, moon_rock, player}
|
|
238
273
|
True
|
|
239
|
-
>>> set(
|
|
274
|
+
>>> set(registry.Q.all_of(relations=[(LandedOn, ...)]).none_of(relations=[(LandedOn, moon)])) == {player}
|
|
240
275
|
True
|
|
241
276
|
|
|
242
277
|
```
|
|
243
|
-
|
|
244
|
-
### Relation queries
|
|
245
|
-
|
|
246
|
-
You can use the following table to help with constructing relation queries.
|
|
247
|
-
`tag` is a component key if you are querying for a component relation.
|
|
248
|
-
|
|
249
|
-
| Includes | Syntax |
|
|
250
|
-
| --------------------------------------------------------------------- | :----: |
|
|
251
|
-
| Entities with a relation tag to the given target | `(tag, target_entity)` |
|
|
252
|
-
| Entities with a relation tag to any target | `(tag, ...)` (Literal dot-dot-dot) |
|
|
253
|
-
| Entities with a relation tag to the targets in the given query | `(tag, world.Q.all_of(...))` |
|
|
254
|
-
| The target entities of a relation of a given entity | `(origin_entity, tag, None)` |
|
|
255
|
-
| The target entities of any entity with the given relation tag | `(..., tag, None)` (Literal dot-dot-dot) |
|
|
256
|
-
| The target entities of the queried entities with the given relation | `(tag, world.Q.all_of(...))` |
|
|
@@ -46,16 +46,6 @@ Changelog = "https://github.com/HexDecimal/python-tcod-ecs/blob/main/CHANGELOG.m
|
|
|
46
46
|
Documentation = "https://python-tcod-ecs.readthedocs.io"
|
|
47
47
|
Source = "https://github.com/HexDecimal/python-tcod-ecs"
|
|
48
48
|
|
|
49
|
-
[tool.black] # https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#configuration-via-a-file
|
|
50
|
-
target-version = ["py38"]
|
|
51
|
-
line-length = 120
|
|
52
|
-
|
|
53
|
-
[tool.isort] # https://pycqa.github.io/isort/docs/configuration/options.html
|
|
54
|
-
py_version = "38"
|
|
55
|
-
line_length = 120
|
|
56
|
-
profile = "black"
|
|
57
|
-
skip_gitignore = true
|
|
58
|
-
|
|
59
49
|
[tool.mypy] # https://mypy.readthedocs.io/en/stable/config_file.html
|
|
60
50
|
files = "."
|
|
61
51
|
exclude = ['^build/', '^\.']
|
|
@@ -86,42 +76,23 @@ testpaths = ["."]
|
|
|
86
76
|
exclude_lines = ['^\s*\.\.\.', "if TYPE_CHECKING:", "# pragma: no cover"]
|
|
87
77
|
|
|
88
78
|
[tool.ruff]
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
"W", # pycodestyle
|
|
94
|
-
"F", # Pyflakes
|
|
95
|
-
"I", # isort
|
|
96
|
-
"UP", # pyupgrade
|
|
97
|
-
"YTT", # flake8-2020
|
|
98
|
-
"ANN", # flake8-annotations
|
|
99
|
-
"S", # flake8-bandit
|
|
100
|
-
"B", # flake8-bugbear
|
|
101
|
-
"C4", # flake8-comprehensions
|
|
102
|
-
"DTZ", # flake8-datetimez
|
|
103
|
-
"EM", # flake8-errmsg
|
|
104
|
-
"EXE", # flake8-executable
|
|
105
|
-
"RET", # flake8-return
|
|
106
|
-
"ICN", # flake8-import-conventions
|
|
107
|
-
"PIE", # flake8-pie
|
|
108
|
-
"PT", # flake8-pytest-style
|
|
109
|
-
"SIM", # flake8-simplify
|
|
110
|
-
"PTH", # flake8-use-pathlib
|
|
111
|
-
"PL", # Pylint
|
|
112
|
-
"TRY", # tryceratops
|
|
113
|
-
"RUF", # NumPy-specific rules
|
|
114
|
-
"G", # flake8-logging-format
|
|
115
|
-
"D", # pydocstyle
|
|
116
|
-
]
|
|
79
|
+
line-length = 120
|
|
80
|
+
|
|
81
|
+
[tool.ruff.lint] # https://docs.astral.sh/ruff/rules/
|
|
82
|
+
select = ["ALL"]
|
|
117
83
|
ignore = [
|
|
118
|
-
"E501", # line-too-long
|
|
119
|
-
"S101", # assert
|
|
120
84
|
"ANN101", # missing-type-self
|
|
121
85
|
"ANN102", # missing-type-cls
|
|
86
|
+
"COM", # flake8-commas, handled by formatter
|
|
87
|
+
"E501", # line-too-long
|
|
88
|
+
"S101", # assert
|
|
89
|
+
"SLF001", # private-member-access
|
|
90
|
+
"T10", # flake8-debugger
|
|
91
|
+
"T20", # flake8-print
|
|
122
92
|
]
|
|
123
|
-
line-length = 120
|
|
124
93
|
|
|
125
|
-
[tool.ruff.
|
|
126
|
-
|
|
127
|
-
|
|
94
|
+
[tool.ruff.lint.isort]
|
|
95
|
+
required-imports = ["from __future__ import annotations"]
|
|
96
|
+
|
|
97
|
+
[tool.ruff.lint.pydocstyle]
|
|
98
|
+
convention = "google" # Use Google-style docstrings.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""A type-hinted Entity Component System based on Python dictionaries and sets."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import importlib.metadata
|
|
@@ -7,12 +8,14 @@ from typing import TypeVar
|
|
|
7
8
|
|
|
8
9
|
from tcod.ecs.constants import IsA
|
|
9
10
|
from tcod.ecs.entity import Entity
|
|
10
|
-
from tcod.ecs.
|
|
11
|
+
from tcod.ecs.registry import Registry
|
|
12
|
+
from tcod.ecs.registry import Registry as World
|
|
11
13
|
|
|
12
14
|
__all__ = (
|
|
13
15
|
"__version__",
|
|
14
16
|
"Entity",
|
|
15
17
|
"IsA",
|
|
18
|
+
"Registry",
|
|
16
19
|
"World",
|
|
17
20
|
)
|
|
18
21
|
|