Systems and the scheduler

Systems and the scheduler

A key feature of entity-component systems is that operations on the entities are organized in systems, which operate independently from one another and can be added or removed as required.

Systems

Systems can be thought of as functions that take a World instance and perform operations on the world’s entities and/or resources. However, to allow storing intermediate variables between multiple system calls, and to support special initialization and finalization operations, systems are expressed as structs implementing the System trait. This trait requires systems to implement initialize, and finalize methods, called before or after the ECS run, respectively, and an update method, called at every step of the ECS run. Each of these methods takes a World instance on which they perform the desired operations.

from larecs import World, System

@value
struct Move(System):
    
    # This is executed once at the beginning
    fn initialize(mut self, mut world: World) raises:
        # We do not need to do anything here
        pass

    # This is executed in each step
    fn update(mut self, mut world: World) raises:

        # Move all entities with a position and velocity
        for entity in world.query[Position, Velocity]():
            entity.get[Position]().x += entity.get[Velocity]().dx
            entity.get[Position]().y += entity.get[Velocity]().dy

    # This is executed at the end
    fn finalize(mut self, mut world: World) raises:
        # We do not need to do anything here
        pass

Scheduler

The Scheduler is responsible for executing the systems in the correct order. A Scheduler contains a World instance and a list of systems. The scheduler has initialize, update, and finalize methods, which call the respective functions of all considered systems in the order they are added to the scheduler. In addition, the scheduler has a run method, which initializes the systems, runs them a desired number of times, and finalizes them.

To construct an example of a scheduler, let us define further systems for adding entities and logging their positions.

@value
struct AddMovers[count: Int](System):

    # This is executed once at the beginning
    fn initialize(mut self, mut world: World) raises:
        _ = world.add_entities(
            Position(0, 0), Velocity(1, 0), count=10
        )

    # This is executed in each step
    fn update(mut self, mut world: World) raises:
        # We do not need to do anything here
        pass

    # This is executed at the end
    fn finalize(mut self, mut world: World) raises:
        # We do not need to do anything here
        pass

@value
struct Logger[interval: Int](System):

    var _logging_step: Int

    fn __init__(out self):
        self._logging_step = 0

    fn _print_positions(self, mut world: World) raises:
        for entity in world.query[Position, Velocity]():
            pos = entity.get_ptr[Position]()
            print("(", pos[].x, ",", pos[].y, ")")

    # This is executed once at the beginning
    fn initialize(mut self, mut world: World) raises:
        print("Starting with", len(world.query[Position, Velocity]()), 
              "moving entities.")

    # This is executed in each step
    fn update(mut self, mut world: World) raises:
        if not self._logging_step % self.interval:
            print("Current Mover positions:")
            self._print_positions(world)
        self._logging_step += 1 

    # This is executed at the end
    fn finalize(mut self, mut world: World) raises:
        print("Final positions:")
        self._print_positions(world)

Now we can create a scheduler and add the systems to it. Import the scheduler struct:

from larecs import Scheduler

Create and run the scheduler:

# Create a scheduler
scheduler = Scheduler[Position, Velocity]()

# Add the systems to the scheduler
scheduler.add_system(AddMovers[10]())
scheduler.add_system(Move())
scheduler.add_system(Logger[2]())

# Run the scheduler for 10 steps
scheduler.run(10)