Skip to main content

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 Entity handles
  • Components — works seamlessly with @dataclass(slots=True), optional on_added/on_removed lifecycle hooks
  • Queries — reusable query signatures, exclusion filters (Exclude(Dead)), and NOT_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)

Table of built distributions (wheels) for ecs-cub 0.1.7
File
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.28+ x86-64, Linux glibc 2.27+ 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 log

Release 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 log

Release 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 log

Release 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 log

Security 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.

Select from 4 files

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 log

A 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.

Release history Release notifications | RSS feed

This release

0.1.7 This release

4 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page