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 + 'staticRust 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 query | You get |
|---|---|
&T | read access to T (entity must have it) |
&mut T | write access to T |
Option<&T>, Option<&mut T> | the component if present; doesn’t filter |
Entity | the entity’s id |
| a tuple of the above | all 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): fillsvecwithfof every match, on all cores, infor_eachorder. The vector keeps its memory, so doing it every tick allocates nothing.iter(): a normal iterator, for when you needmap/filter/collect. It is several times slower thanfor_eachon 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.