esper 2.2__tar.gz → 3.2__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.2/.github/ISSUE_TEMPLATE/question-or-comment.md +10 -0
- esper-3.2/.github/workflows/type-checking.yml +25 -0
- esper-3.2/.github/workflows/unit-tests.yml +25 -0
- esper-3.2/.gitignore +49 -0
- esper-3.2/.mypy.ini +20 -0
- esper-3.2/.readthedocs.yaml +16 -0
- esper-3.2/.ruff.toml +3 -0
- esper-2.2/LICENSE.rst → esper-3.2/LICENSE +2 -3
- esper-3.2/PKG-INFO +410 -0
- esper-3.2/README.md +399 -0
- {esper-2.2 → esper-3.2}/RELEASE_NOTES +57 -0
- esper-3.2/docs/index.rst +54 -0
- esper-3.2/esper/__init__.py +576 -0
- {esper-2.2 → esper-3.2}/examples/benchmark.py +9 -15
- {esper-2.2 → esper-3.2}/examples/benchmark_cache.py +14 -23
- {esper-2.2 → esper-3.2}/examples/headless_example.py +8 -16
- {esper-2.2 → esper-3.2}/examples/pygame_example.py +18 -20
- {esper-2.2 → esper-3.2}/examples/pyglet_example.py +16 -18
- {esper-2.2 → esper-3.2}/examples/pyglet_example_batch.py +18 -20
- {esper-2.2 → esper-3.2}/examples/pysdl2_example.py +18 -20
- esper-3.2/make.py +54 -0
- esper-3.2/pyproject.toml +15 -0
- esper-3.2/tests/test_world.py +561 -0
- esper-2.2/.github/workflows/python-package.yml +0 -26
- esper-2.2/.gitignore +0 -61
- esper-2.2/.travis.yml +0 -12
- esper-2.2/PKG-INFO +0 -382
- esper-2.2/README.rst +0 -360
- esper-2.2/docs/index.rst +0 -32
- esper-2.2/esper/__init__.py +0 -424
- esper-2.2/esper.egg-info/PKG-INFO +0 -382
- esper-2.2/esper.egg-info/SOURCES.txt +0 -32
- esper-2.2/esper.egg-info/dependency_links.txt +0 -1
- esper-2.2/esper.egg-info/top_level.txt +0 -1
- esper-2.2/setup.cfg +0 -4
- esper-2.2/setup.py +0 -32
- esper-2.2/tests/test_world.py +0 -433
- {esper-2.2 → esper-3.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {esper-2.2 → esper-3.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {esper-2.2 → esper-3.2}/MANIFEST.in +0 -0
- {esper-2.2 → esper-3.2}/docs/Makefile +0 -0
- {esper-2.2 → esper-3.2}/docs/conf.py +0 -0
- {esper-2.2 → esper-3.2}/docs/make.bat +0 -0
- {esper-2.2 → esper-3.2}/esper/py.typed +0 -0
- {esper-2.2 → esper-3.2}/examples/bluesquare.png +0 -0
- {esper-2.2 → esper-3.2}/examples/pythonista_ios_example.py +0 -0
- {esper-2.2 → esper-3.2}/examples/redsquare.png +0 -0
- {esper-2.2 → esper-3.2}/tests/__init__.py +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: type checking
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
typechecking:
|
|
10
|
+
runs-on: ${{ matrix.os }}
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
os: [ 'ubuntu-latest', 'macos-latest', 'windows-latest' ]
|
|
14
|
+
python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12-dev' ]
|
|
15
|
+
steps:
|
|
16
|
+
- name: Python ${{ matrix.python-version }} ${{ matrix.os }}
|
|
17
|
+
uses: actions/checkout@v3
|
|
18
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
19
|
+
uses: actions/setup-python@v4
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
- name: Install mypy
|
|
23
|
+
run: pip install mypy
|
|
24
|
+
- name: Run mypy
|
|
25
|
+
run: mypy -v esper
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: unit tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
tests:
|
|
10
|
+
runs-on: ${{ matrix.os }}
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
os: [ 'ubuntu-latest', 'macos-latest', 'windows-latest' ]
|
|
14
|
+
python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12-dev', 'pypy-3.10' ]
|
|
15
|
+
steps:
|
|
16
|
+
- name: Python ${{ matrix.python-version }} ${{ matrix.os }}
|
|
17
|
+
uses: actions/checkout@v3
|
|
18
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
19
|
+
uses: actions/setup-python@v4
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
- name: Install test dependencies
|
|
23
|
+
run: pip install pytest
|
|
24
|
+
- name: Run tests
|
|
25
|
+
run: pytest -v tests
|
esper-3.2/.gitignore
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Benchmark results
|
|
2
|
+
*.pickle
|
|
3
|
+
|
|
4
|
+
# Python byte code
|
|
5
|
+
*.py[co]
|
|
6
|
+
|
|
7
|
+
# C extensions
|
|
8
|
+
*.so
|
|
9
|
+
*.pyd
|
|
10
|
+
|
|
11
|
+
# Packages
|
|
12
|
+
*.egg
|
|
13
|
+
*.egg-info
|
|
14
|
+
dist
|
|
15
|
+
build
|
|
16
|
+
eggs
|
|
17
|
+
parts
|
|
18
|
+
bin
|
|
19
|
+
var
|
|
20
|
+
sdist
|
|
21
|
+
develop-eggs
|
|
22
|
+
.installed.cfg
|
|
23
|
+
lib
|
|
24
|
+
lib64
|
|
25
|
+
|
|
26
|
+
# Installer logs
|
|
27
|
+
pip-log.txt
|
|
28
|
+
|
|
29
|
+
# Unit test / coverage reports
|
|
30
|
+
.coverage
|
|
31
|
+
.tox
|
|
32
|
+
nosetests.xml
|
|
33
|
+
|
|
34
|
+
# Translations
|
|
35
|
+
*.mo
|
|
36
|
+
|
|
37
|
+
# Complexity
|
|
38
|
+
output/*.html
|
|
39
|
+
output/*/index.html
|
|
40
|
+
|
|
41
|
+
# Sphinx
|
|
42
|
+
docs/_build
|
|
43
|
+
|
|
44
|
+
# IDE files
|
|
45
|
+
.idea
|
|
46
|
+
.vscode
|
|
47
|
+
|
|
48
|
+
# Virtual environment
|
|
49
|
+
.env
|
esper-3.2/.mypy.ini
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[mypy]
|
|
2
|
+
warn_unused_configs = True
|
|
3
|
+
disallow_any_generics = True
|
|
4
|
+
disallow_subclassing_any = True
|
|
5
|
+
disallow_incomplete_defs = True
|
|
6
|
+
check_untyped_defs = True
|
|
7
|
+
disallow_untyped_decorators = True
|
|
8
|
+
warn_redundant_casts = True
|
|
9
|
+
warn_unused_ignores = True
|
|
10
|
+
warn_return_any = True
|
|
11
|
+
no_implicit_reexport = True
|
|
12
|
+
strict_equality = True
|
|
13
|
+
extra_checks = True
|
|
14
|
+
|
|
15
|
+
[mypy-esper]
|
|
16
|
+
disallow_untyped_defs = True
|
|
17
|
+
disallow_untyped_calls = True
|
|
18
|
+
|
|
19
|
+
[mypy-tests]
|
|
20
|
+
check_untyped_defs = True
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Read the Docs configuration file for Sphinx projects
|
|
2
|
+
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
|
3
|
+
|
|
4
|
+
# Required
|
|
5
|
+
version: 2
|
|
6
|
+
|
|
7
|
+
# Set the OS, Python version and other tools you might need
|
|
8
|
+
build:
|
|
9
|
+
os: ubuntu-22.04
|
|
10
|
+
tools:
|
|
11
|
+
python: "3.12"
|
|
12
|
+
|
|
13
|
+
# Build documentation in the "docs/" directory with Sphinx
|
|
14
|
+
sphinx:
|
|
15
|
+
configuration: docs/conf.py
|
|
16
|
+
|
esper-3.2/.ruff.toml
ADDED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
The MIT License
|
|
1
|
+
The MIT License (MIT)
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2023 Benjamin Moran
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
21
|
THE SOFTWARE.
|
|
22
|
-
|
esper-3.2/PKG-INFO
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: esper
|
|
3
|
+
Version: 3.2
|
|
4
|
+
Summary: esper is a lightweight Entity System (ECS) for Python, with a focus on performance
|
|
5
|
+
Author-email: Benjamin Moran <benmoran@protonmail.com>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Project-URL: Home, https://github.com/benmoran56/esper
|
|
10
|
+
|
|
11
|
+
[](https://pypi.python.org/pypi/esper)
|
|
12
|
+
[](https://esper.readthedocs.io)
|
|
13
|
+
[](https://github.com/benmoran56/esper/actions/workflows/unit-tests.yml)
|
|
14
|
+
|
|
15
|
+
Esper is a lightweight Entity System module for Python, with a focus on performance
|
|
16
|
+
===================================================================================
|
|
17
|
+
|
|
18
|
+
Esper is an MIT licensed Entity System, or, Entity Component System (ECS).
|
|
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,
|
|
21
|
+
while handling most common use cases.
|
|
22
|
+
|
|
23
|
+
For more information on the ECS pattern, you might find the following
|
|
24
|
+
resources interesting:
|
|
25
|
+
https://github.com/SanderMertens/ecs-faq/blob/master/README.md
|
|
26
|
+
https://github.com/jslee02/awesome-entity-component-system/blob/master/README.md
|
|
27
|
+
https://en.wikipedia.org/wiki/Entity_component_system
|
|
28
|
+
|
|
29
|
+
API documentation is hosted at ReadTheDocs: https://esper.readthedocs.io
|
|
30
|
+
Due to the small size of the project, this README currently serves as general usage
|
|
31
|
+
documentation.
|
|
32
|
+
|
|
33
|
+
> :warning: **Esper 3.0 introduces breaking changes**. Version 3.0 removes the
|
|
34
|
+
> World object, and migrates its methods to module level functions. Multiple
|
|
35
|
+
> contexts can be created and switched between. The v2.x README can be found
|
|
36
|
+
> here: https://github.com/benmoran56/esper/blob/v2_maintenance/README.md
|
|
37
|
+
|
|
38
|
+
- [Compatibility](#compatibility)
|
|
39
|
+
- [Installation](#installation)
|
|
40
|
+
- [Design](#design)
|
|
41
|
+
- [Quick Start](#quick-start)
|
|
42
|
+
- [General Usage](#general-usage)
|
|
43
|
+
* [Adding and Removing Processors](#adding-and-removing-processors)
|
|
44
|
+
* [Adding and Removing Components](#adding-and-removing-components)
|
|
45
|
+
* [Querying Specific Components](#querying-specific-components)
|
|
46
|
+
* [Boolean and Conditional Checks](#boolean-and-conditional-checks)
|
|
47
|
+
* [More Examples](#more-examples)
|
|
48
|
+
- [Event Dispatching](#event-dispatching)
|
|
49
|
+
- [Contributing](#contributing)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
Compatibility
|
|
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.
|
|
56
|
+
Automated testing is currently done for both CPython and PyPy3.
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
Installation
|
|
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*.
|
|
63
|
+
You can also install into your site-packages from PyPi via `pip`::
|
|
64
|
+
|
|
65
|
+
pip install --user --upgrade esper
|
|
66
|
+
|
|
67
|
+
Or from the source directory::
|
|
68
|
+
|
|
69
|
+
pip install . --user
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
Design
|
|
73
|
+
======
|
|
74
|
+
|
|
75
|
+
* World Context
|
|
76
|
+
|
|
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 Processesors, etc., by calling functions
|
|
79
|
+
on the `esper` module. Entities, Components and Processors can be created, assigned, or deleted
|
|
80
|
+
while your game is running. A simple call to `esper.process()` is all that's needed for each
|
|
81
|
+
iteration of your game loop. Advanced users can switch contexts, which can be useful for
|
|
82
|
+
isolating different game scenes that have different Processor requirements.
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
* Entities
|
|
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
|
+
|
|
92
|
+
|
|
93
|
+
* Components
|
|
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::
|
|
99
|
+
|
|
100
|
+
class Position:
|
|
101
|
+
def __init__(self, x=0.0, y=0.0):
|
|
102
|
+
self.x = x
|
|
103
|
+
self.y = y
|
|
104
|
+
|
|
105
|
+
To save on typing, the standard library dataclass decorator is quite useful.
|
|
106
|
+
https://docs.python.org/3/library/dataclasses.html#module-dataclasses
|
|
107
|
+
This decorator simplifies defining your Component classes. The attribute names don't need to
|
|
108
|
+
be repeated, and you can still instantiate the Component with positional or keyword arguments::
|
|
109
|
+
|
|
110
|
+
from dataclasses import dataclass as component
|
|
111
|
+
|
|
112
|
+
@component
|
|
113
|
+
class Position:
|
|
114
|
+
x: float = 0.0
|
|
115
|
+
y: float = 0.0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
* Processors
|
|
119
|
+
|
|
120
|
+
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*.
|
|
122
|
+
Other than that, there are no restrictions. You can define any additional methods you might need.
|
|
123
|
+
A simple Processor might look like::
|
|
124
|
+
|
|
125
|
+
class MovementProcessor(esper.Processor):
|
|
126
|
+
|
|
127
|
+
def process(self):
|
|
128
|
+
for ent, (vel, pos) in esper.get_components(Velocity, Position):
|
|
129
|
+
pos.x += vel.x
|
|
130
|
+
pos.y += vel.y
|
|
131
|
+
|
|
132
|
+
In the above code, you can see the standard usage of the *esper.get_components()* function. This
|
|
133
|
+
function allows efficient iteration over all Entities that contain the specified Component types.
|
|
134
|
+
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.
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
Quick Start
|
|
144
|
+
===========
|
|
145
|
+
|
|
146
|
+
To get started, simply import esper::
|
|
147
|
+
|
|
148
|
+
import esper
|
|
149
|
+
|
|
150
|
+
From there, define some Components, and create Entities that use them::
|
|
151
|
+
|
|
152
|
+
player = esper.create_entity()
|
|
153
|
+
esper.add_component(player, Velocity(x=0.9, y=1.2))
|
|
154
|
+
esper.add_component(player, Position(x=5, y=5))
|
|
155
|
+
|
|
156
|
+
Optionally, Component instances can be assigned directly to the Entity on creation::
|
|
157
|
+
|
|
158
|
+
player = esper.create_entity(Velocity(x=0.9, y=1.2), Position(x=5, y=5))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
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).
|
|
163
|
+
All Processors are priority "0" by default::
|
|
164
|
+
|
|
165
|
+
movement_processor = MovementProcessor()
|
|
166
|
+
collision_processor = CollisionProcessor()
|
|
167
|
+
rendering_processor = RenderingProcessor()
|
|
168
|
+
esper.add_processor(collision_processor, priority=2)
|
|
169
|
+
esper.add_processor(movement_processor, priority=3)
|
|
170
|
+
esper.add_processor(rendering_processor)
|
|
171
|
+
# or just add them in one line:
|
|
172
|
+
esper.add_processor(SomeProcessor())
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
Executing all Processors is done with a single call to esper.process(). This will call the
|
|
176
|
+
`process` method on all assigned Processors, in order of their priority. This is usually called
|
|
177
|
+
once per frame update of your game (every tick of the clock).::
|
|
178
|
+
|
|
179
|
+
esper.process()
|
|
180
|
+
|
|
181
|
+
|
|
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):*
|
|
187
|
+
This is appropriate for libraries such as **pyglet**, which automatically pass a delta time value
|
|
188
|
+
into scheduled functions.
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
General Usage
|
|
192
|
+
=============
|
|
193
|
+
|
|
194
|
+
World Contexts
|
|
195
|
+
--------------
|
|
196
|
+
Esper has the capability of supporting multiple "World" contexts. On import, a "default" World is
|
|
197
|
+
active. All creation of Entities, assignment of Processors, and all other operations occur within
|
|
198
|
+
the confines of the active World. In other words, the World contexts are completely isolated from
|
|
199
|
+
each other. For basic games and designs, you may not need to bother with this functionality. A
|
|
200
|
+
single default World context can often be enough. For advanced use cases, such as when different
|
|
201
|
+
scenes in your game have different Entities and Processor requirements, this functionality can be
|
|
202
|
+
quite useful. World context operations are done with the following functions::
|
|
203
|
+
*
|
|
204
|
+
* esper.list_worlds()
|
|
205
|
+
* esper.switch_world(name)
|
|
206
|
+
* esper.delete_world(name)
|
|
207
|
+
|
|
208
|
+
When switching Worlds, be mindful of the `name`. If a World doesn't exist, it will be created when
|
|
209
|
+
you first switch to it. You can delete old Worlds if they are no longer needed, but you can not
|
|
210
|
+
delete the currently active World.
|
|
211
|
+
|
|
212
|
+
Adding and Removing Processors
|
|
213
|
+
------------------------------
|
|
214
|
+
You have already seen examples of adding Processors in an earlier section. There is also a
|
|
215
|
+
*remove_processor* function available:
|
|
216
|
+
|
|
217
|
+
* esper.add_processor(processor_instance)
|
|
218
|
+
* esper.remove_processor(ProcessorClass)
|
|
219
|
+
|
|
220
|
+
Depending on the structure of your game, you may want to add or remove certain Processors when changing
|
|
221
|
+
scenes, etc.
|
|
222
|
+
|
|
223
|
+
Adding and Removing Components
|
|
224
|
+
------------------------------
|
|
225
|
+
In addition to adding Components to Entities when you're creating them, it's a common pattern to add or
|
|
226
|
+
remove Components inside your Processors. The following functions are available for this purpose:
|
|
227
|
+
|
|
228
|
+
* esper.add_component(entity_id, component_instance)
|
|
229
|
+
* esper.remove_component(entity_id, ComponentClass)
|
|
230
|
+
|
|
231
|
+
As an example of this, you could have a "Blink" component with a *duration* attribute. This can be used
|
|
232
|
+
to make certain things blink for s specific period of time, then disappear. For example, the code below
|
|
233
|
+
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::
|
|
235
|
+
|
|
236
|
+
class BlinkComponent:
|
|
237
|
+
def __init__(self, duration):
|
|
238
|
+
self.duration = duration
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
.....
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class CollisionProcessor(esper.Processor):
|
|
245
|
+
|
|
246
|
+
def process(self, dt):
|
|
247
|
+
for ent, enemy in esper.get_component(Enemy):
|
|
248
|
+
...
|
|
249
|
+
is_damaged = self._some_method()
|
|
250
|
+
if is_damaged:
|
|
251
|
+
esper.add_component(ent, BlinkComponent(duration=1))
|
|
252
|
+
...
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class BlinkProcessor(esper.Processor):
|
|
256
|
+
|
|
257
|
+
def process(self, dt):
|
|
258
|
+
for ent, (rend, blink) in esper.get_components(Renderable, BlinkComponent):
|
|
259
|
+
if blink.duration < 0:
|
|
260
|
+
# Times up. Remove the Component:
|
|
261
|
+
rend.sprite.visible = True
|
|
262
|
+
esper.remove_component(ent, BlinkComponent)
|
|
263
|
+
else:
|
|
264
|
+
blink.duration -= dt
|
|
265
|
+
# Toggle between visible and not visible each frame:
|
|
266
|
+
rend.sprite.visible = not rend.sprite.visible
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
Querying Specific Components
|
|
270
|
+
----------------------------
|
|
271
|
+
If you have an Entity ID and wish to query one specific, or ALL Components that are assigned
|
|
272
|
+
to it, the following functions are available:
|
|
273
|
+
|
|
274
|
+
* esper.component_for_entity
|
|
275
|
+
* esper.components_for_entity
|
|
276
|
+
|
|
277
|
+
The *component_for_entity* function is useful in a limited number of cases where you know a specific
|
|
278
|
+
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*
|
|
280
|
+
function that is explained in the next section. For example::
|
|
281
|
+
|
|
282
|
+
if esper.has_component(ent, SFX):
|
|
283
|
+
sfx = esper.component_for_entity(ent, SFX)
|
|
284
|
+
sfx.play()
|
|
285
|
+
|
|
286
|
+
The *components_for_entity* function is a special function that returns ALL the Components that are
|
|
287
|
+
assigned to a specific Entity, as a tuple. This is a heavy operation, and not something you would
|
|
288
|
+
want to do each frame or inside your `Processor.process` method. It can be useful, however, if
|
|
289
|
+
you wanted to transfer all of a specific Entity's Components between two separate contexts
|
|
290
|
+
(such as when changing Scenes, or levels). For example::
|
|
291
|
+
|
|
292
|
+
player_components = esper.components_for_entity(player_entity_id)
|
|
293
|
+
esper.switch_world('context_name')
|
|
294
|
+
player_entity_id = esper.create_entity(player_components)
|
|
295
|
+
|
|
296
|
+
Boolean and Conditional Checks
|
|
297
|
+
------------------------------
|
|
298
|
+
In some cases you may wish to check if an Entity has a specific Component before performing
|
|
299
|
+
some action. The following functions are available for this task:
|
|
300
|
+
|
|
301
|
+
* esper.has_component(entity, ComponentType)
|
|
302
|
+
* esper.has_components(entity, ComponentTypeA, ComponentTypeB)
|
|
303
|
+
* esper.try_component(entity, ComponentType)
|
|
304
|
+
* esper.try_components(entity, ComponentTypeA, ComponentTypeB)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
For example, you may want projectiles (and only projectiles) to disappear when hitting a wall in
|
|
308
|
+
your game. We can do this by checking if the Entity has a `Projectile` Component. We don't want
|
|
309
|
+
to do anything to this Component, simply check if it's there. Consider this example::
|
|
310
|
+
|
|
311
|
+
class CollisionProcessor(esper.Processor):
|
|
312
|
+
|
|
313
|
+
def process(self, dt):
|
|
314
|
+
for ent, body in esper.get_component(PhysicsBody):
|
|
315
|
+
...
|
|
316
|
+
colliding_with_wall = self._some_method(body):
|
|
317
|
+
if colliding_with_wall and esper.has_component(ent, Projectile):
|
|
318
|
+
esper.delete_entity(ent)
|
|
319
|
+
...
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
In a different scenario, we may want to perform some action on an Entity's Component, *if* it has
|
|
323
|
+
one. For example, a MovementProcessor that skips over Entities that have a `Stun` Component::
|
|
324
|
+
|
|
325
|
+
class MovementProcessor(esper.Processor):
|
|
326
|
+
|
|
327
|
+
def process(self, dt):
|
|
328
|
+
for ent, (body, vel) in esper.get_components(PhysicsBody, Velocity):
|
|
329
|
+
|
|
330
|
+
if esper.has_component(ent, Stun):
|
|
331
|
+
stun = esper.component_for_entity(ent, Stun)
|
|
332
|
+
stun.duration -= dt
|
|
333
|
+
if stun.duration <= 0:
|
|
334
|
+
esper.remove_component(ent, Stun)
|
|
335
|
+
continue # Continue to the next Entity
|
|
336
|
+
|
|
337
|
+
movement_code_here()
|
|
338
|
+
...
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
Let's look at the core part of the code::
|
|
342
|
+
|
|
343
|
+
if esper.has_component(ent, Stun):
|
|
344
|
+
stun = esper.component_for_entity(ent, Stun)
|
|
345
|
+
stun.duration -= dt
|
|
346
|
+
|
|
347
|
+
This code works fine, but the *try_component* function can accomplish the same thing with one
|
|
348
|
+
less function call. The following example will get a specific Component if it exists, or
|
|
349
|
+
return None if it does not::
|
|
350
|
+
|
|
351
|
+
stun = esper.try_component(ent, Stun)
|
|
352
|
+
if stun:
|
|
353
|
+
stun.duration -= dt
|
|
354
|
+
|
|
355
|
+
With Python 3.8+, the new "walrus" operator (`:=`) can also be used, making the `try_component`
|
|
356
|
+
functions even more concise ::
|
|
357
|
+
|
|
358
|
+
if stun := esper.try_component(ent, Stun):
|
|
359
|
+
stun.duration -= dt
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
More Examples
|
|
363
|
+
-------------
|
|
364
|
+
|
|
365
|
+
See the **/examples** folder to get an idea of how the basic structure of a game might look.
|
|
366
|
+
|
|
367
|
+
Event Dispatching
|
|
368
|
+
=================
|
|
369
|
+
|
|
370
|
+
Esper includes basic support for event dispatching and handling. This functionality is
|
|
371
|
+
provided by three functions to set (register), remove, and dispatch events. Minimal error
|
|
372
|
+
checking is done, so it's left up to the user to ensure correct naming and number of
|
|
373
|
+
arguments are used when dispatching and receiving events.
|
|
374
|
+
|
|
375
|
+
Events are dispatched by name::
|
|
376
|
+
|
|
377
|
+
esper.dispatch_event('event_name', arg1, arg2)
|
|
378
|
+
|
|
379
|
+
In order to receive the above event, you must register handlers. An event handler can be a
|
|
380
|
+
function or class method. Registering a handler is also done by name::
|
|
381
|
+
|
|
382
|
+
esper.set_handler('event_name', my_func)
|
|
383
|
+
# or
|
|
384
|
+
esper.set_handler('event_name', self.my_method)
|
|
385
|
+
|
|
386
|
+
**Note:** Only weak-references are kept to the registered handlers. If a handler is garbage
|
|
387
|
+
collected, it will be automatically un-registered by an internal callback.
|
|
388
|
+
|
|
389
|
+
Handlers can also be removed at any time, if you no longer want them to receive events::
|
|
390
|
+
|
|
391
|
+
esper.remove_handler('event_name', my_func)
|
|
392
|
+
# or
|
|
393
|
+
esper.remove_handler('event_name', self.my_method)
|
|
394
|
+
|
|
395
|
+
Registered events and handlers are part of the current `World` context.
|
|
396
|
+
|
|
397
|
+
Contributing
|
|
398
|
+
============
|
|
399
|
+
|
|
400
|
+
Contributions to Esper are always welcome, but there are some specific project goals to keep in mind:
|
|
401
|
+
|
|
402
|
+
- Pure Python code only: no binary extensions, Cython, etc.
|
|
403
|
+
- Try to target all non-EOL Python versions. Exceptions can be made if there is a compelling reason.
|
|
404
|
+
- Avoid bloat as much as possible. New features will be considered if they are commonly useful. Generally speaking, we don't want to add functionality that is better served by another module or library.
|
|
405
|
+
- Performance is preferrable to readability. The public API should remain clean, but ugly internal code is acceptable if it provides a performance benefit. Every cycle counts!
|
|
406
|
+
|
|
407
|
+
If you have any questions before contributing, feel free to [open an issue].
|
|
408
|
+
|
|
409
|
+
[open an issue]: https://github.com/benmoran56/esper/issues
|
|
410
|
+
|