Comparing version 0.1.2 to 0.1.3
@@ -1,179 +0,82 @@ | ||
<!-- Generated by documentation.js. Update this documentation by updating the source code. --> | ||
## World | ||
# ecsy | ||
### registerComponent | ||
## Index | ||
Register a component | ||
### Classes | ||
#### Parameters | ||
* [Component](classes/component.md) | ||
* [Entity](classes/entity.md) | ||
* [System](classes/system.md) | ||
* [SystemStateComponent](classes/systemstatecomponent.md) | ||
* [TagComponent](classes/tagcomponent.md) | ||
* [World](classes/world.md) | ||
- `Component` **Component** | ||
### Interfaces | ||
### registerSystem | ||
* [ComponentConstructor](interfaces/componentconstructor.md) | ||
* [SystemConstructor](interfaces/systemconstructor.md) | ||
* [Types](interfaces/types.md) | ||
Register a system | ||
### Functions | ||
#### Parameters | ||
* [Not](README.md#not) | ||
* [createComponentClass](README.md#createcomponentclass) | ||
* [createType](README.md#createtype) | ||
- `System` **[System][1]** | ||
- `attributes` | ||
## Functions | ||
### getSystem | ||
### Not | ||
Return a registered system based on its class | ||
▸ **Not**<**T**>(`Component`: [ComponentConstructor](interfaces/componentconstructor.md)‹T›): *object* | ||
#### Parameters | ||
Use the Not class to negate a component query. | ||
- `SystemClass` | ||
- `System` **[System][1]** | ||
**Type parameters:** | ||
### getSystems | ||
▪ **T** | ||
Get all the systems registered | ||
**Parameters:** | ||
### execute | ||
Name | Type | | ||
------ | ------ | | ||
`Component` | [ComponentConstructor](interfaces/componentconstructor.md)‹T› | | ||
Update the systems per frame | ||
**Returns:** *object* | ||
#### Parameters | ||
___ | ||
- `delta` **[Number][2]** Delta time since the last call | ||
- `time` **[Number][2]** Elapsed time | ||
### createComponentClass | ||
### createEntity | ||
▸ **createComponentClass**<**T**>(`schema`: object, `name`: string): *[ComponentConstructor](interfaces/componentconstructor.md)‹T›* | ||
Create a new entity | ||
Create a component class from a schemaa. | ||
### stats | ||
**Type parameters:** | ||
Get some stats | ||
▪ **T**: *[Component](classes/component.md)* | ||
## Entity | ||
**Parameters:** | ||
### getComponent | ||
Name | Type | Description | | ||
------ | ------ | ------ | | ||
`schema` | object | An object that describes the schema of the component | | ||
`name` | string | The name of the component | | ||
Return an immutable reference of a component | ||
Note: A proxy will be used on debug mode, and it will just affect | ||
the first level attributes on the object, it won't work recursively. | ||
**Returns:** *[ComponentConstructor](interfaces/componentconstructor.md)‹T›* | ||
#### Parameters | ||
___ | ||
- `Component` | ||
- `Type` **Component** of component to get | ||
### createType | ||
Returns **Component** Immutable component reference | ||
▸ **createType**(`typeDefinition`: object): *object* | ||
### getMutableComponent | ||
Return a mutable reference of a component. | ||
#### Parameters | ||
- `Component` | ||
- `Type` **Component** of component to get | ||
Returns **Component** Mutable component reference | ||
### addComponent | ||
Add a component to the entity | ||
#### Parameters | ||
- `Component` **Component** to add to this entity | ||
- `values` | ||
- `Optional` **[Object][3]** values to replace the default attributes on the component | ||
### removeComponent | ||
Remove a component from the entity | ||
#### Parameters | ||
- `Component` **Component** to remove from the entity | ||
- `forceRemove` | ||
### hasComponent | ||
Check if the entity has a component | ||
#### Parameters | ||
- `Component` **Component** to check | ||
- `include` **Bool** Components queued for removal (Default is false) | ||
### hasAllComponents | ||
Check if the entity has all components in a list | ||
#### Parameters | ||
- `Components` | ||
### hasAnyComponents | ||
Check if the entity has any components in a list | ||
#### Parameters | ||
- `Components` | ||
### removeAllComponents | ||
Remove all the components from the entity | ||
#### Parameters | ||
- `forceRemove` | ||
### reset | ||
Initialize the entity. To be used when returning an entity to the pool | ||
### remove | ||
Remove the entity from the world | ||
#### Parameters | ||
- `forceRemove` | ||
## TagComponent | ||
## System | ||
## Not | ||
Negate a component query | ||
### Parameters | ||
- `Component` | ||
## createType | ||
Use createType to create custom type definitions. | ||
### Parameters | ||
**Parameters:** | ||
- `typeDefinition` | ||
Name | Type | Description | | ||
------ | ------ | ------ | | ||
`typeDefinition` | object | An object with create, reset and clear functions for the custom type. | | ||
## Types | ||
Standard types | ||
## createComponentClass | ||
Create a component class from a schema | ||
### Parameters | ||
- `schema` | ||
- `name` | ||
[1]: #system | ||
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number | ||
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object | ||
**Returns:** *object* |
@@ -5,2 +5,3 @@ window.$docsify = { | ||
homepage: "../README.md", | ||
relativePath: true, | ||
search: { | ||
@@ -7,0 +8,0 @@ paths: "auto", |
# ECSY Architecture | ||
## Entities | ||
An entity is an object that has an unique ID which purpose is to group components together. | ||
![ECSY architecture](https://ecsy.io/docs/manual/images/ECSY%20Architecture.svg) | ||
Entities are always created within a `World` context: | ||
## Overview | ||
The following glossary is extracted from the [Getting started guide](/manual/Getting-started), it is recommended to read the whole section to get an overview on how the framework works. | ||
```javascript | ||
var entity = world.createEntity(); | ||
``` | ||
Some common terms within ECS engines are: | ||
- [entities](/manual/Architecture?id=entities): an object with an unique ID that can have multiple components attached to it. | ||
- [components](/manual/Architecture?id=components): different facets of an entity. ex: geometry, physics, hit points. Data is only stored in components. | ||
- [systems](/manual/Architecture?id=systems): do the actual work with in an application by processing entities and modifying their components. | ||
- [queries](/manual/Architecture?id=queries): used by systems to determine which entities they are interested in, based on the components the entities own. | ||
- [world](/manual/Architecture?id=world): a container for entities, components, systems and queries. | ||
### Adding components | ||
Once you have created an entity you can add components to it: | ||
```javascript | ||
class ComponentA { | ||
constructor() { | ||
this.number = 10; | ||
this.string = "Hello"; | ||
} | ||
} | ||
The usual workflow when building an ECS based application is: | ||
- Create the `components` that shape the data you need to use in your application. | ||
- Create `entities` and attach `components` to them. | ||
- Create the `systems` that will use these `components` to read and transform the data of these entities. | ||
- Execute all the systems each frame. | ||
// Add the component with the default values | ||
entity.addComponent(ComponentA); | ||
## Example | ||
// Add the component replacing the default values | ||
entity.addComponent(ComponentA, {number: 20, string: "Hi"}); | ||
``` | ||
Let's say we want to create a game where the player fights with wolves and dragons. | ||
We will start by defining components that will be attached to entities: | ||
- `Walker` and `Flyer` for entities that will walk and fly (resp.). | ||
- `Enemy` for enemy entitites. | ||
- `Model3D` for all the entities that will have a 3d Model. | ||
### Accessing components and mutability | ||
You can access a component from an entity with two modes: | ||
- `getComponent(Component`: Get the component for read only operations. | ||
- `getMutableComponent(Component)`: Get the components to modify its values. | ||
Then we use these components to define our main entities: | ||
- `wolf`: It's an `Enemy`, can `walk` and has a `model3D`. | ||
- `dragon`: It's an `Enemy`, can `fly` and has a `model3D`. | ||
- `player`: It's an `Player`, can `walk` and has a `model3D`. | ||
> Please notice that if you are in ()[debug mode] it will throw an error if you try to modify a component accessed by `getComponent` but that error won't be available on release mode because of performance reasons. | ||
And finally we define the systems that will add the logic to the game: | ||
- `Walk`: It will modify the `Walker` entities (`Player` and `Wolf`) moving them around. | ||
- `Fly`: It will modify the `Flyer` entities (`Dragon`) moving them around in the sky. | ||
- `AI_Walk`: It will modify the `Enemy` and `Walker` entities (`Wolf`) using AI techniques to compute the path they will follow. | ||
- `Attack`: It will implement all the logic for attacks between `Enemy` and `Player` entities. | ||
- `Draw`: It will draw all the entities that has `Model3D` component on the screen. | ||
These two access mode help us to implement `reactive systems`(**LINK**) without much overhead on the execution (We avoid using custom setters or proxies). This means everytime you request a mutable component, it will get marked as modified and systems listening for that will get notified accordanly. | ||
> It's important to notice that the component will get marked as modified even if you don't change any attribute on it, so try to use `getMutableComponent` only when you know you will actually modify the component and use `getComponent` otherwise. | ||
![Wolves and dragons example](https://ecsy.io/docs/manual/images/dragons.svg) | ||
Side effects of these two modes are allowing automatic schedulers to analyze the code to paralellize it and making the code easily readable as we could understand how the system is acting on the components. | ||
## World | ||
By default your application should have at least one `world`. A world is basically a container for `entities`, `components` and `systems`. Even so, you can have multiple worlds running at the same time and enable or disable them as you need. | ||
[API Reference](/api/classes/world). | ||
### Removing components | ||
Another common operation on entities is to remove components: | ||
```javascript | ||
world = new World(); | ||
``` | ||
## Components | ||
A `Component` ([API Reference](/api/classes/component)) is an object that can store data but should have not behaviour (As that should be handled by systems). There is not a mandatory way to define a component. | ||
It could just be a function: | ||
```javascript | ||
entity.removeComponent(ComponentA); | ||
function ComponentA() { | ||
this.number = 10; | ||
this.string = "Hello"; | ||
} | ||
``` | ||
This will mark the component to be removed and will populate all the queues from the systems that are listening to that event, but the component itself won't be disposed until the end of the frame. | ||
This is done so systems that need to react to it can still access the data of the components. | ||
Once a component is removed from an entity using the default deferred mode, you can still access its contents, before the end of the frame, by calling `getRemovedComponent(Component)`: | ||
The recommended way is using ES6 classes and extending the `Component` class: | ||
```javascript | ||
class SystemFoo extends System { | ||
execute() { | ||
this.queries.boxes.removed.forEach(entity => { | ||
var component = entity.getRemovedComponent(Box); | ||
console.log('Component removed:', component, 'on entity: ', entity.id); | ||
}); | ||
import { Component } from 'ecsy'; | ||
this.queries.boxes.entities.forEach(entity => { | ||
console.log('Iterating on entity: ', entity.id); | ||
}); | ||
class ComponentA extends Component { | ||
constructor() { | ||
super(); | ||
this.number = 10; | ||
this.string = "Hello"; | ||
} | ||
} | ||
SystemFoo.queries = { | ||
boxes: { | ||
components: [ Box ], | ||
removed: true // To listen for removed entities from the query | ||
} | ||
} | ||
var entity = world.createEntity().addComponent(Box); | ||
world.execute(); | ||
entity.removeComponent(Box); | ||
world.execute(); | ||
``` | ||
This example will output: | ||
It is also recommended to implement the following functions on every component class: | ||
- `copy(src)`: Copy the values from the `src` component. | ||
- `reset()`: Reset the component's attributes to their default values. | ||
``` | ||
- Iterating on entity: 1 | ||
- Component removed: box on entity: 1 | ||
``` | ||
### Tag Components | ||
And it will stop the execution there as the query won't get satisfied after the `Box` component is removed from the entity. | ||
Some components don't store data and are used just as tags. In these cases it is recommended to extends `TagComponent` ([API Reference](/api/classes/tagcomponent) so the engine could, eventually, optimize the usage of this component. | ||
**MORE ON THIS ON LIFECYCLE** | ||
You can still remove the component immediately if needed by passing a second parameter to `removeComponent(Component, forceImmediate)`, although is not the recommended behaviour because it could lead to side effect if other systems need to access the removed component: | ||
```javascript | ||
// The component will get removed immediately | ||
entity.removeComponent(Component, A); | ||
``` | ||
class Enemy extends TagComponent {} | ||
## Components | ||
A `Component` is an object that can store data but should have not behaviour. There is not a mandatory way to define a component. | ||
It could just be a function: | ||
```javascript | ||
function ComponentA() { | ||
this.number = 10; | ||
this.string = "Hello"; | ||
} | ||
entity.addComponent(Enemy); | ||
``` | ||
But the recommended way is using ES6 classes: | ||
### Single value components | ||
Components could be made of multiple attributes, but sometimes they just contain a single attribute. | ||
It these cases using the attribute's name to match the component's name may seem handy: | ||
```javascript | ||
class ComponentA { | ||
class Acceleration { | ||
constructor() { | ||
this.number = 10; | ||
this.string = "Hello"; | ||
this.acceleration = 0.1; | ||
} | ||
@@ -117,26 +100,40 @@ } | ||
Currently the `Component` class exporter by ECSY is a dummy class but eventually in the future we could use it for other purposes so we recommend extending the components from it: | ||
But when accessing the value it seems redundant to use two `acceleration` references: | ||
```javascript | ||
import { Component } from 'ecsy'; | ||
let acceleration = entity.getComponent(Acceleration).acceleration; | ||
``` | ||
class ComponentA extends Component { | ||
We suggest to use `value` as the attribute name for these components as: | ||
```javascript | ||
class Acceleration { | ||
constructor() { | ||
this.number = 10; | ||
this.string = "Hello"; | ||
this.value = 0.1; | ||
} | ||
} | ||
let acceleration = entity.getComponent(Acceleration).value; | ||
``` | ||
Eventually we could end up adding some syntactic sugar for these type of components returning directly the `value` attribute: | ||
```javascript | ||
let acceleration = entity.getComponentValue(Acceleration); | ||
``` | ||
### Components pooling | ||
Usually an ECSY application will involve adding and removing components in real time. Allocating resources in a performance sensitive application is considered a bad pattern as the garbage collector will get called often and it will hit the performance. | ||
In order to minimize it ECSY includes automatic pooling for components. | ||
This means that if we add component to an entity as: | ||
Usually an ECSY application will involve adding and removing components in real time. Allocating resources in a performance sensitive application is considered a bad pattern because the garbage collector will get called often and may impact performance. | ||
In order to minimize it, ECSY includes pooling for components. | ||
This means that every time a component is added to an entity: | ||
```javascript | ||
entity.addComponent(ComponentA) | ||
``` | ||
the engine will try to reuse a `ComponentA` instance from a pool of components previously created, and it won't allocate a new one instead. Once we are done with that component and do `entity.removeComponent(ComponentA)`, it will get returned to the pool, ready to be used by other entity. | ||
the engine will try to reuse a `ComponentA` instance, from the pool of components previously created, and it won't allocate a new one instead. | ||
When releasing that component, by calling `entity.removeComponent(ComponentA)`, it will get returned to the pool, ready to be used by another entity. | ||
ECSY should know how to reset a component to its original state, that's why it's mandatory that components implements a `reset` method to get the benefits from pooling, otherwise you will get a warning message about it. | ||
ECSY should know how to reset a component to its original state, that's why it's highly recommended that components implements a `reset` method to get the benefits from pooling. | ||
```javascript | ||
// Example of components with `reset` methods implemented | ||
class List extends Component { | ||
@@ -165,111 +162,390 @@ constructor() { | ||
Please notice that creating a component using the `createComponentClass` helper (**Link**) will include a `reset` implementation. | ||
It is possible to use the helper function `createComponentClass` to ease the creation of components as it will implement the `reset` and `copy` functions automatically. | ||
### Create component helper | ||
### System State Components | ||
## Data types | ||
System State Components (SSC) are components used by a system to hold internal resources for an entity. They are not removed when you delete the entity, you must explicitly remove them when you are done with them. | ||
They can be used to detect when an entity has been added or removed from a query. | ||
### Tag Components | ||
SSC can be defined by extending `SystemStateComponent` [API Reference](/api/classes/systemstatecomponent) instead of `Component`. Once the SSC is defined, it can be used as any other component. | ||
```javascript | ||
class StateComponentGeometry extends SystemStateComponent { | ||
constructor() { | ||
super(); | ||
this.meshReference = null; | ||
} | ||
} | ||
Some components don't store data and are used to tag some entities. It's recommended to extends `TagComponent` on these cases so the engine could, eventually, optimize the usage of this component. | ||
class Geometry { | ||
constructor() { | ||
this.primitive = "box"; | ||
} | ||
} | ||
``` | ||
In this example `StateComponentGeometry` is used to store the mesh resources created as defined in the `Geometry` component. | ||
If any other system removes that entity, the `Geometry` component will get removed but the `StateComponentGeometry` will remain "alive" so this system can detect it and free the mesh resources: | ||
```javascript | ||
class Enemy extends TagComponent {} | ||
class GeometrySystem extends System { | ||
init() { | ||
return { | ||
queries: { | ||
added: { components: [Geometry, Not(StateComponentGeometry)] }, | ||
remove: { components: [Not(Geometry), StateComponentGeometry] }, | ||
normal: { components: [Geometry, StateComponentGeometry] }, | ||
} | ||
}; | ||
}, | ||
execute() { | ||
added.forEach(entity => { | ||
var mesh = new Mesh(entity.getComponent(Geometry).primitive); | ||
entity.addComponent(StateComponentGeometry, {mesh: mesh}); | ||
}); | ||
entity.addComponent(Enemy); | ||
remove.forEach(entity => { | ||
var component = entity.getComponent(StateComponentGeometry); | ||
// free resources for the mesh | ||
component.mesh.dispose(); | ||
entity.removeComponent(StateComponentGeometry); | ||
}); | ||
normal.forEach(entity => { | ||
// use entity and its components (Geometry and StateComponentGeometry) if needed | ||
}); | ||
} | ||
} | ||
MySystem.queries = { | ||
added: { components: [Geometry, Not(StateComponentGeometry)] }, | ||
remove: { components: [Not(Geometry), StateComponentGeometry] }, | ||
normal: { components: [Geometry, StateComponentGeometry] }, | ||
}; | ||
``` | ||
### Single value components | ||
## Create component helper | ||
Creating a component and implementing its `reset` function can be a repetitive task specially when we are working with simple data types. | ||
At the same time it could lead to side effects errors, specially when pooling components, if there is some bug on one of the components' `reset` function for example. | ||
In order to ease this task, it is possible to use a helper function called `createComponentClass(schema, className)` which takes a JSON schema with the definition of the component and generate the class, automatically implementing the `reset`, `copy` and `clear` functions. | ||
Components could be made of multiple attributes, but sometimes they just contain a single attribute. | ||
It these cases using the attribute's name to match the component's name may seem handy: | ||
The JSON defines the number of the attributes of the components, its default value and type. | ||
```javascript | ||
class Acceleration { | ||
var ExampleComponent = createComponentClass({ | ||
number: { default: 0.5 }, | ||
string: { default: "foo" }, | ||
bool: { default: true }, | ||
array: { default: [1, 2, 3] }, | ||
vector3: { default: new Vector3(4, 5, 6), type: CustomTypes.Vector3 } | ||
}, "ExampleComponent"); | ||
``` | ||
Basic types (number, boolean, string and arrays) are inferred by the default value. It is possible to use custom type defined by `createType` (explained in the next section). | ||
The second parameter for `createComponentClass` is the class name for the component. The name is not mandatory but is strongly recommended as it will ease debugging and tracing. | ||
The previous example will create a `ExampleComponent` component that is ready to add to entities, as if it were created manually: | ||
```javascript | ||
entity.addComponent(ExampleComponent); | ||
``` | ||
In fact the equivalent of that code could be something like: | ||
```javascript | ||
class ExampleComponent extends Component { | ||
constructor() { | ||
this.acceleration = 0.1; | ||
super(); | ||
this.reset(); | ||
} | ||
clear() { | ||
this.number = 0; | ||
this.string = ""; | ||
this.bool = false; | ||
this.array.length = 0; | ||
this.vector3.set(0, 0, 0); | ||
} | ||
copy(src) { | ||
this.number = src.number; | ||
this.string = src.string; | ||
this.bool = src.bool; | ||
this.array = src.array.splice();; | ||
this.vector3.copy(src.vector3); | ||
} | ||
reset() { | ||
this.number = 0.5; | ||
this.string = "foo"; | ||
this.bool = true; | ||
this.array = [1, 2, 3]; | ||
this.vector3 = new Vector3(4, 5, 6); | ||
} | ||
} | ||
``` | ||
But when accesing the value it seems reduntant to use two `acceleration` references: | ||
Even using such a simple example, without complex data types, is easy to understand that implementing all the functions `clear`, `copy` and `reset` could lead to small bugs that could have unexpected side effects that should not be present when using the `createComponentClass`. | ||
It is important to note that when defining an schema every attribute must have a known type, if no data type is provided for complex types, `createComponentClass` will not implement `clear`, `copy` and `reset` and it will just return the component class with the attributes defined. | ||
### Data types | ||
It is possible to use custom types, to be used in the schema definition when calling `createComponentClass`, by defining them with `createType`: | ||
```javascript | ||
var acceleration = entity.getComponent(Acceleration).acceleration; | ||
createType({ | ||
baseType: T, | ||
create: defaultValue => {}, | ||
reset (src, key, defaultValue) => {}, | ||
clear: (src, key) => {}, | ||
}) | ||
``` | ||
We suggest to use `value` as the attribute name for these components as: | ||
Where: | ||
- `create(defaultValue)`: Return a value of type `baseType` using a default value. | ||
- `reset(src, key, defaultValue)`: Reset the `key` attribute on the object `src` with the default value. | ||
- `clear(src, key)`: Clear the `key` attribute on the object `src`. | ||
- `copy(src, dst, key)`: Copy the `key` attribute from the object `src` to the object `src`. | ||
Type definition for basic standard types are [already defined](https://github.com/MozillaReality/ecsy/blob/dev/src/StandardTypes.js) in the library: `number`, `boolean`, `string` and `array`. | ||
The following code implements a custom type for a `Vector3` imported from an external library: | ||
```javascript | ||
class Acceleration { | ||
var CustomVector3 = createType({ | ||
baseType: Vector3, | ||
create: defaultValue => { | ||
var v = new Vector3(0, 0, 0); | ||
if (typeof defaultValue !== "undefined") { | ||
v.copy(defaultValue); | ||
} | ||
return v; | ||
}, | ||
reset: (src, key, defaultValue) => { | ||
if (typeof defaultValue !== "undefined") { | ||
src[key].copy(defaultValue); | ||
} else { | ||
src[key].set(0, 0, 0); | ||
} | ||
}, | ||
clear: (src, key) => { | ||
src[key].set(0, 0, 0); | ||
} | ||
}); | ||
``` | ||
As the type for `Vector3` has been already defined, it is possible to use it to define a component: | ||
```javascript | ||
let ExampleComponent = createComponentClass({ | ||
vector3: { default: new Vector3(4, 5, 6), type: CustomVector3 } | ||
}, "ExampleComponent"); | ||
``` | ||
## Entities | ||
An entity is an object that has a unique ID. Its purpose is to group components together. [API Reference](/api/classes/entity). | ||
![Entities](https://ecsy.io/docs/manual/images/entities.svg) | ||
Entities should be created within a `World` context: | ||
```javascript | ||
let entity = world.createEntity(); | ||
``` | ||
### Adding components | ||
Once an entity is created, it is possible to add [components](/manual/Architecture?id=components) to it: | ||
```javascript | ||
class ComponentA { | ||
constructor() { | ||
this.value = 0.1; | ||
this.number = 10; | ||
this.string = "Hello"; | ||
} | ||
} | ||
var acceleration = entity.getComponent(Acceleration).value; | ||
// Add the component with the default values | ||
entity.addComponent(ComponentA); | ||
// Add the component replacing the default values | ||
entity.addComponent(ComponentA, {number: 20, string: "Hi"}); | ||
``` | ||
Eventually we could end up adding some syntactic sugar for these type of components returning directly the `value` attribute: | ||
### Accessing components and modify components | ||
Components can be accessed from an entity in two ways: | ||
- `getComponent(Component`: Get the component for read only operations. | ||
- `getMutableComponent(Component)`: Get the component to modify its values. | ||
If `DEBUG` mode is enabled it will throw an error if you try to modify a component accessed by `getComponent`, but that error will not be thrown on release mode because of performance reasons. | ||
These two access modes help to implement `reactive queries`([more info](/manual/Architecture?id=reactive-queries)), which are basically lists of entities populated with components that have mutated somehow, without much overhead on the execution as we avoid using custom setters or proxies. | ||
This means every time you request a mutable component, it will get marked as modified and systems listening for that will get notified accordingly. | ||
It's important to notice that the component will get marked as modified even if you don't change any attribute on it, so try to use `getMutableComponent` only when you know you will actually modify the component and use `getComponent` otherwise. | ||
Other positive side effects of these two modes are allowing automatic schedulers to analyze the code to parallelize it and making the code easily readable as we could understand how the system is acting on the components. | ||
### Removing components | ||
Another common operation on entities is to remove components: | ||
```javascript | ||
var acceleration = entity.getComponentValue(Acceleration); | ||
entity.removeComponent(ComponentA); | ||
``` | ||
## System State Components | ||
This will mark the component to be removed and will populate all the queues from the systems that are listening to that event, but the component itself won't be disposed until the end of the frame, we call it `deferred removal`. | ||
This is done so systems that need to react to it can still access the data of the components. | ||
## System | ||
Once a component is removed from an entity, it is possible to access its contents, by calling `getRemovedComponent(Component)`: | ||
Systems are stateless processors of groups of entities. | ||
```javascript | ||
class SystemFoo extends System { | ||
execute() { | ||
this.queries.boxes.removed.forEach(entity => { | ||
let component = entity.getRemovedComponent(Box); | ||
console.log('Component removed:', component, 'on entity: ', entity.id); | ||
}); | ||
### init | ||
### execute | ||
### ordering | ||
this.queries.boxes.results.forEach(entity => { | ||
console.log('Iterating on entity: ', entity.id); | ||
}); | ||
} | ||
} | ||
### Reactive systems | ||
SystemFoo.queries = { | ||
boxes: { | ||
components: [ Box ], | ||
removed: true // To listen for removed entities from the query | ||
} | ||
} | ||
### Life cycle | ||
let entity = world.createEntity().addComponent(Box); | ||
world.execute(); // Execute frame 1 | ||
entity.removeComponent(Box); | ||
world.execute(); // Execute frame 2 | ||
``` | ||
## Pooling | ||
This example will output: | ||
### Entity pooling | ||
``` | ||
Frame 1: | ||
- Iterating on entity: 1 | ||
Frame 2: | ||
- Component removed: box on entity: 1 | ||
``` | ||
### Component pooling | ||
Any further `execute()` will not log anything, since none of the queries are satisfied after the component `Box` was removed from the entity. | ||
### Prefab pooling | ||
Even if the deferred removal is the default behaviour, it is possible to remove a component immediately if needed, by passing a second parameter to `removeComponent(Component, forceImmediate)`. | ||
Although this is not the recommended behaviour because it could lead to side effect if other systems need to access the removed component: | ||
```javascript | ||
// The component will get removed immediately | ||
entity.removeComponent(ComponentA, true); | ||
``` | ||
## Deferred removal | ||
## Systems | ||
## Life cycle | ||
Systems are used to transform data stored on the components. Usually each system defines one or more queries of entities and iterates through these lists per frame. [API Reference](/api/classes/system) | ||
## Queries | ||
![Wolves and dragons](https://ecsy.io/docs/manual/images/systems.svg) | ||
A query is a collection of entities that match some condition based on the components they own. | ||
There are multiple ways to create a query: | ||
* Creating an instance of `Query(Components, World)` (Not recommended) | ||
* Getting the query directly from the `QueryManager` by calling `world.entityManager.queryComponents(Components)` | ||
* Defining the queries on a `System`. This is also the recommended way as the engine could use that information to organize and optimize the execution of the systems and queries. | ||
Every frame systems are executed and they create, remove or modify entities and components. | ||
A query is always updated with the entities that matches the components' condition. One the query is initialized it traverse the components groups to determine which entities should get added to it. But after that entities will get added or removed from the query as components are being added or removed from them. | ||
If we create several queries with the same components, the `QueryManager` will just create a single query under the hood and referece it everywhere saving memory and computation. | ||
The system interface is as follows: | ||
### Examples | ||
```javascript | ||
class SystemName extends System { | ||
init() {} | ||
execute(delta, time) {} | ||
} | ||
``` | ||
For example, if we want a query with all the entities that has the component `Position` on a system called `SystemTest`, we should define the query as: | ||
A system should always extends from the `System` class and it can implement two functions: | ||
- `init()`: This function is called when the system is registered in a world (Calling `world.registerSystem`) and can be used to initialize anything the system needs. | ||
- `execute(deltaTime, elapsedTime)`: It will get called each frame by default (unless a custom scheduler is being used). Usually it will be used to loop through the lists of entities from each query and process the value of theirs components. | ||
Systems could define one or more [queries](/manual/Architecture?id=queries) by setting the static `queries` attribute: | ||
```javascript | ||
SystemTest.queries = { | ||
positions: { | ||
components: [ Position ] | ||
} | ||
SystemName.queries = { | ||
boxes: { components: [ Box ] }, | ||
spheres: { components: [ Sphere ] } | ||
}; | ||
``` | ||
The `components` attribute defines the list of components that an entity must have to be included in this query. | ||
If a `queries` attribute is defined, is it possible to access the entities matching these queries on the `execute` method: | ||
You can also define multiple queries; | ||
```javascript | ||
class SystemName extends System { | ||
execute(delta, time) { | ||
this.queries.boxes.results.forEach(entity => { | ||
let box = entity.getComponent(Box); | ||
// Do whatever you want with box | ||
}); | ||
this.queries.Spheres.results.forEach(entity => { | ||
let sphere = entity.getComponent(Sphere); | ||
// Do whatever you want with Sphere | ||
}); | ||
} | ||
} | ||
``` | ||
If there is a `reactive query` (A query that *listens* for entities added or removed to it or which components has changed, [more info](/manual/Architecture?id=reactive-queries)) on the list of queries defined by a system, this system is called `reactive system` as it will react to changes on the entities and its components. | ||
### Registering a system | ||
Systems should be registered in a world in order to initialize them and add them to the default scheduler that will execute them on each frame. | ||
```javascript | ||
SystemTest.queries = { | ||
world.registerSystem(SystemName); | ||
``` | ||
### Execution order | ||
By default systems are executed on the same order they are registered in the world: | ||
```javascript | ||
world | ||
.registerSystem(SystemA) | ||
.registerSystem(SystemB) | ||
.registerSystem(SystemC); | ||
``` | ||
This will execute `SystemA > SystemB > SystemC`. | ||
You can also control the order of execution by adding a `priority: Number` attribute when registering them. | ||
By default systems have `priority=0` and they are sorted in ascending order. The lower the number the earlier the system will be executed. | ||
```javascript | ||
world | ||
.registerSystem(SystemA) | ||
.registerSystem(SystemB, { priority: 2 }) | ||
.registerSystem(SystemC, { priority: -1 }) | ||
.registerSystem(SystemD) | ||
.registerSystem(SystemE); | ||
``` | ||
This will result in the execution order: `SystemC > SystemA > SystemD > SystemE > SystemB`. | ||
## Queries | ||
A query is a collection of entities that match some conditions based on the components they own. | ||
The most common use case for queries is to define them in systems. This is also the recommended way as the engine could use that information to organize and optimize the execution of the systems and queries. Also if several queries are created with the same components, the `QueryManager` will just create a single query under the hood and reference it everywhere saving memory and computation. | ||
A query is always updated with the entities that match the components' condition. Once the query is initialized it traverses the components groups to determine which entities should be added to it. But after that, entities will get added or removed from the query as components are being added or removed from them. | ||
### Query syntax | ||
The only mandatory field in a query is `components` attribute which defines the list of components that an entity must have to be included in this query. | ||
```json | ||
{ | ||
QueryName: { | ||
components: ArrayOfComponents, | ||
listen: { | ||
added: Boolean, | ||
removed: Boolean, | ||
changed: Boolean | ArrayOfComponents | ||
} | ||
} | ||
} | ||
``` | ||
For example, defining a query containing all the entities that have both the components `Position` and `Velocity`: | ||
```javascript | ||
var query = { | ||
positions: { | ||
components: [ Position ] | ||
}, | ||
physicsObjects: { | ||
components: [ PhysicBody, Transform ] | ||
components: [ Position, Velocity ] | ||
} | ||
@@ -279,4 +555,2 @@ }; | ||
In this example the `physicsObjects` query will get populated with the components that have both `PhysicsBody` **and** `Transform`. | ||
### Not operator | ||
@@ -292,5 +566,5 @@ | ||
``` | ||
This will return all the entities that have a `Enemy` component but have not the `Dead` component. | ||
This will return all the entities that **have** a `Enemy` component but **do have not** the `Dead` component. | ||
This operator could be very useful as a factory pattern (**See example link**): | ||
This operator could be very useful as a factory pattern ([example](https://fernandojsg.github.io/ecsy/examples/factory/index.html)): | ||
```javascript | ||
@@ -303,5 +577,5 @@ SystemTest.queries = { | ||
``` | ||
The `playerUnitialized` query will have all the players that don't have a `Name` component yet, a system could get that list and add a random name to them: | ||
The `playerUnitialized` query will have all the players that don't have a `Name` component yet, a system could get this list and add a random name to them: | ||
```javascript | ||
queries.playerUnitialized.forEach(entity => { | ||
queries.playerUnitialized.results.forEach(entity => { | ||
entity.addComponent(Name, {value: getRandomName()}); | ||
@@ -349,10 +623,10 @@ }); | ||
To avoid callbacks and asynchronicity, which is a bad thing for cache and predictability on the execution, entities are queued on the `added` and `removed` lists but they system owning these lists will be able to process them just whenever the `execute` method will get called. | ||
So everytime you call `execute` you will have the list of all the entities added or removed since the last call. After the call has been executed these lists will get cleared. | ||
To avoid callbacks and asynchrony, which is a bad thing for cache and predictability reasons, entities are queued on the `added` and `removed` lists but the system owning these lists will be able to process them just whenever the `execute` method will get called. | ||
So every time you call `execute` you will have the list of all the entities added or removed since the last call. After the call has been executed these lists will be cleared. | ||
#### Changed | ||
Sometimes is also interesting to detect that an entity or a specific component has changed. Detecting these changes is more tricky to do performantly that's why we rely on the `entity.getMutableComponent` function (More info **link**) that basically mark the component as modified. | ||
The syntax to detect if an entity has changed, this means that any of the components from the entity that are part of the query have changed, is similar to the ones for `added` or `removed`: | ||
Sometimes is interesting to detect that an entity or a specific component has changed. This means that any of the components from the entity that are part of the query have changed. | ||
Detecting these changes is tricky to do performantly. That is why we rely on the `entity.getMutableComponent` function that marks the component as modified. | ||
The syntax to detect if an entity has changed, is similar to the ones for `added` or `removed`: | ||
```javascript | ||
@@ -364,3 +638,3 @@ SystemTest.queries = { | ||
added: true, | ||
removed: true | ||
removed: true, | ||
changed: true // Detect that any of the components on the query (Box, Transform) has changed | ||
@@ -376,3 +650,3 @@ } | ||
execute() { | ||
var boxesQuery = this.queries.boxes; | ||
let boxesQuery = this.queries.boxes; | ||
@@ -393,1 +667,109 @@ // All the entities with `Box` component | ||
``` | ||
Defining `changed: true` will populate the list if **any** of the components in the query have been modified. If you are just interested just in some specific components instead, you can define an array of components instead of the boolean value: | ||
```javascript | ||
SystemTest.queries = { | ||
boxes: { | ||
components: [ Box, Transform ], | ||
listen: { | ||
added: true, | ||
removed: true, | ||
changed: [ Box ] // Detect that the Box component has changed | ||
} | ||
} | ||
}; | ||
// ... | ||
boxesQuery.changed.forEach(entity => {}); // Box component has changed | ||
// ... | ||
``` | ||
## Entities and components life cycle | ||
By default ECSY uses deferred removal when removing an entity or a component: | ||
```javascript | ||
// Deferred remove component | ||
entity.removeComponent(Player); | ||
// Deferred remove entity | ||
entity.remove(); | ||
``` | ||
It is possible to override that behaviour and remove the component and entity immediately by passing `true` as an additional parameter to both functions. However this is not recommended behaviour as it could lead to side effects, so it should be used with caution: | ||
```javascript | ||
// Remove component immediately | ||
entity.removeComponent(Player, true); | ||
// Remove entity immediately | ||
entity.remove(true); | ||
``` | ||
When a component or an entity is removed, one `to be removed` flag is activated and the `reactive queries` ([more info](/manual/Architecture?id=reactive-queries)) listening for `removed` events will get populated by them. | ||
```javascript | ||
// Component to identify a wolf | ||
class Wolf extends TagComponent {} | ||
// Component to store how long is sleeping the wolf | ||
class Sleeping extends Component { | ||
constructor() { | ||
super(); | ||
this.startSleepingTime = 0; | ||
} | ||
} | ||
// This system will wake up sleeping wolves randomly | ||
class SystemAwakeWolves extends System { | ||
execute() { | ||
this.queries.sleepingWolves.results.forEach(wolf => { | ||
if (Math.random() > 0.5) { | ||
wolf.removeComponent(Sleeping); | ||
} | ||
}); | ||
} | ||
} | ||
SystemAwakeWolves.queries = { | ||
sleepingWolves: { components: [ Wolf, Sleeping ]} | ||
}; | ||
// This system will implements wolf reactions after just being awake | ||
class SystemWolfReactions extends System { | ||
execute(delta, elapsedTime) { | ||
this.queries.sleepingWolves.removed.forEach(wolf => { | ||
// We have to check if the "Sleeping" component has been removed | ||
// because if the "Wolf" component is removed instead, it will trigger | ||
// also ths "removed" event as is not fulfilling the query anymore either | ||
if (wolf.hasRemovedComponent(Sleeping)) { | ||
let sleeping = wolf.getRemovedComponent(Sleeping); | ||
let duration = elapsedTime - sleeping.startSleepingTime; | ||
// Do whatever with the `duration` value | ||
// eg: Make the wolf move slower if its was sleeping for so long | ||
} | ||
}); | ||
} | ||
} | ||
SystemWolfReactions.queries = { | ||
sleepingWolves: { | ||
components: [ Wolf, Sleeping ], | ||
listen: { | ||
removed: true | ||
} | ||
} | ||
}; | ||
``` | ||
In the previous example, the `SystemAwakeWolves` randomly wakes up wolves by removing the `Sleeping` component from them. The entities representing these wolves will get removed from its `sleepingWolves` query. | ||
As the `SystemWolfReactions` has the same query as the `SystemAwakeWolves`, the entity will also get removed from its query. Because the query is also [reactive](/manual/Architecture?id=reactive-queries) (`removed: true`), the `sleepingWolves.removed` will get populated with the wolves that were awake in the previous system. | ||
When iterating these removed entities, it is possible to access the removed `Sleeping` component by using `getRemovedComponent`. | ||
Please notice that if immediate removal was used, instead of the default deferred method, the component will not be accessible to any systems after it. | ||
This flow is exactly the same when removing entities instead of components. The entities and its components will still be available on the rest of the systems reacting to these deletions. | ||
### Clearing removal queues | ||
When using deferred removal the entities and components are deallocated at the end of the frame. | ||
After all the systems' `execute` method have been called, the components and entities are truly removed and returned to their pools. | ||
Is important to notice that if an application has a running order as: `SystemA > SystemB > SystemC` and `SystemA` removes an entity, `SystemB` and `SystemC` will be able to read it and its components. | ||
But if `SystemB` does the same, just `SystemC` will be able to react to it, as the entity and components data will get cleared at the end of the frame, so `SystemA` will not be able to read that data. | ||
Because of that is important that you define an appropriate execution order based on the needs for your reactive systems. | ||
There is one special use case when removing components and entities. When using `System State Components` they should be removed explicitly and they will not get removed if `entity.remove` is being called. [More info](/manual/Architecture?id=system-state-components) |
# Getting started | ||
## ECS principles | ||
First of all, let's define the common terminology used in ECS: | ||
- `entity`: An entity is an object that has an unique ID which purpose is to group components together. | ||
- `component`: Is an object that just store data. | ||
- `system`: Systems are stateless classes, than can define `queries` Stateless processors of list of entities that match specific condition. | ||
ECSY (Pronounced as "eksi") is an Entity Component System (ECS) engine for web applications. | ||
The basic idea of this pattern is to move from defining application entities using a class hierarchy to using composition in a Data Oriented Programming paradigm. ([More info on wikipedia](https://en.wikipedia.org/wiki/Entity_component_system)). Programming with an ECS can result in code that is more efficient and easier to extend over time. | ||
Some common terms within ECS engines are: | ||
- [entities](/manual/Architecture?id=entities): an object with a unique ID that can have multiple components attached to it. | ||
- [components](/manual/Architecture?id=components): different facets of an entity. ex: geometry, physics, hit points. Data is only stored in components. | ||
- [systems](/manual/Architecture?id=systems): do the actual work with in an application by processing entities and modifying their components. | ||
- [queries](/manual/Architecture?id=queries): used by systems to determine which entities they are interested in, based on the components the entities own. | ||
- [world](/manual/Architecture?id=world): a container for entities, components, systems and queries. | ||
The usual workflow when building an ECS based program: | ||
- Create the `components` that shape the data you need to use in your application. | ||
- Create `entities` and attach `components` to them. | ||
- Create the `systems` that will use these `components` to read and transform the data of these entities. | ||
- Execute all the systems each frame. | ||
## Creating a world | ||
By default your application should have at least one `world`. A world is basically a container for `entities`, `components` and `systems`. Even so, you can have multiple worlds running at the same time and enable or disable them as you need. | ||
A world is a container for `entities`, `components` and `systems`. Most applications have only one `world`, | ||
however you can have multiple worlds running at the same time and enable or disable them as you need. | ||
@@ -18,3 +29,3 @@ Let's start creating our first world: | ||
## Creating components | ||
By definition components are just objects that hold data. So we can use any way to define them, for example ES6 syntax (recommended): | ||
Components are just objects that hold data. We can use any way to define them, for example using ES6 class syntax (recommended): | ||
```javascript | ||
@@ -36,6 +47,6 @@ class Acceleration { | ||
More on how to create components LINK | ||
[More info on how to create components](/manual/Architecture?id=components). | ||
## Creating entities | ||
Having our world and some components already defined lets us create entities and attach theses components to them: | ||
Having our world and some components already defined, let's create [entities](/manual/Architecture?id=entities) and attach these components to them: | ||
```javascript | ||
@@ -48,3 +59,3 @@ var entityA = world | ||
world | ||
.createEntity(); | ||
.createEntity() | ||
.addComponent(Acceleration) | ||
@@ -54,22 +65,28 @@ .addComponent(Position, { x: Math.random() * 10, y: Math.random() * 10, z: 0}); | ||
``` | ||
With that we have just created 11 entities. 10 with the `Acceleration` and `Position` components, and one just with the `Position` component. | ||
With that we have just created 11 entities. 10 with the `Acceleration` and `Position` components, and one with just the `Position` component. | ||
Notice that the Position component is added using custom parameters. If we didn't use the parameters then the | ||
component would use the default values declared in the Position class. | ||
## Creating a system | ||
Now we are going to define a system to process the components we just created. | ||
A system should extend the `System` interface and could implement the following methods: | ||
- `init`: This will get called when the system is registered on a world. | ||
- `execute(delta, time)`: This is called on every frame (By default, but we could modify this eventually when using custom schedulers). | ||
Now we are going to define a [system](/manual/Architecture?id=systems) to process the components we just created. | ||
A system should extend the `System` interface and can implement the following methods: | ||
- `init`: This will get called when the system is registered in a world. | ||
- `execute(delta, time)`: This is called on every frame. | ||
We could also define the queries of entities in which we are interested in processing. The `queries` attribute should be a static attribute of your system. | ||
We could also define the [queries](/manual/Architecture?id=queries) of entities we are interested in based on the components they own. The `queries` attribute should be a static attribute of your system. | ||
Let's first create a system that will just loop through all the entities that has a `Position` component (11 in our example) and log theirs position. | ||
We will start by creating a system that will loop through all the entities that have a `Position` component (11 in our example) and log their positions. | ||
```javascript | ||
class PositionLogSystem extends System { | ||
init() { /* Do whatever you need here */ } | ||
// This method will get called on every frame by default | ||
// This method will get called on every frame | ||
execute(delta, time) { | ||
// Iterate through all the entities on the query | ||
this.queries.position.forEach(entity => { | ||
var position = entity.getComponent(Position); | ||
console.log(`Entity with ID: ${entity.id} has component Position={x: ${position.x}, y: ${position.y}, z: ${position.z}}`); | ||
// Access the component `Position` on the current entity | ||
let pos = entity.getComponent(Position); | ||
console.log(`Entity with ID: ${entity.id} has component Position={x: ${pos.x}, y: ${pos.y}, z: ${pos.z}}`); | ||
}); | ||
@@ -87,2 +104,3 @@ } | ||
The next system moves each entity that has both a Position and an Acceleration. | ||
@@ -92,8 +110,14 @@ ```javascript | ||
init() { /* Do whatever you need here */ } | ||
// This method will get called on every frame by default | ||
execute(delta, time) { | ||
// Iterate through all the entities on the query | ||
this.queries.moving.forEach(entity => { | ||
var acceleration = entity.getComponent(Acceleration).value; | ||
var position = entity.getMutableComponent(Position); | ||
// Get the `Acceleration` component as Read-only | ||
let acceleration = entity.getComponent(Acceleration).value; | ||
// Get the `Position` component as Writable | ||
let position = entity.getMutableComponent(Position); | ||
position.x += acceleration * delta; | ||
@@ -105,3 +129,9 @@ position.y += acceleration * delta; | ||
} | ||
``` | ||
Please note that we are accessing components on an entity by calling: | ||
- `getComponent(Component)`: If the component will be used as read-only. | ||
- `getMutableComponent(Component)`: If we plan to modify the values on the component. | ||
```javascript | ||
// Define a query of entities that have "Acceleration" and "Position" components | ||
@@ -115,18 +145,5 @@ System.queries = { | ||
We should register each system we want to use in one world so it get initialised and added to the default scheduler. | ||
```javascript | ||
world.registerSystem(MovableSystem); | ||
``` | ||
This system's query `moving` holds a list of entities that have both `Acceleration` and `Position`; 10 in total in our example. | ||
Please note that we are accessing components on an entity by calling: | ||
- `getComponent(Component)`: If the component will be used as read-only. | ||
- `getMutableComponent(Component)`: If we plan to modify the values on the components. | ||
These two access mode could help us: | ||
- Implement `reactive systems` without much overhead on the execution. | ||
- Help automatic schedulers to analyze the code to paralellize it. | ||
- Make the code easily readable as we could understand how the system is acting on the components. | ||
This system's query `moving` hold a list to the entities that has both `Acceleration` and `Position`, 10 in total in our example. | ||
Please notice also that we could create an arbitrary number of queries if needed and process them in `execute`, eg: | ||
Please notice that we could create an arbitrary number of queries if needed and process them in `execute`, ex: | ||
```javascript | ||
@@ -146,2 +163,13 @@ class SystemDemo extends System { | ||
Now let's register them in the world so they get initialized and added to the default scheduler to execute them on each frame. | ||
```javascript | ||
world | ||
.registerSystem(MovableSystem) | ||
.registerSystem(PositionLogSystem); | ||
``` | ||
For more information this please check the architecture documentation: [Accessing and modifying components](/manual/Architecture?id=accessing-components-and-modify-components) and [Reactive Queries](/manual/Architecture?id=reactive-queries) | ||
## Start! | ||
@@ -152,4 +180,4 @@ Now you just need to invoke `world.execute()` per frame. Currently ECSY doesn't provide a default scheduler so you must do it yourself. eg: | ||
// Compute delta and elapsed time | ||
var time = performance.now(); | ||
var delta = time - lastTime; | ||
let time = performance.now(); | ||
let delta = time - lastTime; | ||
@@ -169,6 +197,5 @@ // Run all the systems | ||
```javascript | ||
import {World, System} from 'ecsy'; | ||
// Components | ||
// Acceleration component | ||
class Acceleration { | ||
@@ -180,2 +207,3 @@ constructor() { | ||
// Position component | ||
class Position { | ||
@@ -190,5 +218,5 @@ constructor() { | ||
// Create world | ||
var world = new World(); | ||
let world = new World(); | ||
var entityA = world | ||
let entityA = world | ||
.createEntity() | ||
@@ -199,3 +227,3 @@ .addComponent(Position); | ||
world | ||
.createEntity(); | ||
.createEntity() | ||
.addComponent(Acceleration) | ||
@@ -212,4 +240,4 @@ .addComponent(Position, { x: Math.random() * 10, y: Math.random() * 10, z: 0}); | ||
this.queries.moving.forEach(entity => { | ||
var acceleration = entity.getComponent(Acceleration).value; | ||
var position = entity.getMutableComponent(Position); | ||
let acceleration = entity.getComponent(Acceleration).value; | ||
let position = entity.getMutableComponent(Position); | ||
position.x += acceleration * delta; | ||
@@ -230,3 +258,3 @@ position.y += acceleration * delta; | ||
// Initialize entities | ||
var entityA = world | ||
let entityA = world | ||
.createEntity() | ||
@@ -237,3 +265,3 @@ .addComponent(Position); | ||
world | ||
.createEntity(); | ||
.createEntity() | ||
.addComponent(Acceleration) | ||
@@ -244,6 +272,7 @@ .addComponent(Position, { x: Math.random() * 10, y: Math.random() * 10, z: 0}); | ||
// Run! | ||
let lastTime = performance.now(); | ||
function run() { | ||
// Compute delta and elapsed time | ||
var time = performance.now(); | ||
var delta = time - lastTime; | ||
let time = performance.now(); | ||
let delta = time - lastTime; | ||
@@ -257,3 +286,2 @@ // Run all the systems | ||
var lastTime = performance.now(); | ||
run(); | ||
@@ -263,4 +291,2 @@ ``` | ||
## What's next? | ||
This was just a quick overview on how things are structured using ECSY, but we encourage you to read the extended explainers for each feature ECSY provides: | ||
- **TODO** | ||
This was just a quick overview on how things are structured using ECSY, but we encourage you to [read the architecture documentation](/manual/Architecture) for more detailed information. |
{ | ||
"name": "ecsy", | ||
"version": "0.1.2", | ||
"version": "0.1.3", | ||
"description": "Entity Component System in JS", | ||
@@ -8,7 +8,8 @@ "main": "build/ecsy.js", | ||
"module": "build/ecsy.module.js", | ||
"types": "build/ecsy.d.ts", | ||
"types": "src/index.d.ts", | ||
"scripts": { | ||
"build": "rollup -c && npm run docs", | ||
"docs": "documentation build 'src/**' --config docs/api-doc-config.yml --no-markdown-toc --format md --output docs/api/README.md", | ||
"dev": "concurrently --names 'ROLLUP,DOCS,HTTP' -c 'bgBlue.bold,bgYellow.bold,bgGreen.bold' 'rollup -c -w -m inline' 'npm run docs -- --watch' 'npm run dev:server'", | ||
"docs": "rm docs/api/_sidebar.md; typedoc --readme none --mode file --excludeExternals --plugin typedoc-plugin-markdown --hideSources --hideBreadcrumbs --out docs/api/ --includeDeclarations --includes 'src/**/*.d.ts' src; touch docs/api/_sidebar.md", | ||
"dev:docs": "nodemon -e ts -x 'npm run docs' -w src", | ||
"dev": "concurrently --names 'ROLLUP,DOCS,HTTP' -c 'bgBlue.bold,bgYellow.bold,bgGreen.bold' 'rollup -c -w -m inline' 'npm run dev:docs' 'npm run dev:server'", | ||
"dev:server": "http-server -c-1 -p 8080 --cors", | ||
@@ -66,11 +67,15 @@ "lint": "eslint src test examples", | ||
"docsify-cli": "^4.3.0", | ||
"documentation": "^12.1.2", | ||
"eslint": "^5.16.0", | ||
"eslint-config-prettier": "^4.3.0", | ||
"eslint-plugin-prettier": "^3.1.0", | ||
"eslint-plugin-prettier": "^3.1.1", | ||
"http-server": "^0.11.1", | ||
"nodemon": "^1.19.2", | ||
"prettier": "^1.18.2", | ||
"rollup": "^1.21.2", | ||
"rollup-plugin-json": "^4.0.0" | ||
"rollup": "^1.21.4", | ||
"rollup-plugin-json": "^4.0.0", | ||
"rollup-plugin-terser": "^5.1.2", | ||
"typedoc": "^0.15.0", | ||
"typedoc-plugin-markdown": "^2.2.6", | ||
"typescript": "^3.6.3" | ||
} | ||
} |
203
README.md
@@ -10,15 +10,7 @@ # ecsy | ||
#### An highly experimental Entity Component System written in Javascript #### | ||
ECSY (pronounced as "eksi") is an highly experimental Entity Component System framework implemented in javascript, aiming to be lightweight, easy to use and with good performance. | ||
Designed aiming to achieve a "pure" ECS implementation, following two main rules: | ||
- Components are just data, and do not have behaviour. | ||
- Systems have just behaviour, without storing state. | ||
For detailed information on the architecture and API please visit the [documentation page](https://ecsy.io/docs/#/) | ||
# Examples | ||
- Ball example: | ||
- three.js: https://fernandojsg.github.io/ecsy/examples/ball-example/three | ||
- babylon: https://fernandojsg.github.io/ecsy/examples/ball-example/babylon/ (WIP) | ||
- 2D Canvas https://fernandojsg.github.io/ecsy/examples/canvas | ||
# Features | ||
## Features | ||
- Framework agnostic | ||
@@ -28,3 +20,2 @@ - Focused on providing a simple but yet efficient API | ||
- Systems, entities and components are scoped in a `world` instance | ||
- Singleton components can be registered per `world` | ||
- Multiple queries per system | ||
@@ -40,5 +31,14 @@ - Reactive support: | ||
# Roadmap | ||
# Examples | ||
- Ball example: | ||
- three.js: https://ecsy.io/examples/ball-example/three | ||
- babylon: https://ecsy.io/examples/ball-example/babylon | ||
- 2D Canvas https://ecsy.io/examples/canvas | ||
- Factory pattern with `Not` operator: https://ecsy.io/examples/factory | ||
- System state component example: https://ecsy.io/examples/systemstate | ||
# Getting started | ||
# Usage | ||
Installing the package via `npm`: | ||
``` | ||
@@ -48,141 +48,97 @@ npm install --save ecsy | ||
# Complete example | ||
**Warning: Highly experimental API subject to change every day :)** | ||
```javascript | ||
import {World, System, ReactiveSystem} from 'ecs'; | ||
import {World, System} from 'ecsy'; | ||
// Rotating component | ||
class Rotating { | ||
// Acceleration component | ||
class Acceleration { | ||
constructor() { | ||
this.rotatingSpeed = 0.1; | ||
this.decreasingSpeed = 0.001; | ||
this.value = 0.1; | ||
} | ||
} | ||
// Transform component | ||
class Transform { | ||
// Position component | ||
class Position { | ||
constructor() { | ||
this.rotation = { x: 0, y: 0, z: 0 }; | ||
this.position = { x: 0, y: 0, z: 0 }; | ||
this.scale = { x: 1, y: 1, z: 1 }; | ||
this.x = 0; | ||
this.y = 0; | ||
this.z = 0; | ||
} | ||
} | ||
// Mouse component (to be used as singleton) | ||
class MouseState { | ||
constructor() { | ||
this.mouseDown = false; | ||
} | ||
} | ||
// Create world | ||
var world = new World(); | ||
// Create a TestSystem to modify Transform components | ||
class RotatingSystem extends System { | ||
init() { | ||
return { | ||
queries: { | ||
entities: { components: [Rotating, Transform] } | ||
} | ||
}; | ||
} | ||
var entityA = world | ||
.createEntity() | ||
.addComponent(Position); | ||
execute(delta) { | ||
let entities = this.queries.entities; | ||
for (var i = 0; i < entities.length; i++) { | ||
var entity = entities[i]; | ||
var rotating = entity.getComponent(Rotating); | ||
var rotatingSpeed = rotating.rotatingSpeed; | ||
var transform = entity.getMutableComponent(Transform); | ||
transform.rotation.x += rotatingSpeed; | ||
} | ||
} | ||
for (let i = 0; i < 10; i++) { | ||
world | ||
.createEntity(); | ||
.addComponent(Acceleration) | ||
.addComponent(Position, { x: Math.random() * 10, y: Math.random() * 10, z: 0}); | ||
} | ||
// Create a TestSystem | ||
class MouseSystem extends System { | ||
init() { | ||
var mouseState = this.world.components.mouseState; | ||
window.addEventListener('mousedown', () => { | ||
mouseState.mouseDown = true; | ||
// Systems | ||
class MovableSystem extends System { | ||
init() { // Do whatever you need here } | ||
// This method will get called on every frame by default | ||
execute(delta, time) { | ||
// Iterate through all the entities on the query | ||
this.queries.moving.forEach(entity => { | ||
var acceleration = entity.getComponent(Acceleration).value; | ||
var position = entity.getMutableComponent(Position); | ||
position.x += acceleration * delta; | ||
position.y += acceleration * delta; | ||
position.z += acceleration * delta; | ||
}); | ||
window.addEventListener('mouseup', () => { | ||
mouseState.mouseDown = false; | ||
}); | ||
} | ||
} | ||
// Create a reactive system | ||
class ReactiveSystem extends System { | ||
init() { | ||
return { | ||
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] | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
// Define a query of entities that have "Acceleration" and "Position" components | ||
System.queries = { | ||
moving: { | ||
components: [Acceleration, Position] | ||
} | ||
execute() { | ||
console.log('OnAdded', this.events.entities.added); | ||
console.log('OnRemoved', this.events.entities.removed); | ||
console.log('OnChanged entities', this.events.entities.changed); | ||
console.log('OnChanged Rotating Component', this.events.entities.rotatingChanged); | ||
console.log('OnChanged Transform Component', this.events.entities.transformChanged); | ||
} | ||
} | ||
// Create a world and register all the elements on it | ||
var world = new World(); | ||
world | ||
.registerComponent(Rotating) | ||
.registerComponent(Transform) | ||
.registerSingletonComponent(MouseState); | ||
// Initialize entities | ||
var entityA = world | ||
.createEntity() | ||
.addComponent(Position); | ||
world | ||
.registerSystem(RotatingSystem) | ||
.registerSystem(MouseSystem); | ||
for (let i = 0; i < 10; i++) { | ||
world | ||
.createEntity(); | ||
.addComponent(Acceleration) | ||
.addComponent(Position, { x: Math.random() * 10, y: Math.random() * 10, z: 0}); | ||
} | ||
var entity = world.createEntity(); | ||
entity | ||
.addComponent(Rotating, {rotationSpeed: 0.2}) | ||
.addComponent(Transform); | ||
// Update systems per frame | ||
var previousTime = performance.now(); | ||
function update() { | ||
// Run! | ||
function run() { | ||
// Compute delta and elapsed time | ||
var time = performance.now(); | ||
var delta = time - previousTime; | ||
previousTime = time; | ||
var delta = time - lastTime; | ||
// Run all the systems | ||
world.execute(delta, time); | ||
requestAnimationFrame(update); | ||
lastTime = time; | ||
requestAnimationFrame(animate); | ||
} | ||
requestAnimationFrame(update); | ||
var lastTime = performance.now(); | ||
run(); | ||
``` | ||
You can also include the hosted javascript directly on your HTML: | ||
```html | ||
<!-- Using UMD (It will expose a global ECSY namespace) --> | ||
<script src="https://ecsy.io/build/ecsy.js"></script> | ||
<!-- Using ES6 modules --> | ||
<script src="https://ecsy.io/build/ecsy.module.js"></script> | ||
``` | ||
[npm]: https://img.shields.io/npm/v/ecsy.svg | ||
@@ -200,1 +156,2 @@ [npm-url]: https://www.npmjs.com/package/ecsy | ||
[build-status-url]: https://travis-ci.com/fernandojsg/ecsy | ||
import json from "rollup-plugin-json"; | ||
import { terser } from "rollup-plugin-terser"; | ||
export default { | ||
input: "src/index.js", | ||
plugins: [json({ exclude: ["node_modules/**"] })], | ||
output: [ | ||
{ | ||
format: "umd", | ||
name: "ECSY", | ||
noConflict: true, | ||
file: "build/ecsy.js", | ||
indent: "\t" | ||
}, | ||
{ | ||
format: "es", | ||
file: "build/ecsy.module.js", | ||
indent: "\t" | ||
} | ||
] | ||
}; | ||
export default [ | ||
{ | ||
input: "src/index.js", | ||
plugins: [json({ exclude: ["node_modules/**"] })], | ||
output: [ | ||
{ | ||
format: "umd", | ||
name: "ECSY", | ||
noConflict: true, | ||
file: "build/ecsy.js", | ||
indent: "\t" | ||
}, | ||
{ | ||
format: "es", | ||
file: "build/ecsy.module.js", | ||
indent: "\t" | ||
} | ||
] | ||
}, | ||
{ | ||
input: "src/index.js", | ||
plugins: [json({ exclude: ["node_modules/**"] }), terser()], | ||
output: [ | ||
{ | ||
format: "umd", | ||
name: "ECSY", | ||
noConflict: true, | ||
file: "build/ecsy.min.js", | ||
indent: "\t" | ||
} | ||
] | ||
} | ||
]; |
@@ -5,6 +5,2 @@ import ObjectPool from "./ObjectPool.js"; | ||
/** | ||
* @private | ||
* @class ComponentManager | ||
*/ | ||
export class ComponentManager { | ||
@@ -17,6 +13,2 @@ constructor() { | ||
/** | ||
* Register a component | ||
* @param {Component} Component Component to register | ||
*/ | ||
registerComponent(Component) { | ||
@@ -39,6 +31,2 @@ this.Components[Component.name] = Component; | ||
/** | ||
* Get components pool | ||
* @param {Component} Component Type of component type for the pool | ||
*/ | ||
getComponentsPool(Component) { | ||
@@ -45,0 +33,0 @@ var componentName = componentPropertyName(Component); |
import { inferType } from "./InferType"; | ||
/** | ||
* Create a component class from a schema | ||
*/ | ||
export function createComponentClass(schema, name) { | ||
@@ -7,0 +4,0 @@ //var Component = new Function(`return function ${name}() {}`)(); |
@@ -1,4 +0,1 @@ | ||
/** | ||
* Use createType to create custom type definitions. | ||
*/ | ||
export function createType(typeDefinition) { | ||
@@ -5,0 +2,0 @@ var mandatoryFunctions = [ |
@@ -1,5 +0,1 @@ | ||
/** | ||
* @private | ||
* @class DummyObjectPool | ||
*/ | ||
export default class DummyObjectPool { | ||
@@ -6,0 +2,0 @@ constructor(T) { |
@@ -9,11 +9,3 @@ import Query from "./Query.js"; | ||
/** | ||
* @class Entity | ||
*/ | ||
export default class Entity { | ||
/** | ||
* @constructor | ||
* @class Entity | ||
* @param {World} world | ||
*/ | ||
constructor(world) { | ||
@@ -44,9 +36,2 @@ this._world = world || null; | ||
/** | ||
* Return an immutable reference of a component | ||
* Note: A proxy will be used on debug mode, and it will just affect | ||
* the first level attributes on the object, it won't work recursively. | ||
* @param {Component} Type of component to get | ||
* @return {Component} Immutable component reference | ||
*/ | ||
getComponent(Component) { | ||
@@ -73,7 +58,2 @@ var component = this._components[Component.name]; | ||
/** | ||
* Return a mutable reference of a component. | ||
* @param {Component} Type of component to get | ||
* @return {Component} Mutable component reference | ||
*/ | ||
getMutableComponent(Component) { | ||
@@ -95,7 +75,2 @@ var component = this._components[Component.name]; | ||
/** | ||
* Add a component to the entity | ||
* @param {Component} Component to add to this entity | ||
* @param {Object} Optional values to replace the default attributes on the component | ||
*/ | ||
addComponent(Component, values) { | ||
@@ -106,6 +81,2 @@ this._world.entityAddComponent(this, Component, values); | ||
/** | ||
* Remove a component from the entity | ||
* @param {Component} Component to remove from the entity | ||
*/ | ||
removeComponent(Component, forceRemove) { | ||
@@ -116,7 +87,2 @@ this._world.entityRemoveComponent(this, Component, forceRemove); | ||
/** | ||
* Check if the entity has a component | ||
* @param {Component} Component to check | ||
* @param {Bool} include Components queued for removal (Default is false) | ||
*/ | ||
hasComponent(Component) { | ||
@@ -130,6 +96,2 @@ return !!~this._ComponentTypes.indexOf(Component); | ||
/** | ||
* Check if the entity has all components in a list | ||
* @param {Array(Component)} Components to check | ||
*/ | ||
hasAllComponents(Components) { | ||
@@ -142,6 +104,2 @@ for (var i = 0; i < Components.length; i++) { | ||
/** | ||
* Check if the entity has any components in a list | ||
* @param {Array(Component)} Components to check | ||
*/ | ||
hasAnyComponents(Components) { | ||
@@ -154,5 +112,2 @@ for (var i = 0; i < Components.length; i++) { | ||
/** | ||
* Remove all the components from the entity | ||
*/ | ||
removeAllComponents(forceRemove) { | ||
@@ -164,5 +119,3 @@ return this._world.entityRemoveAllComponents(this, forceRemove); | ||
/** | ||
* Initialize the entity. To be used when returning an entity to the pool | ||
*/ | ||
// Initialize the entity. To be used when returning an entity to the pool | ||
reset() { | ||
@@ -176,5 +129,2 @@ this.id = nextId++; | ||
/** | ||
* Remove the entity from the world | ||
*/ | ||
remove(forceRemove) { | ||
@@ -181,0 +131,0 @@ return this._world.removeEntity(this, forceRemove); |
@@ -1,5 +0,1 @@ | ||
/** | ||
* @private | ||
* @class ObjectPool | ||
*/ | ||
export default class ObjectPool { | ||
@@ -6,0 +2,0 @@ // @todo Add initial size |
import EventDispatcher from "./EventDispatcher.js"; | ||
import { queryKey } from "./Utils.js"; | ||
/** | ||
* @private | ||
* @class Query | ||
*/ | ||
export default class Query { | ||
@@ -9,0 +5,0 @@ /** |
import Query from "./Query.js"; | ||
/** | ||
* @class System | ||
*/ | ||
export class System { | ||
@@ -28,5 +25,2 @@ canExecute() { | ||
this._events = {}; | ||
this.events = {}; | ||
this.priority = 0; | ||
@@ -141,56 +135,4 @@ | ||
} | ||
/* | ||
if (queryConfig.events) { | ||
this.events[name] = {}; | ||
let events = this.events[name]; | ||
for (let eventName in queryConfig.events) { | ||
let event = queryConfig.events[eventName]; | ||
events[eventName] = []; | ||
const eventMapping = { | ||
EntityAdded: Query.prototype.ENTITY_ADDED, | ||
EntityRemoved: Query.prototype.ENTITY_REMOVED, | ||
EntityChanged: Query.prototype.COMPONENT_CHANGED // Query.prototype.ENTITY_CHANGED | ||
}; | ||
if (eventMapping[event.event]) { | ||
query.eventDispatcher.addEventListener( | ||
eventMapping[event.event], | ||
entity => { | ||
// @fixme A lot of overhead? | ||
if (events[eventName].indexOf(entity) === -1) | ||
events[eventName].push(entity); | ||
} | ||
); | ||
if (event.event === "EntityChanged") { | ||
query.reactive = true; | ||
} | ||
} else if (event.event === "ComponentChanged") { | ||
query.reactive = true; | ||
query.eventDispatcher.addEventListener( | ||
Query.prototype.COMPONENT_CHANGED, | ||
(entity, component) => { | ||
if (event.components.indexOf(component.constructor) !== -1) { | ||
events[eventName].push(entity); | ||
} | ||
} | ||
); | ||
} | ||
} | ||
} | ||
*/ | ||
} | ||
} | ||
/* | ||
if (this.config.events) { | ||
for (let eventName in this.config.events) { | ||
var event = this.config.events[eventName]; | ||
this.events[eventName] = []; | ||
this.world.addEventListener(event, data => { | ||
this.events[eventName].push(data); | ||
}); | ||
} | ||
} | ||
*/ | ||
} | ||
@@ -216,3 +158,3 @@ | ||
} else { | ||
for (name in query.changed) { | ||
for (let name in query.changed) { | ||
query.changed[name].length = 0; | ||
@@ -222,9 +164,3 @@ } | ||
} | ||
// @todo add changed | ||
} | ||
// @question Standard events TBD? | ||
for (var name in this.events) { | ||
this.events[name].length = 0; | ||
} | ||
} | ||
@@ -238,6 +174,6 @@ | ||
priority: this.priority, | ||
queries: {}, | ||
events: {} | ||
queries: {} | ||
}; | ||
/* | ||
if (this.config) { | ||
@@ -264,10 +200,4 @@ var queries = this.queries; | ||
} | ||
let events = this.config.events; | ||
for (let eventName in events) { | ||
json.events[eventName] = { | ||
eventName: events[eventName] | ||
}; | ||
} | ||
} | ||
*/ | ||
@@ -278,5 +208,2 @@ return json; | ||
/** | ||
* Negate a component query | ||
*/ | ||
export function Not(Component) { | ||
@@ -283,0 +210,0 @@ return { |
@@ -1,5 +0,1 @@ | ||
/** | ||
* @private | ||
* @class SystemManager | ||
*/ | ||
export class SystemManager { | ||
@@ -12,6 +8,2 @@ constructor(world) { | ||
/** | ||
* Register a system | ||
* @param {System} System System to register | ||
*/ | ||
registerSystem(System, attributes) { | ||
@@ -33,6 +25,2 @@ var system = new System(this.world, attributes); | ||
/** | ||
* Return a registered system based on its class | ||
* @param {System} System | ||
*/ | ||
getSystem(System) { | ||
@@ -42,5 +30,2 @@ return this._systems.find(s => s instanceof System); | ||
/** | ||
* Return all the systems registered | ||
*/ | ||
getSystems() { | ||
@@ -50,6 +35,2 @@ return this._systems; | ||
/** | ||
* Remove a system | ||
* @param {System} System System to remove | ||
*/ | ||
removeSystem(System) { | ||
@@ -62,7 +43,2 @@ var index = this._systems.indexOf(System); | ||
/** | ||
* Update all the systems. Called per frame. | ||
* @param {Number} delta Delta time since the last frame | ||
* @param {Number} time Elapsed time | ||
*/ | ||
execute(delta, time) { | ||
@@ -81,5 +57,2 @@ this._executeSystems.forEach(system => { | ||
/** | ||
* Return stats | ||
*/ | ||
stats() { | ||
@@ -86,0 +59,0 @@ var stats = { |
@@ -1,6 +0,3 @@ | ||
/** | ||
* @class | ||
*/ | ||
export class TagComponent { | ||
reset() {} | ||
} |
import { SystemManager } from "./SystemManager.js"; | ||
import { EntityManager } from "./EntityManager.js"; | ||
import { ComponentManager } from "./ComponentManager.js"; | ||
import EventDispatcher from "./EventDispatcher.js"; | ||
/** | ||
* @class World | ||
*/ | ||
export class World { | ||
@@ -18,3 +14,2 @@ constructor() { | ||
this.eventQueues = {}; | ||
this.eventDispatcher = new EventDispatcher(); | ||
@@ -27,18 +22,2 @@ if (typeof CustomEvent !== "undefined") { | ||
emitEvent(eventName, data) { | ||
this.eventDispatcher.dispatchEvent(eventName, data); | ||
} | ||
addEventListener(eventName, callback) { | ||
this.eventDispatcher.addEventListener(eventName, callback); | ||
} | ||
removeEventListener(eventName, callback) { | ||
this.eventDispatcher.removeEventListener(eventName, callback); | ||
} | ||
/** | ||
* Register a component | ||
* @param {Component} Component | ||
*/ | ||
registerComponent(Component) { | ||
@@ -49,6 +28,2 @@ this.componentsManager.registerComponent(Component); | ||
/** | ||
* Register a system | ||
* @param {System} System | ||
*/ | ||
registerSystem(System, attributes) { | ||
@@ -59,6 +34,2 @@ this.systemManager.registerSystem(System, attributes); | ||
/** | ||
* Return a registered system based on its class | ||
* @param {System} System | ||
*/ | ||
getSystem(SystemClass) { | ||
@@ -68,5 +39,2 @@ return this.systemManager.getSystem(SystemClass); | ||
/** | ||
* Get all the systems registered | ||
*/ | ||
getSystems() { | ||
@@ -76,7 +44,2 @@ return this.systemManager.getSystems(); | ||
/** | ||
* Update the systems per frame | ||
* @param {Number} delta Delta time since the last call | ||
* @param {Number} time Elapsed time | ||
*/ | ||
execute(delta, time) { | ||
@@ -97,5 +60,2 @@ if (this.enabled) { | ||
/** | ||
* Create a new entity | ||
*/ | ||
createEntity() { | ||
@@ -105,5 +65,2 @@ return this.entityManager.createEntity(); | ||
/** | ||
* Get some stats | ||
*/ | ||
stats() { | ||
@@ -110,0 +67,0 @@ var stats = { |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
887373
98
19
6161
153