ecs-cub
A high-performance Entity Component System with a C++23 core and Python bindings via pybind11.
⚠️ Research project — not production ready. We're exploring, not shipping.
Features
- Entities — lightweight integer IDs with freelist reuse and optional generation-checked
Entityhandles - Components — works seamlessly with
@dataclass(slots=True), optionalon_added/on_removedlifecycle hooks - Queries — reusable query signatures, exclusion filters (
Exclude(Dead)), andNOT_ANY(A, B, C)NOR semantics - Systems — push model (ECS calls your
update()) or pull model (you ask for results when you want them); opt out of entity IDs when you don't need them - Tags — unique 1:1 string → entity mapping, O(1) lookup
- Groups — 1:many grouping, entities can belong to multiple groups simultaneously
- Events — signals (1:1 handler) and pub/sub (1:many subscribers); internal lifecycle events auto-published for entity/component changes
- Resources — Python-owned registry resources plus explicitly process-wide resources
- Deferred kills —
kill_entity()is deferred by default to prevent iterator invalidation during system updates - Marker components — filter by presence without passing the component into results, saving allocations per entity
Performance
- 65MB memory for 1,000,000 entities
- O(1) entity creation, component add/remove, tag and group lookups
- 32-bit bitmask signatures for fast system matching
- Sparse sets for component storage
Features
Entities
from ecs_cub.api import Health, Inventory, Position, Velocity, create_entity, create_entity_obj
# Lightweight integer IDs
entity_id = create_entity(Position(0, 0), Velocity(1, 0))
# Or a generation-checked wrapper that cannot alias a recycled entity ID
player = create_entity_obj(Position(0, 0), Health(100))
player.tag("player")
player.add_component(Inventory())
player.kill()
IDs are reused via a freelist (LIFO). Max 1,048,576 entities by default, configurable at compile time.
Components
from dataclasses import dataclass
from ecs_cub.api import Component
@dataclass(slots=True)
class Position(Component):
x: float
y: float
Works seamlessly with @dataclass(slots=True). Optional duck-typed lifecycle hooks:
def on_added(self, entity_id): ...
def on_removed(self, entity_id): ...
Queries
from ecs_cub.api import Exclude, create_query, get_components
query = create_query(Position, Velocity)
for entity_id, (pos, vel) in get_components(query):
pos.x += vel.dx
# Exclusion — entities with Position but NOT Dead
query = create_query(Position, Exclude(Dead))
Systems
Two patterns, pick what fits:
# Push model — ECS calls update() for you
class MovementSystem(SystemBase):
required_components = (Position, Velocity)
def update(self, results, dt):
for entity_id, (pos, vel) in results:
pos.x += vel.dx * dt
# Pull model — you ask for results when you want them
sys_idx = register_system(Position, Velocity)
results = get_system_results(sys_idx)
Skip entity IDs when you don't need them:
class DecaySystem(SystemBase):
required_components = (Health,)
include_entity_id = False # Skips Py_INCREF per entity — faster
Tags & Groups
# Tags — 1:1 unique mapping
tag_entity(entity_id, "player")
player_id = get_entity_by_tag("player")
# Groups — 1:many, entities can belong to multiple groups
group_entity(entity_id, "enemies")
group_entity(entity_id, "bosses") # same entity, two groups
enemies = get_entities_by_group("enemies")
Events & Pub/Sub
# Signals — 1:1 handler
connect("player_died", on_player_death)
fire("player_died", player_id)
# Pub/Sub — 1:many subscribers
subscribe("damage_dealt", log_damage)
subscribe("damage_dealt", update_ui)
publish("damage_dealt", entity_id, amount)
Internal lifecycle events are auto-published on _entity:created, _entity:killed, _component:added, and _component:removed.
Resources
Resources are indexed by their exact Python type. Registry-scoped resources stay in Python, so ordinary access does not cross the C++ boundary.
from dataclasses import dataclass
from ecs_cub.api import get_local_resource, set_resource
@dataclass(slots=True)
class FrameClock:
delta_time: float = 0.0
clock = FrameClock(delta_time=0.016)
set_resource(clock)
# Lookups return MaybeIDK; value() raises when the resource is absent.
dt = get_local_resource(FrameClock).value().delta_time
There is one resource slot per exact type, so setting another FrameClock replaces the existing value. Passing a class
instead of an instance default-constructs it with no arguments.
Process-wide resources survive clear_database() for things like input state or configuration:
from ecs_cub.api import get_global_resource, set_global_resource
@dataclass(slots=True)
class InputState:
jump: bool = False
set_global_resource(InputState)
input_state = get_global_resource(InputState).value()
Deferred Kills
kill_entity(entity_id) # Deferred — safe during system iteration
kill_entity(entity_id, immediate=True) # Immediate — use outside systems
Installation
uv sync
mask clean_and_build
Requires Python 3.15+ and a C++23 compiler.
Native C++ build
The public C++ API lives under src/ecs_cub/include and is consumed as, for example,
#include <ecs_cub/registry.hpp>. CMake consumers that add this repository as a subdirectory can link the
ecs_cub::ecs_cub target.
An installed package exposes the same native paths through Python:
import ecs_cub
ecs_cub.get_include() # Public C++ include directories
ecs_cub.get_libs() # Built Python extension files
ecs_cub.get_cmake_dir() # Directory containing EcsCubTarget.cmake
The CLI prints shell-friendly equivalents:
ecs-cub includes # -I/path/to/ecs_cub/include
ecs-cub includes --path-only
ecs-cub libs # One built extension path per line
ecs-cub cmake-dir
To consume an installed wheel from CMake:
execute_process(
COMMAND "${Python_EXECUTABLE}" -c "import ecs_cub; print(ecs_cub.get_cmake_dir())"
OUTPUT_VARIABLE ECS_CUB_CMAKE_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE
COMMAND_ERROR_IS_FATAL ANY
)
include("${ECS_CUB_CMAKE_DIR}/EcsCubTarget.cmake")
target_link_libraries(app PRIVATE ecs_cub::ecs_cub)
For a standalone Clang build against the sibling Fond checkout:
cmake -S . -B build-cmake \
-DCMAKE_CXX_COMPILER=clang++
cmake --build build-cmake --parallel
ctest --test-dir build-cmake --output-on-failure
Set FOND_ROOT when Fond is somewhere other than ../fond. The CMake-built Python extension is written to
<build>/python/ecs_cub/api; pass -DECS_CUB_BUILD_PYTHON_MODULE=OFF when only the native library is needed. Wheel builds
place their extension behind the same ecs_cub.api front door.
Enable diagnostic logging and strict Python callback errors with
-DECS_CUB_DEBUG_MODE=ON -DECS_CUB_VERBOSE_LOGGING=ON -DECS_CUB_STRICT=ON. The equivalent wheel workflow is
mask debug_build.
The native shutdown latch can be polled without running application logic inside a signal handler:
import signal
from ecs_cub.api import install_shutdown_signal, restore_shutdown_signal, shutdown_requested
install_shutdown_signal(signal.SIGTERM)
try:
while not shutdown_requested():
run_frame()
finally:
restore_shutdown_signal(signal.SIGTERM)
consume_shutdown_request() atomically reads and clears the latch. signal_shutdown is also compatible with
signal.signal() when Python-managed signal dispatch is preferred.
The GLFW/OpenGL map editor is an opt-in CMake target:
mask build map-editor
mask map-editor
Its executable lives at build-cmake/bin/map_editor; it runs with tools/map_editor as its working directory so the
existing maps, icons, and sample-game asset paths remain stable.
Metadata
Release files for ecs-cub 0.1.7
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| ecs_cub-0.1.7-cp315-cp315-win_amd64.whl | CPython 3.15 | CPython 3.15 | Windows x86-64 | Details |
| ecs_cub-0.1.7-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl | CPython 3.15 | CPython 3.15 | Linux glibc 2.27+ x86-64, Linux glibc 2.28+ x86-64 | Details |
| ecs_cub-0.1.7-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl | CPython 3.15 | CPython 3.15 | Linux glibc 2.26+ ARM64, Linux glibc 2.28+ ARM64 | Details |
| ecs_cub-0.1.7-cp315-cp315-macosx_15_0_arm64.whl | CPython 3.15 | CPython 3.15 | macOS 15.0+ ARM64 | Details |
Total release size: 9.1 MB
Release files / ecs_cub-0.1.7-cp315-cp315-win_amd64.whl
| Download URL | ecs_cub-0.1.7-cp315-cp315-win_amd64.whl |
|---|---|
| Size | 530.6 kB |
| Tags | CPython 3.15 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
880917d1554b7874545f332fd3cea4c2760f9b539f15f5b8fe65048a47d78767
|
|
BLAKE2b-256 checksum How to use checksums |
47416dea8d41ba3cdacab5521a2f9fb01a6d71025506c4012e512f2d04bf259f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.
Transparency logRelease files / ecs_cub-0.1.7-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
| Download URL | ecs_cub-0.1.7-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 4.1 MB |
| Tags | CPython 3.15 Linux glibc 2.27+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
5fc3b3dd861f2458ab8d2c2d2c42f4b917f84251aa2c48158210e541d1646362
|
|
BLAKE2b-256 checksum How to use checksums |
f9cdb15126ddbebaf17c22f89f7a941b4b855c879c597ffd74c48709988f2f71
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.
Transparency logRelease files / ecs_cub-0.1.7-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl
| Download URL | ecs_cub-0.1.7-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl |
|---|---|
| Size | 4.1 MB |
| Tags | CPython 3.15 Linux glibc 2.26+ ARM64 Linux glibc 2.28+ ARM64 |
|
SHA-256 checksum How to use checksums |
43df218cfd746c7ddac6e16ee2ef1ca24463b5d2894c6a5256cd751ddb1d4563
|
|
BLAKE2b-256 checksum How to use checksums |
d2a7b8dddc6d7eaa81d6b18ebe422bc4f50139dcb3dbc08fd2edf1fc2117817b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.
Transparency logRelease files / ecs_cub-0.1.7-cp315-cp315-macosx_15_0_arm64.whl
| Download URL | ecs_cub-0.1.7-cp315-cp315-macosx_15_0_arm64.whl |
|---|---|
| Size | 307.1 kB |
| Tags | CPython 3.15 macOS 15.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
91c7b16cab5c05ee796e7620b5df63b6de8044dfa1f96d69b472179c4f7c1bfe
|
|
BLAKE2b-256 checksum How to use checksums |
b6e94630e062e884ff421f239bc61610519c312e8f7ca7e28f8858f265a2c1f5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.
Transparency logSecurity for ecs-cub 0.1.7
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 4 files in this release have been uploaded with provenance metadata.
Attestation for ecs_cub-0.1.7-cp315-cp315-win_amd64.whl
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.