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.
Files changed (33) hide show
  1. {esper-3.3 → esper-3.4}/PKG-INFO +72 -48
  2. {esper-3.3 → esper-3.4}/README.md +70 -46
  3. {esper-3.3 → esper-3.4}/docs/index.rst +22 -4
  4. {esper-3.3 → esper-3.4}/esper/__init__.py +18 -16
  5. {esper-3.3 → esper-3.4}/tests/test_world.py +10 -0
  6. {esper-3.3 → esper-3.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  7. {esper-3.3 → esper-3.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  8. {esper-3.3 → esper-3.4}/.github/ISSUE_TEMPLATE/question-or-comment.md +0 -0
  9. {esper-3.3 → esper-3.4}/.github/workflows/type-checking.yml +0 -0
  10. {esper-3.3 → esper-3.4}/.github/workflows/unit-tests.yml +0 -0
  11. {esper-3.3 → esper-3.4}/.gitignore +0 -0
  12. {esper-3.3 → esper-3.4}/.mypy.ini +0 -0
  13. {esper-3.3 → esper-3.4}/.readthedocs.yaml +0 -0
  14. {esper-3.3 → esper-3.4}/.ruff.toml +0 -0
  15. {esper-3.3 → esper-3.4}/LICENSE +0 -0
  16. {esper-3.3 → esper-3.4}/MANIFEST.in +0 -0
  17. {esper-3.3 → esper-3.4}/RELEASE_NOTES +0 -0
  18. {esper-3.3 → esper-3.4}/docs/Makefile +0 -0
  19. {esper-3.3 → esper-3.4}/docs/conf.py +0 -0
  20. {esper-3.3 → esper-3.4}/docs/make.bat +0 -0
  21. {esper-3.3 → esper-3.4}/esper/py.typed +0 -0
  22. {esper-3.3 → esper-3.4}/examples/benchmark.py +0 -0
  23. {esper-3.3 → esper-3.4}/examples/benchmark_cache.py +0 -0
  24. {esper-3.3 → esper-3.4}/examples/bluesquare.png +0 -0
  25. {esper-3.3 → esper-3.4}/examples/headless_example.py +0 -0
  26. {esper-3.3 → esper-3.4}/examples/pygame_example.py +0 -0
  27. {esper-3.3 → esper-3.4}/examples/pyglet_example.py +0 -0
  28. {esper-3.3 → esper-3.4}/examples/pysdl2_example.py +0 -0
  29. {esper-3.3 → esper-3.4}/examples/pythonista_ios_example.py +0 -0
  30. {esper-3.3 → esper-3.4}/examples/redsquare.png +0 -0
  31. {esper-3.3 → esper-3.4}/make.py +0 -0
  32. {esper-3.3 → esper-3.4}/pyproject.toml +0 -0
  33. {esper-3.3 → esper-3.4}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: esper
3
- Version: 3.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
  [![rtd](https://readthedocs.org/projects/esper/badge/?version=latest)](https://esper.readthedocs.io)
13
13
  [![PyTest](https://github.com/benmoran56/esper/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/benmoran56/esper/actions/workflows/unit-tests.yml)
14
14
 
15
- Esper is a lightweight Entity System module for Python, with a focus on performance
15
+ esper is a lightweight Entity System module for Python, with a focus on performance
16
16
  ===================================================================================
17
17
 
18
- Esper is an MIT licensed Entity System, or, Entity Component System (ECS).
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: **Esper 3.0 introduces breaking changes**. Version 3.0 removes the
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
- 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.
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
- 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*.
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
- Esper uses the concept of "World" contexts. When you first `import esper`, a default context is
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 simple integer IDs (1, 2, 3, 4, etc.).
88
- Entities are "created", but they are generally not used directly. Instead, they are
89
- simply used as IDs in the internal Component database to track collections of Components.
90
- Creating an Entity is done with the `esper.create_entity()` function.
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
- design philosophy, they should not contain any logic. They might have initialization
97
- code or perhaps Python properties, but no processing logic whatsoever. A simple
98
- Component can be defined as::
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 *esper.Processor* class, and have a method called *process*.
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 *esper.get_components()* function. This
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: *(vel, pos)*. In addition the Components, you also
136
- get a reference to the Entity ID (the *ent* object) for the current pair of Velocity/Position
137
- Components. This entity ID can be useful in a variety of cases. For example, if your Processor
138
- will need to delete certain Entites, you can call the *esper.delete_entity()* function on
139
- this Entity ID. Another common use is if you wish to add or remove a Component on this Entity
140
- as a result of some condition being met.
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
- Esper for processing. You can specify an optional priority (higher numbers are processed first).
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(). This will call the
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 *esper.process()*, but you
183
- must also make sure to receive them properly in the *process()* methods of your Processors. For
184
- example, if you pass a delta time argument as *esper.process(dt)*, your Processor's *process()*
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
- Esper has the capability of supporting multiple "World" contexts. On import, a "default" World is
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
- *remove_processor* function available:
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 *duration* attribute. This can be used
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 *BlinkProcessor* handles the effect, and then removes the Component after the duration expires::
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 *component_for_entity* function is useful in a limited number of cases where you know a specific
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 *has_component*
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 *components_for_entity* function is a special function that returns ALL the Components that are
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 *try_component* function can accomplish the same thing with one
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 **/examples** folder to get an idea of how the basic structure of a game might look.
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
- Esper includes basic support for event dispatching and handling. This functionality is
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 Esper are always welcome, but there are some specific project goals to keep in mind:
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
  [![rtd](https://readthedocs.org/projects/esper/badge/?version=latest)](https://esper.readthedocs.io)
3
3
  [![PyTest](https://github.com/benmoran56/esper/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/benmoran56/esper/actions/workflows/unit-tests.yml)
4
4
 
5
- Esper is a lightweight Entity System module for Python, with a focus on performance
5
+ esper is a lightweight Entity System module for Python, with a focus on performance
6
6
  ===================================================================================
7
7
 
8
- Esper is an MIT licensed Entity System, or, Entity Component System (ECS).
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: **Esper 3.0 introduces breaking changes**. Version 3.0 removes the
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
- 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.
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
- 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*.
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
- Esper uses the concept of "World" contexts. When you first `import esper`, a default context is
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 simple integer IDs (1, 2, 3, 4, etc.).
78
- Entities are "created", but they are generally not used directly. Instead, they are
79
- simply used as IDs in the internal Component database to track collections of Components.
80
- Creating an Entity is done with the `esper.create_entity()` function.
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
- design philosophy, they should not contain any logic. They might have initialization
87
- code or perhaps Python properties, but no processing logic whatsoever. A simple
88
- Component can be defined as::
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 *esper.Processor* class, and have a method called *process*.
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 *esper.get_components()* function. This
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: *(vel, pos)*. In addition the Components, you also
126
- get a reference to the Entity ID (the *ent* object) for the current pair of Velocity/Position
127
- Components. This entity ID can be useful in a variety of cases. For example, if your Processor
128
- will need to delete certain Entites, you can call the *esper.delete_entity()* function on
129
- this Entity ID. Another common use is if you wish to add or remove a Component on this Entity
130
- as a result of some condition being met.
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
- Esper for processing. You can specify an optional priority (higher numbers are processed first).
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(). This will call the
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 *esper.process()*, but you
173
- must also make sure to receive them properly in the *process()* methods of your Processors. For
174
- example, if you pass a delta time argument as *esper.process(dt)*, your Processor's *process()*
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
- Esper has the capability of supporting multiple "World" contexts. On import, a "default" World is
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
- *remove_processor* function available:
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 *duration* attribute. This can be used
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 *BlinkProcessor* handles the effect, and then removes the Component after the duration expires::
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 *component_for_entity* function is useful in a limited number of cases where you know a specific
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 *has_component*
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 *components_for_entity* function is a special function that returns ALL the Components that are
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 *try_component* function can accomplish the same thing with one
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 **/examples** folder to get an idea of how the basic structure of a game might look.
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
- Esper includes basic support for event dispatching and handling. This functionality is
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 Esper are always welcome, but there are some specific project goals to keep in mind:
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. Instead, a normal Python class can be used.
13
- Also, while it's not required, the the `@dataclass` decorator
14
- from the `dataclasses` module can be useful to help write
15
- compact Component classes.
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.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
- if func not in event_registry.get(name, []):
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(func)
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 funcion returns an Entity ID, which is a plain integer.
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` funcion.
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
- return all(comp_type in _entities[entity] for comp_type in component_types)
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 a multiple component types for an Entity.
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
- call (in milliseconds) in the`esper.process_times` dictionary.
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
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