esper 3.3__tar.gz → 3.4__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.3 → esper-3.4}/PKG-INFO +72 -48
- {esper-3.3 → esper-3.4}/README.md +70 -46
- {esper-3.3 → esper-3.4}/docs/index.rst +22 -4
- {esper-3.3 → esper-3.4}/esper/__init__.py +18 -16
- {esper-3.3 → esper-3.4}/tests/test_world.py +10 -0
- {esper-3.3 → esper-3.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {esper-3.3 → esper-3.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {esper-3.3 → esper-3.4}/.github/ISSUE_TEMPLATE/question-or-comment.md +0 -0
- {esper-3.3 → esper-3.4}/.github/workflows/type-checking.yml +0 -0
- {esper-3.3 → esper-3.4}/.github/workflows/unit-tests.yml +0 -0
- {esper-3.3 → esper-3.4}/.gitignore +0 -0
- {esper-3.3 → esper-3.4}/.mypy.ini +0 -0
- {esper-3.3 → esper-3.4}/.readthedocs.yaml +0 -0
- {esper-3.3 → esper-3.4}/.ruff.toml +0 -0
- {esper-3.3 → esper-3.4}/LICENSE +0 -0
- {esper-3.3 → esper-3.4}/MANIFEST.in +0 -0
- {esper-3.3 → esper-3.4}/RELEASE_NOTES +0 -0
- {esper-3.3 → esper-3.4}/docs/Makefile +0 -0
- {esper-3.3 → esper-3.4}/docs/conf.py +0 -0
- {esper-3.3 → esper-3.4}/docs/make.bat +0 -0
- {esper-3.3 → esper-3.4}/esper/py.typed +0 -0
- {esper-3.3 → esper-3.4}/examples/benchmark.py +0 -0
- {esper-3.3 → esper-3.4}/examples/benchmark_cache.py +0 -0
- {esper-3.3 → esper-3.4}/examples/bluesquare.png +0 -0
- {esper-3.3 → esper-3.4}/examples/headless_example.py +0 -0
- {esper-3.3 → esper-3.4}/examples/pygame_example.py +0 -0
- {esper-3.3 → esper-3.4}/examples/pyglet_example.py +0 -0
- {esper-3.3 → esper-3.4}/examples/pysdl2_example.py +0 -0
- {esper-3.3 → esper-3.4}/examples/pythonista_ios_example.py +0 -0
- {esper-3.3 → esper-3.4}/examples/redsquare.png +0 -0
- {esper-3.3 → esper-3.4}/make.py +0 -0
- {esper-3.3 → esper-3.4}/pyproject.toml +0 -0
- {esper-3.3 → esper-3.4}/tests/__init__.py +0 -0
{esper-3.3 → esper-3.4}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: esper
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4
|
|
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
|
|
@@ -12,12 +12,12 @@ Project-URL: Home, https://github.com/benmoran56/esper
|
|
|
12
12
|
[](https://esper.readthedocs.io)
|
|
13
13
|
[](https://github.com/benmoran56/esper/actions/workflows/unit-tests.yml)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
esper is a lightweight Entity System module for Python, with a focus on performance
|
|
16
16
|
===================================================================================
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
**esper** is an MIT licensed Entity System, or, Entity Component System (ECS).
|
|
19
19
|
The design is based on the Entity System concepts originally popularized by
|
|
20
|
-
Adam Martin and others. The primary focus for esper is to maximize perfomance,
|
|
20
|
+
Adam Martin and others. The primary focus for **esper** is to maximize perfomance,
|
|
21
21
|
while handling most common use cases.
|
|
22
22
|
|
|
23
23
|
For more information on the ECS pattern, you might find the following
|
|
@@ -30,7 +30,7 @@ API documentation is hosted at ReadTheDocs: https://esper.readthedocs.io
|
|
|
30
30
|
Due to the small size of the project, this README currently serves as general usage
|
|
31
31
|
documentation.
|
|
32
32
|
|
|
33
|
-
> :warning: **
|
|
33
|
+
> :warning: **esper 3.0 introduces breaking changes**. Version 3.0 removes the
|
|
34
34
|
> World object, and migrates its methods to module level functions. Multiple
|
|
35
35
|
> contexts can be created and switched between. The v2.x README can be found
|
|
36
36
|
> here: https://github.com/benmoran56/esper/blob/v2_maintenance/README.md
|
|
@@ -51,15 +51,15 @@ documentation.
|
|
|
51
51
|
|
|
52
52
|
Compatibility
|
|
53
53
|
=============
|
|
54
|
-
|
|
55
|
-
not EOL).
|
|
54
|
+
**esper** attempts to target all currently supported Python releases (any Python version that is
|
|
55
|
+
not EOL). **esper** is written in 100% pure Python, so *any* compliant interpreter should work.
|
|
56
56
|
Automated testing is currently done for both CPython and PyPy3.
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
Installation
|
|
60
60
|
============
|
|
61
|
-
|
|
62
|
-
You can simply copy the
|
|
61
|
+
**esper** is a pure Python package with no dependencies, so installation is flexible.
|
|
62
|
+
You can simply copy the `esper` folder right into your project, and `import esper`.
|
|
63
63
|
You can also install into your site-packages from PyPi via `pip`::
|
|
64
64
|
|
|
65
65
|
pip install --user --upgrade esper
|
|
@@ -74,7 +74,7 @@ Design
|
|
|
74
74
|
|
|
75
75
|
* World Context
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
**esper** uses the concept of "World" contexts. When you first `import esper`, a default context is
|
|
78
78
|
active. You create Entities, assign Components, register Processors, etc., by calling functions
|
|
79
79
|
on the `esper` module. Entities, Components and Processors can be created, assigned, or deleted
|
|
80
80
|
while your game is running. A simple call to `esper.process()` is all that's needed for each
|
|
@@ -84,18 +84,19 @@ isolating different game scenes that have different Processor requirements.
|
|
|
84
84
|
|
|
85
85
|
* Entities
|
|
86
86
|
|
|
87
|
-
Entities are
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
Creating an Entity is done with the `esper.create_entity()`
|
|
91
|
-
|
|
87
|
+
Entities are defined internally as plain integer IDs (1, 2, 3, 4, etc.). Generally speaking
|
|
88
|
+
you should not need to care about the individual entity IDs, since entities are queried based
|
|
89
|
+
on their specific combination of Components - not by their ID. An Entity can be thought of as
|
|
90
|
+
a specific combination of Components. Creating an Entity is done with the `esper.create_entity()`
|
|
91
|
+
function. You can pass Component instances on creation or add/remove them later.
|
|
92
92
|
|
|
93
93
|
* Components
|
|
94
94
|
|
|
95
|
-
Components are defined as simple Python classes. In keeping with a pure Entity System
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
Components are defined as simple Python classes. In keeping with a pure Entity System design
|
|
96
|
+
philosophy, Components should not contain any processing logic. They may contain initialization
|
|
97
|
+
logic, and you can take advantage of Python language features, like properties, to simplify data
|
|
98
|
+
lookup. The key point is that game logic does not belong in these classes, and Components should
|
|
99
|
+
have no knowledge of other Components or Entities. A simple Component can be defined as::
|
|
99
100
|
|
|
100
101
|
class Position:
|
|
101
102
|
def __init__(self, x=0.0, y=0.0):
|
|
@@ -114,11 +115,26 @@ be repeated, and you can still instantiate the Component with positional or keyw
|
|
|
114
115
|
x: float = 0.0
|
|
115
116
|
y: float = 0.0
|
|
116
117
|
|
|
118
|
+
Python language features, like properties, can be useful to simplify data access. For example,
|
|
119
|
+
a Body component that is often repositioned may benefit from a local AABB (axis aligned bounding
|
|
120
|
+
box) property::
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class Body:
|
|
124
|
+
width: int
|
|
125
|
+
height: int
|
|
126
|
+
pos_x: float = 0
|
|
127
|
+
pos_y: float = 0
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def aabb(self) -> tuple[float, float, float, float]:
|
|
131
|
+
return self.pos_x, self.pos_y, self.pos_x + self.width, self.pos_y + self.height
|
|
132
|
+
|
|
117
133
|
|
|
118
134
|
* Processors
|
|
119
135
|
|
|
120
136
|
Processors, also commonly known as "Systems", are where all processing logic is defined and executed.
|
|
121
|
-
All Processors must inherit from the
|
|
137
|
+
All Processors must inherit from the `esper.Processor` class, and have a method called `process`.
|
|
122
138
|
Other than that, there are no restrictions. You can define any additional methods you might need.
|
|
123
139
|
A simple Processor might look like::
|
|
124
140
|
|
|
@@ -129,21 +145,31 @@ A simple Processor might look like::
|
|
|
129
145
|
pos.x += vel.x
|
|
130
146
|
pos.y += vel.y
|
|
131
147
|
|
|
132
|
-
In the above code, you can see the standard usage of the
|
|
148
|
+
In the above code, you can see the standard usage of the `esper.get_components()` function. This
|
|
133
149
|
function allows efficient iteration over all Entities that contain the specified Component types.
|
|
134
150
|
This function can be used for querying two or more components at once. Note that tuple unpacking
|
|
135
|
-
is necessary for the return component pairs:
|
|
136
|
-
get a reference to the Entity ID
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
151
|
+
is necessary for the return component pairs: `(vel, pos)`. In addition to Components, you also
|
|
152
|
+
get a reference to the Entity ID for the current pair of Velocity/Position Components. This entity
|
|
153
|
+
ID can be useful in a variety of cases. For example, if your Processor will need to delete certain
|
|
154
|
+
Entites, you can call the `esper.delete_entity()` function on this Entity ID. Another common use
|
|
155
|
+
is if you wish to add or remove a Component on this Entity as a result of some condition being met.
|
|
156
|
+
For example, an Entity that should be deleted once it's `Lifecycle` Component reaches 0::
|
|
157
|
+
|
|
158
|
+
class LifecycleProcessor(esper.Processor):
|
|
159
|
+
def __init__(self, ...):
|
|
160
|
+
...
|
|
161
|
+
|
|
162
|
+
def process(self, dt):
|
|
163
|
+
for ent, (life, rend) in esper.get_components(Lifecycle, Renderable):
|
|
164
|
+
life.lifespan -= dt
|
|
165
|
+
if life.lifespan <= 0:
|
|
166
|
+
esper.delete_entity(ent)
|
|
141
167
|
|
|
142
168
|
|
|
143
169
|
Quick Start
|
|
144
170
|
===========
|
|
145
171
|
|
|
146
|
-
To get started, simply import esper
|
|
172
|
+
To get started, simply import **esper**::
|
|
147
173
|
|
|
148
174
|
import esper
|
|
149
175
|
|
|
@@ -159,7 +185,7 @@ Optionally, Component instances can be assigned directly to the Entity on creati
|
|
|
159
185
|
|
|
160
186
|
|
|
161
187
|
Design some Processors that operate on these Component types, and then register them with
|
|
162
|
-
|
|
188
|
+
**esper** for processing. You can specify an optional priority (higher numbers are processed first).
|
|
163
189
|
All Processors are priority "0" by default::
|
|
164
190
|
|
|
165
191
|
movement_processor = MovementProcessor()
|
|
@@ -172,18 +198,17 @@ All Processors are priority "0" by default::
|
|
|
172
198
|
esper.add_processor(SomeProcessor())
|
|
173
199
|
|
|
174
200
|
|
|
175
|
-
Executing all Processors is done with a single call to esper.process()
|
|
201
|
+
Executing all Processors is done with a single call to `esper.process()`. This will call the
|
|
176
202
|
`process` method on all assigned Processors, in order of their priority. This is usually called
|
|
177
203
|
once per frame update of your game (every tick of the clock).::
|
|
178
204
|
|
|
179
205
|
esper.process()
|
|
180
206
|
|
|
181
207
|
|
|
182
|
-
**Note:** You can pass any arguments (or keyword arguments) you need to
|
|
183
|
-
must also make sure to receive them properly in the
|
|
184
|
-
example, if you pass a delta time argument as
|
|
185
|
-
methods should all receive it as:
|
|
186
|
-
*def process(self, dt):*
|
|
208
|
+
**Note:** You can pass any arguments (or keyword arguments) you need to `esper.process()`, but you
|
|
209
|
+
must also make sure to receive them properly in the `process()` methods of your Processors. For
|
|
210
|
+
example, if you pass a delta time argument as `esper.process(dt)`, your Processor's `process()`
|
|
211
|
+
methods should all receive it as: `def process(self, dt):`
|
|
187
212
|
This is appropriate for libraries such as **pyglet**, which automatically pass a delta time value
|
|
188
213
|
into scheduled functions.
|
|
189
214
|
|
|
@@ -193,14 +218,13 @@ General Usage
|
|
|
193
218
|
|
|
194
219
|
World Contexts
|
|
195
220
|
--------------
|
|
196
|
-
|
|
221
|
+
**esper** has the capability of supporting multiple "World" contexts. On import, a "default" World is
|
|
197
222
|
active. All creation of Entities, assignment of Processors, and all other operations occur within
|
|
198
223
|
the confines of the active World. In other words, the World contexts are completely isolated from
|
|
199
224
|
each other. For basic games and designs, you may not need to bother with this functionality. A
|
|
200
225
|
single default World context can often be enough. For advanced use cases, such as when different
|
|
201
226
|
scenes in your game have different Entities and Processor requirements, this functionality can be
|
|
202
227
|
quite useful. World context operations are done with the following functions::
|
|
203
|
-
*
|
|
204
228
|
* esper.list_worlds()
|
|
205
229
|
* esper.switch_world(name)
|
|
206
230
|
* esper.delete_world(name)
|
|
@@ -212,7 +236,7 @@ delete the currently active World.
|
|
|
212
236
|
Adding and Removing Processors
|
|
213
237
|
------------------------------
|
|
214
238
|
You have already seen examples of adding Processors in an earlier section. There is also a
|
|
215
|
-
|
|
239
|
+
`remove_processor` function available:
|
|
216
240
|
|
|
217
241
|
* esper.add_processor(processor_instance)
|
|
218
242
|
* esper.remove_processor(ProcessorClass)
|
|
@@ -228,10 +252,10 @@ remove Components inside your Processors. The following functions are available
|
|
|
228
252
|
* esper.add_component(entity_id, component_instance)
|
|
229
253
|
* esper.remove_component(entity_id, ComponentClass)
|
|
230
254
|
|
|
231
|
-
As an example of this, you could have a "Blink" component with a
|
|
255
|
+
As an example of this, you could have a "Blink" component with a `duration` attribute. This can be used
|
|
232
256
|
to make certain things blink for s specific period of time, then disappear. For example, the code below
|
|
233
257
|
shows a simplified case of adding this Component to an Entity when it takes damage in one processor. A
|
|
234
|
-
dedicated
|
|
258
|
+
dedicated `BlinkProcessor` handles the effect, and then removes the Component after the duration expires::
|
|
235
259
|
|
|
236
260
|
class BlinkComponent:
|
|
237
261
|
def __init__(self, duration):
|
|
@@ -274,16 +298,16 @@ to it, the following functions are available:
|
|
|
274
298
|
* esper.component_for_entity
|
|
275
299
|
* esper.components_for_entity
|
|
276
300
|
|
|
277
|
-
The
|
|
301
|
+
The `component_for_entity` function is useful in a limited number of cases where you know a specific
|
|
278
302
|
Entity ID, and wish to get a specific Component for it. An error is raised if the Component does not
|
|
279
|
-
exist for the Entity ID, so it may be more useful when combined with the
|
|
303
|
+
exist for the Entity ID, so it may be more useful when combined with the `has_component`
|
|
280
304
|
function that is explained in the next section. For example::
|
|
281
305
|
|
|
282
306
|
if esper.has_component(ent, SFX):
|
|
283
307
|
sfx = esper.component_for_entity(ent, SFX)
|
|
284
308
|
sfx.play()
|
|
285
309
|
|
|
286
|
-
The
|
|
310
|
+
The `components_for_entity` function is a special function that returns ALL the Components that are
|
|
287
311
|
assigned to a specific Entity, as a tuple. This is a heavy operation, and not something you would
|
|
288
312
|
want to do each frame or inside your `Processor.process` method. It can be useful, however, if
|
|
289
313
|
you wanted to transfer all of a specific Entity's Components between two separate contexts
|
|
@@ -344,7 +368,7 @@ Let's look at the core part of the code::
|
|
|
344
368
|
stun = esper.component_for_entity(ent, Stun)
|
|
345
369
|
stun.duration -= dt
|
|
346
370
|
|
|
347
|
-
This code works fine, but the
|
|
371
|
+
This code works fine, but the `try_component` function can accomplish the same thing with one
|
|
348
372
|
less function call. The following example will get a specific Component if it exists, or
|
|
349
373
|
return None if it does not::
|
|
350
374
|
|
|
@@ -362,12 +386,12 @@ functions even more concise ::
|
|
|
362
386
|
More Examples
|
|
363
387
|
-------------
|
|
364
388
|
|
|
365
|
-
See the
|
|
389
|
+
See the `/examples` folder to get an idea of how the basic structure of a game might look.
|
|
366
390
|
|
|
367
391
|
Event Dispatching
|
|
368
392
|
=================
|
|
369
393
|
|
|
370
|
-
|
|
394
|
+
**esper** includes basic support for event dispatching and handling. This functionality is
|
|
371
395
|
provided by three functions to set (register), remove, and dispatch events. Minimal error
|
|
372
396
|
checking is done, so it's left up to the user to ensure correct naming and number of
|
|
373
397
|
arguments are used when dispatching and receiving events.
|
|
@@ -397,7 +421,7 @@ Registered events and handlers are part of the current `World` context.
|
|
|
397
421
|
Contributing
|
|
398
422
|
============
|
|
399
423
|
|
|
400
|
-
Contributions to
|
|
424
|
+
Contributions to **esper** are always welcome, but there are some specific project goals to keep in mind:
|
|
401
425
|
|
|
402
426
|
- Pure Python code only: no binary extensions, Cython, etc.
|
|
403
427
|
- Try to target all non-EOL Python versions. Exceptions can be made if there is a compelling reason.
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
[](https://esper.readthedocs.io)
|
|
3
3
|
[](https://github.com/benmoran56/esper/actions/workflows/unit-tests.yml)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
esper is a lightweight Entity System module for Python, with a focus on performance
|
|
6
6
|
===================================================================================
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
**esper** is an MIT licensed Entity System, or, Entity Component System (ECS).
|
|
9
9
|
The design is based on the Entity System concepts originally popularized by
|
|
10
|
-
Adam Martin and others. The primary focus for esper is to maximize perfomance,
|
|
10
|
+
Adam Martin and others. The primary focus for **esper** is to maximize perfomance,
|
|
11
11
|
while handling most common use cases.
|
|
12
12
|
|
|
13
13
|
For more information on the ECS pattern, you might find the following
|
|
@@ -20,7 +20,7 @@ API documentation is hosted at ReadTheDocs: https://esper.readthedocs.io
|
|
|
20
20
|
Due to the small size of the project, this README currently serves as general usage
|
|
21
21
|
documentation.
|
|
22
22
|
|
|
23
|
-
> :warning: **
|
|
23
|
+
> :warning: **esper 3.0 introduces breaking changes**. Version 3.0 removes the
|
|
24
24
|
> World object, and migrates its methods to module level functions. Multiple
|
|
25
25
|
> contexts can be created and switched between. The v2.x README can be found
|
|
26
26
|
> here: https://github.com/benmoran56/esper/blob/v2_maintenance/README.md
|
|
@@ -41,15 +41,15 @@ documentation.
|
|
|
41
41
|
|
|
42
42
|
Compatibility
|
|
43
43
|
=============
|
|
44
|
-
|
|
45
|
-
not EOL).
|
|
44
|
+
**esper** attempts to target all currently supported Python releases (any Python version that is
|
|
45
|
+
not EOL). **esper** is written in 100% pure Python, so *any* compliant interpreter should work.
|
|
46
46
|
Automated testing is currently done for both CPython and PyPy3.
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
Installation
|
|
50
50
|
============
|
|
51
|
-
|
|
52
|
-
You can simply copy the
|
|
51
|
+
**esper** is a pure Python package with no dependencies, so installation is flexible.
|
|
52
|
+
You can simply copy the `esper` folder right into your project, and `import esper`.
|
|
53
53
|
You can also install into your site-packages from PyPi via `pip`::
|
|
54
54
|
|
|
55
55
|
pip install --user --upgrade esper
|
|
@@ -64,7 +64,7 @@ Design
|
|
|
64
64
|
|
|
65
65
|
* World Context
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
**esper** uses the concept of "World" contexts. When you first `import esper`, a default context is
|
|
68
68
|
active. You create Entities, assign Components, register Processors, etc., by calling functions
|
|
69
69
|
on the `esper` module. Entities, Components and Processors can be created, assigned, or deleted
|
|
70
70
|
while your game is running. A simple call to `esper.process()` is all that's needed for each
|
|
@@ -74,18 +74,19 @@ isolating different game scenes that have different Processor requirements.
|
|
|
74
74
|
|
|
75
75
|
* Entities
|
|
76
76
|
|
|
77
|
-
Entities are
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
Creating an Entity is done with the `esper.create_entity()`
|
|
81
|
-
|
|
77
|
+
Entities are defined internally as plain integer IDs (1, 2, 3, 4, etc.). Generally speaking
|
|
78
|
+
you should not need to care about the individual entity IDs, since entities are queried based
|
|
79
|
+
on their specific combination of Components - not by their ID. An Entity can be thought of as
|
|
80
|
+
a specific combination of Components. Creating an Entity is done with the `esper.create_entity()`
|
|
81
|
+
function. You can pass Component instances on creation or add/remove them later.
|
|
82
82
|
|
|
83
83
|
* Components
|
|
84
84
|
|
|
85
|
-
Components are defined as simple Python classes. In keeping with a pure Entity System
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
Components are defined as simple Python classes. In keeping with a pure Entity System design
|
|
86
|
+
philosophy, Components should not contain any processing logic. They may contain initialization
|
|
87
|
+
logic, and you can take advantage of Python language features, like properties, to simplify data
|
|
88
|
+
lookup. The key point is that game logic does not belong in these classes, and Components should
|
|
89
|
+
have no knowledge of other Components or Entities. A simple Component can be defined as::
|
|
89
90
|
|
|
90
91
|
class Position:
|
|
91
92
|
def __init__(self, x=0.0, y=0.0):
|
|
@@ -104,11 +105,26 @@ be repeated, and you can still instantiate the Component with positional or keyw
|
|
|
104
105
|
x: float = 0.0
|
|
105
106
|
y: float = 0.0
|
|
106
107
|
|
|
108
|
+
Python language features, like properties, can be useful to simplify data access. For example,
|
|
109
|
+
a Body component that is often repositioned may benefit from a local AABB (axis aligned bounding
|
|
110
|
+
box) property::
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class Body:
|
|
114
|
+
width: int
|
|
115
|
+
height: int
|
|
116
|
+
pos_x: float = 0
|
|
117
|
+
pos_y: float = 0
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def aabb(self) -> tuple[float, float, float, float]:
|
|
121
|
+
return self.pos_x, self.pos_y, self.pos_x + self.width, self.pos_y + self.height
|
|
122
|
+
|
|
107
123
|
|
|
108
124
|
* Processors
|
|
109
125
|
|
|
110
126
|
Processors, also commonly known as "Systems", are where all processing logic is defined and executed.
|
|
111
|
-
All Processors must inherit from the
|
|
127
|
+
All Processors must inherit from the `esper.Processor` class, and have a method called `process`.
|
|
112
128
|
Other than that, there are no restrictions. You can define any additional methods you might need.
|
|
113
129
|
A simple Processor might look like::
|
|
114
130
|
|
|
@@ -119,21 +135,31 @@ A simple Processor might look like::
|
|
|
119
135
|
pos.x += vel.x
|
|
120
136
|
pos.y += vel.y
|
|
121
137
|
|
|
122
|
-
In the above code, you can see the standard usage of the
|
|
138
|
+
In the above code, you can see the standard usage of the `esper.get_components()` function. This
|
|
123
139
|
function allows efficient iteration over all Entities that contain the specified Component types.
|
|
124
140
|
This function can be used for querying two or more components at once. Note that tuple unpacking
|
|
125
|
-
is necessary for the return component pairs:
|
|
126
|
-
get a reference to the Entity ID
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
141
|
+
is necessary for the return component pairs: `(vel, pos)`. In addition to Components, you also
|
|
142
|
+
get a reference to the Entity ID for the current pair of Velocity/Position Components. This entity
|
|
143
|
+
ID can be useful in a variety of cases. For example, if your Processor will need to delete certain
|
|
144
|
+
Entites, you can call the `esper.delete_entity()` function on this Entity ID. Another common use
|
|
145
|
+
is if you wish to add or remove a Component on this Entity as a result of some condition being met.
|
|
146
|
+
For example, an Entity that should be deleted once it's `Lifecycle` Component reaches 0::
|
|
147
|
+
|
|
148
|
+
class LifecycleProcessor(esper.Processor):
|
|
149
|
+
def __init__(self, ...):
|
|
150
|
+
...
|
|
151
|
+
|
|
152
|
+
def process(self, dt):
|
|
153
|
+
for ent, (life, rend) in esper.get_components(Lifecycle, Renderable):
|
|
154
|
+
life.lifespan -= dt
|
|
155
|
+
if life.lifespan <= 0:
|
|
156
|
+
esper.delete_entity(ent)
|
|
131
157
|
|
|
132
158
|
|
|
133
159
|
Quick Start
|
|
134
160
|
===========
|
|
135
161
|
|
|
136
|
-
To get started, simply import esper
|
|
162
|
+
To get started, simply import **esper**::
|
|
137
163
|
|
|
138
164
|
import esper
|
|
139
165
|
|
|
@@ -149,7 +175,7 @@ Optionally, Component instances can be assigned directly to the Entity on creati
|
|
|
149
175
|
|
|
150
176
|
|
|
151
177
|
Design some Processors that operate on these Component types, and then register them with
|
|
152
|
-
|
|
178
|
+
**esper** for processing. You can specify an optional priority (higher numbers are processed first).
|
|
153
179
|
All Processors are priority "0" by default::
|
|
154
180
|
|
|
155
181
|
movement_processor = MovementProcessor()
|
|
@@ -162,18 +188,17 @@ All Processors are priority "0" by default::
|
|
|
162
188
|
esper.add_processor(SomeProcessor())
|
|
163
189
|
|
|
164
190
|
|
|
165
|
-
Executing all Processors is done with a single call to esper.process()
|
|
191
|
+
Executing all Processors is done with a single call to `esper.process()`. This will call the
|
|
166
192
|
`process` method on all assigned Processors, in order of their priority. This is usually called
|
|
167
193
|
once per frame update of your game (every tick of the clock).::
|
|
168
194
|
|
|
169
195
|
esper.process()
|
|
170
196
|
|
|
171
197
|
|
|
172
|
-
**Note:** You can pass any arguments (or keyword arguments) you need to
|
|
173
|
-
must also make sure to receive them properly in the
|
|
174
|
-
example, if you pass a delta time argument as
|
|
175
|
-
methods should all receive it as:
|
|
176
|
-
*def process(self, dt):*
|
|
198
|
+
**Note:** You can pass any arguments (or keyword arguments) you need to `esper.process()`, but you
|
|
199
|
+
must also make sure to receive them properly in the `process()` methods of your Processors. For
|
|
200
|
+
example, if you pass a delta time argument as `esper.process(dt)`, your Processor's `process()`
|
|
201
|
+
methods should all receive it as: `def process(self, dt):`
|
|
177
202
|
This is appropriate for libraries such as **pyglet**, which automatically pass a delta time value
|
|
178
203
|
into scheduled functions.
|
|
179
204
|
|
|
@@ -183,14 +208,13 @@ General Usage
|
|
|
183
208
|
|
|
184
209
|
World Contexts
|
|
185
210
|
--------------
|
|
186
|
-
|
|
211
|
+
**esper** has the capability of supporting multiple "World" contexts. On import, a "default" World is
|
|
187
212
|
active. All creation of Entities, assignment of Processors, and all other operations occur within
|
|
188
213
|
the confines of the active World. In other words, the World contexts are completely isolated from
|
|
189
214
|
each other. For basic games and designs, you may not need to bother with this functionality. A
|
|
190
215
|
single default World context can often be enough. For advanced use cases, such as when different
|
|
191
216
|
scenes in your game have different Entities and Processor requirements, this functionality can be
|
|
192
217
|
quite useful. World context operations are done with the following functions::
|
|
193
|
-
*
|
|
194
218
|
* esper.list_worlds()
|
|
195
219
|
* esper.switch_world(name)
|
|
196
220
|
* esper.delete_world(name)
|
|
@@ -202,7 +226,7 @@ delete the currently active World.
|
|
|
202
226
|
Adding and Removing Processors
|
|
203
227
|
------------------------------
|
|
204
228
|
You have already seen examples of adding Processors in an earlier section. There is also a
|
|
205
|
-
|
|
229
|
+
`remove_processor` function available:
|
|
206
230
|
|
|
207
231
|
* esper.add_processor(processor_instance)
|
|
208
232
|
* esper.remove_processor(ProcessorClass)
|
|
@@ -218,10 +242,10 @@ remove Components inside your Processors. The following functions are available
|
|
|
218
242
|
* esper.add_component(entity_id, component_instance)
|
|
219
243
|
* esper.remove_component(entity_id, ComponentClass)
|
|
220
244
|
|
|
221
|
-
As an example of this, you could have a "Blink" component with a
|
|
245
|
+
As an example of this, you could have a "Blink" component with a `duration` attribute. This can be used
|
|
222
246
|
to make certain things blink for s specific period of time, then disappear. For example, the code below
|
|
223
247
|
shows a simplified case of adding this Component to an Entity when it takes damage in one processor. A
|
|
224
|
-
dedicated
|
|
248
|
+
dedicated `BlinkProcessor` handles the effect, and then removes the Component after the duration expires::
|
|
225
249
|
|
|
226
250
|
class BlinkComponent:
|
|
227
251
|
def __init__(self, duration):
|
|
@@ -264,16 +288,16 @@ to it, the following functions are available:
|
|
|
264
288
|
* esper.component_for_entity
|
|
265
289
|
* esper.components_for_entity
|
|
266
290
|
|
|
267
|
-
The
|
|
291
|
+
The `component_for_entity` function is useful in a limited number of cases where you know a specific
|
|
268
292
|
Entity ID, and wish to get a specific Component for it. An error is raised if the Component does not
|
|
269
|
-
exist for the Entity ID, so it may be more useful when combined with the
|
|
293
|
+
exist for the Entity ID, so it may be more useful when combined with the `has_component`
|
|
270
294
|
function that is explained in the next section. For example::
|
|
271
295
|
|
|
272
296
|
if esper.has_component(ent, SFX):
|
|
273
297
|
sfx = esper.component_for_entity(ent, SFX)
|
|
274
298
|
sfx.play()
|
|
275
299
|
|
|
276
|
-
The
|
|
300
|
+
The `components_for_entity` function is a special function that returns ALL the Components that are
|
|
277
301
|
assigned to a specific Entity, as a tuple. This is a heavy operation, and not something you would
|
|
278
302
|
want to do each frame or inside your `Processor.process` method. It can be useful, however, if
|
|
279
303
|
you wanted to transfer all of a specific Entity's Components between two separate contexts
|
|
@@ -334,7 +358,7 @@ Let's look at the core part of the code::
|
|
|
334
358
|
stun = esper.component_for_entity(ent, Stun)
|
|
335
359
|
stun.duration -= dt
|
|
336
360
|
|
|
337
|
-
This code works fine, but the
|
|
361
|
+
This code works fine, but the `try_component` function can accomplish the same thing with one
|
|
338
362
|
less function call. The following example will get a specific Component if it exists, or
|
|
339
363
|
return None if it does not::
|
|
340
364
|
|
|
@@ -352,12 +376,12 @@ functions even more concise ::
|
|
|
352
376
|
More Examples
|
|
353
377
|
-------------
|
|
354
378
|
|
|
355
|
-
See the
|
|
379
|
+
See the `/examples` folder to get an idea of how the basic structure of a game might look.
|
|
356
380
|
|
|
357
381
|
Event Dispatching
|
|
358
382
|
=================
|
|
359
383
|
|
|
360
|
-
|
|
384
|
+
**esper** includes basic support for event dispatching and handling. This functionality is
|
|
361
385
|
provided by three functions to set (register), remove, and dispatch events. Minimal error
|
|
362
386
|
checking is done, so it's left up to the user to ensure correct naming and number of
|
|
363
387
|
arguments are used when dispatching and receiving events.
|
|
@@ -387,7 +411,7 @@ Registered events and handlers are part of the current `World` context.
|
|
|
387
411
|
Contributing
|
|
388
412
|
============
|
|
389
413
|
|
|
390
|
-
Contributions to
|
|
414
|
+
Contributions to **esper** are always welcome, but there are some specific project goals to keep in mind:
|
|
391
415
|
|
|
392
416
|
- Pure Python code only: no binary extensions, Cython, etc.
|
|
393
417
|
- Try to target all non-EOL Python versions. Exceptions can be made if there is a compelling reason.
|
|
@@ -9,10 +9,28 @@ Processors
|
|
|
9
9
|
Components
|
|
10
10
|
----------
|
|
11
11
|
**esper** does not define any specific Component base class
|
|
12
|
-
to inherit from.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
to inherit from. Any valid Python class can be used. For more
|
|
13
|
+
compact definitions, the ``@dataclass`` decorator from the
|
|
14
|
+
``dataclasses`` module is quite useful. You can also use a
|
|
15
|
+
``namedtuple`` instead of a class, but this is limited to
|
|
16
|
+
cases where the Component's data does not need to be modified.
|
|
17
|
+
Three examples of valid Components::
|
|
18
|
+
|
|
19
|
+
class Velocity:
|
|
20
|
+
def __init__(self, x=0.0, y=0.0, accel=0.1, decel=0.75, maximum=3):
|
|
21
|
+
self.vector = Vec2(x, y)
|
|
22
|
+
self.accel = accel
|
|
23
|
+
self.decel = decel
|
|
24
|
+
self.maximum = maximum
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Camera:
|
|
29
|
+
current_x_offset: float = 0
|
|
30
|
+
current_y_offset: float = 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
Interaction = namedtuple('Interaction', 'interaction_type target_name')
|
|
16
34
|
|
|
17
35
|
|
|
18
36
|
The World context
|
|
@@ -23,7 +23,7 @@ from weakref import WeakMethod as _WeakMethod
|
|
|
23
23
|
|
|
24
24
|
from itertools import count as _count
|
|
25
25
|
|
|
26
|
-
__version__ = version = '3.
|
|
26
|
+
__version__ = version = '3.4'
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
###################
|
|
@@ -81,10 +81,11 @@ def remove_handler(name: str, func: _Callable[..., None]) -> None:
|
|
|
81
81
|
receive the named event, or if the named event does
|
|
82
82
|
not exist, this function call will pass silently.
|
|
83
83
|
"""
|
|
84
|
-
|
|
84
|
+
func_ref = _ref(func)
|
|
85
|
+
if func_ref not in event_registry.get(name, []):
|
|
85
86
|
return
|
|
86
87
|
|
|
87
|
-
event_registry[name].remove(
|
|
88
|
+
event_registry[name].remove(func_ref)
|
|
88
89
|
if not event_registry[name]:
|
|
89
90
|
del event_registry[name]
|
|
90
91
|
|
|
@@ -139,15 +140,10 @@ _dead_entities: _Set[int] = set()
|
|
|
139
140
|
_get_component_cache: _Dict[_Type[_Any], _List[_Any]] = {}
|
|
140
141
|
_get_components_cache: _Dict[_Tuple[_Type[_Any], ...], _List[_Any]] = {}
|
|
141
142
|
_processors: _List[Processor] = []
|
|
142
|
-
process_times: _Dict[str, int] = {}
|
|
143
143
|
event_registry: _Dict[str, _Any] = {}
|
|
144
|
+
process_times: _Dict[str, int] = {}
|
|
144
145
|
current_world: str = "default"
|
|
145
|
-
"""The name of the currently active World context.
|
|
146
146
|
|
|
147
|
-
This attribute can be checked to confirm the name of the
|
|
148
|
-
currently active World context. Modifying this has no effect;
|
|
149
|
-
to switch Worlds, use the :py:func:`~switch_world` function.
|
|
150
|
-
"""
|
|
151
147
|
|
|
152
148
|
# {context_name: (entity_count, components, entities, dead_entities,
|
|
153
149
|
# comp_cache, comps_cache, processors, process_times, event_registry)}
|
|
@@ -236,10 +232,10 @@ def get_processor(processor_type: _Type[Processor]) -> _Optional[Processor]:
|
|
|
236
232
|
def create_entity(*components: _C) -> int:
|
|
237
233
|
"""Create a new Entity, with optional initial Components.
|
|
238
234
|
|
|
239
|
-
This
|
|
235
|
+
This function returns an Entity ID, which is a plain integer.
|
|
240
236
|
You can optionally pass one or more Component instances to be
|
|
241
237
|
assigned to the Entity on creation. Components can be also be
|
|
242
|
-
added later with the :py:func:`esper.add_component`
|
|
238
|
+
added later with the :py:func:`esper.add_component` function.
|
|
243
239
|
"""
|
|
244
240
|
entity = next(_entity_count)
|
|
245
241
|
|
|
@@ -329,7 +325,8 @@ def has_component(entity: int, component_type: _Type[_C]) -> bool:
|
|
|
329
325
|
|
|
330
326
|
def has_components(entity: int, *component_types: _Type[_C]) -> bool:
|
|
331
327
|
"""Check if an Entity has all the specified Component types."""
|
|
332
|
-
|
|
328
|
+
components_dict = _entities[entity]
|
|
329
|
+
return all(comp_type in components_dict for comp_type in component_types)
|
|
333
330
|
|
|
334
331
|
|
|
335
332
|
def add_component(entity: int, component_instance: _C, type_alias: _Optional[_Type[_C]] = None) -> None:
|
|
@@ -451,7 +448,7 @@ def try_components(entity: int, __c1: _Type[_C], __c2: _Type[_C2], __c3: _Type[_
|
|
|
451
448
|
|
|
452
449
|
|
|
453
450
|
def try_components(entity: int, *component_types: _Type[_C]) -> _Optional[_Tuple[_C, ...]]:
|
|
454
|
-
"""Try to get
|
|
451
|
+
"""Try to get multiple component types for an Entity.
|
|
455
452
|
|
|
456
453
|
This function will return the requested Components if they exist,
|
|
457
454
|
or None if they do not. This allows a way to access optional Components
|
|
@@ -506,7 +503,8 @@ def timed_process(*args: _Any, **kwargs: _Any) -> None:
|
|
|
506
503
|
|
|
507
504
|
This function is identical to :py:func:`esper.process`, but
|
|
508
505
|
it additionally records the elapsed time of each processor
|
|
509
|
-
|
|
506
|
+
(in milliseconds) in the :py:attr:`~process_times` dictionary
|
|
507
|
+
after each call.
|
|
510
508
|
"""
|
|
511
509
|
clear_dead_entities()
|
|
512
510
|
for processor in _processors:
|
|
@@ -541,13 +539,17 @@ def switch_world(name: str) -> None:
|
|
|
541
539
|
Esper can have one or more "Worlds". Each World is a dedicated
|
|
542
540
|
context, and does not share Entities, Components, events, etc.
|
|
543
541
|
Some game designs can benefit from using a dedicated World
|
|
544
|
-
for each scene. For other designs, a single World may
|
|
545
|
-
be sufficient.
|
|
542
|
+
for each scene. For other designs, a single World may be sufficient.
|
|
546
543
|
|
|
547
544
|
This function will allow you to create and switch between as
|
|
548
545
|
many World contexts as required. If the requested name does not
|
|
549
546
|
exist, a new context is created automatically with that name.
|
|
550
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
|
+
|
|
551
553
|
.. note:: At startup, a "default" World context is active.
|
|
552
554
|
"""
|
|
553
555
|
if name not in _context_map:
|
|
@@ -505,6 +505,16 @@ def test_event_handler_switch_world():
|
|
|
505
505
|
esper.dispatch_event("foo")
|
|
506
506
|
assert called == 2
|
|
507
507
|
|
|
508
|
+
def test_remove_handler():
|
|
509
|
+
def handler():
|
|
510
|
+
pass
|
|
511
|
+
|
|
512
|
+
assert esper.event_registry == {}
|
|
513
|
+
esper.set_handler("foo", handler)
|
|
514
|
+
assert "foo" in esper.event_registry
|
|
515
|
+
esper.remove_handler("foo", handler)
|
|
516
|
+
assert esper.event_registry == {}
|
|
517
|
+
|
|
508
518
|
|
|
509
519
|
##################################################
|
|
510
520
|
# Some helper functions and Component templates:
|
|
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.3 → esper-3.4}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{esper-3.3 → esper-3.4}/make.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|