docs/introduction/entity-component-system.md
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:
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:
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-->A basic definition of ECS involves:
<div>s.Some abstract examples of different types of entities built from composing together different components:
Box = Position + Geometry + MaterialLight Bulb = Position + Light + Geometry + Material + ShadowSign = Position + Geometry + Material + TextVR Controller = Position + Rotation + Input + Model + Grab + GesturesBall = Position + Velocity + Physics + Geometry + MaterialPlayer = Position + Camera + Input + Avatar + IdentityAs another abstract example, imagine we want to build a car entity by assembling components:
material component that has properties such as "color" or
"shininess" that affects the appearance of the car.engine component that has properties such as "horsepower" or
"weight" that affects the functionality of the car.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:
tire component.tire component's number of tires to 2,
configure the engine component to be smaller.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.
A-Frame has APIs that represents each piece of ECS:
<a-entity> element and prototype.<a-entity>'s. Underneath,
components are objects containing a schema, lifecycle handlers, and methods.
Components are registered via the AFRAME.registerComponent (name, definition)
API.<a-scene>'s HTML attributes. System are
similar to components in definition. Systems are registered via the
AFRAME.registerSystem (name, definition) API.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"
light="type: point; color: white; intensity: 2"
material="color: white; shader: flat; src: glow.jpg"
position="0 0 -5"></a-entity>
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
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. -->
>
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:
document.querySelector('#player').ball.emit('collided')..setAttribute,
.removeAttribute, .createElement, and .removeChild. These can be used as
is just like in normal web development.document.querySelector('[enemy]:not([alive])').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', {
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:
<a-entity foo="bar: 5; baz: bazValue"></a-entity>
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
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 meta-touch-controls component which in turn build on top of the tracked-controls component.
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.
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!
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.
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 on the A-Frame Wiki.
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:
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.
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.
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 the particle system component:
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.
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/[email protected]/dist/aframe-particle-system-component.min.js
Now we can include it into our HTML:
<html>
<head>
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
<script src="https://unpkg.com/@c-frame/[email protected]/dist/aframe-particle-system-component.min.js"></script>
</head>
<body>
<a-scene>
</a-scene>
</body>
</html>
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>
<head>
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
<script src="https://unpkg.com/@c-frame/[email protected]/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>
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
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.
<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/[email protected]/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/[email protected]/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>