gale.ecs

gale.ecs is a Data-Oriented Design (ECS) toolkit for managing large numbers of similar entities efficiently: the ball, every player on the pitch, every collider in a match. An Entity is just an integer id, a component is any plain Python object the game defines itself (typically a small @dataclass), and a System queries the World for a combination of components and processes every matching entity in one pass, instead of each entity carrying its own update method.

World, entities, and components

from dataclasses import dataclass

from gale.ecs import World


@dataclass
class Position:
    x: float
    y: float


@dataclass
class Velocity:
    dx: float
    dy: float


@dataclass
class Fatigue:
    stamina: float


world = World()

ball = world.create_entity()
world.add_component(ball, Position(0, 0))
world.add_component(ball, Velocity(120, 0))

player = world.create_entity()
world.add_component(player, Position(10, 10))
world.add_component(player, Velocity(0, 0))
world.add_component(player, Fatigue(stamina=100))

get_component, has_component, and remove_component round out the basic storage API, and destroy_entity removes an entity together with every component attached to it:

world.has_component(player, Fatigue)  # True
world.get_component(player, Fatigue)  # Fatigue(stamina=100)

world.destroy_entity(ball)  # ball's Position and Velocity are gone too

Querying entities

World.query yields (entity, component1, component2, ...) tuples for every entity that has all of the requested component types, which is exactly what a system needs to process a batch of similar entities without caring about anything else attached to them:

for entity, position, fatigue in world.query(Position, Fatigue):
    print(entity, position, fatigue)  # only entities with both components

Systems

A System is a small class with an update(world, dt) method that queries the world and mutates whatever components it cares about. SystemScheduler runs an ordered list of systems every frame, the same way gale.state.StateMachine is a small orchestrator kept separate from the states it drives:

from gale.ecs import System, SystemScheduler


class MovementSystem(System):
    def update(self, world, dt) -> None:
        for entity, position, velocity in world.query(Position, Velocity):
            position.x += velocity.dx * dt
            position.y += velocity.dy * dt


class FatigueSystem(System):
    def update(self, world, dt) -> None:
        for entity, fatigue, velocity in world.query(Fatigue, Velocity):
            moving = velocity.dx != 0 or velocity.dy != 0
            fatigue.stamina -= (5 if moving else -2) * dt
            fatigue.stamina = max(0, min(100, fatigue.stamina))


scheduler = SystemScheduler([MovementSystem(), FatigueSystem()])

# In your game loop:
scheduler.update(world, dt)

Systems stay decoupled from one another this way: MovementSystem only ever looks at Position/Velocity and FatigueSystem only at Fatigue/Velocity — neither one needs to know the other exists, or anything about the ball versus a player, only about the components they agree to operate on.