esper

repository·master·Indexed 20 days ago

https://github.com/benmoran56/esper

A lightweight, high-performance Entity Component System (ECS) for Python. It separates data (Components) from logic (Processors) and manages them within isolated World contexts. esper allows developers to build complex game logic using a system of entities (integer IDs), components (data classes), and processors (logic classes inheriting from esper.Processor). It includes a lightweight event system and supports multiple world contexts for managing different game scenes.

Tokens
3.9K
Snippets
11
Records
16
Agent score
68%

What's inside esper

  1. Define Components in esper

    master

    In esper, a Component is any valid Python class. There is no specific base class required.

    Common patterns for defining components include:

    • Standard Classes: Best for components that require complex initialization or internal state management.
    • Dataclasses: Using the @dataclass decorator from the dataclasses module is recommended for compact, data-focused definitions.
    • Namedtuples: Useful for immutable components where data does not need to be modified after creation.
    class Velocity:
        def __init__(self, x=0.0, y=0.0, accel=0.1, decel=0.75, maximum=3):
            self.vector = Vec2(x, y)
            self.accel = accel
            self.decel = decel
            self.maximum = maximum
    
    @dataclass
    class Camera:
        current_x_offset:   float = 0
        current_y_offset:   float = 0
    
    Interaction = namedtuple('Interaction', 'interaction_type target_name')
  2. How esper's Entity Component System (ECS) works

    master

    esper implements an Entity Component System (ECS) pattern based on the following mental model:

    • World Contexts: The top-level container. By default, a single context is active upon import. All operations (creating entities, adding components, etc.) happen within the active context. You can switch between isolated contexts to manage different game scenes.
    • Entities: Represented internally as plain integer IDs. They are not objects with logic; instead, they are collections of components. You query entities based on the components they possess.
    • Components: Simple Python classes used to store data. They should contain no processing logic and should not know about other components or entities. Using dataclasses is recommended for brevity.
    • Processors (Systems): Classes that contain the game logic. All processors must inherit from esper.Processor and implement a process() method. Processors iterate over entities that match specific component requirements.
    import esper
    from dataclasses import dataclass
    
    # Component: Data only
    @dataclass
    class Position:
        x: float = 0.0
        y: float = 0.0
    
    # Processor: Logic only
    class MovementProcessor(esper.Processor):
        def process(self):
            # Query entities with both Velocity and Position
            for ent, (vel, pos) in esper.get_components(Velocity, Position):
                pos.x += vel.x
                pos.y += vel.y
  3. Quick Start: Create entities and run the loop

    master

    To use esper, define your components, create entities (optionally with components attached), register your processors, and call esper.process() in your main loop.

    import esper
    
    # 1. Define Components
    class Velocity:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
    class Position:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
    # 2. Create Entities
    # Method A: Create then add
    player = esper.create_entity()
    esper.add_component(player, Velocity(x=0.9, y=1.2))
    esper.add_component(player, Position(x=5, y=5))
    
    # Method B: Add during creation
    # player = esper.create_entity(Velocity(x=0.9, y=1.2), Position(x=5, y=5))
    
    # 3. Define and Register Processors
    class MovementProcessor(esper.Processor):
        def process(self):
            for ent, (vel, pos) in esper.get_components(Velocity, Position):
                pos.x += vel.x
                pos.y += vel.y
    
    esper.add_processor(MovementProcessor(), priority=3)
    
    # 4. The Game Loop
    while True:
        # You can pass arguments (like delta time) to process()
        # but processors must be defined to accept them
        esper.process()
  4. Add and remove Processors

    master

    Processors can be added with an optional priority (higher numbers are processed first). Default priority is 0.

    • esper.add_processor(processor_instance, priority=0)
    • esper.remove_processor(ProcessorClass): Removes a processor by its class type.
  5. Manage multiple World contexts

    master

    Use world contexts to isolate different game scenes. Operations in one world do not affect another.

    • esper.list_worlds(): List all existing worlds.
    • esper.switch_world(name): Switch to a specific world. If the name doesn't exist, a new world is created.
    • esper.delete_world(name): Delete a world (cannot delete the currently active one).
  6. Check for component existence

    master

    Use these functions to perform conditional checks on an entity before acting:

    • esper.has_component(entity, ComponentType): Returns True if the entity has the component.
    • esper.has_components(entity, TypeA, TypeB, ...): Returns True if the entity has all specified components.
    • esper.try_component(entity, ComponentType): Returns the component instance if it exists, otherwise returns None. This is more efficient than checking has_component followed by component_for_entity.
    • esper.try_components(entity, TypeA, TypeB, ...): Returns a tuple of components if the entity has all of them, otherwise returns None.
    # Using the walrus operator for conciseness
    if stun := esper.try_component(ent, Stun):
        stun.duration -= dt
  7. Query components for an Entity

    master

    If you have a specific Entity ID, you can retrieve its components:

    • esper.component_for_entity(entity_id, ComponentClass): Returns the specific component instance. Raises an error if the component is not present.
    • esper.components_for_entity(entity_id): Returns a tuple of ALL components assigned to that entity. Use this sparingly as it is a heavy operation (e.g., for transferring entities between worlds).
  8. Dispatch and handle events

    master

    esper provides a lightweight event system. Events are dispatched by name and handlers are registered by name. Handlers are stored as weak-references; if the handler is garbage collected, it is automatically un-registered.

    • esper.dispatch_event(event_name, *args): Dispatches an event with any number of arguments.
    • esper.set_handler(event_name, handler_func): Registers a function or class method as a handler for an event.
    • esper.remove_handler(event_name, handler_func): Un-registers a specific handler.

    Note: Events and handlers are scoped to the current World context.

    def on_explosion(power):
        print(f"Explosion with power {power}")
    
    esper.set_handler('explosion', on_explosion)
    esper.dispatch_event('explosion', 10)
  9. Manage the World context

    master

    The World context is the primary interface for managing entities, components, and processors. Use the following functions to interact with the ECS (Entity Component System) state:

    Entity Management

    • create_entity(): Creates a new entity.
    • delete_entity(entity): Removes an entity.
    • entity_exists(entity): Checks if an entity is still valid.

    Component Management

    • add_component(entity, component): Attaches a component to an entity.
    • remove_component(entity, component_type): Removes a specific component type from an entity.
    • try_remove_component(entity, component_type): Attempts to remove a component without raising an error if it doesn't exist.
    • get_component(entity, component_type): Retrieves a component instance for an entity.
    • get_components(entity): Retrieves all components for an entity.
    • has_component(entity, component_type): Checks if an entity has a specific component.
    • component_for_entity(entity, component_type): Accesses a component for a specific entity.
    • components_for_entity(entity): Accesses all components for a specific entity.

    Processor Management

    • add_processor(processor): Adds a processor to the world.
    • remove_processor(processor): Removes a processor.
    • get_processor(processor_type): Retrieves a processor.

    Execution and Cleanup

    • process(): Runs the current frame's processing logic.
    • timed_process(dt): Runs processing logic with a specific delta time.
    • clear_database(): Clears all entities and components.
    • clear_cache(): Clears internal caches.
    • clear_dead_entities(): Cleans up entities marked for deletion.

    World Lifecycle

    • switch_world(...)
    • delete_world(...)
    • list_worlds()
  10. Create and manipulate Entities and Components

    master

    Entities are represented by unique integer IDs. You can create them with create_entity(*components).

    Common operations:

    • add_component(entity, instance): Add a component to an entity.
    • remove_component(entity, type): Remove a component by its type.
    • try_remove_component(entity, type): Remove a component without raising KeyError if it doesn't exist.
    • delete_entity(entity, immediate=False): Mark an entity for deletion. By default, deletion is deferred until the next esper.process() call to avoid issues during iteration.
    import esper
    
    class Velocity:
        def __init__(self, dx, dy):
            self.dx = dx
            self.dy = dy
    
    # Create entity with initial component
    entity = esper.create_entity(Velocity(1, 1))
    
    # Add component later
    esper.add_component(entity, Position(10, 10))
    
    # Delete entity (deferred until next process() call)
    esper.delete_entity(entity)