Skip to main content

Systems and Queries

Systems#

Entities are just collections of Facets which only contain data.

Systems provide all the behavior of your ECS.

Defining Systems#

Use the useSystem hook to define a new system. It takes a callback that is called every time ECS.update is called.

const PrintFrameTimeSystem = () => {
return useSystem((dt) => {
console.log(`Last frame took ${dt} seconds.`);
});
};
Tip

useSystem returns null so you can return it, to keep your system components tidy.

System Priorities#

By default, all systems have a priority of 0 and are called in the order of your useSystem calls.

Pass a priority as the second parameter (lower is first) to override this behavior:

useSystem((dt) => console.log("I'm called second."), 1);
useSystem((dt) => console.log("I'm called first."), 0);

Using Systems#

To use a system just include it anywhere within the context of an ECS.Provider:

<ECS.Provider>
<PrintFrameTimeSystem />
<GravitySystem vector={[0, -9.8, 0]}>
{/* ... */}
</ECS.Provider>
Tip

Systems don't need to appear as direct children of ECS.Provider. Organize things however you'd like.

Processing Entities#

The useEngine hook can be used to access all Entities with your ECS:

const CoolSim = () => {
const engine = useEngine();
return useSystem((dt) => {
for (const entity of engine.entities) {
// ...
}
});
};
Warning:

Like with most things, The useEngine hook must be used within the context of an ECS.Provider.

Queries#

Queries keep track of which Entities which have a specific set of Facets.

Creating Queries#

Use the useQuery hook:

const PoisonSystem = () => {
const query = useQuery((e) => e.hasAll(Health, PoisonStatus));
return useSystem((dt) => {
for (const entity of query.entities) {
// ...
}
});
};

It takes a predicate function that should return true if the passed Entity has the desired Facets.

Tip

Anytime an Entity is added or removed from the query, it will re-render the component.

Query Entities#

Every Query has a .entities property that contains all the entities matched by the query.

However, there is also the Query.loop method:

const PoisonSystem = () => {
const query useQuery((e) => e.hasAll(IsEnemy, Health, PoisonStatus));
return useSystem((dt) => {
query.loop([Health, PoisonStatus], (e, [health, poison]) => {
health.current -= poison.amount;
})
})
}

Query.loop takes two arguments:

  • a tuple of Facets types you're interested in.
  • a function called for every entity matched by the query.

The callback receives two arguments:

  • an entity
  • the facet instances of that entity, that you requested with the tuple of types

In the example above, the query matches all entities with the IsEnemy, Health and PoisonStatus. But the loop call only requests the Health and PoisonStatus effects, which it receives as health and poison.

The facet instances passed to your callbacks are type-safe and appear in the order in which you requested them:

A tuple of [Health, PoisonStatus] results in a tuple [health, poison] where health is statically-typed as Health and poison is statically-typed as PoisonStatus. (Pretty cool right?)

Warning:

The tuple of types you request doesn't have to be the same facets used in the query's predicate. However, it is a good idea to keep it to a subset of the facets used in the query's predicate, to ensure the entities you loop over actually have the facets you request.

Query Events#

The useQuery hook has a second argument where you can supply event callbacks:

const query = useQuery(e => e.has(Invisible), {
added: e => ... , // this entity just started matching the query
removed: e => ... , // this entity no longer matches the query
})