
Today we are introducing ECSY (Pronounced “eck-see”): a new -highly experimental- Entity Component System framework for Javascript.
After working on many interactive graphics projects for the web in the last few years we were trying to identify the common issues when developing something bigger than a simple example.
Based on our findings we discussed what an ideal framework would need:
These requirements are high-level features that are not usually provided by graphics engines like three.js or babylon.js. On the other hand, A-Frame provides a nice component-based architecture, which is really handy when developing bigger projects, but it lacks the rest of the previously mentioned features. For example:
A-Frame applications with good performance, this could be done by breaking the API contract, for example by accessing the values of the components directly instead of using setAttribute/getAttribute. This can lead to some unwanted side effects, such as incompatibility between components and a lack of reactive behavior.A-Frame and its components are so strongly tied to Three.js that it makes no sense to change it to any other engine.After analyzing these points, gathering our experience with three.js and A-Frame, and looking at the state of the art on game engines like Unity, we decided to work on building this new framework using a pure Entity Component System architecture. The difference between a pure ECS like Unity DOTS, entt, or Entitas, and a more object oriented approach, such as Unity’s MonoBehaviour or A-Frame's Components, is that in the latter the components and systems both have logic and data, while with a pure ECS approach components just have data (without logic) and the logic resides in the systems.
Focusing on building a simple core for this new framework helps iterate faster when developing new applications and lets us implement new features on top of it as needed. It also allows us to use it with existing libraries as three.js, Babylon.js, Phaser, PixiJS, interacting directly with the DOM, Canvas or WebGL APIs, or prototype around new APIs as WebGPU, WebAssembly or WebWorkers.
Technology stack examples
We decided to use a data-oriented architecture as we noticed that having data and logic separated helps us better think about the structure of our applications. This also allows us to work internally on optimizations, for example how to store and access this data or how to get the advantage of parallelism for the logic.
The terms you must know in order to work with our framework are mostly the same as any other ECS:
ECSY Architecture
So far all the information has been quite abstract, so let’s dig into a simple example to see how the API feels.
The example will consist of boxes and circles moving around the screen, nothing fancy but enough to understand how the API works.
We will start by defining components that will be attached to the entities in our application:
circle or box.speed and position and it will update their position component.
Circles and balls example design
Below is the code for the example described, some parts have been omitted to abbreviate (Check the full source code on Github or Glitch)
We start by defining the components we will be using:
// Velocity component
class Velocity {
constructor() {
this.x = this.y = 0;
}
}
// Position component
class Position {
constructor() {
this.x = this.y = 0;
}
}
// Shape component
class Shape {
constructor() {
this.primitive = 'box';
}
}
// Renderable component
class Renderable extends TagComponent {}
Now we implement the two systems our example will use:
// MovableSystem
class MovableSystem extends System {
// This method will get called on every frame by default
execute(delta, time) {
// Iterate through all the entities on the query
this.queries.moving.results.forEach(entity => {
var velocity = entity.getComponent(Velocity);
var position = entity.getMutableComponent(Position);
position.x += velocity.x * delta;
position.y += velocity.y * delta;
if (position.x > canvasWidth + SHAPE_HALF_SIZE) position.x = - SHAPE_HALF_SIZE;
if (position.x < - SHAPE_HALF_SIZE) position.x = canvasWidth + SHAPE_HALF_SIZE;
if (position.y > canvasHeight + SHAPE_HALF_SIZE) position.y = - SHAPE_HALF_SIZE;
if (position.y < - SHAPE_HALF_SIZE) position.y = canvasHeight + SHAPE_HALF_SIZE;
});
}
}
// Define a query of entities that have "Velocity" and "Position" components
MovableSystem.queries = {
moving: {
components: [Velocity, Position]
}
}
// RendererSystem
class RendererSystem extends System {
// This method will get called on every frame by default
execute(delta, time) {
ctx.globalAlpha = 1;
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
// Iterate through all the entities on the query
this.queries.renderables.results.forEach(entity => {
var shape = entity.getComponent(Shape);
var position = entity.getComponent(Position);
if (shape.primitive === 'box') {
this.drawBox(position);
} else {
this.drawCircle(position);
}
});
}
// drawCircle and drawCircle hidden for simplification
}
// Define a query of entities that have "Renderable" and "Shape" components
RendererSystem.queries = {
renderables: { components: [Renderable, Shape] }
}
We create a world and register the systems that it will use:
var world = new World();
world
.registerSystem(MovableSystem)
.registerSystem(RendererSystem);
We create some entities with random position, speed, and shape.
for (let i = 0; i < NUM_ELEMENTS; i++) {
world
.createEntity()
.addComponent(Velocity, getRandomVelocity())
.addComponent(Shape, getRandomShape())
.addComponent(Position, getRandomPosition())
.addComponent(Renderable)
}
Finally, we just have to update it on each frame:
function run() {
// Compute delta and elapsed time
var time = performance.now();
var delta = time - lastTime;
// Run all the systems
world.execute(delta, time);
lastTime = time;
requestAnimationFrame(run);
}
var lastTime = performance.now();
run();
The main features that the framework currently has are:
Babylon.js, three.js, and 2D canvas. To make things even easier, we plan to release a set of bindings and helper components for the most commonly used engines, starting with three.js.This project is still in its early days so you can expect a lot of changes with the API and many new features to arrive in the upcoming weeks. Some of the ideas we would like to work on are:
Please feel free to use Github to follow the development, request new features or file issues on bugs you find, our discourse forum to discuss how to use ecsy on your projects, and ecsy.io for more examples and documentation.