Skip to main content

Custom Components

Components are data types that can be attached to entities. Studio supports creating custom components using scripts. If creating a game or interactive experience, you will likely find you need components that go beyond what is provided out-of-box with Studio. Custom components allow you to create custom behaviors and interactions, and respond to user input in any way you like.

You can create a new custom component quickly by going to Files > New Component File which provides a starter template for registering your custom component.


A properly configured custom component and its attributes will appear in the Component interface (as a “Custom” component) where you can configure the component settings visually. Here’s an example of a custom agentSpawner component.


Register your component

The (+) New Component File function in the Files tab, provides boilerplate code for properly registering a component. However, any code file can designate a component with ecs.registerComponent. Example registration looks like:

// Register the component with the ECS system
const moveOnSpacebar = ecs.registerComponent({
name: 'moveOnSpacebar',
schema: {

Write your component schema

Almost all components and attributes follow the same API for managing their data.

setEnsures the component exists on the entity, then assigns the (optional) data to the component.ecs.GltfModel.set(world, eid, {url: 'https:...'})
removeRemoves the component from the entity.ecs.Hidden.remove(world, eid)
resetAdds, or resets the component to its default state.ecs.Quaternion.reset(world, eid)
hasReturns true if the component is present on the entity.ecs.Material.has(world, eid)
getReturns a read-only reference.ecs.Scale.get(world, eid).x
mutatePerform an update to the component within a callback function. Return true to indicate no changes made.ecs.Position.mutate(world, eid, (cursor) => { cursor.x++ }) ecs.Position.mutate(world, eid, (cursor) => { if (cursor.x > 10) { cursor.x = 10 return false } else { return true // Not mutated } })
cursorReturns a mutable reference. Cursors are reused so only one cursor for each component can exist at a time. Calling cursor() will add the component to the entity if it doesn’t yet contain it.const position = ecs.Position.cursor(world, eid) position.x = position.x + 1
acquireSame behavior as cursor, but commit must be called after the cursor is done being used.const quaternion = ecs.Quaternion.acquire(world, eid) Object.assign(quaternion, newQuaternion)
commitCalled after acquire. An optional third argument determines whether the cursor was mutated or not.ecs.Quaternion.commit(world, eid) ecs.Quaternion.commit(world, eid, false)
dirtyMark the entity as having been mutated. Only needed in a specific case where systems are mutating data.position.x = 2 ecs.Position.dirty(world, eid)


Component data is stored in a tightly packed format which means we need to declare the schema of every component up front.

The supported types of data are:

ecs.eidEntity reference
ecs.f3232-bit floating-point number
ecs.f6464-bit floating-point number
ecs.i3232-bit integer
ecs.ui88-bit unsigned integer
ecs.ui3232-bit unsigned integer

It is not currently possible to store dynamically sized objects or lists. This is something we’re still thinking about, and welcome hearing about your use cases.

An example schema might look like:

level: ecs.ui8,
health: ecs.ui32,
isFlying: ecs.boolean,
weaponObject: ecs.eid,

This would be surfaced in the Custom Component’s UI as:



Accepts an object to define a new component, returns a handle to the created component.

nameThe name that can be used to call ecs.getAttribute(name)
schemaSchemaData that can be configured on the component
dataSchemaUsed for internal bookkeeping, cannot be set from outside the component code
schemaDefaultsObjectOptional, contains defaults for each field in schema
addFunction accepting (world, component)See example components for usage

Component handle

The component handle is the second argument to callbacks such as add/remove/tick. It contains:

eidEidThe current entity ID
schemaCursorPoints to the current entity’s schema
schemaAttributeWorld-scoped componentThe current component

This allows callbacks to load and set data on the entity even in callbacks.

dataCursorPoints to the current entity’s data (if defined)
dataAttributeWorld-scoped componentThe component reference of the data (if defined)

This allows callbacks to load and set data on the entity even in callbacks.

Note that the component handle object is reused and updated in place when being applied to multiple entities. The eid, schema, and data properties can only be used synchronously within the add/tick/remove and should not be accessed at any other time such as within timers or event handlers.


Returns the attribute that has been registered with that name. Built-in components are also exposed as a property of ecs, so:

ecs.Position === ecs.getAttribute('position')

Example: A Quick Custom Component

  1. Create a JavaScript file (say hello-world.js).
  2. Edit it to contain the following:
import * as ecs from '@8thwall/ecs'

name: 'hello-world',
add: () => {
console.log('hello world')
  1. Add your component in the hierarchy:
  2. Click ‘Run’ to see the result (a message in the developer log)

Example: A Component with a Schema

The following example adds three properties. Note the schemaDefaults block (lets you set default values for each property).

name: 'example-schema',

schema: {
someBool: ecs.boolean,
someNumber: ecs.f64,
someString: ecs.string,

schemaDefaults: {
someNumber: 1000,
someString: 'go fish',

add: (world, component) => {
console.log('Schema someBool is', component.schema.someBool)
console.log('Schema someNumber is', component.schema.someNumber)
console.log('Schema someString is', component.schema.someString)

Example: Ticks and Data

We can add a tick function that will get called every frame and a data block for tracking things from one frame to the next (it uses the same types as schema). For example:

name: 'tick-and-data',

data: {
tempNumber: ecs.i32,


tick: (world, component) => {
console.log('tempNumber is',

Custom Components as Attributes of other Components

Custom Components can be added, set and modified. Example:

// The handle returned by registerComponent let's us use a custom component programmatically 
const numOfFishComponent = ecs.registerComponent({
name: 'num-of-fish',
schema: {
fishNum: ecs.f32,

// Component that adds and removes num-of-fish
name: 'fish-num-tracker',
add: (world, component) => {
// Add or update num-of-fish
numOfFishComponent.set(world, component.eid, {fishNum: 5})
tick: (world, component) => {
// When no longer needed, the component can be removed
numOfFishComponent.remove(world, component.eid)

Custom Editor Fields

Display and functionality of your components in the entity editor can be customized in various ways: labels can be changed, conditions set for display and other things. This is all done using comments inside the schema where fields are marked // @..


Sometimes labels in the editor need to be more descriptive than their names in code. For example:

  schema: {
// @label Target (optional)
target: 'eid',

Appearance in the editor:



Properties can be set to only show depending on the values of other properties. Examples:

  schema: {
// 'from' will only show if autoFrom set false:
autoFrom: 'boolean',
// @condition autoFrom=false
from: 'f32',

// 'easingFunction' will show if either easeIn or easeOut set:
easeIn: 'boolean',
easeOut: 'boolean',
// @condition easeIn=true|easeOut=true
easingFunction: 'string',

// 'targetX' only shows if no target set:
target: 'eid',
// @condition target=null
targetX: 'f32',


String properties can be limited to a set list:

  schema: {
// @enum Quadratic, Cubic, Quartic, Quintic, Sinusoidal, Exponential
easingFunction: 'string',

In the Editor, this shows up as a drop-down list:



Certain groups of properties can be instructed to be treated specially in the editor. Groups are configured as follows:

  • The start and end of the group is marked with // @group start … and // @group end
  • Conditions can be applied to the whole group with // @group condition
  • Two kinds of group currently supported: vector3 and color


Groups of properties that represent 3D vectors can be indicated as follows:

  schema: {
autoFrom: 'boolean',
// @group start from:vector3
// @group condition autoFrom=false
fromX: 'f32',
fromY: 'f32',
fromZ: 'f32',
// @group end

Assuming the condition is met, this is displayed tidily in the editor:



Colors can be indicated as in the following example:

  schema: {
// @group start background:color
bgRed: ecs.f32,
bgGreen: ecs.f32,
bgBlue: ecs.f32,
// @group end

Displays a color selector:



Custom labels can still be used for individual fields:

  schema: {
// @group start orient:vector3
// @label Pitch
orientPitch: 'f32',
// @label Yaw
orientYaw: 'f32',
// @label Roll
orientRoll: 'f32',
// @group end

This looks like: