How to Write an A-Frame VR Component
17 Jan 2016
A-Frame is a WebVR framework that introduces the entity-component system (docs) to the DOM. The entity-component system treats every entity in the scene as a placeholder object which we apply and mix components to in order to add appearance, behavior, and functionality. A-Frame comes with some standard components out of the box like camera, geometry, material, light, or sound. However, people can write, publish, and register their own components to do whatever they want like have entities collide/explode/spawn, be controlled by physics, or follow a path. Today, we’ll be going through how we can write our own A-Frame components.
Note: This article is now part of the A-Frame documentation. View the most up-to-date version.
Table of Contents
What a Component Looks Like
A component contains a bucket of data in the form of component properties. This data is used to modify the entity. For example, we might have an engine component. Possible properties might be horsepower or cylinders.
From the DOM
Let’s first see what a component looks like from the DOM.
For example, the light component has properties such as type, color, and intensity. In A-Frame, we register and configure a component to an entity using an HTML attribute and a style-like syntax:
<a-entity light="type: point; color: crimson; intensity: 2.5"></a-entity>
This would give us a light in the scene. To demonstrate composability, we could give the light a spherical representation by mixing in the geometry component.
<a-entity geometry="primitive: sphere; radius: 5"
light="type: point; color: crimson; intensity: 2.5"></a-entity>
Or we can configure the position component to move the light sphere a bit to the right.
<a-entity geometry="primitive: sphere; radius: 5"
light="type: point; color: crimson; intensity: 2.5"
position="5 0 0"></a-entity>
Given the style-like syntax and that it modifies the appearance and behavior of DOM nodes, component properties can be thought of as a rough analog to CSS. In the near future, I can imagine component property stylesheets.
Under the Hood
Now let’s see what a component looks like under the hood. A-Frame’s most basic component is the position component:
AFRAME.registerComponent('position', {
schema: { type: 'vec3' },
update: function () {
var object3D = this.el.object3D;
var data = this.data;
object3D.position.set(data.x, data.y, data.z);
}
});
The position component uses only a tiny subset of the component API, but what
this does is register the component with the name “position”, define a schema
where the component’s value with be parsed to an {x, y, z}
object, and when
the component initializes or the component’s data updates, set the position of
the entity with the update
callback. this.el
is a reference from the
component to the DOM element, or entity, and object3D
is the entity’s
three.js. Note that A-Frame is built on top of three.js so many
components will be using the three.js API.
So we see that components consist of a name and a definition, and then they can
be registered to A-Frame. We saw the the position component definition defined
a schema
and an update
handler. Components simply consist of the schema
,
which defines the shape of the data, and several handlers for the component to
modify the entity in reaction to different types of events.
Here is the current list of properties and methods of a component definition:
Property | Description |
---|---|
data | Data of the component derived from the schema default values, mixins, and the entity’s attributes. |
el | Reference to the entity element. |
schema | Names, types, and default values of the component property value(s). |
Method | Description |
---|---|
init | Called once when the component is initialized. |
update | Called both when the component is initialized and whenever the component’s data changes (e.g, via setAttribute). |
remove | Called when the component detaches from the element (e.g., via removeAttribute). |
tick | Called on each render loop or tick of the scene. |
play | Called whenever the scene or entity plays to add any background or dynamic behavior. |
pause | Called whenever the scene or entity pauses to remove any background or dynamic behavior. |
Defining the Schema
The component’s schema defines what type of data it takes. A component can
either be single-property or consist of multiple properties. And properties
have property types. Note that single-property schemas and property types are
being released in A-Frame v0.2.0
.
A property might look like:
{ type: 'int', default: 5 }
And a schema consisting of multiple properties might look like:
{
color: { default: '#FFF' },
target: { type: 'selector' },
uv: {
default: '1 1',
parse: function (value) {
return value.split(' ').map(parseFloat);
}
},
}
Since components in the entity-component system are just buckets of data that are used to affect the appearance or behavior of the entity, the schema plays a crucial role in the definition of the component.
Property Types
A-Frame comes with several built-in property types such as boolean
, int
,
number
, selector
, string
, or vec3
. Every single property is assigned a
type, whether explicitly through the type
key or implictly via inferring the
value. And each type is used to assign parse
and stringify
functions. The
parser deserializes the incoming string value from the DOM to be put into the
component’s data object. The stringifier is used when using setAttribute
to
serialize back to the DOM.
We can actually define and register our own property types:
AFRAME.registerPropertyType('radians', {
parse: function () {
}
// Default stringify is .toString().
});
Single-Property Schemas
If a component has only one property, then it must either have a type
or a
default
value. If the type is defined, then the type is used to parse and
coerce the string retrieved from the DOM (e.g., getAttribute
). Or if the
default value is defined, the default value is used to infer the type.
Take for instance the visible component. The schema property definition implicitly defines it as a boolean:
AFRAME.registerComponent('visible', {
schema: {
// Type will be inferred to be boolean.
default: true
},
// ...
});
Or the rotation component which explicitly defines the value as a vec3
:
AFRAME.registerComponent('rotation', {
schema: {
// Default value will be 0, 0, 0 as defined by the vec3 property type.
type: 'vec3'
}
// ...
});
Using these defined property types, schemas are processed by
registerComponent
to inject default values, parsers, and stringifiers for
each property. So if a default value is not defined, the default value will be
whatever the property type defines as the “default default value”.
Multiple-Property Schemas
If a component has multiple properties (or one named property), then it consists of one or more property definitions, in the form described above, in an object keyed by property name. For instance, a physics body component might define a schema:
AFRAME.registerComponent('physics-body', {
schema: {
boundingBox: {
type: 'vec3',
default: { x: 1, y: 1, z: 1 }
},
mass: {
default: 0
},
velocity: {
type: 'vec3'
}
}
}
Having multiple properties is what makes the component take the syntax in the
form of physics="mass: 2; velocity: 1 1 1"
.
With the schema defined, all data coming into the component will be passed
through the schema for parsing. Then in the lifecycle methods, the component
has access to this.data
which in a single-property schema is a value and in a
multiple-propery schema is an object.
Defining the Lifecycle Methods
Component.init() - Set Up
init
is called once in the component’s lifecycle when it is mounted to the
entity. init
is generally used to set up variables or members that may used
throughout the component or to set up state. Though not every component will
need to define an init
handler. Sort of like the component-equivalent method
to createdCallback
or React.ComponentDidMount
.
For example, the look-at
component’s init
handler sets up some variables:
init: function () {
this.target3D = null;
this.vector = new THREE.Vector3();
},
// ...
Example uses of init
by some of the standard A-Frame components:
Component | Usage |
---|---|
camera | Create and set a THREE.PerspectiveCamera on the entity. |
cursor | Attach event listeners. |
light | Register light to the lighting system. |
look-at | Create a helper vector. |
material | Set up variables, mainly to visualize the state of the component. |
wasd-controls | Set up an object to keep track of pressed keys. Bind methods. |
Component.update(oldData) - Do the Magic
The update
handler is called both at the beginning of the component’s
lifecycle with the initial this.data
and every time the component’s data
changes (generally during the entity’s attributeChangedCallback
like with a
setAttribute
). The update handler gets access to the previous state of the
component data passed in through oldData
. The previous state of the component
can be used to tell exactly which properties changed to do more granular
updates.
The update handler uses this.data
to modify the entity, usually interacting
with three.js APIs. One of the simplest update handlers is the
visible component’s:
update: function () {
this.el.object3D.visible = this.data;
}
A slightly more complex update handler might be the light component’s, which we’ll show via abbreviated code:
update: function (oldData) {
var diffData = diff(data, oldData || {});
if (this.light && !('type' in diffData)) {
// If there is an existing light and the type hasn't changed, update light.
Object.keys(diffData).forEach(function (property) {
light[property] = diffData[property];
});
} else {
// No light exists yet or the type of light has changed, create a new light.
this.light = this.getLight(this.data));
// Register the object3D of type `light` to the entity.
this.el.setObject3D('light', this.light);
}
}
The entity’s object3D
is a plain THREE.Object3D. Other three.js object types
such as meshes, lights, and cameras can be set with setObject3D
where they
will be appeneded to the entity’s object3D
.
Example uses of update
by some of the standard A-Frame components:
Component | Usage |
---|---|
camera | Set THREE.PerspectiveCamera object properties such as aspect ratio, fov, or near/far clipping planes. |
look-at | Set or update target entity to track the position of. |
material | If component is just attaching, create a material. If shader has not changed, update material. If shader has changed, replace the material. |
wasd-controls | Update the position based on the current velocity. Update the velocity based on the keys pressed. |
Component.remove() - Tear Down
The remove
handler is called when the component detaches from the entity such
as with removeAttribute
. This is generally used to remove all modifications,
listeners, and behaviors to the entity that the component added.
For example, when the light component detaches, it removes the light it previously attached from the entity and thus the scene:
remove: function () {
this.el.removeObject3D('light');
}
Example uses of remove
by some of the standard A-Frame components:
Component | Usage |
---|---|
camera | Remove the THREE.PerspectiveCamera from the entity. |
geometry | Set a plain THREE.Geometry on the mesh. |
material | Set a default THREE.MeshBasicMaterial on the mesh and unregister material from the system. |
wasd-controls | Remove keydown and keyup listeners. |
Component.tick(time) - Background Behavior
The tick
handler is called on every single tick or render loop of the scene.
So expect it to run on the order of 60-120 times for second. The global uptime of
the scene in seconds is passed into the tick handler.
For example, the look-at component, which instructs an entity to look at another target entity, uses the tick handler to update the rotation in case the target entity changes its position:
tick: function (t) {
// target3D and vector are set from the update handler.
if (this.target3D) {
this.el.object3D.lookAt(this.vector.setFromMatrixPosition(target3D.matrixWorld));
}
}
Example uses of tick
by some of the standard A-Frame components:
Component | Usage |
---|---|
look-at | Update rotation of entity to face towards tracked target, in case the target is moving. |
physics | Update the physics world simulation. |
wasd-controls | Use current velocity to move the entity (generally the camera), update velocity if keys are pressed. |
Component.pause() and Component.play() - Stop and Go
To support pause and play, just as with a video game or to toggle entities for
performance, components can implement play
and pause
handlers. These are
invoked when the component’s entity runs its play
or pause
method. When an
entity plays or pauses, all of its child entities are also played or paused.
Components should implement play or pause handlers if they register any dynamic, asynchronous, or background behavior such as animations, event listeners, or tick handlers.
For example, the look-controls
component simply removes its event listeners
such that the camera does not move when the scene is paused, and it adds its
event listeners when the scene starts playing or is resumed:
pause: function () {
this.removeEventListeners()
},
play: function () {
this.addEventListeners()
}
Example uses of pause
and play
by some of the standard A-Frame components:
Component | Usage |
---|---|
sound | Pause/play sound. |
wasd-controls | Remove/attach event listeners. |
Boilerplate
I suggest that people start off with my component boilerplate, even hardcore tool junkies. This will get you straight into building a component and comes with everything you will need to publish your component into the wild. The boilerplate handles creating a stubbed component, build steps for both NPM and browser distribution files, and publishing to Github Pages.
Generally with boilerplates, it is better to start from scratch and build your own boilerplate, but the A-Frame component boilerplate contains a lot of tribal inside knowledge about A-Frame and is updated frequently to reflect new things landing on A-Frame. The only possibly opinionated pieces about the boilerplate is the development tools it internally uses that are hidden away by NPM scripts.
Examples
Line Component
Let’s go through building a basic complete component, a line component. This line component will simply render a line. We will make it the component flexible to be able to specify the vertices and color.
Play with this example line component in Codepen.
Line Component - Skeleton
Here is a high-level view of the skeleton of the component, without the meat.
For this component, we’ll need the schema
, as all components require, the
update
handler, and the remove
handler. The rest of the lifecycle method
handlers aren’t necessary.
var coordinates = AFRAME.utils.coordinates;
AFRAME.registerComponent('line', {
// Allow line component to accept vertices and color.
schema: {},
// Create or update the line geometry.
update: {},
// Remove the line geometry.
remove: {}
});
Line Component -Schema
Since we have two properties we want to accept, color
and path
, we will
need a multi-property schema. The color
property will just be a simple string
that will be fed to THREE.Color
which does a lot of work for us. The path
property will need a custom property type to parse an array of vec3
s. That
property type does not exist as a built-in type yet, but we can define an
inline parse and stringifier.
// Allow line component to accept vertices and color.
schema: {
color: { default: '#333' },
path: {
default: [
{ x: -0.5, y: 0, z: 0 },
{ x: 0.5, y: 0, z: 0 }
],
// Deserialize path in the form of comma-separated vec3s: `0 0 0, 1 1 1, 2 0 3`.
parse: function (value) {
return value.split(',').map(coordinates.parse);
},
// Serialize array of vec3s in case someone does
// setAttribute('line', 'path', [...]).
stringify: function (data) {
return data.map(coordinates.stringify).join(',');
}
}
},
//...
The component API is entirely up to us. If we wanted the path to take a different syntax or abstract it further such that it maybe only accepts a starting point and a length and handle the math for the developer, that is our perogative to permissionlessly implement.
The schema will handle the shape of the data so by the time it gets to the lifecycle handlers, it will be in a nice data structure.
Line Component - Update
The update
handler is called both on component attach and on the entity’s
attributeChangedCallback
. In the update for the line component, we want to
reate a line geometry if it doesn’t exist yet, or update it if it does. We can
create a line in three.js
by combining a THREE.LineBasicMaterial
and
THREE.Geometry
and manually pushing vertices.
update: function (oldData) {
// Set color with material.
var material = new THREE.LineBasicMaterial({
color: this.data.color
});
// Add vertices to geometry.
var geometry = new THREE.Geometry();
this.data.path.forEach(function (vec3) {
geometry.vertices.push(
new THREE.Vector3(vec3.x, vec3.y, vec3.z)
);
});
// Apply mesh.
this.el.setObject3D('mesh', new THREE.Line(geometry, material));
},
// ...
For simplicity, we can just update the line by completely replacing it. In other components, we might want to more granularly update objects for performance.
When we set the object with setObject3D
, we specify the object type. In this
case, it is a mesh
, which is a geometry combined with a material. Other
object types might be light
or camera
. setObject3D
just puts the object
into a map and appends the object under the entity’s scene graph
(THREE.Group
).
Line Component - Remove
For removal, we can just use removeObject3D
:
remove: function () {
this.el.removeObject3D('mesh');
}
This will remove the object from the entity’s scene graph.
Line Component - Usage
Then we with the line component written and registered, we can use it in HTML:
<a-scene>
<a-assets>
<a-mixin id="red" line="color: #E20049"></a-mixin>
</a-assets>
<a-entity id="happy-face" position="0 2 -10">
<a-entity mixin="red" line="path: -1 1 0, -1 0.5 0, -1 0 0"></a-entity>
<a-entity mixin="red" line="path: 1 1 0, 1 0.5 0, 1 0 0"></a-entity>
<a-entity mixin="red" line="path: -2 -1 0, 0 -2 0, 2 -1"></a-entity>
</a-entity>
<a-sky color="#FFEED0"></a-sky>
</a-scene>
And voila!