Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Attachments: things that follow things

Health bars over units, turrets on tanks, selection rings, a carried flag: entities that should stay at some place relative to another entity. Give them an Attached component and add the built-in follow_attachments system after the systems that move things:

#![allow(unused)]
fn main() {
struct Tank {
    speed: f32,
}

fn drive(ctx: &mut Ctx) {
    ctx.world
        .query::<(&mut Transform2D, &Tank)>()
        .for_each(|(t, tank)| t.position.x += tank.speed);
}

#[test]
fn turrets_ride_on_tanks() {
    let mut turret = None;
    let world = Game::new("tanks", 800, 600)
        .sim_system(drive)
        .sim_system(follow_attachments) // after the systems that move things
        .run_headless(10, |ctx| {
            let tank = ctx.world.spawn((Transform2D::at(vec2(0.0, 0.0)), Tank { speed: 1.0 }));
            let on_top = Attached::to(tank).offset(vec2(0.0, -4.0));
            turret = Some(ctx.world.spawn((Transform2D::default(), on_top)));
        });
    assert_eq!(position(&world, turret.unwrap()), vec2(10.0, -4.0));
}
}
struct Tank { float speed; };

static void turrets_ride_on_tanks() {
    crabby::entity turret = 0;
    auto world = crabby::game("tanks", 800, 600)
        .sim_system([](crabby::ctx& ctx) {
            ctx.world().each([](crabby::transform2d& t, const Tank& tank) { t.position.x += tank.speed; });
        }, "drive")
        .follow_attachments() // after the systems that move things
        .run_headless(10, [&](crabby::ctx& ctx) {
            auto w = ctx.world();
            const auto tank = w.spawn(crabby::transform_at({0, 0}), Tank{1});
            turret = w.spawn(crabby::transform_at({0, 0}), crabby::attach_to(tank, {0, -4}));
        });
    const auto at = world.get<crabby::transform2d>(turret).position;
    if (at != crabby::vec2{10, -4}) { std::fprintf(stderr, "turret at %f,%f\n", double(at.x), double(at.y)); std::abort(); }
}

Every tick, each attached entity’s Transform2D gets:

  • position: the followed entity’s position plus offset, turned by its rotation and stretched by its scale (so flipping a sprite with a negative x scale mirrors what hangs off it);
  • rotation: the followed entity’s rotation plus rotation.

Its own scale is left alone, so a small icon stays small on a big unit.

RustC++Meaning
Attached::to(e)crabby::attach_to(e)right on top of e
.offset(v)attach_to(e, v)v away, in e’s space
.rotated(r)attach_to(e, v, r)turned r radians more

Chains and cleanup

Attachments can be attached to attachments (an arm on a body, a hand on the arm): each link is placed in its parent’s space, from the root down, all in one tick, whatever order the entities were spawned in.

When the entity at the top of a chain is despawned, everything attached to it (directly or down the chain) is despawned in the same tick. A chain that loops back on itself, or is more than 64 links deep, is treated as broken and despawned too.

Cost

Placing runs on all cores and allocates nothing in steady state: about 0.7 ms per tick for 100,000 attachments (release, M-series Mac, cargo run --release -p ponko_bench -- attach). Entities without Attached cost nothing.

Attachments are a sim system, so they’re deterministic, saved with the world (Attached is reflected) and visible to tools and agents like any other component.