Entity-Component-System (ECS) in JavaScript A-Frame
Pan Chasinga
Posted on February 15, 2018
I have been developing in A-Frame, an opensource WebVR framework from Mozilla. What makes A-Frame very unique from WebGL library like Three.js are — first, it’s built on top of Three.js to be more declarative via HTML DOM and attributes, second, it’s catered toward WebVR, which means it comes with VR mode out of the box, and lastly and most importantly, it adopts the Entity-Component-System (ECS) Pattern.
I have not been in the game design scene, and thus this was the first time I’ve ever been exposed to ECS. In a nutshell, it is very similar to composition as a general design pattern. The difference at the abstraction level is composition focuses on the “HAS-A” in polymorphism while ECS focuses on applying behaviors to entities.
However, in Object-Oriented Pattern (OOP), any instance of a subclass “IS-A” instance of its superclass. This Darwinian abstraction model has been very useful for decades and serving as a great polymorphic model for the tech industry until recently. Somehow, we discovered, carrying over the baggage from your ancestor(s) isn’t always optimal or necessarily easy to comprehend. Moreover, multiple inheritance (sub-classing more than one super class) is almost always a mess because it suddenly raises an existential problem for a child instance whose class inherits more than one parent.
Interface, on the other hand, is a good abstraction. It does imply a light “IS-A” relationship, but thankfully it does not let other objects inherit it or act like a super being to them. It is nothing more than a loose gatekeeper who let any instance pass through the door and become a member of the club as long as it has the right set of properties or methods. Thus this means an instance can belong to any “clubs” it is allow to do as long as when it’s there it can do stuff others do and blend in. Somehow, as a programmer, you still need to trade off the new complexity of maintaining interface code with this flexibility. Also, when you look at it from another perspective, it can feel like a shallow inheritance that doesn’t go beyond one level.
Coming back to ECS. It is quite well-known in the game design and development circle. Imagine the Street Fighter-style fighting game where you select a character to play against the other side. The entity is an empty skeleton of a human (or unhuman) fighter which may owns a set of simple behaviors like punching, kicking, or jumping and a property like the HP (Health Point). The character you choose is actually a set of predefined components / behaviors which can be applied and enhance the entities. For instance, a Chun-Li component can modify a base entity’s jump behavior to become unique to Chun-Li, add Spinning-bird Kick move, and of course, apply the texture of the character. These components can also interact with other components i.e. Spinning-bird Kick can have a default damage of -5 HP, but when interact with an entity with a Psycho Crusher component, it can inflict a humble -1 HP.
The S in the ECS, the System, is not mentioned much in A-Frame. It is mentioned as an optional service layer that centralizes persistent state and control of all its registered components, much like the service in Angular. I am currently using A-Frame system to communicate with the Angular UI component to isolate the two frameworks as much as possible.
ECS is a very flexible pattern and I can see why it’s suitable for game development. It’s focused on decoupling and usability minus the attempt to achieve the bookish abstraction other design patterns are striving for.
For instance, here is an a-box primitive entity in A-Frame, which basically renders a 3D cube on the canvas:
<a-box color="#FFF" position="0 0 0"></a-box>
In order to make a-box sings (actually, console log a message), you can register a sing
component like this one:
AFRAME.registerComponent('sing', {
schema: {type: 'string', default: "doh re me!"},
init: function() {
console.log(this.data);
}
});
And makes a-box
adopt this component:
<a-box sing="helloooo" color="#FFF" position="0 0 0"></a-box>
Then upon loaded, you will see the little log on the browser “helloooo”.
A component has a certain life-cycle hooks that allows us to control the timing of the behavior it carries. Let’s hook into update cycle and sing a bit louder:
AFRAME.registerComponent('sing', {
schema: {type: 'string', default: "doh re me!"},
init: function() {
console.log(this.data);
}
// update
update: function() {
alert(this.data);
}
});
Now, try setting the sing
attribute to something else, maybe nothing:
let box = document.querySelector('a-box');
box.setAttribute('sing', '');
The window pops up an alert with the default string “doh re me!”.
This entity-component relationship can be really flexible and powerful. Imagine you can add and remove elements, change colors, positions, movement, etc. base on DOM events. We have not yet even tread into the realm of WebVR yet, and already this pattern is sticking.
Originally published here.
Posted on February 15, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.