microecs 0.2.2__tar.gz → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: microecs
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: MicroECS: Minimal Entity Component System (ECS) in python and numpy
5
5
  Home-page: https://gitlab.com/meehai/microecs
6
6
  License: MIT
@@ -20,25 +20,26 @@ Usage:
20
20
  - Sandbox: `python main.py`
21
21
  - Tests: `pytest test/`
22
22
 
23
- There are only 5 primitives (bottom up): `Component`, `Pool`, `QueryResult`, `World` and `System`.
23
+ There are only 4 primitives (bottom up): `Component`, `Pool`, `QueryResult`, `World`:
24
24
 
25
- - `Component` is a simple python dataclass holding only data. All entries must be numpy arrays with metadata fields: shape and dtype. We support 5 dtypes only: `int32`, `float32`, `bool`, `str` and `object`.
25
+ - `Component` is a simple python dataclass holding only data. All entries must be numpy arrays with metadata fields: shape and dtype. We support 5 dtypes only: `int32`, `float32`, `bool`, `str` and `object`. A component with no fields is a valid **tag** for querying (e.g. `class Frozen(Component): pass`).
26
26
  - `Pool` is a simple 'archetype' dynamic array, holding entities of the same type (same set of components). Usses `Components` metadata to construct contiguous arrays for all entities of the same type.
27
27
  - `QueryResult` is a list of pools that match some query on all the entities of the `World`. It acts as a contiguous numpy-like container that implements numpy's `__array_function__` and `__array_ufunc__`. For all intents and purposes it should feel like a `(N, ...)` view over all the entities. If you need a numpy array (not all ops are supported, for e.g. indexing on the first axis), use `QueryResult.numpy()`. It also exposes `entity_ids`: a flat `(N,)` array of the matched entities' ids, in the same pool-by-pool order as the fields, so you can `zip(qr.entity_ids, qr.position)` or feed an id back to `world.get_entity` / `world.remove_entity`.
28
- - `World` is a manager of `Pools` and has an overview of all the entities in the scene. It also manages the migration of entities from one pool to the other.
29
- - `System` is an abstract class that queries the `World` for a subset of `Pools` matching some components. It updates the entities in these pools given some logic (e.g. collisions, motion physics or simply calls the drawing functions). They are merely a convention, not tied to `World` per se.
28
+ - `World` is a manager of `Pools` and has an overview of all the entities in the scene. It also manages the migration of entities from one pool to the other. A `World` can also require extra metadata keys on every field via `World(extra_field_metadata=["serializable"])`, to enforce component-level behavior such as field serialization.
30
29
 
31
30
  Few relevant concepts:
32
31
 
33
32
  - `Pool` operates on array indices, while `World` operates on entity IDs (also integers). This allows seamless movement between pools while the high-level systems still working as intended.
34
33
  - All mutable operations on `World` are lazy. These are: `add_entity`, `remove_entity`, `add_component`, `remove_component`. They are added to a command buffer which is only executed when calling `world.update()`.
34
+ - `Systems` are a convention, they are not part of this library. They can be defined at application level and act as hooks or callbacks. The `World` object doesn't need to know more than entities and components.
35
35
 
36
36
  Super simplified main loop structure:
37
37
 
38
38
  ```python
39
+ from typing import Callable
39
40
  import numpy as np
40
41
  import raylib as rl
41
- from microecs import World, Component, TickSystem
42
+ from microecs import World, Component
42
43
 
43
44
  # components
44
45
  class HasPosition(Component):
@@ -48,15 +49,15 @@ class HasVelocity(Component):
48
49
  class HasColor(Component):
49
50
  color: np.ndarray = field(metadata={"shape": (4, ), "dtype": "int32"})
50
51
 
51
- # systems
52
- class RenderSystem(TickSystem):
53
- def on_tick(self, world: World): # must override
52
+ # systems: Note they are a convention!
53
+ class RenderSystem:
54
+ def __call__(self, world: World): # must override
54
55
  query_result = world.query_and((HasPosition, HasColor)) # contiguous-like view of all entities matching
55
56
  for position, color in zip(query_result.position, query_result.color): # draw each entity
56
57
  DrawEntity(position, color)
57
58
 
58
- class MotionSystem(TickSystem)
59
- def on_tick(self, world: World): # must override
59
+ class MotionSystem:
60
+ def __call__(self, world: World): # must override
60
61
  qr = world.query_and((HasPosition, HasVelocity))
61
62
  qr.position[:] = qr.position + qr.velocity * DT # writes back to all the underlying pools using numpy's rules
62
63
  # Alternative for per-pool update. Less ergonomic, but maybe faster in extreme cases as it avoids the _Field obj
@@ -64,10 +65,10 @@ class MotionSystem(TickSystem)
64
65
  pool.position[:] = pool.position + pool.velocity * DT
65
66
 
66
67
  def main():
67
- render_system = RenderSystem()
68
- update_systems: list[TickSystem] = [MotionSystem()]
68
+ render_system: list[Callable] = RenderSystem()
69
+ update_systems: list[Callable] = [MotionSystem()]
69
70
 
70
- world = World(components=[HasPosition, HasColor, HasVelocity])
71
+ world = World(components=[HasPosition, HasColor, HasVelocity], extra_field_metadata=None)
71
72
  for _ in range(n_objects):
72
73
  # NOTE: world.{add/remove}_{entity/component} are lazy. They take effect after the first world.update() call.
73
74
  world.add_entity(components=(HasPosition, HasVelocity, HasColor), # tuple of components (types)
@@ -78,11 +79,11 @@ def main():
78
79
  while not rl.WindowShouldClose():
79
80
  world.update() # must be called at each tick so the lazy methods are processed and entities are updated
80
81
  # update stuff...
81
- _ = [system.on_tick(world=world) for system in update_systems]
82
+ _ = [system(world=world) for system in update_systems]
82
83
  # draw stuff, e.g. using raylib
83
84
  rl.BeginDrawing()
84
85
  rl.ClearBackground(rl.RAYWHITE)
85
86
  rl.DrawFPS(rl.GetScreenWidth() - 100, 0)
86
- render_system.on_tick(world=world)
87
+ render_system(world=world)
87
88
  rl.EndDrawing()
88
89
  ```
@@ -8,25 +8,26 @@ Usage:
8
8
  - Sandbox: `python main.py`
9
9
  - Tests: `pytest test/`
10
10
 
11
- There are only 5 primitives (bottom up): `Component`, `Pool`, `QueryResult`, `World` and `System`.
11
+ There are only 4 primitives (bottom up): `Component`, `Pool`, `QueryResult`, `World`:
12
12
 
13
- - `Component` is a simple python dataclass holding only data. All entries must be numpy arrays with metadata fields: shape and dtype. We support 5 dtypes only: `int32`, `float32`, `bool`, `str` and `object`.
13
+ - `Component` is a simple python dataclass holding only data. All entries must be numpy arrays with metadata fields: shape and dtype. We support 5 dtypes only: `int32`, `float32`, `bool`, `str` and `object`. A component with no fields is a valid **tag** for querying (e.g. `class Frozen(Component): pass`).
14
14
  - `Pool` is a simple 'archetype' dynamic array, holding entities of the same type (same set of components). Usses `Components` metadata to construct contiguous arrays for all entities of the same type.
15
15
  - `QueryResult` is a list of pools that match some query on all the entities of the `World`. It acts as a contiguous numpy-like container that implements numpy's `__array_function__` and `__array_ufunc__`. For all intents and purposes it should feel like a `(N, ...)` view over all the entities. If you need a numpy array (not all ops are supported, for e.g. indexing on the first axis), use `QueryResult.numpy()`. It also exposes `entity_ids`: a flat `(N,)` array of the matched entities' ids, in the same pool-by-pool order as the fields, so you can `zip(qr.entity_ids, qr.position)` or feed an id back to `world.get_entity` / `world.remove_entity`.
16
- - `World` is a manager of `Pools` and has an overview of all the entities in the scene. It also manages the migration of entities from one pool to the other.
17
- - `System` is an abstract class that queries the `World` for a subset of `Pools` matching some components. It updates the entities in these pools given some logic (e.g. collisions, motion physics or simply calls the drawing functions). They are merely a convention, not tied to `World` per se.
16
+ - `World` is a manager of `Pools` and has an overview of all the entities in the scene. It also manages the migration of entities from one pool to the other. A `World` can also require extra metadata keys on every field via `World(extra_field_metadata=["serializable"])`, to enforce component-level behavior such as field serialization.
18
17
 
19
18
  Few relevant concepts:
20
19
 
21
20
  - `Pool` operates on array indices, while `World` operates on entity IDs (also integers). This allows seamless movement between pools while the high-level systems still working as intended.
22
21
  - All mutable operations on `World` are lazy. These are: `add_entity`, `remove_entity`, `add_component`, `remove_component`. They are added to a command buffer which is only executed when calling `world.update()`.
22
+ - `Systems` are a convention, they are not part of this library. They can be defined at application level and act as hooks or callbacks. The `World` object doesn't need to know more than entities and components.
23
23
 
24
24
  Super simplified main loop structure:
25
25
 
26
26
  ```python
27
+ from typing import Callable
27
28
  import numpy as np
28
29
  import raylib as rl
29
- from microecs import World, Component, TickSystem
30
+ from microecs import World, Component
30
31
 
31
32
  # components
32
33
  class HasPosition(Component):
@@ -36,15 +37,15 @@ class HasVelocity(Component):
36
37
  class HasColor(Component):
37
38
  color: np.ndarray = field(metadata={"shape": (4, ), "dtype": "int32"})
38
39
 
39
- # systems
40
- class RenderSystem(TickSystem):
41
- def on_tick(self, world: World): # must override
40
+ # systems: Note they are a convention!
41
+ class RenderSystem:
42
+ def __call__(self, world: World): # must override
42
43
  query_result = world.query_and((HasPosition, HasColor)) # contiguous-like view of all entities matching
43
44
  for position, color in zip(query_result.position, query_result.color): # draw each entity
44
45
  DrawEntity(position, color)
45
46
 
46
- class MotionSystem(TickSystem)
47
- def on_tick(self, world: World): # must override
47
+ class MotionSystem:
48
+ def __call__(self, world: World): # must override
48
49
  qr = world.query_and((HasPosition, HasVelocity))
49
50
  qr.position[:] = qr.position + qr.velocity * DT # writes back to all the underlying pools using numpy's rules
50
51
  # Alternative for per-pool update. Less ergonomic, but maybe faster in extreme cases as it avoids the _Field obj
@@ -52,10 +53,10 @@ class MotionSystem(TickSystem)
52
53
  pool.position[:] = pool.position + pool.velocity * DT
53
54
 
54
55
  def main():
55
- render_system = RenderSystem()
56
- update_systems: list[TickSystem] = [MotionSystem()]
56
+ render_system: list[Callable] = RenderSystem()
57
+ update_systems: list[Callable] = [MotionSystem()]
57
58
 
58
- world = World(components=[HasPosition, HasColor, HasVelocity])
59
+ world = World(components=[HasPosition, HasColor, HasVelocity], extra_field_metadata=None)
59
60
  for _ in range(n_objects):
60
61
  # NOTE: world.{add/remove}_{entity/component} are lazy. They take effect after the first world.update() call.
61
62
  world.add_entity(components=(HasPosition, HasVelocity, HasColor), # tuple of components (types)
@@ -66,11 +67,11 @@ def main():
66
67
  while not rl.WindowShouldClose():
67
68
  world.update() # must be called at each tick so the lazy methods are processed and entities are updated
68
69
  # update stuff...
69
- _ = [system.on_tick(world=world) for system in update_systems]
70
+ _ = [system(world=world) for system in update_systems]
70
71
  # draw stuff, e.g. using raylib
71
72
  rl.BeginDrawing()
72
73
  rl.ClearBackground(rl.RAYWHITE)
73
74
  rl.DrawFPS(rl.GetScreenWidth() - 100, 0)
74
- render_system.on_tick(world=world)
75
+ render_system(world=world)
75
76
  rl.EndDrawing()
76
77
  ```
@@ -4,6 +4,5 @@ from .component import Component
4
4
  from .pool import Pool
5
5
  from .query_result import QueryResult
6
6
  from .world import World
7
- from .system import TickSystem
8
7
 
9
- __all__ = ["Component", "Pool", "QueryResult", "World", "TickSystem"]
8
+ __all__ = ["Component", "Pool", "QueryResult", "World"]
@@ -70,10 +70,16 @@ class _Field(np.lib.mixins.NDArrayOperatorsMixin):
70
70
  for v, chunk in zip(views, np.split(full, np.cumsum(self._lens)[:-1])):
71
71
  v[:] = chunk
72
72
 
73
- # qr.position[:, 0] (or any >1 axis as well as slices not exact indices) is allowed. qr.position[0] is not.
74
73
  def __getitem__(self, key):
74
+ if isinstance(key, (int, np.integer)): # qr.position[i] -> entity i's row, from its own pool
75
+ key = range(self.len)[key] # numpy's exact int-index rule, for free: wraps neg, raises on OOB
76
+ pool_ix = int(np.searchsorted(self._bounds, key, side="right")) - 1
77
+ return self.parts[pool_ix][key - self._bounds[pool_ix]]
78
+
75
79
  if not (isinstance(key, tuple) and key and key[0] == slice(None)):
76
- raise TypeError("entity-axis indexing crosses pools; use [:, k]. Use .numpy() to get a proper np.ndarray.")
80
+ raise TypeError("Unsupported indexing. Use .numpy() for a proper array. To set items, use qr.attr[:, k]=xxx"
81
+ ". For fancy indexing qr[i, 0:3, k], use qr[i][0:3, k].")
82
+
77
83
  return _Field([part[key] for part in self.parts])
78
84
 
79
85
  def __iter__(self):
@@ -9,8 +9,14 @@ from .component import ComponentType
9
9
  from .query_result import QueryResult
10
10
 
11
11
  class World:
12
- """Generic container for pools of components. Newly added components are assigned a unique id and go in a pool"""
13
- def __init__(self, components: list[ComponentType]):
12
+ """
13
+ Generic container for pools of components. Newly added components are assigned a unique id and go in a pool
14
+ Parameters
15
+ - components The list of components that the world accepts
16
+ - extra_field_metadata The list of required extra metadata for each field besides shape and dtype.
17
+ """
18
+ def __init__(self, components: list[ComponentType], extra_field_metadata: list[str] | None = None):
19
+ self.extra_field_metadata = extra_field_metadata or []
14
20
  self._check_components(components)
15
21
  self.pools: dict[PoolKey, Pool] = {}
16
22
  self.pool_to_components: dict[Pool, list[ComponentType]] = {}
@@ -178,7 +184,7 @@ class World:
178
184
  hints = get_type_hints(component) # make it work with from __future__ import annotations
179
185
  for field_name, _field in component.__dataclass_fields__.items():
180
186
  assert hints[field_name] is np.ndarray, f"Field '{field_name}' of '{component=}' not an array: {_field}"
181
- assert _field.metadata.keys() == {"shape", "dtype"}, _field.metadata
187
+ assert _field.metadata.keys() == {"shape", "dtype", *self.extra_field_metadata}, _field.metadata
182
188
  assert isinstance(_field.metadata["shape"], tuple), _field.metadata["shape"]
183
189
  assert isinstance(fmd := _field.metadata["dtype"], str) and fmd in _dtypes, f"{fmd} not in {_dtypes}"
184
190
  assert field_name not in _query_result_reserved_names, f"Field '{field_name}' in {_qres}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: microecs
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: MicroECS: Minimal Entity Component System (ECS) in python and numpy
5
5
  Home-page: https://gitlab.com/meehai/microecs
6
6
  License: MIT
@@ -20,25 +20,26 @@ Usage:
20
20
  - Sandbox: `python main.py`
21
21
  - Tests: `pytest test/`
22
22
 
23
- There are only 5 primitives (bottom up): `Component`, `Pool`, `QueryResult`, `World` and `System`.
23
+ There are only 4 primitives (bottom up): `Component`, `Pool`, `QueryResult`, `World`:
24
24
 
25
- - `Component` is a simple python dataclass holding only data. All entries must be numpy arrays with metadata fields: shape and dtype. We support 5 dtypes only: `int32`, `float32`, `bool`, `str` and `object`.
25
+ - `Component` is a simple python dataclass holding only data. All entries must be numpy arrays with metadata fields: shape and dtype. We support 5 dtypes only: `int32`, `float32`, `bool`, `str` and `object`. A component with no fields is a valid **tag** for querying (e.g. `class Frozen(Component): pass`).
26
26
  - `Pool` is a simple 'archetype' dynamic array, holding entities of the same type (same set of components). Usses `Components` metadata to construct contiguous arrays for all entities of the same type.
27
27
  - `QueryResult` is a list of pools that match some query on all the entities of the `World`. It acts as a contiguous numpy-like container that implements numpy's `__array_function__` and `__array_ufunc__`. For all intents and purposes it should feel like a `(N, ...)` view over all the entities. If you need a numpy array (not all ops are supported, for e.g. indexing on the first axis), use `QueryResult.numpy()`. It also exposes `entity_ids`: a flat `(N,)` array of the matched entities' ids, in the same pool-by-pool order as the fields, so you can `zip(qr.entity_ids, qr.position)` or feed an id back to `world.get_entity` / `world.remove_entity`.
28
- - `World` is a manager of `Pools` and has an overview of all the entities in the scene. It also manages the migration of entities from one pool to the other.
29
- - `System` is an abstract class that queries the `World` for a subset of `Pools` matching some components. It updates the entities in these pools given some logic (e.g. collisions, motion physics or simply calls the drawing functions). They are merely a convention, not tied to `World` per se.
28
+ - `World` is a manager of `Pools` and has an overview of all the entities in the scene. It also manages the migration of entities from one pool to the other. A `World` can also require extra metadata keys on every field via `World(extra_field_metadata=["serializable"])`, to enforce component-level behavior such as field serialization.
30
29
 
31
30
  Few relevant concepts:
32
31
 
33
32
  - `Pool` operates on array indices, while `World` operates on entity IDs (also integers). This allows seamless movement between pools while the high-level systems still working as intended.
34
33
  - All mutable operations on `World` are lazy. These are: `add_entity`, `remove_entity`, `add_component`, `remove_component`. They are added to a command buffer which is only executed when calling `world.update()`.
34
+ - `Systems` are a convention, they are not part of this library. They can be defined at application level and act as hooks or callbacks. The `World` object doesn't need to know more than entities and components.
35
35
 
36
36
  Super simplified main loop structure:
37
37
 
38
38
  ```python
39
+ from typing import Callable
39
40
  import numpy as np
40
41
  import raylib as rl
41
- from microecs import World, Component, TickSystem
42
+ from microecs import World, Component
42
43
 
43
44
  # components
44
45
  class HasPosition(Component):
@@ -48,15 +49,15 @@ class HasVelocity(Component):
48
49
  class HasColor(Component):
49
50
  color: np.ndarray = field(metadata={"shape": (4, ), "dtype": "int32"})
50
51
 
51
- # systems
52
- class RenderSystem(TickSystem):
53
- def on_tick(self, world: World): # must override
52
+ # systems: Note they are a convention!
53
+ class RenderSystem:
54
+ def __call__(self, world: World): # must override
54
55
  query_result = world.query_and((HasPosition, HasColor)) # contiguous-like view of all entities matching
55
56
  for position, color in zip(query_result.position, query_result.color): # draw each entity
56
57
  DrawEntity(position, color)
57
58
 
58
- class MotionSystem(TickSystem)
59
- def on_tick(self, world: World): # must override
59
+ class MotionSystem:
60
+ def __call__(self, world: World): # must override
60
61
  qr = world.query_and((HasPosition, HasVelocity))
61
62
  qr.position[:] = qr.position + qr.velocity * DT # writes back to all the underlying pools using numpy's rules
62
63
  # Alternative for per-pool update. Less ergonomic, but maybe faster in extreme cases as it avoids the _Field obj
@@ -64,10 +65,10 @@ class MotionSystem(TickSystem)
64
65
  pool.position[:] = pool.position + pool.velocity * DT
65
66
 
66
67
  def main():
67
- render_system = RenderSystem()
68
- update_systems: list[TickSystem] = [MotionSystem()]
68
+ render_system: list[Callable] = RenderSystem()
69
+ update_systems: list[Callable] = [MotionSystem()]
69
70
 
70
- world = World(components=[HasPosition, HasColor, HasVelocity])
71
+ world = World(components=[HasPosition, HasColor, HasVelocity], extra_field_metadata=None)
71
72
  for _ in range(n_objects):
72
73
  # NOTE: world.{add/remove}_{entity/component} are lazy. They take effect after the first world.update() call.
73
74
  world.add_entity(components=(HasPosition, HasVelocity, HasColor), # tuple of components (types)
@@ -78,11 +79,11 @@ def main():
78
79
  while not rl.WindowShouldClose():
79
80
  world.update() # must be called at each tick so the lazy methods are processed and entities are updated
80
81
  # update stuff...
81
- _ = [system.on_tick(world=world) for system in update_systems]
82
+ _ = [system(world=world) for system in update_systems]
82
83
  # draw stuff, e.g. using raylib
83
84
  rl.BeginDrawing()
84
85
  rl.ClearBackground(rl.RAYWHITE)
85
86
  rl.DrawFPS(rl.GetScreenWidth() - 100, 0)
86
- render_system.on_tick(world=world)
87
+ render_system(world=world)
87
88
  rl.EndDrawing()
88
89
  ```
@@ -5,7 +5,6 @@ microecs/__init__.py
5
5
  microecs/component.py
6
6
  microecs/pool.py
7
7
  microecs/query_result.py
8
- microecs/system.py
9
8
  microecs/utils.py
10
9
  microecs/world.py
11
10
  microecs.egg-info/PKG-INFO
@@ -3,7 +3,7 @@ from pathlib import Path
3
3
  from setuptools import setup, find_packages
4
4
 
5
5
  NAME = "microecs"
6
- VERSION = "0.2.2"
6
+ VERSION = "0.2.4"
7
7
  DESCRIPTION = "MicroECS: Minimal Entity Component System (ECS) in python and numpy"
8
8
  URL = "https://gitlab.com/meehai/microecs"
9
9
 
@@ -1,10 +0,0 @@
1
- """systems.py - Interfaces for various types of systems in ECS"""
2
- from abc import ABC, abstractmethod
3
-
4
- from .world import World
5
-
6
- class TickSystem(ABC):
7
- """Generic tick-level system. Called inside the hot main engine loop on every tick"""
8
- @abstractmethod
9
- def on_tick(self, scene: World):
10
- """callback called on each tick"""
File without changes
File without changes
File without changes
File without changes
File without changes