esper 3.4__tar.gz → 3.7__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.
- {esper-3.4 → esper-3.7}/.github/workflows/unit-tests.yml +3 -3
- {esper-3.4 → esper-3.7}/PKG-INFO +4 -3
- {esper-3.4 → esper-3.7}/README.md +1 -1
- {esper-3.4 → esper-3.7}/RELEASE_NOTES +41 -0
- {esper-3.4 → esper-3.7}/docs/index.rst +1 -0
- {esper-3.4 → esper-3.7}/esper/__init__.py +262 -171
- {esper-3.4 → esper-3.7}/examples/benchmark.py +109 -0
- {esper-3.4 → esper-3.7}/examples/pythonista_ios_example.py +14 -14
- {esper-3.4 → esper-3.7}/tests/test_world.py +196 -3
- {esper-3.4 → esper-3.7}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {esper-3.4 → esper-3.7}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {esper-3.4 → esper-3.7}/.github/ISSUE_TEMPLATE/question-or-comment.md +0 -0
- {esper-3.4 → esper-3.7}/.github/workflows/type-checking.yml +0 -0
- {esper-3.4 → esper-3.7}/.gitignore +0 -0
- {esper-3.4 → esper-3.7}/.mypy.ini +0 -0
- {esper-3.4 → esper-3.7}/.readthedocs.yaml +0 -0
- {esper-3.4 → esper-3.7}/.ruff.toml +0 -0
- {esper-3.4 → esper-3.7}/LICENSE +0 -0
- {esper-3.4 → esper-3.7}/MANIFEST.in +0 -0
- {esper-3.4 → esper-3.7}/docs/Makefile +0 -0
- {esper-3.4 → esper-3.7}/docs/conf.py +0 -0
- {esper-3.4 → esper-3.7}/docs/make.bat +0 -0
- {esper-3.4 → esper-3.7}/esper/py.typed +0 -0
- {esper-3.4 → esper-3.7}/examples/benchmark_cache.py +0 -0
- {esper-3.4 → esper-3.7}/examples/bluesquare.png +0 -0
- {esper-3.4 → esper-3.7}/examples/headless_example.py +0 -0
- {esper-3.4 → esper-3.7}/examples/pygame_example.py +0 -0
- {esper-3.4 → esper-3.7}/examples/pyglet_example.py +0 -0
- {esper-3.4 → esper-3.7}/examples/pysdl2_example.py +0 -0
- {esper-3.4 → esper-3.7}/examples/redsquare.png +0 -0
- {esper-3.4 → esper-3.7}/make.py +0 -0
- {esper-3.4 → esper-3.7}/pyproject.toml +0 -0
- {esper-3.4 → esper-3.7}/tests/__init__.py +0 -0
|
@@ -11,12 +11,12 @@ jobs:
|
|
|
11
11
|
strategy:
|
|
12
12
|
matrix:
|
|
13
13
|
os: [ 'ubuntu-latest', 'macos-latest', 'windows-latest' ]
|
|
14
|
-
python-version: [ '3.
|
|
14
|
+
python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13', '3.14', 'pypy-3.10' ]
|
|
15
15
|
steps:
|
|
16
16
|
- name: Python ${{ matrix.python-version }} ${{ matrix.os }}
|
|
17
|
-
uses: actions/checkout@
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
18
|
- name: Set up Python ${{ matrix.python-version }}
|
|
19
|
-
uses: actions/setup-python@
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
20
|
with:
|
|
21
21
|
python-version: ${{ matrix.python-version }}
|
|
22
22
|
- name: Install test dependencies
|
{esper-3.4 → esper-3.7}/PKG-INFO
RENAMED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: esper
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.7
|
|
4
4
|
Summary: esper is a lightweight Entity System (ECS) for Python, with a focus on performance
|
|
5
5
|
Author-email: Benjamin Moran <benmoran@protonmail.com>
|
|
6
6
|
Requires-Python: >=3.8
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
License-File: LICENSE
|
|
9
10
|
Project-URL: Home, https://github.com/benmoran56/esper
|
|
10
11
|
|
|
11
12
|
[](https://pypi.python.org/pypi/esper)
|
|
@@ -253,7 +254,7 @@ remove Components inside your Processors. The following functions are available
|
|
|
253
254
|
* esper.remove_component(entity_id, ComponentClass)
|
|
254
255
|
|
|
255
256
|
As an example of this, you could have a "Blink" component with a `duration` attribute. This can be used
|
|
256
|
-
to make certain things blink for
|
|
257
|
+
to make certain things blink for a specific period of time, then disappear. For example, the code below
|
|
257
258
|
shows a simplified case of adding this Component to an Entity when it takes damage in one processor. A
|
|
258
259
|
dedicated `BlinkProcessor` handles the effect, and then removes the Component after the duration expires::
|
|
259
260
|
|
|
@@ -243,7 +243,7 @@ remove Components inside your Processors. The following functions are available
|
|
|
243
243
|
* esper.remove_component(entity_id, ComponentClass)
|
|
244
244
|
|
|
245
245
|
As an example of this, you could have a "Blink" component with a `duration` attribute. This can be used
|
|
246
|
-
to make certain things blink for
|
|
246
|
+
to make certain things blink for a specific period of time, then disappear. For example, the code below
|
|
247
247
|
shows a simplified case of adding this Component to an Entity when it takes damage in one processor. A
|
|
248
248
|
dedicated `BlinkProcessor` handles the effect, and then removes the Component after the duration expires::
|
|
249
249
|
|
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
esper 3.7
|
|
2
|
+
=========
|
|
3
|
+
Maintenance release
|
|
4
|
+
|
|
5
|
+
Changes
|
|
6
|
+
-------
|
|
7
|
+
- Add a new `get_entities` method that returns a generator over all entity IDs.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
esper 3.6
|
|
11
|
+
=========
|
|
12
|
+
Maintenance release
|
|
13
|
+
|
|
14
|
+
Changes
|
|
15
|
+
-------
|
|
16
|
+
- Add a new `try_remove_component` function as a convenience instead of (has_component + remove_component).
|
|
17
|
+
- Start migrating type hints to the modern syntax.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
esper 3.5
|
|
21
|
+
=========
|
|
22
|
+
Performance-focused release with major internal optimizations.
|
|
23
|
+
|
|
24
|
+
Changes
|
|
25
|
+
-------
|
|
26
|
+
- Optimized `get_components` to iterate over the smallest set of entities, drastically speeding up queries with rare components.
|
|
27
|
+
- Implemented lazy cache invalidation to reduce overhead from frequent entity modifications.
|
|
28
|
+
- Sped up `get_processor` and `remove_processor` to be constant time (O(1)) operations.
|
|
29
|
+
- Minor performance improvements to entity creation and deletion routines.
|
|
30
|
+
- Expanded Benchmarks.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
esper 3.4
|
|
34
|
+
=========
|
|
35
|
+
Maintenance release
|
|
36
|
+
|
|
37
|
+
Changes
|
|
38
|
+
-------
|
|
39
|
+
- `esper.remove_handler` should use a reference instead of direct object comparison. (#107)
|
|
40
|
+
|
|
41
|
+
|
|
1
42
|
esper 3.3
|
|
2
43
|
=========
|
|
3
44
|
Maintenance release
|
|
@@ -49,6 +49,7 @@ The World context
|
|
|
49
49
|
.. autofunction:: esper.components_for_entity
|
|
50
50
|
.. autofunction:: esper.add_component
|
|
51
51
|
.. autofunction:: esper.remove_component
|
|
52
|
+
.. autofunction:: esper.try_remove_component
|
|
52
53
|
.. autofunction:: esper.get_component
|
|
53
54
|
.. autofunction:: esper.get_components
|
|
54
55
|
.. autofunction:: esper.has_component
|
|
@@ -2,28 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
More information is available at https://github.com/benmoran56/esper
|
|
4
4
|
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
5
7
|
import time as _time
|
|
6
8
|
|
|
7
9
|
from types import MethodType as _MethodType
|
|
8
10
|
|
|
9
11
|
from typing import Any as _Any
|
|
10
12
|
from typing import Callable as _Callable
|
|
11
|
-
from typing import
|
|
12
|
-
from typing import List as _List
|
|
13
|
-
from typing import Set as _Set
|
|
14
|
-
from typing import Type as _Type
|
|
15
|
-
from typing import Tuple as _Tuple
|
|
13
|
+
from typing import Generator as _Generator
|
|
16
14
|
from typing import TypeVar as _TypeVar
|
|
17
15
|
from typing import Iterable as _Iterable
|
|
18
|
-
from typing import Optional as _Optional
|
|
19
16
|
from typing import overload as _overload
|
|
20
17
|
|
|
21
18
|
from weakref import ref as _ref
|
|
22
19
|
from weakref import WeakMethod as _WeakMethod
|
|
23
20
|
|
|
21
|
+
from math import inf
|
|
22
|
+
|
|
24
23
|
from itertools import count as _count
|
|
25
24
|
|
|
26
|
-
__version__ = version = '3.
|
|
25
|
+
__version__ = version = '3.7'
|
|
27
26
|
|
|
28
27
|
|
|
29
28
|
###################
|
|
@@ -104,18 +103,19 @@ _C4 = _TypeVar('_C4')
|
|
|
104
103
|
class Processor:
|
|
105
104
|
"""Base class for all Processors to inherit from.
|
|
106
105
|
|
|
107
|
-
Processor instances must
|
|
108
|
-
free to define the class any way you wish. Processors should be
|
|
109
|
-
and then added to the current World context by calling
|
|
110
|
-
For example::
|
|
106
|
+
Processor instances must define a :py:meth:`process` method, but you are
|
|
107
|
+
otherwise free to define the class any way you wish. Processors should be
|
|
108
|
+
instantiated, and then added to the current World context by calling
|
|
109
|
+
:py:func:`esper.add_processor`. For example::
|
|
111
110
|
|
|
112
111
|
my_processor_instance = MyProcessor()
|
|
113
112
|
esper.add_processor(my_processor_instance)
|
|
114
113
|
|
|
115
|
-
|
|
116
|
-
:py:meth:`esper.Processor.process` methods called by a single call
|
|
117
|
-
:py:func:`esper.process`. Inside the
|
|
118
|
-
should iterate over Entities with one (or more) calls to the
|
|
114
|
+
All the Processors that have been added to the World context will have
|
|
115
|
+
their :py:meth:`esper.Processor.process` methods called by a single call
|
|
116
|
+
to :py:func:`esper.process`. Inside the :py:meth:`process` method is
|
|
117
|
+
where you should iterate over Entities with one (or more) calls to the
|
|
118
|
+
appropriate esper functions::
|
|
119
119
|
|
|
120
120
|
def process(self):
|
|
121
121
|
for ent, (rend, vel) in esper.get_components(Renderable, Velocity):
|
|
@@ -128,46 +128,124 @@ class Processor:
|
|
|
128
128
|
raise NotImplementedError
|
|
129
129
|
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
#
|
|
133
|
-
|
|
131
|
+
###########################################
|
|
132
|
+
# World Context template and functions
|
|
133
|
+
###########################################
|
|
134
134
|
|
|
135
|
+
# Global variables that contain the state of the current world context:
|
|
135
136
|
_current_world: str = "default"
|
|
136
|
-
_entity_count:
|
|
137
|
-
_components:
|
|
138
|
-
_entities:
|
|
139
|
-
_dead_entities:
|
|
140
|
-
_get_component_cache:
|
|
141
|
-
_get_components_cache:
|
|
142
|
-
_processors:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
_context_map
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
]
|
|
137
|
+
_entity_count: _count[int] = _count(start=1)
|
|
138
|
+
_components: dict[type[_Any], set[_Any]] = {}
|
|
139
|
+
_entities: dict[int, dict[type[_Any], _Any]] = {}
|
|
140
|
+
_dead_entities: set[int] = set()
|
|
141
|
+
_get_component_cache: dict[type[_Any], list[_Any]] = {}
|
|
142
|
+
_get_components_cache: dict[tuple[type[_Any], ...], list[_Any]] = {}
|
|
143
|
+
_processors: list[Processor] = []
|
|
144
|
+
_processors_dict: dict[type[Processor], Processor] = {}
|
|
145
|
+
_cache_dirty: bool = False
|
|
146
|
+
event_registry: dict[str, _Any] = {}
|
|
147
|
+
process_times: dict[str, int] = {}
|
|
148
|
+
# 'public' alias that gets set when switching worlds:
|
|
149
|
+
current_world: str = _current_world
|
|
150
|
+
|
|
151
|
+
# The _context_map holds all variables for the active & inactive contexts.
|
|
152
|
+
# We initialize it with default values::
|
|
153
|
+
_context_map: dict[str, tuple[
|
|
154
|
+
_count[int],
|
|
155
|
+
dict[type[_Any], set[_Any]],
|
|
156
|
+
dict[int, dict[type[_Any], _Any]],
|
|
157
|
+
set[int],
|
|
158
|
+
dict[type[_Any], list[_Any]],
|
|
159
|
+
dict[tuple[type[_Any], ...], list[_Any]],
|
|
160
|
+
list[Processor],
|
|
161
|
+
dict[type[Processor], Processor],
|
|
162
|
+
bool,
|
|
163
|
+
dict[str, int],
|
|
164
|
+
dict[str, _Any]
|
|
165
|
+
]] = {_current_world: (_entity_count, {}, {}, set(), {}, {}, [], {}, False, {}, {})}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def list_worlds() -> list[str]:
|
|
169
|
+
"""A list all World context names."""
|
|
170
|
+
return list(_context_map)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def delete_world(name: str) -> None:
|
|
174
|
+
"""Delete a World context.
|
|
175
|
+
|
|
176
|
+
This will completely delete the World, including all entities
|
|
177
|
+
that are contained within it.
|
|
178
|
+
|
|
179
|
+
Raises `PermissionError` if you attempt to delete the currently
|
|
180
|
+
active World context.
|
|
181
|
+
"""
|
|
182
|
+
if _current_world == name:
|
|
183
|
+
raise PermissionError("The active World context cannot be deleted.")
|
|
184
|
+
|
|
185
|
+
del _context_map[name]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def switch_world(name: str) -> None:
|
|
189
|
+
"""Switch to a new World context by name.
|
|
190
|
+
|
|
191
|
+
Esper can have one or more "Worlds". Each World is a dedicated
|
|
192
|
+
context, and does not share Entities, Components, events, etc.
|
|
193
|
+
Some game designs can benefit from using a dedicated World
|
|
194
|
+
for each scene. For other designs, a single World may be sufficient.
|
|
195
|
+
|
|
196
|
+
This function will allow you to create and switch between as
|
|
197
|
+
many World contexts as required. If the requested name does not
|
|
198
|
+
exist, a new context is created automatically with that name.
|
|
199
|
+
|
|
200
|
+
The name of the currently active World context can be checked
|
|
201
|
+
by examining :py:attr:`esper.current_world` attribute. This
|
|
202
|
+
attribute gets updated whenever you switch Worlds.
|
|
203
|
+
|
|
204
|
+
.. note:: At startup, a "default" World context is active.
|
|
205
|
+
"""
|
|
206
|
+
if name not in _context_map:
|
|
207
|
+
_context_map[name] = (_count(start=1), {}, {}, set(), {}, {}, [], {}, False, {}, {})
|
|
208
|
+
|
|
209
|
+
global _current_world
|
|
210
|
+
global _entity_count
|
|
211
|
+
global _components
|
|
212
|
+
global _entities
|
|
213
|
+
global _dead_entities
|
|
214
|
+
global _get_component_cache
|
|
215
|
+
global _get_components_cache
|
|
216
|
+
global _processors
|
|
217
|
+
global _processors_dict
|
|
218
|
+
global _cache_dirty
|
|
219
|
+
global process_times
|
|
220
|
+
global event_registry
|
|
221
|
+
global current_world
|
|
222
|
+
|
|
223
|
+
(_entity_count, _components, _entities, _dead_entities, _get_component_cache,
|
|
224
|
+
_get_components_cache, _processors, _processors_dict, _cache_dirty,
|
|
225
|
+
process_times, event_registry) = _context_map[name]
|
|
226
|
+
_current_world = current_world = name
|
|
161
227
|
|
|
162
228
|
|
|
229
|
+
#####################
|
|
230
|
+
# ECS functions
|
|
231
|
+
#####################
|
|
232
|
+
|
|
163
233
|
def clear_cache() -> None:
|
|
164
234
|
"""Manually clear the Component lookup cache.
|
|
165
235
|
|
|
166
236
|
Clearing the cache is not necessary to do manually,
|
|
167
237
|
but may be useful for benchmarking or debugging.
|
|
168
238
|
"""
|
|
239
|
+
global _cache_dirty
|
|
240
|
+
_cache_dirty = True
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _clear_cache_now() -> None:
|
|
244
|
+
"""Actually clear the cache (internal use)."""
|
|
245
|
+
global _cache_dirty
|
|
169
246
|
_get_component_cache.clear()
|
|
170
247
|
_get_components_cache.clear()
|
|
248
|
+
_cache_dirty = False
|
|
171
249
|
|
|
172
250
|
|
|
173
251
|
def clear_database() -> None:
|
|
@@ -181,7 +259,7 @@ def clear_database() -> None:
|
|
|
181
259
|
_components.clear()
|
|
182
260
|
_entities.clear()
|
|
183
261
|
_dead_entities.clear()
|
|
184
|
-
|
|
262
|
+
_clear_cache_now()
|
|
185
263
|
|
|
186
264
|
|
|
187
265
|
def add_processor(processor_instance: Processor, priority: int = 0) -> None:
|
|
@@ -196,9 +274,10 @@ def add_processor(processor_instance: Processor, priority: int = 0) -> None:
|
|
|
196
274
|
processor_instance.priority = priority
|
|
197
275
|
_processors.append(processor_instance)
|
|
198
276
|
_processors.sort(key=lambda proc: proc.priority, reverse=True)
|
|
277
|
+
_processors_dict[type(processor_instance)] = processor_instance
|
|
199
278
|
|
|
200
279
|
|
|
201
|
-
def remove_processor(processor_type:
|
|
280
|
+
def remove_processor(processor_type: type[Processor]) -> None:
|
|
202
281
|
"""Remove a Processor from the World, by type.
|
|
203
282
|
|
|
204
283
|
Make sure to provide the class itself, **not** an instance. For example::
|
|
@@ -210,23 +289,19 @@ def remove_processor(processor_type: _Type[Processor]) -> None:
|
|
|
210
289
|
self.world.remove_processor(my_processor_instance)
|
|
211
290
|
|
|
212
291
|
"""
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
292
|
+
processor = _processors_dict.pop(processor_type, None)
|
|
293
|
+
if processor:
|
|
294
|
+
_processors.remove(processor)
|
|
216
295
|
|
|
217
296
|
|
|
218
|
-
def get_processor(processor_type:
|
|
297
|
+
def get_processor(processor_type: type[Processor]) -> Processor | None:
|
|
219
298
|
"""Get a Processor instance, by type.
|
|
220
299
|
|
|
221
300
|
This function returns a Processor instance by type. This could be
|
|
222
301
|
useful in certain situations, such as wanting to call a method on a
|
|
223
302
|
Processor, from within another Processor.
|
|
224
303
|
"""
|
|
225
|
-
|
|
226
|
-
if type(processor) is processor_type:
|
|
227
|
-
return processor
|
|
228
|
-
else:
|
|
229
|
-
return None
|
|
304
|
+
return _processors_dict.get(processor_type)
|
|
230
305
|
|
|
231
306
|
|
|
232
307
|
def create_entity(*components: _C) -> int:
|
|
@@ -238,21 +313,19 @@ def create_entity(*components: _C) -> int:
|
|
|
238
313
|
added later with the :py:func:`esper.add_component` function.
|
|
239
314
|
"""
|
|
240
315
|
entity = next(_entity_count)
|
|
241
|
-
|
|
242
|
-
if entity not in _entities:
|
|
243
|
-
_entities[entity] = {}
|
|
316
|
+
entity_dict = {}
|
|
244
317
|
|
|
245
318
|
for component_instance in components:
|
|
246
|
-
|
|
247
319
|
component_type = type(component_instance)
|
|
248
320
|
|
|
249
321
|
if component_type not in _components:
|
|
250
322
|
_components[component_type] = set()
|
|
251
323
|
|
|
252
324
|
_components[component_type].add(entity)
|
|
325
|
+
entity_dict[component_type] = component_instance
|
|
253
326
|
|
|
254
|
-
|
|
255
|
-
|
|
327
|
+
_entities[entity] = entity_dict
|
|
328
|
+
clear_cache()
|
|
256
329
|
|
|
257
330
|
return entity
|
|
258
331
|
|
|
@@ -270,15 +343,16 @@ def delete_entity(entity: int, immediate: bool = False) -> None:
|
|
|
270
343
|
Raises a KeyError if the given entity does not exist in the database.
|
|
271
344
|
"""
|
|
272
345
|
if immediate:
|
|
273
|
-
|
|
274
|
-
|
|
346
|
+
entity_comps = _entities[entity]
|
|
347
|
+
for component_type in entity_comps:
|
|
348
|
+
comp_set = _components[component_type]
|
|
349
|
+
comp_set.discard(entity)
|
|
275
350
|
|
|
276
|
-
if not
|
|
351
|
+
if not comp_set:
|
|
277
352
|
del _components[component_type]
|
|
278
353
|
|
|
279
354
|
del _entities[entity]
|
|
280
355
|
clear_cache()
|
|
281
|
-
|
|
282
356
|
else:
|
|
283
357
|
_dead_entities.add(entity)
|
|
284
358
|
|
|
@@ -292,7 +366,12 @@ def entity_exists(entity: int) -> bool:
|
|
|
292
366
|
return entity in _entities and entity not in _dead_entities
|
|
293
367
|
|
|
294
368
|
|
|
295
|
-
def
|
|
369
|
+
def get_entities() -> _Generator[int]:
|
|
370
|
+
"""Iterate over all living Entity IDs."""
|
|
371
|
+
yield from _entities
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def component_for_entity(entity: int, component_type: type[_C]) -> _C:
|
|
296
375
|
"""Retrieve a Component instance for a specific Entity.
|
|
297
376
|
|
|
298
377
|
Retrieve a Component instance for a specific Entity. In some cases,
|
|
@@ -304,7 +383,7 @@ def component_for_entity(entity: int, component_type: _Type[_C]) -> _C:
|
|
|
304
383
|
return _entities[entity][component_type] # type: ignore[no-any-return]
|
|
305
384
|
|
|
306
385
|
|
|
307
|
-
def components_for_entity(entity: int) ->
|
|
386
|
+
def components_for_entity(entity: int) -> tuple[_C, ...]:
|
|
308
387
|
"""Retrieve all Components for a specific Entity, as a Tuple.
|
|
309
388
|
|
|
310
389
|
Retrieve all Components for a specific Entity. This function is probably
|
|
@@ -318,18 +397,18 @@ def components_for_entity(entity: int) -> _Tuple[_C, ...]:
|
|
|
318
397
|
return tuple(_entities[entity].values())
|
|
319
398
|
|
|
320
399
|
|
|
321
|
-
def has_component(entity: int, component_type:
|
|
400
|
+
def has_component(entity: int, component_type: type[_C]) -> bool:
|
|
322
401
|
"""Check if an Entity has a specific Component type."""
|
|
323
402
|
return component_type in _entities[entity]
|
|
324
403
|
|
|
325
404
|
|
|
326
|
-
def has_components(entity: int, *component_types:
|
|
405
|
+
def has_components(entity: int, *component_types: type[_C]) -> bool:
|
|
327
406
|
"""Check if an Entity has all the specified Component types."""
|
|
328
|
-
|
|
329
|
-
return all(comp_type in
|
|
407
|
+
entity_comps = _entities[entity]
|
|
408
|
+
return all(comp_type in entity_comps for comp_type in component_types)
|
|
330
409
|
|
|
331
410
|
|
|
332
|
-
def add_component(entity: int, component_instance: _C, type_alias:
|
|
411
|
+
def add_component(entity: int, component_instance: _C, type_alias: type[_C] | None = None) -> None:
|
|
333
412
|
"""Add a new Component instance to an Entity.
|
|
334
413
|
|
|
335
414
|
Add a Component instance to an Entiy. If a Component of the same type
|
|
@@ -345,12 +424,11 @@ def add_component(entity: int, component_instance: _C, type_alias: _Optional[_Ty
|
|
|
345
424
|
_components[component_type] = set()
|
|
346
425
|
|
|
347
426
|
_components[component_type].add(entity)
|
|
348
|
-
|
|
349
427
|
_entities[entity][component_type] = component_instance
|
|
350
428
|
clear_cache()
|
|
351
429
|
|
|
352
430
|
|
|
353
|
-
def remove_component(entity: int, component_type:
|
|
431
|
+
def remove_component(entity: int, component_type: type[_C]) -> _C:
|
|
354
432
|
"""Remove a Component instance from an Entity, by type.
|
|
355
433
|
|
|
356
434
|
A Component instance can only be removed by providing its type.
|
|
@@ -360,66 +438,133 @@ def remove_component(entity: int, component_type: _Type[_C]) -> _C:
|
|
|
360
438
|
Raises a KeyError if either the given entity or Component type does
|
|
361
439
|
not exist in the database.
|
|
362
440
|
"""
|
|
363
|
-
_components[component_type]
|
|
441
|
+
comp_set = _components[component_type]
|
|
442
|
+
comp_set.discard(entity)
|
|
364
443
|
|
|
365
|
-
if not
|
|
444
|
+
if not comp_set:
|
|
366
445
|
del _components[component_type]
|
|
367
446
|
|
|
368
447
|
clear_cache()
|
|
369
448
|
return _entities[entity].pop(component_type) # type: ignore[no-any-return]
|
|
370
449
|
|
|
371
450
|
|
|
372
|
-
def
|
|
451
|
+
def try_remove_component(entity: int, component_type: type[_C]) -> _C | None:
|
|
452
|
+
"""Try to remove a Component instance from an Entity, by type.
|
|
453
|
+
|
|
454
|
+
This operation is similar to :py:func:`esper.remove_component`, but
|
|
455
|
+
will NOT raise an exception if the Component does not exist.
|
|
456
|
+
"""
|
|
457
|
+
if comp_set := _components.get(component_type):
|
|
458
|
+
comp_set.discard(entity)
|
|
459
|
+
|
|
460
|
+
if not comp_set:
|
|
461
|
+
del _components[component_type]
|
|
462
|
+
|
|
463
|
+
clear_cache()
|
|
464
|
+
return _entities[entity].pop(component_type) # type: ignore[no-any-return]
|
|
465
|
+
|
|
466
|
+
return None
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _get_component(component_type: type[_C]) -> _Iterable[tuple[int, _C]]:
|
|
373
470
|
entity_db = _entities
|
|
471
|
+
comp_set = _components.get(component_type)
|
|
374
472
|
|
|
375
|
-
|
|
473
|
+
if comp_set is None:
|
|
474
|
+
return
|
|
475
|
+
|
|
476
|
+
for entity in comp_set:
|
|
376
477
|
yield entity, entity_db[entity][component_type]
|
|
377
478
|
|
|
378
479
|
|
|
379
|
-
def _get_components(*component_types:
|
|
480
|
+
def _get_components(*component_types: type[_C]) -> _Iterable[tuple[int, tuple[_C, ...]]]:
|
|
481
|
+
if not component_types:
|
|
482
|
+
return
|
|
483
|
+
|
|
380
484
|
entity_db = _entities
|
|
381
485
|
comp_db = _components
|
|
382
486
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
487
|
+
min_set = None
|
|
488
|
+
min_size = inf
|
|
489
|
+
other_types = []
|
|
490
|
+
|
|
491
|
+
for ct in component_types:
|
|
492
|
+
comp_set = comp_db.get(ct)
|
|
493
|
+
if comp_set is None:
|
|
494
|
+
return
|
|
495
|
+
set_size = len(comp_set)
|
|
496
|
+
if set_size < min_size:
|
|
497
|
+
if min_set is not None:
|
|
498
|
+
other_types.append(component_types[len(other_types)])
|
|
499
|
+
min_size = set_size
|
|
500
|
+
min_set = comp_set
|
|
501
|
+
else:
|
|
502
|
+
other_types.append(ct)
|
|
503
|
+
|
|
504
|
+
if min_set is None:
|
|
505
|
+
return
|
|
389
506
|
|
|
390
|
-
|
|
507
|
+
if not other_types:
|
|
508
|
+
for entity in min_set:
|
|
509
|
+
entity_comps = entity_db[entity]
|
|
510
|
+
yield entity, tuple(entity_comps[ct] for ct in component_types)
|
|
511
|
+
else:
|
|
512
|
+
for entity in min_set:
|
|
513
|
+
entity_comps = entity_db[entity]
|
|
514
|
+
has_all = True
|
|
515
|
+
for ct in other_types:
|
|
516
|
+
if ct not in entity_comps:
|
|
517
|
+
has_all = False
|
|
518
|
+
break
|
|
519
|
+
if has_all:
|
|
520
|
+
yield entity, tuple(entity_comps[ct] for ct in component_types)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def get_component(component_type: type[_C]) -> list[tuple[int, _C]]:
|
|
391
524
|
"""Get an iterator for Entity, Component pairs."""
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
525
|
+
if _cache_dirty:
|
|
526
|
+
_clear_cache_now()
|
|
527
|
+
|
|
528
|
+
cached = _get_component_cache.get(component_type)
|
|
529
|
+
if cached is not None:
|
|
530
|
+
return cached
|
|
531
|
+
|
|
532
|
+
result = list(_get_component(component_type))
|
|
533
|
+
_get_component_cache[component_type] = result
|
|
534
|
+
return result
|
|
396
535
|
|
|
397
536
|
|
|
398
537
|
@_overload
|
|
399
|
-
def get_components(__c1:
|
|
538
|
+
def get_components(__c1: type[_C], __c2: type[_C2]) -> list[tuple[int, tuple[_C, _C2]]]:
|
|
400
539
|
...
|
|
401
540
|
|
|
402
541
|
|
|
403
542
|
@_overload
|
|
404
|
-
def get_components(__c1:
|
|
543
|
+
def get_components(__c1: type[_C], __c2: type[_C2], __c3: type[_C3]) -> list[tuple[int, tuple[_C, _C2, _C3]]]:
|
|
405
544
|
...
|
|
406
545
|
|
|
407
546
|
|
|
408
547
|
@_overload
|
|
409
|
-
def get_components(__c1:
|
|
410
|
-
|
|
548
|
+
def get_components(__c1: type[_C], __c2: type[_C2], __c3: type[_C3], __c4: type[_C4]) -> list[
|
|
549
|
+
tuple[int, tuple[_C, _C2, _C3, _C4]]]:
|
|
411
550
|
...
|
|
412
551
|
|
|
413
552
|
|
|
414
|
-
def get_components(*component_types:
|
|
553
|
+
def get_components(*component_types: type[_Any]) -> list[tuple[int, tuple[_Any, ...]]]:
|
|
415
554
|
"""Get an iterator for Entity and multiple Component sets."""
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
555
|
+
if _cache_dirty:
|
|
556
|
+
_clear_cache_now()
|
|
557
|
+
|
|
558
|
+
cached = _get_components_cache.get(component_types)
|
|
559
|
+
if cached is not None:
|
|
560
|
+
return cached
|
|
420
561
|
|
|
562
|
+
result = list(_get_components(*component_types))
|
|
563
|
+
_get_components_cache[component_types] = result
|
|
564
|
+
return result
|
|
421
565
|
|
|
422
|
-
|
|
566
|
+
|
|
567
|
+
def try_component(entity: int, component_type: type[_C]) -> _C | None:
|
|
423
568
|
"""Try to get a single component type for an Entity.
|
|
424
569
|
|
|
425
570
|
This function will return the requested Component if it exists,
|
|
@@ -427,27 +572,28 @@ def try_component(entity: int, component_type: _Type[_C]) -> _Optional[_C]:
|
|
|
427
572
|
that may or may not exist, without having to first query if the Entity
|
|
428
573
|
has the Component type.
|
|
429
574
|
"""
|
|
430
|
-
|
|
431
|
-
|
|
575
|
+
entity_comps = _entities.get(entity)
|
|
576
|
+
if entity_comps and component_type in entity_comps:
|
|
577
|
+
return entity_comps[component_type] # type: ignore[no-any-return]
|
|
432
578
|
return None
|
|
433
579
|
|
|
434
580
|
|
|
435
581
|
@_overload
|
|
436
|
-
def try_components(entity: int, __c1:
|
|
582
|
+
def try_components(entity: int, __c1: type[_C], __c2: type[_C2]) -> tuple[_C, _C2]:
|
|
437
583
|
...
|
|
438
584
|
|
|
439
585
|
|
|
440
586
|
@_overload
|
|
441
|
-
def try_components(entity: int, __c1:
|
|
587
|
+
def try_components(entity: int, __c1: type[_C], __c2: type[_C2], __c3: type[_C3]) -> tuple[_C, _C2, _C3]:
|
|
442
588
|
...
|
|
443
589
|
|
|
444
590
|
|
|
445
591
|
@_overload
|
|
446
|
-
def try_components(entity: int, __c1:
|
|
592
|
+
def try_components(entity: int, __c1: type[_C], __c2: type[_C2], __c3: type[_C3], __c4: type[_C4]) -> tuple[_C, _C2, _C3, _C4]:
|
|
447
593
|
...
|
|
448
594
|
|
|
449
595
|
|
|
450
|
-
def try_components(entity: int, *component_types:
|
|
596
|
+
def try_components(entity: int, *component_types: type[_C]) -> tuple[_C, ...] | None:
|
|
451
597
|
"""Try to get multiple component types for an Entity.
|
|
452
598
|
|
|
453
599
|
This function will return the requested Components if they exist,
|
|
@@ -455,8 +601,9 @@ def try_components(entity: int, *component_types: _Type[_C]) -> _Optional[_Tuple
|
|
|
455
601
|
that may or may not exist, without first having to query if the Entity
|
|
456
602
|
has the Component types.
|
|
457
603
|
"""
|
|
458
|
-
|
|
459
|
-
|
|
604
|
+
entity_comps = _entities.get(entity)
|
|
605
|
+
if entity_comps and all(comp_type in entity_comps for comp_type in component_types):
|
|
606
|
+
return tuple(entity_comps[comp_type] for comp_type in component_types)
|
|
460
607
|
return None
|
|
461
608
|
|
|
462
609
|
|
|
@@ -471,12 +618,17 @@ def clear_dead_entities() -> None:
|
|
|
471
618
|
# In the interest of performance, this function duplicates code from the
|
|
472
619
|
# `delete_entity` function. If that function is changed, those changes should
|
|
473
620
|
# be duplicated here as well.
|
|
621
|
+
if not _dead_entities:
|
|
622
|
+
return
|
|
623
|
+
|
|
474
624
|
for entity in _dead_entities:
|
|
625
|
+
entity_comps = _entities[entity]
|
|
475
626
|
|
|
476
|
-
for component_type in
|
|
477
|
-
_components[component_type]
|
|
627
|
+
for component_type in entity_comps:
|
|
628
|
+
comp_set = _components[component_type]
|
|
629
|
+
comp_set.discard(entity)
|
|
478
630
|
|
|
479
|
-
if not
|
|
631
|
+
if not comp_set:
|
|
480
632
|
del _components[component_type]
|
|
481
633
|
|
|
482
634
|
del _entities[entity]
|
|
@@ -511,64 +663,3 @@ def timed_process(*args: _Any, **kwargs: _Any) -> None:
|
|
|
511
663
|
start_time = _time.process_time()
|
|
512
664
|
processor.process(*args, **kwargs)
|
|
513
665
|
process_times[processor.__class__.__name__] = int((_time.process_time() - start_time) * 1000)
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
def list_worlds() -> _List[str]:
|
|
517
|
-
"""A list all World context names."""
|
|
518
|
-
return list(_context_map)
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
def delete_world(name: str) -> None:
|
|
522
|
-
"""Delete a World context.
|
|
523
|
-
|
|
524
|
-
This will completely delete the World, including all entities
|
|
525
|
-
that are contained within it.
|
|
526
|
-
|
|
527
|
-
Raises `PermissionError` if you attempt to delete the currently
|
|
528
|
-
active World context.
|
|
529
|
-
"""
|
|
530
|
-
if _current_world == name:
|
|
531
|
-
raise PermissionError("The active World context cannot be deleted.")
|
|
532
|
-
|
|
533
|
-
del _context_map[name]
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
def switch_world(name: str) -> None:
|
|
537
|
-
"""Switch to a new World context by name.
|
|
538
|
-
|
|
539
|
-
Esper can have one or more "Worlds". Each World is a dedicated
|
|
540
|
-
context, and does not share Entities, Components, events, etc.
|
|
541
|
-
Some game designs can benefit from using a dedicated World
|
|
542
|
-
for each scene. For other designs, a single World may be sufficient.
|
|
543
|
-
|
|
544
|
-
This function will allow you to create and switch between as
|
|
545
|
-
many World contexts as required. If the requested name does not
|
|
546
|
-
exist, a new context is created automatically with that name.
|
|
547
|
-
|
|
548
|
-
The name of the currently active World context can be checked
|
|
549
|
-
at any time by examining the :py:attr:`esper.current_world`.
|
|
550
|
-
This attribute gets updated whenever you switch Worlds, and
|
|
551
|
-
modifying it has no effect.
|
|
552
|
-
|
|
553
|
-
.. note:: At startup, a "default" World context is active.
|
|
554
|
-
"""
|
|
555
|
-
if name not in _context_map:
|
|
556
|
-
# Create a new context if the name does not already exist:
|
|
557
|
-
_context_map[name] = (_count(start=1), {}, {}, set(), {}, {}, [], {}, {})
|
|
558
|
-
|
|
559
|
-
global _current_world
|
|
560
|
-
global _entity_count
|
|
561
|
-
global _components
|
|
562
|
-
global _entities
|
|
563
|
-
global _dead_entities
|
|
564
|
-
global _get_component_cache
|
|
565
|
-
global _get_components_cache
|
|
566
|
-
global _processors
|
|
567
|
-
global process_times
|
|
568
|
-
global event_registry
|
|
569
|
-
global current_world
|
|
570
|
-
|
|
571
|
-
# switch the references to the objects in the named context_map:
|
|
572
|
-
(_entity_count, _components, _entities, _dead_entities, _get_component_cache,
|
|
573
|
-
_get_components_cache, _processors, process_times, event_registry) = _context_map[name]
|
|
574
|
-
_current_world = current_world = name
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
|
|
4
4
|
import gc
|
|
5
|
+
import random
|
|
5
6
|
import sys
|
|
6
7
|
import time
|
|
7
8
|
import optparse
|
|
@@ -90,6 +91,11 @@ class Brain:
|
|
|
90
91
|
smarts: int = 9000
|
|
91
92
|
|
|
92
93
|
|
|
94
|
+
@component
|
|
95
|
+
class IsPlayer:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
93
99
|
#############################
|
|
94
100
|
# Set up some dummy entities:
|
|
95
101
|
#############################
|
|
@@ -99,6 +105,14 @@ def create_entities(number):
|
|
|
99
105
|
esper.create_entity(Position(), Health(), Damageable())
|
|
100
106
|
|
|
101
107
|
|
|
108
|
+
def create_mixed_entities(number):
|
|
109
|
+
for _ in range(number - 5):
|
|
110
|
+
esper.create_entity(Position(), Velocity())
|
|
111
|
+
|
|
112
|
+
for _ in range(5):
|
|
113
|
+
esper.create_entity(Position(), Velocity(), IsPlayer())
|
|
114
|
+
|
|
115
|
+
|
|
102
116
|
#############################
|
|
103
117
|
# Some timed query functions:
|
|
104
118
|
#############################
|
|
@@ -120,10 +134,56 @@ def three_comp_query():
|
|
|
120
134
|
pass
|
|
121
135
|
|
|
122
136
|
|
|
137
|
+
@timing
|
|
138
|
+
def rare_comp_query():
|
|
139
|
+
"""
|
|
140
|
+
Benchmark a query involving a common and a rare component.
|
|
141
|
+
|
|
142
|
+
This scenario is designed to highlight the performance gain from the
|
|
143
|
+
"iterate over the smallest set" optimization. The query for
|
|
144
|
+
(Position, Velocity, IsPlayer) should be extremely fast, as it only
|
|
145
|
+
needs to iterate over the few entities that have the rare `IsPlayer`
|
|
146
|
+
component, instead of all entities with `Position`.
|
|
147
|
+
"""
|
|
148
|
+
for _, (_, _, _) in esper.get_components(Position, Velocity, IsPlayer):
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@timing
|
|
153
|
+
def dynamic_world_frame(entities_to_kill, new_entities_to_create):
|
|
154
|
+
"""
|
|
155
|
+
Benchmark a single frame in a dynamic world with entity churn.
|
|
156
|
+
|
|
157
|
+
This function simulates a typical game loop frame to measure performance
|
|
158
|
+
under dynamic conditions. It tests the combined cost of:
|
|
159
|
+
1. Running common queries.
|
|
160
|
+
2. Deleting a batch of existing entities (testing `delete_entity`).
|
|
161
|
+
3. Creating a batch of new entities (testing `create_entity`).
|
|
162
|
+
4. Cleaning up dead entities (testing `clear_dead_entities`).
|
|
163
|
+
|
|
164
|
+
This is a good test for the lazy cache invalidation and optimized
|
|
165
|
+
entity cleanup mechanisms.
|
|
166
|
+
"""
|
|
167
|
+
for _, (_, _) in esper.get_components(Position, Velocity):
|
|
168
|
+
pass
|
|
169
|
+
for _, (_, _, _) in esper.get_components(Position, Damageable, Health):
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
for ent_id in entities_to_kill:
|
|
173
|
+
if esper.entity_exists(ent_id):
|
|
174
|
+
esper.delete_entity(ent_id, immediate=False)
|
|
175
|
+
|
|
176
|
+
create_entities(new_entities_to_create)
|
|
177
|
+
|
|
178
|
+
esper.clear_dead_entities()
|
|
179
|
+
|
|
180
|
+
|
|
123
181
|
#################################################
|
|
124
182
|
# Perform several queries, and print the results:
|
|
125
183
|
#################################################
|
|
126
184
|
results = {1: {}, 2: {}, 3: {}}
|
|
185
|
+
# result_times = []
|
|
186
|
+
new_results = {"Rare Comp": {}, "Dynamic World": {}}
|
|
127
187
|
result_times = []
|
|
128
188
|
|
|
129
189
|
for amount in range(500, MAX_ENTITIES, MAX_ENTITIES//50):
|
|
@@ -163,6 +223,40 @@ for amount in range(500, MAX_ENTITIES, MAX_ENTITIES//50):
|
|
|
163
223
|
gc.collect()
|
|
164
224
|
|
|
165
225
|
|
|
226
|
+
print("\n--- Benchmarking: Optimized Scenarios ---")
|
|
227
|
+
|
|
228
|
+
for amount in range(500, MAX_ENTITIES, MAX_ENTITIES//50):
|
|
229
|
+
create_mixed_entities(amount)
|
|
230
|
+
for _ in range(50):
|
|
231
|
+
rare_comp_query()
|
|
232
|
+
result_min = min(result_times)
|
|
233
|
+
print("Query rare component, {} Entities: {:f} ms".format(amount, result_min))
|
|
234
|
+
new_results["Rare Comp"][amount] = result_min
|
|
235
|
+
result_times = []
|
|
236
|
+
esper.clear_database()
|
|
237
|
+
gc.collect()
|
|
238
|
+
|
|
239
|
+
for amount in range(500, MAX_ENTITIES, MAX_ENTITIES//50):
|
|
240
|
+
create_entities(amount)
|
|
241
|
+
all_entities = list(esper.get_entities())
|
|
242
|
+
k = min(10, len(all_entities))
|
|
243
|
+
entities_to_kill_per_frame = random.sample(all_entities, k=k)
|
|
244
|
+
|
|
245
|
+
for _ in range(50):
|
|
246
|
+
dynamic_world_frame(entities_to_kill_per_frame, 10)
|
|
247
|
+
all_entities = list(esper.get_entities())
|
|
248
|
+
k = min(10, len(all_entities))
|
|
249
|
+
if k > 0:
|
|
250
|
+
entities_to_kill_per_frame = random.sample(all_entities, k=k)
|
|
251
|
+
|
|
252
|
+
result_min = min(result_times)
|
|
253
|
+
print("Dynamic world frame, {} Entities: {:f} ms".format(amount, result_min))
|
|
254
|
+
new_results["Dynamic World"][amount] = result_min
|
|
255
|
+
result_times = []
|
|
256
|
+
esper.clear_database()
|
|
257
|
+
gc.collect()
|
|
258
|
+
|
|
259
|
+
|
|
166
260
|
#############################################
|
|
167
261
|
# Save the results to disk, or plot directly:
|
|
168
262
|
#############################################
|
|
@@ -177,6 +271,7 @@ if options.plot:
|
|
|
177
271
|
print("\nThe matplotlib module is required for plotting results.")
|
|
178
272
|
sys.exit(1)
|
|
179
273
|
|
|
274
|
+
plt.figure(1)
|
|
180
275
|
lines = []
|
|
181
276
|
for num, result in results.items():
|
|
182
277
|
x, y = zip(*sorted(result.items()))
|
|
@@ -185,5 +280,19 @@ if options.plot:
|
|
|
185
280
|
|
|
186
281
|
plt.ylabel("Query Time (ms)")
|
|
187
282
|
plt.xlabel("Number of Entities")
|
|
283
|
+
plt.title("Basic Component Queries")
|
|
188
284
|
plt.legend(handles=lines, bbox_to_anchor=(0.5, 1))
|
|
285
|
+
|
|
286
|
+
plt.figure(2)
|
|
287
|
+
lines = []
|
|
288
|
+
for name, result in new_results.items():
|
|
289
|
+
if result:
|
|
290
|
+
x, y = zip(*sorted(result.items()))
|
|
291
|
+
lines.extend(plt.plot(x, y, label=name, marker='o'))
|
|
292
|
+
|
|
293
|
+
plt.ylabel("Query Time (ms)")
|
|
294
|
+
plt.xlabel("Number of Entities")
|
|
295
|
+
plt.title("Optimized Scenarios")
|
|
296
|
+
plt.legend(handles=lines, bbox_to_anchor=(0.5, 1))
|
|
297
|
+
|
|
189
298
|
plt.show()
|
|
@@ -3,12 +3,12 @@ import esper
|
|
|
3
3
|
|
|
4
4
|
##################################
|
|
5
5
|
## Here are a couple of Components
|
|
6
|
-
##################################
|
|
6
|
+
##################################
|
|
7
7
|
class Renderable(SpriteNode):
|
|
8
8
|
def __init__(self, **kwargs):
|
|
9
9
|
SpriteNode.__init__(self, **kwargs)
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
|
|
11
|
+
|
|
12
12
|
class Velocity:
|
|
13
13
|
def __init__(self, x=0.0, y=0.0):
|
|
14
14
|
self.x = x
|
|
@@ -17,11 +17,11 @@ class Velocity:
|
|
|
17
17
|
|
|
18
18
|
##############
|
|
19
19
|
## A processor
|
|
20
|
-
##############
|
|
20
|
+
##############
|
|
21
21
|
class MovementProcessor(esper.Processor):
|
|
22
22
|
def __init__(self):
|
|
23
23
|
super().__init__()
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
def process(self):
|
|
26
26
|
for ent, (rend, vel) in self.world.get_components(Renderable, Velocity):
|
|
27
27
|
rend.position += (vel.x, vel.y)
|
|
@@ -33,37 +33,37 @@ class MyScene (Scene):
|
|
|
33
33
|
def setup(self):
|
|
34
34
|
#Create a World Object
|
|
35
35
|
self.newworld = esper.World()
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
#Add the processor
|
|
38
38
|
self.movement_processor = MovementProcessor()
|
|
39
39
|
self.newworld.add_processor(self.movement_processor)
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
#Create a couple of entities
|
|
42
42
|
self.player = self.newworld.create_entity()
|
|
43
43
|
self.newworld.add_component(self.player, Renderable(parent=self,
|
|
44
44
|
texture='plc:Character_Boy', position=(100, 100)))
|
|
45
45
|
self.newworld.add_component(self.player, Velocity(x=1, y=.5))
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
self.enemy = self.newworld.create_entity()
|
|
48
48
|
self.newworld.add_component(self.enemy, Renderable(parent=self,
|
|
49
49
|
texture='plc:Character_Pink_Girl', position=(200, 200)))
|
|
50
50
|
self.newworld.add_component(self.enemy, Velocity(x=.5, y=0))
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
def did_change_size(self):
|
|
53
53
|
pass
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
def update(self):
|
|
56
56
|
# Process the world at each update!
|
|
57
57
|
self.newworld.process()
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
def touch_began(self, touch):
|
|
60
60
|
pass
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
def touch_moved(self, touch):
|
|
63
63
|
pass
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
def touch_ended(self, touch):
|
|
66
66
|
pass
|
|
67
67
|
|
|
68
68
|
if __name__ == '__main__':
|
|
69
|
-
run(MyScene(), show_fps=True)
|
|
69
|
+
run(MyScene(), show_fps=True)
|
|
@@ -2,6 +2,8 @@ import pytest
|
|
|
2
2
|
|
|
3
3
|
import esper
|
|
4
4
|
|
|
5
|
+
from types import GeneratorType
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
# ECS test
|
|
7
9
|
@pytest.fixture(autouse=True)
|
|
@@ -65,6 +67,14 @@ def test_delete_entity():
|
|
|
65
67
|
esper.delete_entity(empty_entity, immediate=True)
|
|
66
68
|
|
|
67
69
|
|
|
70
|
+
def test_get_entities():
|
|
71
|
+
create_entities(100)
|
|
72
|
+
generator = esper.get_entities()
|
|
73
|
+
assert isinstance(generator, GeneratorType)
|
|
74
|
+
assert isinstance(next(generator), int)
|
|
75
|
+
assert len(list(generator)) == 99 # one already exhausted
|
|
76
|
+
|
|
77
|
+
|
|
68
78
|
def test_component_for_entity():
|
|
69
79
|
entity = esper.create_entity(ComponentC())
|
|
70
80
|
assert isinstance(esper.component_for_entity(entity, ComponentC), ComponentC)
|
|
@@ -118,7 +128,7 @@ def test_get_two_components():
|
|
|
118
128
|
|
|
119
129
|
for ent, comps in esper.get_components(ComponentD, ComponentE):
|
|
120
130
|
assert isinstance(ent, int)
|
|
121
|
-
assert isinstance(comps,
|
|
131
|
+
assert isinstance(comps, tuple)
|
|
122
132
|
assert len(comps) == 2
|
|
123
133
|
|
|
124
134
|
for ent, (d, e) in esper.get_components(ComponentD, ComponentE):
|
|
@@ -133,7 +143,7 @@ def test_get_three_components():
|
|
|
133
143
|
|
|
134
144
|
for ent, comps in esper.get_components(ComponentC, ComponentD, ComponentE):
|
|
135
145
|
assert isinstance(ent, int)
|
|
136
|
-
assert isinstance(comps,
|
|
146
|
+
assert isinstance(comps, tuple)
|
|
137
147
|
assert len(comps) == 3
|
|
138
148
|
|
|
139
149
|
for ent, (c, d, e) in esper.get_components(ComponentC, ComponentD, ComponentE):
|
|
@@ -157,7 +167,7 @@ def test_try_components():
|
|
|
157
167
|
entity1 = esper.create_entity(ComponentA(), ComponentB())
|
|
158
168
|
|
|
159
169
|
one_item = esper.try_components(entity1, ComponentA, ComponentB)
|
|
160
|
-
assert isinstance(one_item,
|
|
170
|
+
assert isinstance(one_item, tuple)
|
|
161
171
|
assert len(one_item) == 2
|
|
162
172
|
assert isinstance(one_item[0], ComponentA)
|
|
163
173
|
assert isinstance(one_item[1], ComponentB)
|
|
@@ -255,6 +265,21 @@ class TestRemoveComponent:
|
|
|
255
265
|
assert not esper.has_component(entity, ComponentB)
|
|
256
266
|
assert esper.component_for_entity(entity, ComponentC) is component_c
|
|
257
267
|
|
|
268
|
+
def test_try_remove_component(self):
|
|
269
|
+
component_a = ComponentA()
|
|
270
|
+
component_b = ComponentB()
|
|
271
|
+
component_c = ComponentC()
|
|
272
|
+
entity = esper.create_entity(component_a, component_b, component_c)
|
|
273
|
+
|
|
274
|
+
# The component should be deleted the first time:
|
|
275
|
+
_deleted_component = esper.try_remove_component(entity, ComponentB)
|
|
276
|
+
assert _deleted_component is component_b
|
|
277
|
+
assert not esper.has_component(entity, ComponentB)
|
|
278
|
+
|
|
279
|
+
# Future calls should return None, and not raise any Exception:
|
|
280
|
+
_deleted_component = esper.try_remove_component(entity, ComponentB)
|
|
281
|
+
assert _deleted_component is None
|
|
282
|
+
|
|
258
283
|
|
|
259
284
|
def test_clear_dead_entities():
|
|
260
285
|
component = ComponentA()
|
|
@@ -516,6 +541,174 @@ def test_remove_handler():
|
|
|
516
541
|
assert esper.event_registry == {}
|
|
517
542
|
|
|
518
543
|
|
|
544
|
+
##################################################
|
|
545
|
+
# Advanced Feature Tests #
|
|
546
|
+
##################################################
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def test_delayed_delete_entity():
|
|
550
|
+
"""
|
|
551
|
+
Verify that delayed entity deletion works as expected.
|
|
552
|
+
|
|
553
|
+
This test checks the default deletion behavior (`immediate=False`).
|
|
554
|
+
It ensures that:
|
|
555
|
+
1. An entity marked for deletion is immediately considered non-existent
|
|
556
|
+
by the public `entity_exists()` function.
|
|
557
|
+
2. The entity's data, however, remains in the internal database
|
|
558
|
+
structures until the cleanup process is run.
|
|
559
|
+
3. After `clear_dead_entities()` is called, the entity is completely
|
|
560
|
+
purged from the database.
|
|
561
|
+
"""
|
|
562
|
+
entity = esper.create_entity(ComponentA())
|
|
563
|
+
esper.delete_entity(entity, immediate=False)
|
|
564
|
+
|
|
565
|
+
assert esper.entity_exists(entity) is False
|
|
566
|
+
assert entity in esper._entities
|
|
567
|
+
|
|
568
|
+
esper.clear_dead_entities()
|
|
569
|
+
|
|
570
|
+
assert entity not in esper._entities
|
|
571
|
+
with pytest.raises(KeyError):
|
|
572
|
+
esper.components_for_entity(entity)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def test_cache_invalidation_on_add_component():
|
|
576
|
+
"""
|
|
577
|
+
Verify that adding a component correctly invalidates the query cache.
|
|
578
|
+
|
|
579
|
+
This test ensures that:
|
|
580
|
+
1. A query is run to populate the cache (making it "hot").
|
|
581
|
+
2. A new component is added to an existing entity, which should trigger
|
|
582
|
+
the lazy cache invalidation mechanism by setting the dirty flag.
|
|
583
|
+
3. A subsequent query correctly rebuilds the cache and returns the
|
|
584
|
+
expected results.
|
|
585
|
+
"""
|
|
586
|
+
entity = esper.create_entity(ComponentA())
|
|
587
|
+
|
|
588
|
+
result1 = esper.get_component(ComponentA)
|
|
589
|
+
assert len(result1) == 1
|
|
590
|
+
|
|
591
|
+
esper.add_component(entity, ComponentB())
|
|
592
|
+
|
|
593
|
+
esper.clear_cache()
|
|
594
|
+
result2 = esper.get_component(ComponentA)
|
|
595
|
+
assert len(result2) == 1
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def test_cache_invalidation_on_remove_component():
|
|
599
|
+
"""
|
|
600
|
+
Verify that removing a component correctly invalidates the query cache.
|
|
601
|
+
|
|
602
|
+
This test ensures that:
|
|
603
|
+
1. A query for a specific component combination is run to populate the cache.
|
|
604
|
+
2. One of the components is removed from the entity, which should
|
|
605
|
+
trigger the lazy cache invalidation by setting the dirty flag.
|
|
606
|
+
3. A subsequent query for the same component combination correctly
|
|
607
|
+
returns an empty result and clears the dirty flag.
|
|
608
|
+
"""
|
|
609
|
+
entity = esper.create_entity(ComponentA(), ComponentB())
|
|
610
|
+
|
|
611
|
+
result1 = esper.get_components(ComponentA, ComponentB)
|
|
612
|
+
assert len(result1) == 1
|
|
613
|
+
|
|
614
|
+
esper.remove_component(entity, ComponentB)
|
|
615
|
+
|
|
616
|
+
result2 = esper.get_components(ComponentA, ComponentB)
|
|
617
|
+
assert len(result2) == 0
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def test_processor_priority():
|
|
621
|
+
"""
|
|
622
|
+
Verify that processors are executed in the correct order based on priority.
|
|
623
|
+
|
|
624
|
+
This test adds two processors, A and B, with different priority values
|
|
625
|
+
(A has a higher priority of 10, B has a lower priority of 5). It then
|
|
626
|
+
confirms that when `esper.process()` is called, the processor with the
|
|
627
|
+
higher numerical priority (Processor A) is executed before the one with
|
|
628
|
+
the lower priority.
|
|
629
|
+
"""
|
|
630
|
+
|
|
631
|
+
class PriorityProcessorA(esper.Processor):
|
|
632
|
+
priority = 10
|
|
633
|
+
|
|
634
|
+
def process(self, order_list):
|
|
635
|
+
order_list.append('A')
|
|
636
|
+
|
|
637
|
+
class PriorityProcessorB(esper.Processor):
|
|
638
|
+
priority = 5
|
|
639
|
+
|
|
640
|
+
def process(self, order_list):
|
|
641
|
+
order_list.append('B')
|
|
642
|
+
|
|
643
|
+
proc_b = PriorityProcessorB()
|
|
644
|
+
proc_a = PriorityProcessorA()
|
|
645
|
+
|
|
646
|
+
esper.add_processor(proc_b, priority=proc_b.priority)
|
|
647
|
+
esper.add_processor(proc_a, priority=proc_a.priority)
|
|
648
|
+
|
|
649
|
+
order = []
|
|
650
|
+
esper.process(order)
|
|
651
|
+
|
|
652
|
+
assert order == ['A', 'B']
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def test_weak_reference_handler_removal():
|
|
656
|
+
"""
|
|
657
|
+
Verify that event handlers are automatically unregistered when garbage collected.
|
|
658
|
+
|
|
659
|
+
The event system uses weak references to handlers to prevent memory leaks.
|
|
660
|
+
This test confirms that:
|
|
661
|
+
1. A method from a temporary object instance is registered as an event handler.
|
|
662
|
+
2. After the only strong reference to the instance is deleted, the garbage
|
|
663
|
+
collector reclaims the object.
|
|
664
|
+
3. The weak reference in the event registry becomes dead, and the handler
|
|
665
|
+
is automatically removed via its callback.
|
|
666
|
+
4. Dispatching the event no longer calls the handler, and the event name
|
|
667
|
+
is removed from the registry.
|
|
668
|
+
"""
|
|
669
|
+
called = 0
|
|
670
|
+
|
|
671
|
+
class TempHandler:
|
|
672
|
+
def handle(self):
|
|
673
|
+
nonlocal called
|
|
674
|
+
called += 1
|
|
675
|
+
|
|
676
|
+
temp_instance = TempHandler()
|
|
677
|
+
esper.set_handler("temp_event", temp_instance.handle)
|
|
678
|
+
|
|
679
|
+
assert "temp_event" in esper.event_registry
|
|
680
|
+
|
|
681
|
+
del temp_instance
|
|
682
|
+
|
|
683
|
+
import gc
|
|
684
|
+
gc.collect()
|
|
685
|
+
|
|
686
|
+
esper.dispatch_event("temp_event")
|
|
687
|
+
assert called == 0
|
|
688
|
+
|
|
689
|
+
assert "temp_event" not in esper.event_registry
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def test_delete_world():
|
|
693
|
+
"""
|
|
694
|
+
Verify the functionality of creating and deleting world contexts.
|
|
695
|
+
|
|
696
|
+
This test checks that:
|
|
697
|
+
1. A new world context can be created implicitly via `switch_world`.
|
|
698
|
+
2. The `list_worlds` function correctly reports the existence of the new world.
|
|
699
|
+
3. The `delete_world` function successfully removes the specified world.
|
|
700
|
+
4. Attempting to delete a non-existent world correctly raises a KeyError.
|
|
701
|
+
"""
|
|
702
|
+
esper.switch_world("temp_world")
|
|
703
|
+
esper.switch_world("default")
|
|
704
|
+
|
|
705
|
+
assert "temp_world" in esper.list_worlds()
|
|
706
|
+
esper.delete_world("temp_world")
|
|
707
|
+
assert "temp_world" not in esper.list_worlds()
|
|
708
|
+
|
|
709
|
+
with pytest.raises(KeyError):
|
|
710
|
+
esper.delete_world("non_existent")
|
|
711
|
+
|
|
519
712
|
##################################################
|
|
520
713
|
# Some helper functions and Component templates:
|
|
521
714
|
##################################################
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{esper-3.4 → esper-3.7}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{esper-3.4 → esper-3.7}/make.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|