3mcd / javelin

ECS and multiplayer game development toolkit
https://javelin.games
198 stars 16 forks source link

Query API Improvements #149

Closed 3mcd closed 3 years ago

3mcd commented 3 years ago

This (absolutely massive) PR introduces a bunch of improvements to the core ECS including: 1) Simplified query interface 2) Improved iteration performance (#148) 3) Not filters (#125) 4) Topic registration (#138)

Simplified query interface

Filters (attached, detached and changed, custom filters) have been removed. Change detection should be event oriented – checking each component to determine if it is attached or detached is just too slow. The old strategy also required that we defer operations an additional tick so that entities flagged for attach/detach had a chance to get picked up by a filter.

This means queries will always look like this:

query(A, B, C)

instead of this:

query(A, attached(B), C)

Now, when a component is attached/detached, or an entity is spawned/destroyed, the corresponding operation happens immediately at the beginning of the next tick. World and Storage expose events (signals) to detect these events, and a couple of new built-in effects were introduced that expose a simple API for iterating modified entities, which are described below.

Triggers

The simplest way to detect a change is with a trigger. A trigger is an effect that yields entity-component pairs where the entity was modified in some way during the previous tick.

onAttach(Velocity).forEach((entity, v) => {
  // `v` was attached to `entity` last tick
});
onDetach(Freeze).forEach((entity, v) => {
  // `v` was detached from `entity` last tick
});

Monitors

Monitors are slightly more complex. They monitor a query and yield entities that become eligible (or ineligible) for iteration by the query.

onInsert(queries.spaceships).forEach(entity => {
  // entity now matches the spaceship query
});
onRemove(queries.lightSources).forEach(entity => {
  // entity no longer matches the lightSources query
});

You can see some examples of Triggers/Monitors (and other effects) in the updated "space junk" example here: https://github.com/3mcd/javelin/blob/ab3dea578ad49f44f89566e5e42ff663a2bb0d99/docs-src/static/examples/space-junk.js.

I haven't done much performance testing on triggers/monitors (or effects for that matter), but I plan on doing that soon. I assume they are okay right now but could still use some work.

Improved iteration performance

Iteration speed was improved around 100% with the removal of component filters. Even more exciting perf gains were made with the introduction of forEach and a new, "lower level" way of iterating queries manually.

forEach

const nodes = query(Node)

nodes.forEach((entity, [node]) => ...)

Manually iterate a query

Queries can be manually iterated for maximum performance (read in the Crysis voice) using a for..of loop:

for (const [entities, [p, v]] of queries.moving) {
  for (let i = 0; i < entities.length; i++) {
    p[i].x += v[i].x
  }
}

Using these new iteration methods, Javelin is able to compete with some of the faster JS ECS libraries while (hopefully) remaining fairly easy to understand and use.

Not filters

A query can now exclude entities with certain component(s) using query.not().

const root = query(Node).not(Parent)

Topic registration

Topics can now be registered with a world. Registered topics are auto-flushed at the beginning of each tick.

const logger = createTopic()
const world = createWorld({
  topics: [
    logger,
  ]
});

To-do