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.

NewCustomComponent

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.

CustomComponentExample

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.

FunctionDescriptionExample
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)

Schema

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:

TypeDescription
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
ecs.stringString
ecs.booleanBoolean

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:

CustomComponentSchema

ecs.registerComponent

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

PropertyTypeNotes
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
remove
tick

Component handle

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

PropertyTypeNotes
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.

PropertyTypeNotes
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.

ecs.getAttribute(name)

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'

ecs.registerComponent({
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).

ecs.registerComponent({
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:

ecs.registerComponent({
name: 'tick-and-data',

data: {
tempNumber: ecs.i32,
},

...

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

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
ecs.registerComponent({
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 // @..

Labels

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:

CustomComponentLabels

Conditions

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',
},

Enumerations

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:

CustomComponentEnums

Groups

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

Vector3

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:

StudioGroups

Color

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:

StudioColorSelector

Labels

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:

StudioFieldLabels