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

Entities, components, queries

The game world lives in ctx.world, an entity-component system (ECS).

  • An entity is an id (Entity). It has no data of its own.
  • A component is any Send + Sync + 'static Rust type. No derive needed. In C++ it is any plain struct (trivially copyable), found by its type: nothing to register.
  • A query visits every entity that has a given set of components.

Spawning

#![allow(unused)]
fn main() {
struct Health(f32);
struct Enemy;

let e: Entity = ctx.world.spawn((Transform2D::at(vec2(10.0, 20.0)), Health(100.0), Enemy));

// Many at once is much faster than a loop of `spawn`:
ctx.world.spawn_batch((0..100_000).map(|i| (Transform2D::at(vec2(i as f32, 0.0)), Health(10.0))));
}
    crabby::entity e = world.spawn(transform_at({10, 20}), Health{100}, Enemy{});

    // Many at once:
    for (int i = 0; i < 100000; ++i)
        world.spawn(transform_at({float(i), 0}), Health{10});

A tuple of components is a bundle; bundles go up to 12 components. C++ passes the components straight to spawn.

struct Health { float value; };
struct Enemy {};

Reading and changing one entity

#![allow(unused)]
fn main() {
if let Some(h) = ctx.world.get_mut::<Health>(e) {
    h.0 -= 5.0;
}
ctx.world.insert(e, Enemy);        // add or replace a component
ctx.world.remove::<Enemy>(e);      // returns the removed value
ctx.world.despawn(e);
assert!(!ctx.world.is_alive(e));   // old handles stay dead, even if the slot is reused
}
    Health h = world.get<Health>(e);
    world.set(e, Health{h.value - 5});
    world.set(e, Enemy{});            // add or replace a component
    if (world.has<Enemy>(e)) world.remove<Enemy>(e);
    world.despawn(e);
    assert(!world.alive(e));          // old handles stay dead, even if the slot is reused
    assert(!world.try_get<Health>(e));

Queries

#![allow(unused)]
fn main() {
ctx.world
    .query::<(&mut Transform2D, &Health)>()
    .without::<Enemy>()
    .for_each(|(t, h)| t.position.y += h.0);
}
    world.each([](transform2d& t, const Health& hp) {
        t.position.y += hp.value;
    });

In C++, each reads the components to visit from your function’s parameters: T& to change one, const T& to read it, and a first crabby::entity parameter gets the entity’s id. world.each_par is the C++ par_for_each: your function runs on all cores at once, so it must only touch its own entity.

For code that works on whole arrays, world.query<A, B>(fn) hands you one chunk (archetype) at a time, with an array per component:

    world.query<transform2d, Health>([](auto chunk) {
        auto [t, hp] = chunk.columns();   // one array per component
        for (std::size_t i = 0; i < chunk.size(); ++i)
            t[i].position.y += hp[i].value;
    });

each compiles to the same loop, so it’s as fast. With query_par, size a buffer with world.count<A, B>() and write row i of a chunk to slot chunk.first() + i to fill it from all cores: rows are numbered the same way in every query.

What you can ask for (Rust):

In the queryYou get
&Tread access to T (entity must have it)
&mut Twrite access to T
Option<&T>, Option<&mut T>the component if present; doesn’t filter
Entitythe entity’s id
a tuple of the aboveall of them, up to 8

Filters with::<T>() and without::<T>() narrow the match without fetching data. Asking for the same component twice with &mut panics.

Ways to run a query:

  • for_each(f): the fastest loop; use this by default.
  • par_for_each(f): same, split over all CPU cores. Use it for per-entity work on many entities.
  • par_collect_into(&mut vec, f): fills vec with f of every match, on all cores, in for_each order. The vector keeps its memory, so doing it every tick allocates nothing.
  • iter(): a normal iterator, for when you need map/filter/collect. It is several times slower than for_each on big worlds.
  • count(), get(entity).

Commands: changing the world while you iterate

You can’t spawn or despawn inside a query loop, because the query is borrowing the world. Record the change in ctx.commands instead:

#![allow(unused)]
fn main() {
fn die(ctx: &mut Ctx) {
    let cmds = &mut ctx.commands;
    ctx.world.query::<(Entity, &Health)>().for_each(|(e, h)| {
        if h.0 <= 0.0 { cmds.despawn(e); }
    });
}
}
static void die(crabby::ctx& ctx) {
    ctx.world().each([&](crabby::entity e, const Health& hp) {
        if (hp.value <= 0) ctx.despawn_later(e);
    });
}

The engine applies commands after every system, so the next system sees them.

Resources

A resource is one global value of a type, stored in the world and found by its type:

#![allow(unused)]
fn main() {
struct Score(u32);
ctx.world.insert_resource(Score(0));
ctx.world.resource_mut::<Score>().0 += 1;
}
struct Score { std::uint32_t points; };

[[maybe_unused]] static void score_point(crabby::ctx& ctx) {
    auto world = ctx.world();
    Score score = world.try_resource<Score>().value_or(Score{0});
    score.points += 1;
    world.set_resource(score);
}

C++ resources are plain structs, copied in and out: resource<T>() (throws if the world has none), try_resource<T>(), set_resource(value) and remove_resource<T>(). Read them before an each loop, not inside it (the world is borrowed while the loop runs). In C: crabby_resource_set, crabby_resource_get and crabby_resource_remove, by name.

Sorting for cache locality

world.sort_by::<T>(key) reorders entities that have T by a u64 key. Entity ids stay valid. The built-in spatial grid uses it to keep entities that are close in space close in memory; see Spatial queries.