🎮 ECS in Raku: A Toy Framework for Entities, Components, and Systems

🎮 ECS in Raku: A Toy Framework for Entities, Components, and Systems

Publish Date: Jul 27
3 0

⚠️ Note: This is a personal experiment. I’m not experienced in game development or ECS, and I’ve only recently learned about these concepts. This framework is not production-ready, and the API is still in flux. But I’d love your feedback! 🙏

🧠 What is ECS?

ECS stands for Entity-Component-System, a popular architecture pattern in game development.

  • Entities are just unique identifiers.
  • Components are data — like position, velocity, health, etc.
  • Systems are the logic that runs on entities with certain components.

Instead of having objects with both data and behavior (like in OOP), ECS separates those concerns cleanly. It encourages data-driven design and enables powerful querying and parallelism (though we’re far from that in this toy project).


🧱 What’s an Archetype?

This ECS implementation uses the concept of archetypes, which means:

Entities are grouped by the exact combination of components (and optionally tags) they have.

This means the world knows: "All entities with position and velocity, but not health are in this group."

This makes querying more efficient and predictable — we only iterate over relevant entities per system.

Archetypes are typically used in high-performance ECS engines (like Unity’s DOTS or Bevy in Rust), but here it also simplifies reasoning about how entities are grouped.


Why?

Recently, I came across the idea of Entity Component System (ECS) architectures, and it instantly clicked with my love for declarative APIs and composable logic.

So I decided to implement a minimal ECS framework in Raku — not for performance or production use, but just to explore the paradigm and learn from it. And who knows? Maybe others in the Raku community will enjoy hacking on it too.

The Demo 🎬

Here’s a simple bouncing animation built with this ECS framework and Raylib::Bindings:

Bouncing Camelia

The Code 🧩

Here’s the full example:

use Raylib::Bindings;
use ECS;

constant $screen-width  = 1024;
constant $screen-height = 450;
my $white               = init-white;
my $background          = init-skyblue;
init-window($screen-width, $screen-height, "Bouncing Camelias");

my $string         = "./camelia.png";
my $camelia        = load-image($string);
my $camelia-height = $camelia.height;
my $camelia-width  = $camelia.width;
my $camelia-pos    = Vector2.init: $camelia-width/2e0, $camelia-height/2e0;
my $texture        = load-texture-from-image($camelia);
unload-image($camelia);

set-target-fps(60);
END {
    unload-texture($texture);
    close-window;
}

# We define a few basic vector operators to help with math:
sub term:<vector2-zero> { Vector2.init: 0e0, 0e0 }

multi infix:<+>(Vector2 $a, Vector2 $b) { Vector2.init: $a.x + $b.x, $a.y + $b.y }
multi infix:<->(Vector2 $a, Vector2 $b) { Vector2.init: $a.x - $b.x, $a.y - $b.y }
multi infix:<*>(Vector2 $a, Numeric $i) { Vector2.init: $a.x * $i, $a.y * $i }
multi infix:</>(Vector2 $a, Numeric $i) { Vector2.init: $a.x / $i, $a.y / $i }

# Then comes the fun part: defining the ECS world.

my $world = world {
    component position => Vector2;
    component velocity => Vector2;

    entity "camelia";

    # Input system: spawn a new “camelia” on mouse click
    system "click", :when{is-mouse-button-pressed MOUSE_BUTTON_LEFT}, -> {
        world-self.new-camelia:
            :position(get-mouse-position - $camelia-pos),
            :velocity(vector2-zero),
        ;
    }

    system-group "input", <click>;

    # Movement, gravity and bounce logic
    system "move", -> :$position! is rw, :$velocity! {
        using-params -> Num $delta {
            $position += $velocity * $delta
        }
    }

    system "bounce", -> :$position! where *.y >= $screen-height - $camelia-height.Num, :$velocity! where *.y > 0 {
        $velocity.y *= -.8
    }

    system "gravity", -> :$velocity! {
        using-params -> Num $delta {
            $velocity.y += 100 * $delta;
        }
    }

    system-group "physics", <move gravity bounce>;

    # Draw each camelia
    system "draw", -> :$position! {
        draw-texture-v $texture, $position, $white;
    }
}

    system "draw", -> :$position! {

# And finally the game loop:
until window-should-close {
    $world.input;
    $world.physics: get-frame-time;
    begin-drawing;
    clear-background $background;
    $world.draw;
    draw-fps 10, 10;
    end-drawing;
}
Enter fullscreen mode Exit fullscreen mode

Understanding the ECS Framework API

world

The world function is the entry point to define your ECS universe. Inside it, you declare your components, entity types, systems, and system groups. It returns an object that you will use to create entities and run your systems.

component

The component function defines a component that entities can have. You can call it in two ways:

component position => Vector2;
Enter fullscreen mode Exit fullscreen mode

Or more concisely

component Color;
Enter fullscreen mode Exit fullscreen mode

In the second case, the name of the component will be automatically derived from the type by converting it to kebab-case (e.g., Color becomes "color"). This helps reduce repetition when the type name already describes the data well.

entity

The entity function defines a named entity type. This name becomes a tag automatically added to all instances of that entity. For example:

entity "ball";
Enter fullscreen mode Exit fullscreen mode

After this, you can create new entities with $world.new-ball(...), and those entities will be tagged with "ball".

system

The system keyword defines a system — a unit of logic that processes entities. By default, a system automatically performs a query based on its parameters: it runs once per entity that has all the required components and tags.

For example:

system "gravity", -> :$velocity! { ... }
Enter fullscreen mode Exit fullscreen mode

This system runs once per frame for each entity with the velocity component.

However, if you pass a :condition or :when parameter, the system behaves differently: it no longer queries entities, and runs only once per frame (or tick), executing only when the condition is true. This is ideal for global events like input, timers, or other non-entity-specific logic.

Example:

system "click", :when { is-mouse-button-pressed MOUSE_BUTTON_LEFT }, -> {
    ...
}
Enter fullscreen mode Exit fullscreen mode

This system executes once per frame only if the left mouse button is pressed.

system-group

The system-group function defines a reusable group of systems. This allows you to bundle related systems and execute them together. You can call the group like a method, optionally passing arguments that will be forwarded to any using-params blocks inside the systems.

Example:

system-group "physics", <move gravity bounce>;
...
$world.physics: get-frame-time;
Enter fullscreen mode Exit fullscreen mode

using-params

The using-params function allows a system to access parameters passed when the system or system group is invoked. This is useful for values like frame delta time or external input.

Example:

system "gravity", -> :$velocity! {
    using-params -> Num $delta {
        $velocity.y += 100 * $delta;
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, the system needs a time delta value to apply acceleration due to gravity. It gets the value passed to the system group (e.g., physics: get-frame-time).

current-entity

Inside a system or query, you can call current-entity to get the entity object currently being processed. This is useful for adding or removing tags or other manipulations.

Example:

if some-condition {
    current-entity.add-tag: "jumping";
}
Enter fullscreen mode Exit fullscreen mode

This gives you fine-grained control over the entity’s state and behavior beyond component data.

world-self

Inside a system or query, world-self gives you access to the current world instance. You can use it to create or modify entities, trigger systems, or manage state globally.

Example:

world-self.new-ball: :position(...), :velocity(...);
Enter fullscreen mode Exit fullscreen mode

This allows a system to spawn new entities as part of its logic.

About the Framework 🛠️

Here are some things you should know:

  • It’s written entirely in Raku.
  • The world is declared with world { ... }.
  • Components are just names mapped to types.
  • Systems are defined using system with a name and a sub signature.
  • The system’s parameters are automatically injected from matching entities.
  • The using-params block lets you access runtime values (like delta time).
  • Tag filtering, conditions, and entity creation are built-in.
  • It’s not optimized in any way — it’s designed to be fun and expressive.

What’s next? 🚧

Nothing is fixed. The API might change. This is just the beginning of a small experiment. If you’re curious about ECS, or if you have experience with game development and want to help shape a more solid design, please get in touch!

You can find the project here:

👉 https://github.com/FCO/ECS

Feel free to open issues, create examples, or criticize design decisions.

Thanks for reading!

Comments 0 total

    Add comment