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.
| Rust | C++ | 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.