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) => {
})
}
}