Query - vux0303/typescript-ECS-framework GitHub Wiki

Query All

Query is used to iterate over all entities with a specific set of components, this set is called a Filter.
If you run a query using a Filter [A, B] (A,B are ecs.Component), all entities that contain component A and B will be matched. For example:

  • [A, B]
  • [A, B, C]
  • [A, B, D]
  • [A, B, C, D]

Follow the example, since you only query for A and B, there only instance of A and B are passed into your callback arguments, even if the query are running on entities with [A, B, C] and [A, B, C, D] signatures. This should be the way of querying to simplify and unify query behavior. But in case you need access to instances of C or D, add ecs.Sibling into your filter list. E.g.

    class ExampleSystem extends ecs.System {

        onUpdate(dt) {
            //example query
            this.admin.queryAll([A, B, ecs.Sibling], (a, b, sibling, enityID) => {
                //do things with a,b

                let c = sibling.get(C)
                if (c) { //always make sure c exist
                   //do things with c
                }
            })
        }
    }

Query should only perform in a system.

To do: I follow the Archetype approach of designing an ECS where Archetype is a group of entities share the same signature. Those query should be an object so it can cache the Archetype it iterate over. For now, a query iterate over all the archetypes causing negative impact on performance.

Signal

Signal is useful if use want a system to run conditionally, not by every frames. Signals are just components that have an state of active/inactive.
Define component as a signal when creating an entity

    //Transform and Velocity are extended from ecs.Component
    this.admin.createEntity([Transform, Velocity],
        ([transform, velocity]) => 
            transform.name = 'example';
            velocity.name = 'example';
        }, {
            pool: 16,
            signals: new ecs.Signal([ExampleSignal], (signal) => { //ExampleSignal is extended ecs.Component
                signal.name = 'example';
            })
        };

signals is a option of CreateOpion, same as pool.
Since signals are just components, no duplicate rule still applied, counting regular components.
Since a component are defined as signal when creating, it can be a regular components in creating other entities. This shouldn't happened because it make component having multiple roles.

May enforce this with a separated class to define signals.

Query active signals

As mentioned, signals have state of active and inactive. If you query by queryAcitve instead of queryAll and the filter contain signals, it can only iterate over entities having that active signals.
When using queryAcitve, assume all components are active. Only signals can change their state.

    // this query doesn't mind if A or B or C or [A,B] or [A,B,C] is signal. If any of them are signal, they must in active state for this query to match.
    this.admin.queryActive([A, B, C], (a, c, b, entityID) => {
        if (entityID) {
            //...
        }
    })

How to active a signal?

A signal become become active if one property or all properties are changed depend on your choice when creating them. By changing, it use equal comparison (===) so be careful with reference types.
Signals have inherit activeByAll property that is false by default. Set to true if you when it active by changing all properties. Otherwise, it become active by any property change. This is default option.
The framework don't account changing of properties on creating entities, if you want it to be active on creation, enable activeOnCreate.

    //Transform and Velocity are extended from ecs.Component
    this.admin.createEntity([], () => }, {
            signals: new ecs.Signal([ExampleSignal], (signal) => {
                signal.activeByAll = true; //if not set here, this is fault.
                signal.name = 'something different from initial value'; //this changing of value do not account to activate the signal
                signal.activeOnCreate = true; //but you can you this flag to make it active when creating entity. By default, this is false.
            })
        };

There two other ways to access inactive signals to activate them:

  • use queryAll, this query treat signals as regular component.
  • use ecs.Sibling, signals also count as siblings.

In case of activeByAll, the framework only check on its first class properties by its references, no nested properties or incursion check involved.

Once a signal activated, it only last one frame. It will be set to inactive before the next frame by the framework.

Internally, a signal detect changes using Javascript built-in proxy which is quite slow in performance. This design it is simpler to implement and I don't have to worry about memory layout to run query concurrently in the first place (like Unity) since Javascript doesn't really support multi-threading for now.

Singleton components

Singleton components are components that are not belong to a particular entities and there are one unique instance of them by admins. If a component class defined as singleton, they can not be used as component for creating entities.

Add a singleton

    this.admin.addSingleton([SingletonClass], (singletonInstance) => {
        //initiate singletonInstance
    })

Access a singleton

Singletons can be accessed directly by admin

    //use class
    let singleton = this.admin.getSingleton(SingletonClass);
    //or use class name
    let singleton = this.admin.getSingleton("SingletonClass");

or using query.

    class ExampleSystem extends ecs.System {

        onUpdate(dt) {
            this.admin.queryAll([A, B, Singleton], (a, b, singleton, enityID) => {

            })
        }
    }

To avoid multiple call to getSingleton, can include singleton class to your filter like above example or cache the instance outside of query like this.

    class ExampleSystem extends ecs.System {

        onUpdate(dt) {
            let singleton = this.admin.getSingleton(Singleton);
            this.admin.queryAll([A, B], (a, b, enityID) => {
                //so you can access the singleton here
            })
        }
    }

or even better

    class ExampleSystem extends ecs.System {
        singleton: Singleton;
        onRegister() {
            this.singleton = this.admin.getSingleton(Singleton);
        }
        onUpdate(dt) {
            this.admin.queryAll([A, B], (a, b, enityID) => {
                
            })
        }
    }