Entity-Component-System
Note: This documentation is for the old 1.4.0 version of A-Frame. Check out the documentation for the current 1.6.0 version
A-Frame is a three.js framework with an entity-component-system (ECS) architecture. ECS architecture is a common and desirable pattern in 3D and game development that follows the composition over inheritance and hierarchy principle.
The benefits of ECS include:
- Greater flexibility when defining objects by mixing and matching reusable parts.
- Eliminates the problems of long inheritance chains with complex interwoven functionality.
- Promotes clean design via decoupling, encapsulation, modularization, reusability.
- Most scalable way to build a VR application in terms of complexity.
- Proven architecture for 3D and VR development.
- Allows for extending new features (possibly sharing them as community components).
On the 2D Web, we lay out elements that have fixed behavior in a hierarchy. 3D and VR is different; there are infinite types of possible objects that have unbounded behavior. ECS provides a manageable pattern to construct types of objects.
Below are great introductory materials to ECS architecture. We recommend skimming through them to get a better grasp of the benefits. ECS is well-suited for VR development, and A-Frame is based entirely around this paradigm:
- Entity-component-system on Wikipedia
- What is an Entity System? by Adam Martin
- Decoupling Patterns — Component on Game Programming Patterns
- Evolve Your Hierarchy by Mick West
A well-known game engine implementing ECS is Unity. Although there are pain points in cross-entity communication, we’ll see how A-Frame, the DOM, and declarative HTML really make ECS shine.
Concept
A basic definition of ECS involves:
- Entities are container objects into which components can be
attached. Entities are the base of all objects in the scene. Without
components, entities neither do nor render anything, similar to empty
<div>
s. - Components are reusable modules or data containers that can be attached to entities to provide appearance, behavior, and/or functionality. Components are like plug-and-play for objects. All logic is implemented through components, and we define different types of objects by mixing, matching, and configuring components. Like alchemy!
- Systems provide global scope, management, and services for classes of components. Systems are often optional, but we can use them to separate logic and data; systems handle the logic, components act as data containers.
Examples
Some abstract examples of different types of entities built from composing together different components:
Box = Position + Geometry + Material
Light Bulb = Position + Light + Geometry + Material + Shadow
Sign = Position + Geometry + Material + Text
VR Controller = Position + Rotation + Input + Model + Grab + Gestures
Ball = Position + Velocity + Physics + Geometry + Material
Player = Position + Camera + Input + Avatar + Identity
As another abstract example, imagine we want to build a car entity by assembling components:
- We can attach a
material
component that has properties such as “color” or “shininess” that affects the appearance of the car. - We can attach an
engine
component that has properties such as “horsepower” or “weight” that affects the functionality of the car. - We can attach a
tire
component that has properties such as “number of tires” or “steering angle” that affects the behavior of the car.
So we can create different types of cars by varying the properties of the
material
, engine
, and tire
component. The material
, engine
, and
tire
components don’t have to know about each other and can even be used in
isolation for other cases. We could mix and match them to create even
different types of vehicles:
- To create a boat entity: remove the
tire
component. - To create a motorcycle entity: change
tire
component’s number of tires to 2, configure theengine
component to be smaller. - To create an airplane entity: attach
wing
andjet
components.
Contrast this to traditional inheritance where if we want to extend an object, we would have to create a large class that tries to handle everything or an inheritance chain.
ECS in A-Frame
A-Frame has APIs that represents each piece of ECS:
- Entities are represented by the
<a-entity>
element and prototype. - Components are represented by HTML attributes on
<a-entity>
‘s. Underneath, components are objects containing a schema, lifecycle handlers, and methods. Components are registered via theAFRAME.registerComponent (name, definition)
API. - Systems are represented by
<a-scene>
‘s HTML attributes. System are similar to components in definition. Systems are registered via theAFRAME.registerSystem (name, definition)
API.
Syntax
We create <a-entity>
and attach components as HTML attributes. Most
components have multiple properties that are represented by a syntax similar to
HTMLElement.style
CSS. This syntax takes the form with a colon
(:
) separating property names from property values, and a semicolon (;
)
separating different property declarations:
<a-entity ${componentName}="${propertyName1}: ${propertyValue1}; ${propertyName2}: ${propertyValue2}">
For example, we have <a-entity>
and attach the geometry, material,
light, and position components with various properties and property values:
<a-entity geometry="primitive: sphere; radius: 1.5" |
Composition
From there, we could attach more components to add additional appearance,
behavior, or functionality (e.g., physics). Or we could update the component
values to configure the entity (either declaratively or through
.setAttribute
).
A common type of entity to compose from multiple components are the player’s hands in VR. The player’s hands can have many components: appearance, gestures, behaviors, interactivity with other objects.
We plug in components into a hand entity to provide it behavior as if we were attaching superpowers or augmentations for VR! Each of the components below have no knowledge of each other, but can be combined to define a complex entity:
<a-entity |
Declarative DOM-Based ECS
A-Frame takes ECS to another level by making it declarative and based on the DOM. Traditionally, ECS-based engines would create entities, attach components, update components, remove components all through code. But A-Frame has HTML and the DOM which makes ECS ergonomic and resolves many of its weaknesses. Below are abilities that the DOM provides for ECS:
- Referencing Other Entities with Query Selectors: The DOM provides a powerful
query selector system which lets us query the scene graph and select an entity
or entities that match a condition. We can get references to entities by IDs,
classes, or data attributes. Because A-Frame is based on HTML, we can use query
selectors out of the box.
document.querySelector('#player')
. - Decoupled Cross-Entity Communication with Events: The DOM provides the
ability to listen to and emit events. This provides a publish-subscribe
communication system between entities. Components don’t have to know about each
other, they can just emit an event (which could bubble up), and other
components can listen to those events without calling each other.
ball.emit('collided')
. - APIs for Lifecycle Management with DOM APIs: The DOM provides APIs to
update HTML elements and the tree including
.setAttribute
,.removeAttribute
,.createElement
, and.removeChild
. These can be used as is just like in normal web development. - Entity-Filtering with Attribute Selectors: The DOM provides attribute
selectors which allows us to query for an entity or entities that have or don’t
have certain HTML attributes. This means we can ask for entities that have or
don’t have a certain set of components.
document.querySelector('[enemy]:not([alive])')
. - Declarativeness: Lastly, the DOM provides HTML. A-Frame bridges between ECS and HTML making an already clean pattern declarative, readable, and copy-and-pasteable.
Extensibility
A-Frame components can do anything. Developers are given permissionless innovation to create components to extend any feature. Components have full access to JavaScript, three.js, and Web APIs (e.g., WebRTC, Speech Recognition).
We will later go over in detail how to write an A-Frame component. As a preview, the structure of a basic component may look like:
AFRAME.registerComponent('foo', { |
Declarative ECS grants us the ability to write a JavaScript module and abstract
it through HTML. Once the component is registered, we can declaratively plug
this module of code into an entity via an HTML attribute. This code-to-HTML
abstraction makes ECS powerful and easy to reason. foo
is the name of the
component we just registered, and the data contains bar
and baz
properties:
<a-entity foo="bar: 5; baz: bazValue"></a-entity> |
Component-Based Development
For building VR applications, we recommend placing all application code within components (and systems). An ideal A-Frame codebase consists purely of modular, encapsulated, and decoupled components. These components can be unit tested in isolation or alongside other components.
When an application is created solely with components, all parts of its codebase become reusable! Components can be shared for other developers to use or we can reuse them in our other projects. Or the components can be forked and modified to adapt to other use cases.
A simple ECS codebase might be structured like:
index.html |
Higher-Order Components
Components can set other components on the entity, making them a higher-order or higher-level component in abstraction.
For example, the cursor component sets and builds on top of the raycaster component. Or the hand-controls component sets and builds on top of the vive-controls component and oculus-touch-controls component which in turn build on top of the tracked-controls component.
Community Component Ecosystem
Components can be shared into the A-Frame ecosystem for the community to use.
The wonderful thing about A-Frame’s ECS is extensibility. An experienced
developer can develop a physics system or graphics shader components, and an
novice developer can take those components and use them in their scene from
HTML just by dropping in a <script>
tag. We can use powerful published
components without having to touch JavaScript.
Where to Find Components
There are hundreds of components out in the wild. We try our best to make them discoverable. If you develop a component, please submit it through these channels to share!
npm
Most A-Frame components are published on npm as well as GitHub. We can use
npm’s search to search for aframe-components
. npm lets us sort by
quality, popularity, and maintenance. This is a great place to look for a more
complete list of components.
GitHub Projects
Many A-Frame applications are developed purely from components, and many of those A-Frame applications are open source on GitHub. Their codebases will contain components that we can use directly, refer to, or copy from. Projects to look at include:
A-Frame Blog
The A-Frame Blog archives include details of components as they were released or updated, and can be a good place to find links to components.
A-Frame Wiki
The A-Frame Wiki is a useful community-driven initiative that collects information and tips about available A-Frame components. Everyone is encouraged to participate. It’s very easy to add and edit information.
Using a Community Component
Once we find a component that we want to use, we can include the component as a
<script>
tag and use it from HTML.
For example, let’s use IdeaSpaceVR’s particle system component:
Using unpkg
First, we have to grab a CDN link to the component JS file. The documentation of the component will usually have a CDN link or usage information. But a way to get the most up-to-date CDN link is to use unpkg.com.
unpkg is a CDN that automatically hosts everything that is published to npm. unpkg can resolve semantic versioning and provide us the version of the component we want. A URL takes the form of:
https://unpkg.com/<npm package name>@<version>/<path to file> |
If we want the latest version, we can exclude the version
:
https://unpkg.com/<npm package name>/<path to file> |
Rather than typing in the path to the built component JS file, we can exclude
path to file
to be able to browse the directories of the component package.
The JS file will usually be in a folder called dist/
or build/
and end with
.min.js
.
For the particle system component, we head to:
https://unpkg.com/aframe-particle-system-component/ |
Note the ending slash (/
). Find the file we need, right click, and hit Copy
Link to Address to copy the CDN link into our clipboard.
Including the Component JS File
Then head to our HTML. Under the <head>
, after the A-Frame JS <script>
tag, and before <a-scene>
, we will include our JS file with a <script>
tag.
For the particle system component, the CDN link we found earlier (at time of writing) was:
https://unpkg.com/[email protected]/dist/aframe-particle-system-component.min.js |
Now we can include it into our HTML:
<html> |
Using the Component
Follow the documentation of the component on how to use it in implementation. But generally, the usage involves attaching the component to an entity and configuring it. For the particle system component:
Now we can include it into our HTML:
<html> |
Example
Below is a complete example of using various community components from the Registry and using the unpkg CDN. We can remix or check out this example on Glitch.
<html> |