esper 3.2__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.4/.ruff.toml +2 -0
- {esper-3.2 → esper-3.4}/LICENSE +1 -1
- {esper-3.2 → esper-3.4}/PKG-INFO +73 -49
- {esper-3.2 → esper-3.4}/README.md +71 -47
- {esper-3.2 → esper-3.4}/RELEASE_NOTES +11 -0
- {esper-3.2 → esper-3.4}/docs/index.rst +22 -4
- {esper-3.2 → esper-3.4}/esper/__init__.py +30 -32
- {esper-3.2 → esper-3.4}/examples/pyglet_example.py +1 -1
- {esper-3.2 → esper-3.4}/tests/test_world.py +10 -0
- esper-3.2/.ruff.toml +0 -3
- esper-3.2/examples/pyglet_example_batch.py +0 -245
- {esper-3.2 → esper-3.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {esper-3.2 → esper-3.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {esper-3.2 → esper-3.4}/.github/ISSUE_TEMPLATE/question-or-comment.md +0 -0
- {esper-3.2 → esper-3.4}/.github/workflows/type-checking.yml +0 -0
- {esper-3.2 → esper-3.4}/.github/workflows/unit-tests.yml +0 -0
- {esper-3.2 → esper-3.4}/.gitignore +0 -0
- {esper-3.2 → esper-3.4}/.mypy.ini +0 -0
- {esper-3.2 → esper-3.4}/.readthedocs.yaml +0 -0
- {esper-3.2 → esper-3.4}/MANIFEST.in +0 -0
- {esper-3.2 → esper-3.4}/docs/Makefile +0 -0
- {esper-3.2 → esper-3.4}/docs/conf.py +0 -0
- {esper-3.2 → esper-3.4}/docs/make.bat +0 -0
- {esper-3.2 → esper-3.4}/esper/py.typed +0 -0
- {esper-3.2 → esper-3.4}/examples/benchmark.py +0 -0
- {esper-3.2 → esper-3.4}/examples/benchmark_cache.py +0 -0
- {esper-3.2 → esper-3.4}/examples/bluesquare.png +0 -0
- {esper-3.2 → esper-3.4}/examples/headless_example.py +0 -0
- {esper-3.2 → esper-3.4}/examples/pygame_example.py +0 -0
- {esper-3.2 → esper-3.4}/examples/pysdl2_example.py +0 -0
- {esper-3.2 → esper-3.4}/examples/pythonista_ios_example.py +0 -0
- {esper-3.2 → esper-3.4}/examples/redsquare.png +0 -0
- {esper-3.2 → esper-3.4}/make.py +0 -0
- {esper-3.2 → esper-3.4}/pyproject.toml +0 -0
- {esper-3.2 → esper-3.4}/tests/__init__.py +0 -0
esper-3.4/.ruff.toml
ADDED
{esper-3.2 → esper-3.4}/LICENSE
RENAMED
{esper-3.2 → 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,8 +74,8 @@ Design
|
|
|
74
74
|
|
|
75
75
|
* World Context
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
active. You create Entities, assign Components, register
|
|
77
|
+
**esper** uses the concept of "World" contexts. When you first `import esper`, a default context is
|
|
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
|
|
81
81
|
iteration of your game loop. Advanced users can switch contexts, which can be useful for
|
|
@@ -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,8 +64,8 @@ Design
|
|
|
64
64
|
|
|
65
65
|
* World Context
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
active. You create Entities, assign Components, register
|
|
67
|
+
**esper** uses the concept of "World" contexts. When you first `import esper`, a default context is
|
|
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
|
|
71
71
|
iteration of your game loop. Advanced users can switch contexts, which can be useful for
|
|
@@ -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.
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
esper 3.3
|
|
2
|
+
=========
|
|
3
|
+
Maintenance release
|
|
4
|
+
|
|
5
|
+
Changes
|
|
6
|
+
-------
|
|
7
|
+
- Fix unreadable `esper.current_world` property. (#100)
|
|
8
|
+
- Minor typing configuration updates.
|
|
9
|
+
- Remove outdated pyglet example. (Another one already exists).
|
|
10
|
+
|
|
11
|
+
|
|
1
12
|
esper 3.2
|
|
2
13
|
=========
|
|
3
14
|
Maintenance release
|
|
@@ -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
|
|
|
@@ -131,7 +132,7 @@ class Processor:
|
|
|
131
132
|
# ECS functions
|
|
132
133
|
###################
|
|
133
134
|
|
|
134
|
-
|
|
135
|
+
_current_world: str = "default"
|
|
135
136
|
_entity_count: "_count[int]" = _count(start=1)
|
|
136
137
|
_components: _Dict[_Type[_Any], _Set[_Any]] = {}
|
|
137
138
|
_entities: _Dict[int, _Dict[_Type[_Any], _Any]] = {}
|
|
@@ -139,10 +140,13 @@ _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] = {}
|
|
145
|
+
current_world: str = "default"
|
|
144
146
|
|
|
145
|
-
|
|
147
|
+
|
|
148
|
+
# {context_name: (entity_count, components, entities, dead_entities,
|
|
149
|
+
# comp_cache, comps_cache, processors, process_times, event_registry)}
|
|
146
150
|
_context_map: _Dict[str, _Tuple[
|
|
147
151
|
"_count[int]",
|
|
148
152
|
_Dict[_Type[_Any], _Set[_Any]],
|
|
@@ -228,10 +232,10 @@ def get_processor(processor_type: _Type[Processor]) -> _Optional[Processor]:
|
|
|
228
232
|
def create_entity(*components: _C) -> int:
|
|
229
233
|
"""Create a new Entity, with optional initial Components.
|
|
230
234
|
|
|
231
|
-
This
|
|
235
|
+
This function returns an Entity ID, which is a plain integer.
|
|
232
236
|
You can optionally pass one or more Component instances to be
|
|
233
237
|
assigned to the Entity on creation. Components can be also be
|
|
234
|
-
added later with the :py:func:`esper.add_component`
|
|
238
|
+
added later with the :py:func:`esper.add_component` function.
|
|
235
239
|
"""
|
|
236
240
|
entity = next(_entity_count)
|
|
237
241
|
|
|
@@ -321,7 +325,8 @@ def has_component(entity: int, component_type: _Type[_C]) -> bool:
|
|
|
321
325
|
|
|
322
326
|
def has_components(entity: int, *component_types: _Type[_C]) -> bool:
|
|
323
327
|
"""Check if an Entity has all the specified Component types."""
|
|
324
|
-
|
|
328
|
+
components_dict = _entities[entity]
|
|
329
|
+
return all(comp_type in components_dict for comp_type in component_types)
|
|
325
330
|
|
|
326
331
|
|
|
327
332
|
def add_component(entity: int, component_instance: _C, type_alias: _Optional[_Type[_C]] = None) -> None:
|
|
@@ -387,9 +392,7 @@ def get_component(component_type: _Type[_C]) -> _List[_Tuple[int, _C]]:
|
|
|
387
392
|
try:
|
|
388
393
|
return _get_component_cache[component_type]
|
|
389
394
|
except KeyError:
|
|
390
|
-
return _get_component_cache.setdefault(
|
|
391
|
-
component_type, list(_get_component(component_type))
|
|
392
|
-
)
|
|
395
|
+
return _get_component_cache.setdefault(component_type, list(_get_component(component_type)))
|
|
393
396
|
|
|
394
397
|
|
|
395
398
|
@_overload
|
|
@@ -404,7 +407,7 @@ def get_components(__c1: _Type[_C], __c2: _Type[_C2], __c3: _Type[_C3]) -> _List
|
|
|
404
407
|
|
|
405
408
|
@_overload
|
|
406
409
|
def get_components(__c1: _Type[_C], __c2: _Type[_C2], __c3: _Type[_C3], __c4: _Type[_C4]) -> _List[
|
|
407
|
-
|
|
410
|
+
_Tuple[int, _Tuple[_C, _C2, _C3, _C4]]]:
|
|
408
411
|
...
|
|
409
412
|
|
|
410
413
|
|
|
@@ -413,9 +416,7 @@ def get_components(*component_types: _Type[_Any]) -> _Iterable[_Tuple[int, _Tupl
|
|
|
413
416
|
try:
|
|
414
417
|
return _get_components_cache[component_types]
|
|
415
418
|
except KeyError:
|
|
416
|
-
return _get_components_cache.setdefault(
|
|
417
|
-
component_types, list(_get_components(*component_types))
|
|
418
|
-
)
|
|
419
|
+
return _get_components_cache.setdefault(component_types, list(_get_components(*component_types)))
|
|
419
420
|
|
|
420
421
|
|
|
421
422
|
def try_component(entity: int, component_type: _Type[_C]) -> _Optional[_C]:
|
|
@@ -447,7 +448,7 @@ def try_components(entity: int, __c1: _Type[_C], __c2: _Type[_C2], __c3: _Type[_
|
|
|
447
448
|
|
|
448
449
|
|
|
449
450
|
def try_components(entity: int, *component_types: _Type[_C]) -> _Optional[_Tuple[_C, ...]]:
|
|
450
|
-
"""Try to get
|
|
451
|
+
"""Try to get multiple component types for an Entity.
|
|
451
452
|
|
|
452
453
|
This function will return the requested Components if they exist,
|
|
453
454
|
or None if they do not. This allows a way to access optional Components
|
|
@@ -502,7 +503,8 @@ def timed_process(*args: _Any, **kwargs: _Any) -> None:
|
|
|
502
503
|
|
|
503
504
|
This function is identical to :py:func:`esper.process`, but
|
|
504
505
|
it additionally records the elapsed time of each processor
|
|
505
|
-
|
|
506
|
+
(in milliseconds) in the :py:attr:`~process_times` dictionary
|
|
507
|
+
after each call.
|
|
506
508
|
"""
|
|
507
509
|
clear_dead_entities()
|
|
508
510
|
for processor in _processors:
|
|
@@ -525,7 +527,7 @@ def delete_world(name: str) -> None:
|
|
|
525
527
|
Raises `PermissionError` if you attempt to delete the currently
|
|
526
528
|
active World context.
|
|
527
529
|
"""
|
|
528
|
-
if
|
|
530
|
+
if _current_world == name:
|
|
529
531
|
raise PermissionError("The active World context cannot be deleted.")
|
|
530
532
|
|
|
531
533
|
del _context_map[name]
|
|
@@ -537,20 +539,24 @@ def switch_world(name: str) -> None:
|
|
|
537
539
|
Esper can have one or more "Worlds". Each World is a dedicated
|
|
538
540
|
context, and does not share Entities, Components, events, etc.
|
|
539
541
|
Some game designs can benefit from using a dedicated World
|
|
540
|
-
for each scene. For other designs, a single World may
|
|
541
|
-
be sufficient.
|
|
542
|
+
for each scene. For other designs, a single World may be sufficient.
|
|
542
543
|
|
|
543
544
|
This function will allow you to create and switch between as
|
|
544
545
|
many World contexts as required. If the requested name does not
|
|
545
546
|
exist, a new context is created automatically with that name.
|
|
546
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
|
+
|
|
547
553
|
.. note:: At startup, a "default" World context is active.
|
|
548
554
|
"""
|
|
549
555
|
if name not in _context_map:
|
|
550
556
|
# Create a new context if the name does not already exist:
|
|
551
557
|
_context_map[name] = (_count(start=1), {}, {}, set(), {}, {}, [], {}, {})
|
|
552
558
|
|
|
553
|
-
global
|
|
559
|
+
global _current_world
|
|
554
560
|
global _entity_count
|
|
555
561
|
global _components
|
|
556
562
|
global _entities
|
|
@@ -560,17 +566,9 @@ def switch_world(name: str) -> None:
|
|
|
560
566
|
global _processors
|
|
561
567
|
global process_times
|
|
562
568
|
global event_registry
|
|
569
|
+
global current_world
|
|
563
570
|
|
|
564
571
|
# switch the references to the objects in the named context_map:
|
|
565
572
|
(_entity_count, _components, _entities, _dead_entities, _get_component_cache,
|
|
566
573
|
_get_components_cache, _processors, process_times, event_registry) = _context_map[name]
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
@property # type: ignore
|
|
571
|
-
def current_world() -> str:
|
|
572
|
-
"""The currently active World context.
|
|
573
|
-
|
|
574
|
-
To switch World contexts, see :py:func:`esper.switch_world`.
|
|
575
|
-
"""
|
|
576
|
-
return _current_context
|
|
574
|
+
_current_world = current_world = name
|
|
@@ -36,7 +36,7 @@ class MovementProcessor:
|
|
|
36
36
|
def process(self, dt):
|
|
37
37
|
# This will iterate over every Entity that has BOTH of these components:
|
|
38
38
|
for ent, (vel, rend) in esper.get_components(Velocity, Renderable):
|
|
39
|
-
# Update the Renderable Component's position by
|
|
39
|
+
# Update the Renderable Component's position by its Velocity:
|
|
40
40
|
# An example of keeping the sprite inside screen boundaries. Basically,
|
|
41
41
|
# adjust the position back inside screen boundaries if it is outside:
|
|
42
42
|
new_x = max(self.minx, rend.sprite.x + vel.x)
|
|
@@ -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:
|
esper-3.2/.ruff.toml
DELETED
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
import pyglet
|
|
2
|
-
from pyglet.gl import *
|
|
3
|
-
from pyglet.window import key
|
|
4
|
-
|
|
5
|
-
import esper
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
FPS = 60
|
|
9
|
-
RESOLUTION = 720, 480
|
|
10
|
-
BGCOLOR = (0, 0, 0, 255)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
##################################
|
|
14
|
-
# Define some Components:
|
|
15
|
-
##################################
|
|
16
|
-
class Velocity:
|
|
17
|
-
def __init__(self, x=0.0, y=0.0):
|
|
18
|
-
self.x = x
|
|
19
|
-
self.y = y
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class Renderable:
|
|
23
|
-
def __init__(self, texture, width, height, posx, posy):
|
|
24
|
-
self.texture = texture
|
|
25
|
-
self._x = posx
|
|
26
|
-
self._y = posy
|
|
27
|
-
self.w = width
|
|
28
|
-
self.h = height
|
|
29
|
-
self.group = TextureBindGroup(texture)
|
|
30
|
-
self.vertex_list = None
|
|
31
|
-
self._dirty = True
|
|
32
|
-
|
|
33
|
-
@property
|
|
34
|
-
def x(self):
|
|
35
|
-
return self._x
|
|
36
|
-
|
|
37
|
-
@x.setter
|
|
38
|
-
def x(self, val):
|
|
39
|
-
if val != self._x:
|
|
40
|
-
self._x = val
|
|
41
|
-
self._dirty = True
|
|
42
|
-
|
|
43
|
-
@property
|
|
44
|
-
def y(self):
|
|
45
|
-
return self._y
|
|
46
|
-
|
|
47
|
-
@y.setter
|
|
48
|
-
def y(self, val):
|
|
49
|
-
if val != self._y:
|
|
50
|
-
self._y = val
|
|
51
|
-
self._dirty = True
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
################################
|
|
55
|
-
# Define some Processors:
|
|
56
|
-
################################
|
|
57
|
-
class MovementProcessor:
|
|
58
|
-
def __init__(self, minx, miny, maxx, maxy):
|
|
59
|
-
super().__init__()
|
|
60
|
-
self.minx = minx
|
|
61
|
-
self.miny = miny
|
|
62
|
-
self.maxx = maxx
|
|
63
|
-
self.maxy = maxy
|
|
64
|
-
|
|
65
|
-
def process(self):
|
|
66
|
-
# This will iterate over every Entity that has BOTH of these components:
|
|
67
|
-
for ent, (vel, rend) in esper.get_components(Velocity, Renderable):
|
|
68
|
-
# Update the Renderable Component's position by it's Velocity:
|
|
69
|
-
rend.x += vel.x
|
|
70
|
-
rend.y += vel.y
|
|
71
|
-
# An example of keeping the sprite inside screen boundaries. Basically,
|
|
72
|
-
# adjust the position back inside screen boundaries if it tries to go outside:
|
|
73
|
-
rend.x = max(self.minx, rend.x)
|
|
74
|
-
rend.y = max(self.miny, rend.y)
|
|
75
|
-
rend.x = min(self.maxx - rend.w, rend.x)
|
|
76
|
-
rend.y = min(self.maxy - rend.h, rend.y)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class TextureRenderProcessor():
|
|
80
|
-
def __init__(self, batch):
|
|
81
|
-
super().__init__()
|
|
82
|
-
self.batch = batch
|
|
83
|
-
|
|
84
|
-
def process(self):
|
|
85
|
-
# This will iterate over every Entity that has this Component, and
|
|
86
|
-
# add the texture associated with the Renderable Component instance
|
|
87
|
-
# and its vertice_list to the render batch. The batch will then be
|
|
88
|
-
# drawn by the 'on_draw' event handler of teh main window:
|
|
89
|
-
for entity, renderable in esper.get_component(Renderable):
|
|
90
|
-
self.draw_texture(renderable)
|
|
91
|
-
|
|
92
|
-
def draw_texture(self, renderable):
|
|
93
|
-
texture = renderable.texture
|
|
94
|
-
|
|
95
|
-
if renderable.vertex_list is None:
|
|
96
|
-
vertex_format = 'v2i/dynamic'
|
|
97
|
-
renderable.vertex_list = self.batch.add(4, GL_QUADS,
|
|
98
|
-
renderable.group,
|
|
99
|
-
vertex_format, 'c4B',
|
|
100
|
-
('t3f', texture.tex_coords))
|
|
101
|
-
|
|
102
|
-
if renderable._dirty:
|
|
103
|
-
x1 = renderable.x - texture.anchor_x
|
|
104
|
-
y1 = renderable.y - texture.anchor_y
|
|
105
|
-
x2 = x1 + texture.width
|
|
106
|
-
y2 = y1 + texture.height
|
|
107
|
-
renderable.vertex_list.vertices[:] = [x1, y1, x2, y1, x2, y2, x1, y2]
|
|
108
|
-
renderable.vertex_list.colors[:] = [255, 255, 255, 255] * 4
|
|
109
|
-
renderable._dirty = False
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
############################################
|
|
113
|
-
# Some pyglet helper classes and functions:
|
|
114
|
-
############################################
|
|
115
|
-
def texture_from_image(image_name):
|
|
116
|
-
"""Create a pyglet Texture from an image file"""
|
|
117
|
-
return pyglet.resource.image(image_name).get_texture()
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
# Code below cobbled together from
|
|
121
|
-
# https://pyglet.readthedocs.org/en/latest/programming_guide/graphics.html#hierarchical-state
|
|
122
|
-
# and pyglet.sprite.SpriteGroup
|
|
123
|
-
|
|
124
|
-
class TextureEnableGroup(pyglet.graphics.Group):
|
|
125
|
-
def set_state(self):
|
|
126
|
-
glEnable(GL_TEXTURE_2D)
|
|
127
|
-
|
|
128
|
-
def unset_state(self):
|
|
129
|
-
glDisable(GL_TEXTURE_2D)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
texture_enable_group = TextureEnableGroup()
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
class TextureBindGroup(pyglet.graphics.Group):
|
|
136
|
-
def __init__(self, texture):
|
|
137
|
-
super().__init__(parent=texture_enable_group)
|
|
138
|
-
assert texture.target == GL_TEXTURE_2D
|
|
139
|
-
self.texture = texture
|
|
140
|
-
self.blend_src = GL_SRC_ALPHA
|
|
141
|
-
self.blend_dest = GL_ONE_MINUS_SRC_ALPHA
|
|
142
|
-
|
|
143
|
-
def set_state(self):
|
|
144
|
-
glBindTexture(GL_TEXTURE_2D, self.texture.id)
|
|
145
|
-
glPushAttrib(GL_COLOR_BUFFER_BIT)
|
|
146
|
-
glEnable(GL_BLEND)
|
|
147
|
-
glBlendFunc(self.blend_src, self.blend_dest)
|
|
148
|
-
|
|
149
|
-
def unset_state(self):
|
|
150
|
-
glPopAttrib()
|
|
151
|
-
glDisable(self.texture.target)
|
|
152
|
-
|
|
153
|
-
def __eq__(self, other):
|
|
154
|
-
return (other.__class__ is self.__class__ and
|
|
155
|
-
self.parent is other.parent and
|
|
156
|
-
self.texture.target == other.texture.target and
|
|
157
|
-
self.texture.id == other.texture.id and
|
|
158
|
-
self.blend_src == other.blend_src and
|
|
159
|
-
self.blend_dest == other.blend_dest)
|
|
160
|
-
|
|
161
|
-
def __hash__(self):
|
|
162
|
-
return hash((id(self.parent),
|
|
163
|
-
self.texture.id, self.texture.target,
|
|
164
|
-
self.blend_src, self.blend_dest))
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
################################
|
|
168
|
-
# The main core of the program:
|
|
169
|
-
################################
|
|
170
|
-
def run(args=None):
|
|
171
|
-
# Initialize the main window stuff
|
|
172
|
-
window = pyglet.window.Window(width=RESOLUTION[0], height=RESOLUTION[1])
|
|
173
|
-
window.set_caption("Esper pyglet Example")
|
|
174
|
-
pyglet.gl.glClearColor(*BGCOLOR)
|
|
175
|
-
# pyglet graphics batch for efficient rendering
|
|
176
|
-
renderbatch = pyglet.graphics.Batch()
|
|
177
|
-
|
|
178
|
-
# Initialize Esper world, and create a "player" Entity with a few Components.
|
|
179
|
-
player = esper.create_entity()
|
|
180
|
-
esper.add_component(player, Velocity(x=0, y=0))
|
|
181
|
-
redsquare = Renderable(texture=texture_from_image("redsquare.png"),
|
|
182
|
-
width=64,
|
|
183
|
-
height=64,
|
|
184
|
-
posx=100,
|
|
185
|
-
posy=100)
|
|
186
|
-
esper.add_component(player, redsquare)
|
|
187
|
-
|
|
188
|
-
# Another motionless Entity:
|
|
189
|
-
enemy = esper.create_entity()
|
|
190
|
-
bluesquare = Renderable(texture=texture_from_image("bluesquare.png"),
|
|
191
|
-
width=64,
|
|
192
|
-
height=64,
|
|
193
|
-
posx=400,
|
|
194
|
-
posy=250)
|
|
195
|
-
esper.add_component(enemy, bluesquare)
|
|
196
|
-
|
|
197
|
-
# Create some Processor instances, and asign them to be processed.
|
|
198
|
-
render_processor = TextureRenderProcessor(batch=renderbatch)
|
|
199
|
-
movement_processor = MovementProcessor(minx=0, maxx=RESOLUTION[0], miny=0,
|
|
200
|
-
maxy=RESOLUTION[1])
|
|
201
|
-
|
|
202
|
-
@window.event
|
|
203
|
-
def on_key_press(symbol, modifiers):
|
|
204
|
-
if symbol == key.UP:
|
|
205
|
-
# Here is a way to directly access a specific Entity's Velocity
|
|
206
|
-
# Component's attribute (y) without making a temporary variable.
|
|
207
|
-
esper.component_for_entity(player, Velocity).y = 3
|
|
208
|
-
elif symbol == key.DOWN:
|
|
209
|
-
# For clarity, here is an alternate way in which a temporary variable
|
|
210
|
-
# is created and modified. The previous way above is recommended instead.
|
|
211
|
-
player_velocity_component = esper.component_for_entity(player, Velocity)
|
|
212
|
-
player_velocity_component.y = -3
|
|
213
|
-
elif symbol == key.LEFT:
|
|
214
|
-
esper.component_for_entity(player, Velocity).x = -3
|
|
215
|
-
elif symbol == key.RIGHT:
|
|
216
|
-
esper.component_for_entity(player, Velocity).x = 3
|
|
217
|
-
elif symbol == key.ESCAPE:
|
|
218
|
-
pyglet.app.exit()
|
|
219
|
-
|
|
220
|
-
@window.event
|
|
221
|
-
def on_key_release(symbol, modifiers):
|
|
222
|
-
if symbol in (key.UP, key.DOWN):
|
|
223
|
-
esper.component_for_entity(player, Velocity).y = 0
|
|
224
|
-
if symbol in (key.LEFT, key.RIGHT):
|
|
225
|
-
esper.component_for_entity(player, Velocity).x = 0
|
|
226
|
-
|
|
227
|
-
@window.event
|
|
228
|
-
def on_draw():
|
|
229
|
-
# Clear the window:
|
|
230
|
-
window.clear()
|
|
231
|
-
# Draw the batch of Renderables:
|
|
232
|
-
renderbatch.draw()
|
|
233
|
-
|
|
234
|
-
def update(dt):
|
|
235
|
-
# A single call to esper.process() will update all Processors:
|
|
236
|
-
render_processor.process()
|
|
237
|
-
movement_processor.process()
|
|
238
|
-
|
|
239
|
-
pyglet.clock.schedule_interval(update, 1.0 / FPS)
|
|
240
|
-
pyglet.app.run()
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if __name__ == "__main__":
|
|
244
|
-
import sys
|
|
245
|
-
sys.exit(run(sys.argv[1:]) or 0)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{esper-3.2 → esper-3.4}/make.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|