aframe
Version:
A web framework for building virtual reality experiences.
508 lines (388 loc) • 20.6 kB
Markdown
---
title: Entity-Component-System
type: introduction
layout: docs
order: 5
examples:
- title: Community Components in Action
src: https://glitch.com/edit/#!/aframe-registry?path=index.html
- title: Animated Lights
src: https://glitch.com/edit/#!/aframe-animated-lights?path=index.html
---
[ecs]: https://wikipedia.org/wiki/Entity_component_system
A-Frame is a three.js framework with an [entity-component-system][ecs] (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:
1. Greater flexibility when defining objects by mixing and matching reusable parts.
2. Eliminates the problems of long inheritance chains with complex interwoven functionality.
3. Promotes clean design via decoupling, encapsulation, modularization, reusability.
4. Most scalable way to build a VR application in terms of complexity.
5. Proven architecture for 3D and VR development.
6. 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](https://en.wikipedia.org/wiki/Entity%E2%80%93component%E2%80%93system)
- [*What is an Entity System?* by Adam Martin](http://t-machine.org/index.php/2007/11/11/entity-systems-are-the-future-of-mmog-development-part-2/)
- [*Decoupling Patterns — Component* on Game Programming Patterns](http://gameprogrammingpatterns.com/component.html)
- [*Evolve Your Hierarchy* by Mick West](http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/)
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.
<!--toc-->
## Concept
[entity]: ../core/entity.md
[component]: ../core/component.md
[system]: ../core/systems.md

A basic definition of ECS involves:
- **[Entities][entity]** 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][component]** 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][system]** 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 the `engine` component to be smaller.
- To create an *airplane* entity: attach `wing` and `jet` 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 the `AFRAME.registerComponent (name, definition)`
API.
- **Systems** are represented by `<a-scene>`'s HTML attributes. System are
similar to components in definition. Systems are registered via the
`AFRAME.registerSystem (name, definition)` API.
### Syntax
[style]: https://developer.mozilla.org/docs/Web/API/HTMLElement/style
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][style]. 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}">`
[geometry]: ../components/geometry.md
[material]: ../components/material.md
[light]: ../components/light.md
[position]: ../components/position.md
For example, we have `<a-entity>` and attach the [geometry], [material],
[light], and [position] components with various properties and property values:
```html
<a-entity geometry="primitive: sphere; radius: 1.5"
light="type: point; color: white; intensity: 2"
material="color: white; shader: flat; src: glow.jpg"
position="0 0 -5"></a-entity>
```
### 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`).
[composegif]: https://cloud.githubusercontent.com/assets/674727/25463804/896c04c2-2aad-11e7-8015-2fc84118a01c.gif
![Composing an Entity][composegif]
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:
```html
<a-entity
tracked-controls <!-- Hook into the Gamepad API for pose. -->
vive-controls <!-- Vive button mappings. -->
meta-touch-controls <!-- Oculus button mappings. -->
hand-controls <!-- Appearance (model), gestures, and events. -->
laser-controls <!-- Laser to interact with menus and UI. -->
sphere-collider <!-- Listen when hand is in contact with an object. -->
grab <!-- Provide ability to grab objects. -->
throw <!-- Provide ability to throw objects. -->
event-set="_event: grabstart; visible: false" <!-- Hide hand when grabbing object. -->
event-set="_event: grabend; visible: true" <!-- Show hand when no longer grabbing object. -->
>
```
### 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:
1. **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')`.
2. **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')`.
3. **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.
4. **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])')`.
5. **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).
[writecomponent]: ./writing-a-component.md
We will later go over in detail how to [write an A-Frame
component][writecomponent]. As a preview, the structure of a basic component
may look like:
```js
AFRAME.registerComponent('foo', {
schema: {
bar: {type: 'number'},
baz: {type: 'string'}
},
init: function () {
// Do something when component first attached.
},
update: function () {
// Do something when component's data is updated.
},
remove: function () {
// Do something when the component or its entity is detached.
},
tick: function (time, timeDelta) {
// Do something on every scene tick or frame.
}
});
```
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:
```html
<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
components/
ball.js
collidable.js
grabbable.js
enemy.js
scoreboard.js
throwable.js
```
### Higher-Order Components
[cursor]: ../components/cursor.md
[hand-controls]: ../components/hand-controls.md
[meta-touch-controls]: ../components/meta-touch-controls.md
[raycaster]: ../components/raycaster.md
[tracked-controls]: ../components/tracked-controls.md
[vive-controls]: ../components/vive-controls.md
Components can set other components on the entity, making them a higher-order
or higher-level component in abstraction.
For example, the [cursor component][cursor] sets and builds on top of the
[raycaster component][raycaster]. Or the [hand-controls
component][hand-controls] sets and builds on top of the [vive-controls
component][vive-controls] and [meta-touch-controls
component][meta-touch-controls] which in turn build on top of the
[tracked-controls component][tracked-controls].
## 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
[search]: https://www.npmjs.com/search?q=keywords:aframe-component
[directory]: https://aframe.wiki/en/#!pages/component-directory.md
Most A-Frame components are published on npm as well as GitHub. We can use
[npm's search to search for packages tagged with `aframe-component`][search].
This is a great place to look for a more complete list of components.
For a list of components with the A-Frame version that the component
was last tested with, check out the community-maintained [Component Directory][directory]
on the [A-Frame Wiki](#a-frame-wiki).
#### GitHub Projects
[github]: https://github.com
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:
- [BeatSaver Viewer](https://github.com/supermedium/beatsaver-viewer/)
- [Super Says](https://github.com/supermedium/supersays/)
- [A-Painter](https://github.com/aframevr/a-painter/)
- [A-Blast](https://github.com/aframevr/a-blast/)
#### A-Frame Blog
[blog]: https://aframe.io/blog/
[homepage]: https://aframe.io/
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](https://aframe.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
[particlesystem]: https://www.npmjs.com/package/@c-frame/aframe-particle-system-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.
[unpkg.com]: http://unpkg.com/
For example, let's use the [particle system component][particlesystem]:
#### 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/@c-frame/aframe-particle-system-component@1.2.x/dist/aframe-particle-system-component.min.js
```
Now we can include it into our HTML:
```html
<html>
<head>
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
<script src="https://unpkg.com/@c-frame/aframe-particle-system-component@1.2.x/dist/aframe-particle-system-component.min.js"></script>
</head>
<body>
<a-scene>
</a-scene>
</body>
</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
<html>
<head>
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
<script src="https://unpkg.com/@c-frame/aframe-particle-system-component@1.2.x/dist/aframe-particle-system-component.min.js"></script>
</head>
<body>
<a-scene>
<a-entity particle-system="preset: snow" position="0 0 -10"></a-entity>
</a-scene>
</body>
</html>
```
### Using jsdelivr for CDN
[jsdelivr]: https://www.jsdelivr.com/
[JSDELIVR][jsdelivr] is an alternative CDN to unpkg. One benefit of JSDELIVR is that it can download files from GitHub as well as NPM.
You can convert unpkg URLs to JSDELIVR URLs using this link: https://www.jsdelivr.com/unpkg
You can convert GitHub URLs to JSDELIVR URLs using this link: https://www.jsdelivr.com/github
### Example
[community-example]: https://aframe.io/examples/docs/community-components

Below is a complete example of using various community components from the
Registry and using the JSDELIVR CDN. This example can also be viewed in the [A-Frame Examples][community-example].
```html
<html>
<head>
<title>Community Components Example</title>
<meta name="description" content="Community Components Example">
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
<script src="https://unpkg.com/@c-frame/aframe-particle-system-component@1.2.x/dist/aframe-particle-system-component.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/aframe-simple-sun-sky@^1.2.2/simple-sun-sky.js"></script>
<script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-extras@7.5.0/dist/aframe-extras.min.js"></script>
</head>
<body>
<a-scene>
<a-entity id="rain" particle-system="preset: rain; color: #24CAFF; particleCount: 5000"></a-entity>
<a-entity id="sphere" geometry="primitive: sphere"
material="color: #EFEFEF; shader: flat"
position="0 0.15 -5"
light="type: point; intensity: 5"
animation="property: position; easing: easeInOutQuad; dir: alternate; dur: 1000; to: 0 -0.10 -5; loop: true"></a-entity>
<a-entity id="ocean" ocean="density: 20; width: 50; depth: 50; speed: 4"
material="color: #9CE3F9; opacity: 0.75; metalness: 0; roughness: 1"
rotation="-90 0 0"></a-entity>
<a-simple-sun-sky sun-position="1 0.4 0"></a-simple-sun-sky>
<a-entity id="light" light="type: ambient; color: #888"></a-entity>
</a-scene>
</body>
</html>
```