ecsyjs / ecsy Goto Github PK
View Code? Open in Web Editor NEWEntity Component System for javascript
Home Page: https://ecsyjs.github.io/ecsy/
License: MIT License
Entity Component System for javascript
Home Page: https://ecsyjs.github.io/ecsy/
License: MIT License
Similar to registerDefaultSystems
(https://github.com/fernandojsg/ecsy/issues/67), in this case this exported function will take care of everything needed to get the library to a ready to go state, it will call internally to registerDefaultSystems
but also will create entities and add components needed to get the initial setup for you.
Currently the query on the systems will look for entities with all the components on the list.
[Transform, Rotating] => Transform AND Rotating
We should be able to modify the query to do things like:
[ [Transform, Rotating], [Scale], [NOT(Transform), PulsatingColor]]
=> (Transform AND Rotating) OR (Scale) OR (!Transform AND PulsatingColor)
Basically a more generic implementation of the mandatory
attribute (#28) so instead of checking for entities.length > 0
it could be entities.length > min
:
class SystemB extends System {
init() {
return {
queries: {
entities: {
components: [FooComponent],
min: 2
}
}
};
}
execute() {}
}
So we could define if we are going to get mutable or immutable components so we could implement more advanced schedulers using workers for example.
queries: {
bullets: {components: [Write(Position), Speed]}
}
Read
could be implicit if not defined, though
Add a simple syntactic sugar so you can define a single
modifier on the queries to define that the query should expect just one item out of that query (as a way to replace singleton too).
Currently it could be just a prettier way to access that entity instead of doing this.queries.renderer[0]
, but we can imagine that in the future it could help doing some optimizations for this kind of queries in under the hood if needed.
Also together with https://github.com/fernandojsg/ecsy/issues/28 will make using the singleton concept easier.
class TestSystem extends System {
init() {
return {
queries: {
meshes: { components: [Mesh] }
renderer: { components: [RendererComponent], single: true }
}
};
}
execute(delta) {
let meshes = this.queries.meshes;
meshes.forEach(mesh => {
this.queries.renderer.renderMesh(mesh);
});
}
}
Based on the Unity ECS implementation: https://docs.unity3d.com/Packages/[email protected]/manual/system_state_components.html
TL;DR State Components are components used by a system to hold internal resources for an entity, they are not removed when you delete the entity, you must remove them from your system once you are done with them. It lets you detect Add/Remove events without callback, eg:
class MySystem extends System {
init() {
return {
queries: {
added: { components: [ComponentA, Not(StateComponentA)] },
remove: { components: [Not(ComponentA), StateComponentA] },
normal: { components: [ComponentA, StateComponentA] },
}
};
},
execute() {
added.forEach(entity => {
entity.addStateComponent(StateComponentA, {data});
});
remove.forEach(entity => {
var component = entity.getStateComponent(StateComponentA);
// free resources for `component`
entity.removeStateComponent(StateComponentA);
});
normal.forEach(entity => {
// use entity and its components
});
}
}
On #3 I exposed the need of deferred remove component method. I opened this one to keep track of possible use cases where we will need to remove right away the component instead of waiting to the end of the whole tick to do it.
I remember on aframe we had the documentation as a .md and generates that pretty website. Do you have any suggestion how to include the documentation as getting started, core components and things like that along with the autogenerated docs from the code?
/cc @brianpeiris
Basically if the query.length === 0
it won't call execute
class TestSystem extends System {
init() {
return {
queries: {
renderer: { components: [RendererComponent], mandatory: true }
}
};
}
execute(delta) {
// Safely because we know query will have at least one element
this.queries.renderer[0].doWhatever();
}
}
So we could get rid of singletons for example (https://github.com/fernandojsg/ecsy/issues/30)
Some thoughts:
World
from Entity
seems cool in the way that we could do world.getComponent(Renderer)
to get a "singleton component". The problem is that it also inherit all the function from entity, as reset
, remove
that would be misleadingWorld
, so you could access it as world.entity.getComponent
, or world.singletonEntity.getComponent
? could be another option. We could even implement the bypass functions getComponent
, addComponent
and so at the world level, just for the sake of avoiding that level of indirection, so users could still be using world.getComponent
.world.registerSingletonComponent(ComponentA)
as a helper for world.entity.addComponent(ComponentA)
or just add directly the component to the world and that's all?execute() {
this.world.getComponent(Renderer).render();
}
Using queries:
class SystemA extend System
init() {
return {
queries: {
renderer: { components: [Renderer], single: true, mandatory: true }
}
};
}
execute(delta) {
this.queries.renderer.render();
}
}
Currently the syntax for queries is:
{
balls: {
components: [Ball, Transform, Rotating],
events: {
added: {
event: "EntityAdded"
},
removed: {
event: "EntityRemoved"
},
changed: {
event: "EntityChanged"
},
rotatingChanged: {
event: "ComponentChanged",
components: [Rotating]
},
transformChanged: {
event: "ComponentChanged",
components: [Transform]
}
}
}
}
In order to simplify the way to define that the query will react to events when an entity is added, removed or changed, we defined the following proposal:
{
queries: {
balls: {
components: [Ball, Transform, Rotating],
added: true,
removed: true,
changed: [Any, Ball, Rotating]
}
}
}
Where:
added
: Is a boolean, false by default, that will determine if the query will store a queue of entities added to the query.removed
: as added
but when an entity has been removed from the query.changed
:
true
is defined: It will listen for any entity change (= any component on that entity that is part of the query changes).list
is defined, it will listen if any of the components of the list has changed.This will help simplify the access to the entity lists as discussed on https://github.com/fernandojsg/ecsy/issues/70 from:
queries.balls
events.balls.added
events.balls.removed
events.balls.changed
events.balls.transformChanged
events.balls.rotatingChanged
to
queries.balls.results
queries.balls.added
queries.balls.removed
queries.balls.changed.Transform
queries.balls.changed.Rotating
Some systems will have just init
behaviour, other will have execute
or onEntityAdded
keeping list of the systems that has each function will avoid looping through all the registered systems per loop.
We expect that sometimes libraries should register some internal systems in order to work properly, and it could be tedious for the user to need to import all of them and register them.
So it could be nice to export a registerDefaultSystems(world)
from the libraries that to handle that for you.
In any case, you could still skip using that and import the systems and do that by yourself
we need a getting started guide which is included with the rest of the docs.
You can create a system with world.registerSystem() so you should also be able to get a reference to the created instance with world.getSystem()
I find myself using a lot of components with a single attribute as for example:
class Scene {
constructor() {
this.scene = null;
}
}
class Parent {
constructor() {
this.parent = null;
}
}
and then I need to do:
entity.getComponent(Scene).scene;
entity.getComponent(Parent).parent;
sometimes I struggle with the naming as is a bit redundant Scene
and scene
so I was thinking if it could be a good practice to call these single attribute components something like value
?
entity.getComponent(Scene).value;
entity.getComponent(Parent).value;
warning: Probably too much level of syntactic sugar here :)
And we could just include a helper like getComponentValue()
entity.getComponentValue(Scene);
entity.getComponentValue(Parent);
Currently is possible to add a component by passing the component class and an object to replace the default values:
entity.addComponent(ComponentA);
entity.addComponent(ComponentB, {value: 23, other: 'test'});
It would be interesting to be able to request a component without adding it to an entity automatically:
var component = world.createComponent(ComponentB);
component.value = 23;
component.other = 'test';
entity.addComponent(component);
// You could still do this the "old" way
entity.addComponent(ComponentA); // Just some sugar to detect the component
Currently init
on Systems is used to initialize the systems and returning the list of queries and events:
export class DemoSystem extends System {
init() {
//... init code
return {
queries: {
entities: { components: [ComponentA] }
},
events: {
eventA: "eventA"
}
};
}
}
I believe it could be nice if we divide it into functions or static variables, eg:
export class DemoSystem extends System {
init() {
//... init code
}
config() {
return {
queries: {
entities: { components: [ComponentA] }
},
events: {
eventA: "eventA"
}
};
}
}
There should be a list of 3rd party components people might be interested in. This could be as simple as a markdown doc in the documentation.
@fernandojsg shared a proposal for world.createComponent()
which returns an instance of a component created by a ComponentManager
. We should probably rename the top level createComponent
function to something else to prevent that conflict.
Some ideas:
createComponentClass()
defineComponent()
... by doing swallow copy.
The pros is that it's a powerful feature used on UI/Editors a lot, the downside is that it could fail in several cases and if people rely on the default implementation they won't have a clue what is failing there.
To start exploring best practices
It's better to throw an error or just leave them undefined so the user can understand what is going on
For example if we have a component with {a,b,c} but we send a {a,b}, instead of keeping the original value of {c} it will assign undefined
. Happens when initializing components
Currently the syntax for queries and its events is the following:
{
queries: {
entities: {
components: [Rotating, Transform]
events: {
added: {
event: "EntityAdded"
},
removed: {
event: "EntityRemoved"
},
changed: {
event: "EntityChanged"
},
rotatingChanged: {
event: "ComponentChanged",
components: [Rotating]
},
transformChanged: {
event: "ComponentChanged",
components: [Transform]
}
}
}
}
}```
And the way to access the entities on the `execute` method is:
```javascript
execute() {
// Queries
this.queries.entities.forEach(entity => {})
// Events
this.events.entities.added.forEach(entity => {})
this.events.entities.removed.forEach(entity => {})
this.events.entities.changed.forEach(entity => {})
this.events.entities.rotatingChanged.forEach(entity => {})
this.events.entities.transformChanged.forEach(entity => {})
}
I'd like to get feedback on using a common path this.queries.entities.*
for both type of queries. Something like:
execute() {
// Queries
this.queries.entities.results.forEach(entity => {})
// Events
this.queries.entities.events.added.forEach(entity => {})
this.queries.entities.events.removed.forEach(entity => {})
this.queries.entities.events.changed.forEach(entity => {})
this.queries.entities.events.rotatingChanged.forEach(entity => {})
this.queries.entities.events.transformChanged.forEach(entity => {})
}
Imagine we have a system that remove a component for an object on tick
:
export class CollisionSystem extends System {
init() {
return {
entities: [Colliding]
};
}
execute(delta) {
let entities = this.queries.entities;
for (let i = 0; i < entities.length; i++) {
let entity = entities[i];
entity.removeComponent(Colliding);
}
}
}
This has two problems:
Colliding
component on the same tick they won't be able to access to that entityIdeas:
forceRemove
to remove at the same time. But we should find a use case for this before allow it. #4I'd like to propose moving the queries
object from the return value of the init()
function to a static queries
property on the System
class. This change will make it possible to know what dependencies a system has before it is instantiated which is important for writing system schedulers.
Ex.
class MySystem extends System {
static queries = {
foo: { components: [ComponentA, ComponentB] }
bar: { components: [ComponentB, ComponentC] }
};
init() {}
execute() {}
}
Or without static property support (currently unsupported without babel / typescript)
class MySystem extends System {
init() {}
execute() {}
}
MySystem.queries = {
foo: { components: [ComponentA, ComponentB] }
bar: { components: [ComponentB, ComponentC] }
};
This proposal is similar to the syntax of React's propTypes
or defaultProps
.
An alternate proposal could be introducing a static query()
method which returns the queries for the system.
Ex.
class MySystem extends System {
static query() {
return {
foo: { components: [ComponentA, ComponentB] }
bar: { components: [ComponentB, ComponentC] }
};
}
init() {}
execute() {}
}
I think this should be avoided since the queries
object should not change and could be referenced multiple times in the scheduler which would allocate additional objects unnecessarily. One benefit is that static class methods are currently supported in evergreen browsers without transpilation. However, React has made the decision to go forward with this syntax for propTypes
and defaultProps
and I think we should as well.
One more proposal would be to do nothing to the syntax other than change examples to set queries in the constructor or with a class property:
Ex.
class MySystem extends System {
constructor(world) {
super(world);
this.queries = {
foo: { components: [ComponentA, ComponentB] }
bar: { components: [ComponentB, ComponentC] }
};
}
init() {}
execute() {}
}
Class properties have better support in current browsers. See the Class Fields Proposal.
class MySystem extends System {
queries = {
return {
foo: { components: [ComponentA, ComponentB] }
bar: { components: [ComponentB, ComponentC] }
};
}
init() {}
execute() {}
}
A scheduler would then be assumed to operate after a system has been registered with the World
(which calls the system's constructor). Custom schedulers could then be implemented by subclassing SystemManager
and providing a custom SystemManager
when constructing the World
.
It should be defined when the system is added to a ECS context/instance.
world.registerSystem(SystemA);
world.registerSystem(SystemB);
orderId
(Similar to z-index
), by default every system has orderId = 0
. In case you want to add a system to be executed always at the beginning or at the end no matter how many systems you will be adding later.world.registerSystem(SystemA, {order: 0});
world.registerSystem(SystemB, {order: -1});
world.registerSystem(SystemA, {before: [systemC]});
...
world.registerSystem(SystemD, {after: [systemE, systemC]});
...
Eventually the three bindings will move to the Three org, so we should create a separate repo now.
Currently addComponent
return the entity instead of the component, so you could chain them as:
entity.addComponent(ComponentA).addComponent(ComponentB);
But if you want to set component values directly without passing an object on the addComponent
function, it could be nice to return the component directly:
let componentA = entity.addComponent(ComponentA);
componentA.value = 23;
One question would be if we would like that behaviour to be the same as if we would do getMutableComponent
so it will trigger the changed
. But as we are just creating it, I believe that just triggering the added
event should be enough for now.
Hi @fernandojsg, really nice API you got there! Thanks for sharing this.
Following ECS and other design patterns, I'm wondering who's the responsibility of adding an entity to the scene, into the right container? A "controller"? ๐
Speaking of THREE.js, an Object3D
should be added (.add()
) somewhere. Maybe in the root scene, maybe as a child in another object.
Do you have an opinion about this?
Thanks!
Many times we need to store data related to systems internally, a good practice to avoid storing data on the systems could be to use singleton components and add some syntactic sugar for that:
If we create a system in this way:
world.registerSystem(SystemA, {valueA: 1, valueB: 2});
it will create a singleton component named systemA
(As if we had previously created a SystemA
component with valueA
and valueB
attributes).
And we could access the component from within the system by referencing this.component
:
execute() {
this.component.valueA += 2;
}
Currently you can create a new type by calling: createType()
, and we have support for the basic type Array.
But what happens if you want to have an array of a custom type?
Currently you could just go ahead and implement a custom type eg: (https://github.com/fernandojsg/ecsy/blob/master/test/unit/createcomponent.test.js#L138-L170)
var Vector3Array = createType({
create: defaultValue => {
var v = [];
if (typeof defaultValue !== "undefined") {
for (var i = 0; i < defaultValue.length; i++) {
var value = defaultValue[i];
v.push(new Vector3(value.x, value.y, value.z));
}
}
return v;
},
reset: (src, key, defaultValue) => {
if (typeof defaultValue !== "undefined") {
for (var i = 0; i < defaultValue.length; i++) {
if (i < src[key].length) {
src[key][i].copy(defaultValue[i]);
} else {
var value = defaultValue[i];
src[key].push(new Vector3(value.x, value.y, value.z));
}
}
// Remove if the number of elements on the default value is lower than the current value
var diff = src[key].length - defaultValue.length;
src[key].splice(defaultValue.length - diff + 1, diff);
} else {
src[key].length = 0;
}
},
clear: (src, key) => {
src[key].length = 0;
}
});
Or we could have a helper function as:
createArrayType(Vector3)
And it will generate a type definition based similar to the one pasted above based on the Vector3
type.
Allow users to define a filter for the queries could be useful to filter by component's' values easily.
The drawback of this is that filter will create a new instance of the array, and the events (onComponentAdded
, onComponentChanged
,...) should also be modified by the filter
class SystemB extends System {
init() {
return {
queries: {
entities: {
components: [FooComponent],
filter: entity => entity.getComponent(FooComponent).value > 2
}
}
};
}
execute() {}
}
And keep github just for issues on the implementation and bugs
If we take the factory example as reference, imagine we remove (deferred) the Name
component when clicking the button https://github.com/fernandojsg/ecsy/blob/master/examples/factory/index.html#L108
It will just enter on the next tick because of the Not
operator on the name system and it will add the Name
component to it, removing it from the original query: https://github.com/fernandojsg/ecsy/blob/master/examples/factory/index.html#L49-L65
The problem is that, that component won't get added correctly as the deferral removal has not been executed yet, so in this line https://github.com/fernandojsg/ecsy/blob/master/src/EntityManager.js#L47 the _ComponentTypes
for that entity will still have Name
on it, so it will just return.
After that, the deferral removal will come into action and will do the actual removal of the Name
component on the entity... resulting in adding that entity again to the Not(Name)
query. And so on the next frame, it will get executed and end up there, so we are executing the call twice.
At first I thought about just doing an extra check when adding the component to see if it's on the "to remove" state so we could just go ahead and add the new component. The problem is that when you will get reading the getComponent()
on the onRemove
query on your system, you will access to the new one, and not the removed one, that it's probably the one you are interested in.
We already have a componentsToRemove
attribute on Entity
, so I'm thinking about just move the removed components there, so getComponent()
will return always a valid (not mark for removal) component, and we could introduce a getRemovedComponent()
so people could use it on the onRemoveEvent
queries to access the removed component even if the entity has a new alive one already. With that we could also avoid strange side effects usages like calling entity.getMutableComponent()
on an marked for remove component.
Just an idea, not sure if it's a good one yet. If systems are supposed to be stateless, it may be a good idea to make them a function.
function RotateObjectsSystem(world, queries) {
const { dt } = queries.clock[0].getComponent(Clock);
queries.entities.forEach((entity) => {
const rotateX = entity.getComponent(RotateX);
const transform = entity.getComponent(Transform);
transform.rotation.x += rotate.speed * dt;
});
}
RotateObjectsSystem.query = {
clock: { components: [Clock]},
entities: { components: [Transform, RotateX] }
};
Currently every system is expected to have a synchronous init()
, but some systems could be async and it could be nice to have some examples and best practices around it
move ECSY to moz reality. and ECYS-three to the three org
During our last call we were discussing around if queries could be dynamic or should be static. We didn't come up with any use case for dynamic queries (although currently you could create queries dynamically).
Thinking on the way queries are handled, every time you create a new query, you are traversing the list of components/entities to check if they match the query, to build the initial set of entities for that query, and after that you just wait for events to happen to add or remove the entities from that list.
The problem is that the initial traversing when creating the entity could be expensive depending on the size of your world, so overall I don't think it's a good pattern to do that dynamically.
For that reason, I'll just close this issue right now and reopen if we find a specific use case that really needs this feature. I just wanted to have it documented for further discussions or to point people asking for this feature.
Hi,
Exciting project!
I really like how you're making this lib reactive and framework agnostic off the bat. It occurs to me though for folks wishing to transition to this from A-Frame it could help to have a declarative abstraction layer over it?
I've been working on an experimental build system for A-Frame using TypeScript, Webpack, and custom elements v1: https://github.com/edsilv/aframe-ts-webpack
Perhaps this could consume ecsy to provide all of the underlying logic, maintaining separation of concerns and giving A-Frame users a way to pick it up quickly?
During our internal hackathon I noticed that most people were running into some confusion around the entity.addComponent()
API.
The current signature is this:
class World {
registerComponent<C extends Component>(component: ComponentConstructor<C>): this
}
interface Component {
copy(source: Component): this // Optional
}
class Entity {
addComponent(component: ComponentConstructor, parameters: Component | {}): this
}
Where the parameters are copied to the instance of the component acquired from the object pool. In the case that it is an instance of Component
with a copy()
method it uses that otherwise it just does a shallow copy of the object.
I'd like to propose moving to this:
class World {
registerComponent<C extends Component>(component: ComponentConstructor<C>, componentManager?: ComponentManager<C>): this
}
class ComponentManager<C extends Component> {
components: C[]
get(entityId: number): C
add(entityId: number): C
has(entityId: number): boolean
remove(entityId: number): void
}
class Component<T extends Component> {
clone(): T {
return new this.constructor().copy(this);
}
copy(source: T): this {
for (const key in source) {
if (this.hasOwnProperty(key)) {
const destValue = this[key];
const srcValue = source[key];
if (destValue && destValue.copy && srcValue && srcValue.copy) {
destValue.copy(srcValue);
} else {
this[key] = source[key];
}
}
}
}
}
class Entity {
addComponent<C extends Component>(component: ComponentConstructor<C>, instance?: C): C
}
The differences are the following:
addComponent()
does not implement pooling by default. I think most of us had issues with the default pooling behavior. The shallow copy doesn't work for non-value types like Matrix4
, Vector3
, etc. which produces undesirable results. I'll let others comment on their experiences with object pooling, but I think the Hubs team was generally in favor of making object pooling a higher level concept and not turned on by default.addComponent()
returns the component instance. In my experience this is more common than adding multiple components to an entity which is why we currently return the instance of the entity rather than the component.registerComponent()
takes an optional ComponentManager which allows you to implement pooling yourself on a per component basis. You can also store your components with different backing data structures such as Map
or a BitSet
or TypedArray
. The default should probably just use a Map
or Array
I did some benchmarks in HECS that you can look at to determine the best default.clone()
to Component
. I think it's good to allow people to add arguments to the component constructor. However, this makes it tough to clone components in editors like Spoke. Adding the clone()
method lets you specify those constructor parameters or override how the component is instantiated when cloned.Component
from an interface to a class with default implementations of clone
and copy
. These implementations support ThreeJS classes such as Vector3
, Matrix4
, etc.In the sense of having specific code for them and custom behavior, in favor of keeping the concept, by adding an entity to the world (or the world itself being an entity) and add components to it, so you can just query for them as any other component.
That "world.entity" approach together with https://github.com/fernandojsg/ecsy/issues/29 and https://github.com/fernandojsg/ecsy/issues/28 will make it easier and "prettier" to use the same concept on your code.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.