Wow, it is very nice to have an actual text document that doesn’t artificially cap you at 240 characters to write an FFF in. Anyways, for this week the topic is … more backend changes!

I know, exciting.

I’ve been moving the Foxfire engine from running on Specs ECS to a custom ECS-like crate named palkia.1 Like I mentioned in the previous FFF, this is less of an entity-component-system architecture and more of an entity-component-message architecture.

Update: I’ve now open-sourced Palkia, along with all the other helper crates in Foxfire! Get it here.

Why this change? Brian Bucklew goes over a lot of the advantages of this event/message based architecture in his presentation about Caves of Qud and Sproggiwood (you can find the link in the previous FFF). But here’s my takes on it.

Systems get Really Complicated#

ECM makes it easier to have tons and tons of components that interact with each other. ECS was designed to solve monolithic approaches like object-orientation, but the systems in it are still pretty top-down.

As an example, say we want to have enemies that can attack each other. Alright then, let’s make up an Attacker component and a HasHP component:

N.B. I did not actually test any of this code so it’s possible it doesn’t compile.

#[derive(Component)]
struct Attacker {
    /// Stores targets of the attack and the damage we wish to do to them.
    ///
    /// Presumably this is cleared and then filled every frame by some kind of `SysAI`.
    targets: Vec<(Entity, DamageInstance)>,    
}

struct DamageInstance {
    amount: u32,
    kind: DamageKind,
}

#[derive(Component)]
struct HasHP(pub u32);

Great, now let’s write the SysDealDamage system:

struct SysDealDamage;

impl<'a> System<'a> for SysDealDamage {
    type SystemData = (
        ReadStorage<'a, Attacker>,
        WriteStorage<'a, HasHP>,
    );

    fn run(&mut self, (attacker, mut has_hp): Self::SystemData) {
        for (attacker,) in (attacker,).join() {
            for (target, dmg) in &attacker.targets {
                if let Some(target_hp) = has_hp.get_mut(*target) {
                    target_hp.0 = target_hp.0.saturating_sub(dmg.amount);
                }
            }
        }
    }
}

OK, great.

But now, I want to add a potion of invincibility. Alright, let’s add a component for that…

#[derive(Component)]
struct IsInvincible;

impl<'a> System<'a> for SysDealDamage {
    type SystemData = (
        ReadStorage<'a, Attacker>,
        WriteStorage<'a, HasHP>,
        ReadStorage<'a, IsInvincible>,
    );

    fn run(&mut self, (attacker, mut has_hp, invincible): Self::SystemData) {
        for (attacker,) in (attacker,).join() {
            for (target, dmg) in &attacker.targets {
                if let Some(target_hp) = has_hp.get_mut(*target) {
                    if invincible.get(*target).is_none() {
                        target_hp.0 = target_hp.0.saturating_sub(dmg.amount);
                    }
                }
            }
        }
    }
}

Great. There’s a lot of rightward drift, but we could factor that out into a helper function…

But now say I want to add resistances to certain types of damage. Maybe I want a gelatinous cube that’s only hurt by slashing and magic weapons, for example.

Alright, let’s add another few levels of indentation …

#[derive(Component)]
struct ResistsDamage(HashMap<DamageKind, u32>);

impl<'a> System<'a> for SysDealDamage {
    type SystemData = (
        ReadStorage<'a, Attacker>,
        WriteStorage<'a, HasHP>,
        ReadStorage<'a, IsInvincible>,
        ReadStorage<'a, ResistsDamage>,
    );

    fn run(&mut self, (attacker, mut has_hp, invincible, resists): Self::SystemData) {
        for (attacker,) in (attacker,).join() {
            for (target, dmg) in &attacker.targets {
                if let Some(target_hp) = has_hp.get_mut(*target) {
                    if invincible.get(*target).is_none() {
                        let mut amount = dmg.amount;
                        if let Some(resists) = resists.get(*target) {
                            if let Some(resist_amt) = resists.0.get(&dmg.kind) {
                                amount = amount.saturating_sub(*resist_amt);
                            }
                        }
                        target_hp.0 = target_hp.0.saturating_sub(amount);
                    }
                }
            }
        }
    }
}

And now what if I want to add an iron golem that’s healed by fire damage? What if I want slimes to split in two when damaged?

I don’t mean to claim that entity-component-message is the be-all-end-all successor to ECS2. But, for roguelikes, where it’s really fun to have everything be able to influence everything else in the world, all of your systems are just going to become horrid tangled knots.

For example, here’s the (old) code in Foxfire that makes mechanisms apply their effects to the entities they’re on.

pub struct SysApplyMechanismEffects;

impl<'a> System<'a> for SysApplyMechanismEffects {
    type SystemData = (
        Entities<'a>,
        ReadStorage<'a, HasMechanisms>,
        ReadStorage<'a, Activatable>,
        ReadStorage<'a, ProvidesSenses>,
        WriteStorage<'a, HasSenses>,
        ReadStorage<'a, ProvidesRadiation>,
        WriteStorage<'a, Radiator>,
    );

    fn run(
        &mut self,
        (
            entities,
            mechanisms,
            active,
            provides_senses,
            mut senses,
            provides_radiation,
            mut radiator,
        ): Self::SystemData,
    ) {
        for (owner, mechanisms) in (&entities, &mechanisms).join() {
            if let Some(senses) = senses.get_mut(owner) {
                senses.senses_mut().clear();
            }
            if let Some(radiation) = radiator.get_mut(owner) {
                radiation.radiations.clear();
            }
            // Without this helper function I get up to 10 levels of indentation!
            // I've elided it for brevity but it's quite long.
            for &mech in mechanisms.contained.iter() {
                SysApplyMechanismEffects::run_inner(
                    owner,
                    mech,
                    &active,
                    &provides_senses,
                    &mut senses,
                    &provides_radiation,
                    &mut radiator,
                );
            }
        }
    }
}

ECS promises that it won’t get harder and harder to add new components the more components you add. Which is true! But, the systems do get harder and more complicated. Every time I want to have a mechanism able to do something else, I’ll need to update that gigantic function.

And it makes sense organizationally to have the components and their behavior near each other. In the damage example above, wouldn’t it be nice if the code for the IsInvincible component removing all the damage was right there next to the component declaration, instead of off somewhere in the middle of the SysDealDamage function?

So, the solution Brian Bucklew hit upon, which I am shamelessly stealing, is

Message Passing#

Like in ECS, under ECM you have entities, which are lists of components. But, instead of linking behavior of different components with systems, you do it by passing messages.

When you implement Component for a struct, you implement a method that registers that struct with different message types. Then, from a message handler, you can fire messages to other entities.

When an entity gets a message, it runs through its components in order, and if that component type registers a handler for that message type, it runs the handler and passes the updated value to the next component, and so on … and then finally returns the modified message to the caller.

And there’s a method on World to pass a message to all entities, as your entrypoint.

How does this all work in practice? Here’s the damage example from above written with Palkia.

N.B. palkia currently calls the messages “events,” so I suppose it’s an ECE architecture. But it doesn’t really matter what you call it.

struct EvDealDamage {
    amount: u32,
    kind: DamageKind,
}
impl Event for EvDealDamage {}

struct HasHP(pub u32);
struct IsInvincible;
struct ResistsDamage(HashMap<DamageKind, u32>);

impl HasHP {
    // Here's the signature for an event listener: the component (& or &mut), the event,
    // the entity the component is on, and a restricted accessor to the world for doing lazy
    // updates, returning the event.
    fn on_deal_damage(&mut self, ev: EvDealDamage, e: Entity, world: &WorldAccess) -> EvDealDamage {
        match self.0.checked_sub(ev.amount) {
            Some(it) => self.0 = it,
            None => {
                // Set this entity to be despawned when `world.maintain` is called.
                world.lazy_despawn(e);
            }
        }
        ev
    }
}

impl Component for HasHP {
    fn register_listeners(builder: ListenerBuilder<Self>) -> ListenerBuilder<Self>
    where
        Self: Sized,
    {
        // Register the listeners here.
        builder
            .listen_write(Self::on_deal_damage)
    }
}

impl Component for IsInvincible {
    fn register_listeners(builder: ListenerBuilder<Self>) -> ListenerBuilder<Self>
    where
        Self: Sized,
    {
        // You can use closures too, as long as they don't close over anything.
        // In the backend, the read and write listeners are treated more or less the same, but
        // there is dynamic borrow checking. So, components can send events to themselves iff it's
        // immutably borrowed.
        builder
            .listen_read(|_: &Self, mut ev: EvDealDamage, _: Entity, _: &WorldAccess| {
                // Reduce the amount of damage to zero, easy as that.
                ev.amount = 0;
                ev
            })
    }
}

impl Component for ResistsDamage {
    fn register_listeners(builder: ListenerBuilder<Self>) -> ListenerBuilder<Self>
    where
        Self: Sized,
    {
        builder
            .listen_read(|this: &Self, mut ev: EvDealDamage, _: Entity, _: &WorldAccess| {
                if let Some(reduction) = this.0.get(&ev.kind) {
                    ev.amount = ev.amount.saturating_sub(*reduction);
                }
                ev
            })
    }
}

fn main() {
    let mut world = World::new();
    // You have to register component types before you use them.
    // This function calls its `register_listeners` impl.
    world.register::<HasHP>();
    world.register::<IsInvincible>();
    world.register::<ResistsDamage>();

    // When you create an entity you should make sure the components are in a sensible order.
    // In this case, put the things that modify the event before the thing that uses it ...
    // I might add some kind of sorting function to the `Component` trait to facilitate this.
    // Caves of Qud solves this by having things be loaded from blueprint XML files, which are just
    // trusted to have the components be in the right order.
    // Anyways,
    let target1 = world.spawn().with(HasHP(10)).build();
    world.dispatch(EvDealDamage { 
        amount: 15,
        DamageKind: DamageKind::Whatever,
    }, target1);
    // Target1 is dead!
    // You have to call this method to finalize lazy updates, like spawning and despawning entities.
    world.finalize();

    let target2 = world.spawn()
        .with(HasHP(10))
        .with(IsInvincible)
        .build();
    world.dispatch(EvDealDamage { 
        amount: 7,
        DamageKind: DamageKind::Whatever,
    }, target2);
    // And `target2` has 10 HP still.
    // I haven't added querying methods yet (turns out, you often don't need them except for debugging!)
    // but I pinky-promise
    world.finalize();

    let target3 = world.spawn()
        .with(HasHP(10))
        .with(ResistsDamage([(DamageKind::Whatever, 4)].into_iter().collect()))
        .build();
    world.dispatch(EvDealDamage { 
        amount: 7,
        DamageKind: DamageKind::Whatever,
    }, target3);
    // And `target3` will have 10 - (7 - 4) = 7 HP left.
    world.finalize();
    
    // 3 entities spawned, 1 killed.
    assert_eq!(world.len(), 2);
}

And that’s about it!

Why Messages for Foxfire?#

I want Foxfire to be intricate and linked. I want my players to have interactions with the game that I never could have dreamed of, but I don’t want them to be bugs. And this entity-component-message architecture is the best way I’ve seen of doing that.

I’m also used to working with events/messages from Minecraft modding. Both Forge and Fabric3 have event systems that let you run code when something happens in game, like a tree growing or a mob taking damage, and call your own code or modify the event itself (or both).

On that point, messages make the game really moddable and hackable. The eventual-eventual goal is to have Foxfire be mostly written in a scripting language, probably rhai, and for there to be official mod support! I’m really inspired by Factorio on this front.


But that’s all very far in the future. For now, I’m going to keep porting Foxfire to using Palkia. I’ve been running into the problem of trying to port the whole thing at once, so I think this week I’m going to cut a lot of the code, implement simple things like movement and graphics, and then build back up to where I was with Specs.

So, check back next week …

P.S.#

Message passing is not a new idea. Check out the Smalltalk language, which was released in 1980!


  1. Why “Palkia”? I’ve been naming most of the helper crates in Foxfire after Pokemon, just cause there’s a lot of them and I like the theming. I picked Palkia specifically because Palkia has dominion over space and palkia the crate helps me organize things. ↩︎

  2. Especially because I haven’t even introduced what it is yet ↩︎

  3. The two largest modding toolchains ↩︎