decentraland-ecs-utils
This library includes a number of helpful pre-built tools that include components, methods, and systems. They offer simple solutions to common scenarios that you're likely to run into.
Using the Utils library
To use any of the helpers provided by the utils library
- Install it as an
npm
package. Run this command in your scene's project folder:
npm install decentraland-ecs-utils
- Import the library into the scene's script. Add this line at the start of your
game.ts
file, or any other TypeScript files that require it:
import utils from '../node_modules/decentraland-ecs-utils/index'
- In your TypeScript file, write
utils.
and let the suggestions of your IDE show the available helpers.
Gradual Movement
Move an entity
To move an entity over a period of time, from one position to another, use the MoveTransformComponent
component.
MoveTransformComponent
has three required arguments:
start
: Vector3
for the start position
end
: Vector3
for the end position
duration
: duration (in seconds) of the translation
This example moves an entity from one position to another over 2 seconds:
import utils from '../node_modules/decentraland-ecs-utils/index'
const box = new Entity()
box.addComponent(new BoxShape())
box.addComponent(new Transform())
let StartPos = new Vector3(1, 1, 1)
let EndPos = new Vector3(15, 1, 15)
box.addComponent(new utils.MoveTransformComponent(StartPos, EndPos, 2))
engine.addEntity(box)
Follow a path
To move an entity over several points of a path over a period of time, use the FollowPathComponent
component.
FollowPathComponent
has two required arguments:
points
: An array of Vector3
positions that form the path.
duration
: The duration (in seconds) of the whole path.
This example moves an entity over through four points over 5 seconds:
import utils from '../node_modules/decentraland-ecs-utils/index'
const box = new Entity()
box.addComponent(new BoxShape())
box.addComponent(new Transform())
let path = []
path[0] = new Vector3(1, 1, 1)
path[1] = new Vector3(1, 1, 15)
path[2] = new Vector3(15, 1, 15)
path[3] = new Vector3(15, 1, 1)
box.addComponent(new utils.FollowPathComponent(path, 2))
engine.addEntity(box)
Follow a curved path
To move an entity following a curved path over a period of time, use the FollowCurvedPathComponent
component.
The curved path is composed of multiple straight line segments put together. You only need to supply a series of fixed path points and a smooth curve is drawn to pass through all of these.
FollowCurvedPathComponent
has three required arguments:
points
: An array of Vector3
positions that the curve must pass through.
duration
: The duration (in seconds) of the whole path.
numberOfSegments
: How many straight-line segments to use to construct the curve.
Tip: Each segment takes at least one frame to complete. Avoid using more than 30 segments per second in the duration of the path, or the entity will move significantly slower while it stops for each segment.
This example moves an entity over through a curve that covers four points:
import utils from '../node_modules/decentraland-ecs-utils/index'
const box = new Entity()
box.addComponent(new BoxShape())
box.addComponent(new Transform())
let path = []
path[0] = new Vector3(1, 1, 1)
path[1] = new Vector3(1, 1, 15)
path[2] = new Vector3(15, 1, 15)
path[3] = new Vector3(15, 1, 1)
box.addComponent(new utils.FollowCurvedPathComponent(path, 5, 40))
engine.addEntity(box)
The FollowCurvedPathComponent
also lets you set:
turnToFaceNext
: If true, the entity will rotate on each segment of the curve to always face forward.
closedCircle
: If true, traces a circle that starts back at the beginning, keeping the curvature rounded in the seams too
Rotate an entity
To rotate an entity over a period of time, from one direction to another, use the rotateTransformComponent
component, which works very similarly to the MoveTransformComponent
component.
rotateTransformComponent
has three required arguments:
start
: Quaternion
for the start rotation
end
: Quaternion
for the end rotation
duration
: duration (in seconds) of the rotation
This example rotates an entity from one rotation to another over 2 seconds:
import utils from '../node_modules/decentraland-ecs-utils/index'
const box = new Entity()
box.addComponent(new BoxShape())
box.addComponent(new Transform())
let StartRot = Quaternion.Euler(90, 0, 0)
let EndRot = Quaternion.Euler(270, 0, 0)
box.addComponent(new utils.RotateTransformComponent(StartRot, EndRot, 2))
engine.addEntity(box)
Sustain rotation
To rotates an entity continuously, use KeepRotatingComponent
. The entity will keep rotating forever until it's explicitly stopped or the component is removed.
KeepRotatingComponent
has one required argument:
rotationVelocity
: A quaternion describing the desired rotation to perform each second second. For example Quaternion.Euler(0, 45, 0)
rotates the entity on the Y axis at a speed of 45 degrees per second, meaning that it makes a full turn every 8 seconds.
The component also contains the following method:
stop()
: stops rotation and removes the component from any entities its added to.
In the following example, a cube rotates continuously until clicked:
import utils from '../node_modules/decentraland-ecs-utils/index'
const box = new Entity()
box.addComponent(new BoxShape())
box.addComponent(new Transform({ position: new Vector3(1, 1, 1) }))
box.addComponent(new utils.KeepRotatingComponent(Quaternion.Euler(0, 45, 0)))
box.addComponent(
new OnClick(() => {
box.getComponent(utils.KeepRotatingComponent).stop()
})
)
engine.addEntity(box)
Change scale
To adjust the scale of an entity over a period of time, from one size to another, use the ScaleTransformComponent
component, which works very similarly to the MoveTransformComponent
component.
ScaleTransformComponent
has three required arguments:
start
: Vector3
for the start scale
end
: Vector3
for the end scale
duration
: duration (in seconds) of the scaling
This example scales an entity from one size to another over 2 seconds:
import utils from '../node_modules/decentraland-ecs-utils/index'
const box = new Entity()
box.addComponent(new BoxShape())
box.addComponent(new Transform())
let StartSize = new Vector3(1, 1, 1)
let EndSize = new Vector3(0.75, 2, 0.75)
box.addComponent(new utils.ScaleTransformComponent(StartSize, EndSize, 2))
engine.addEntity(box)
Non-linear changes
All of the translation components, the MoveTransformComponent
, rotateTransformComponent
, ScaleTransformComponent
, and FollowPathComponent
have an optional argument to set the rate of change. By default, the movement, rotation, or scaling occurs at a linear rate, but this can be set to other options.
The following values are accepted:
Interpolation.LINEAR
Interpolation.EASEINQUAD
Interpolation.EASEOUTQUAD
Interpolation.EASEQUAD
The following example moves a box following an ease-in rate:
box.addComponent(
new utils.MoveTransformComponent(
StartPos,
EndPos,
2,
null,
utils.InterpolationType.EASEINQUAD
)
)
Callback on finish
All of the translation components, the MoveTransformComponent
, rotateTransformComponent
, ScaleTransformComponent
, FollowPathComponent
, and FollowCurvedPathComponent
have an optional argument that executes a function when the translation is complete.
onFinishCallback
: function to execute when movement is done.
The following example logs a message when the box finishes its movement. The example uses MoveTransformComponent
, but the same applies to rotateTransformComponent
and ScaleTransformComponent
.
box.addComponent(
new utils.MoveTransformComponent(StartPos, EndPos, 2, () => {
log('finished moving box')
})
)
The FollowPathComponent
has a two optional arguments that execute functions when a section of the path is complete and when the whole path is complete.
The following example logs a messages when the box finishes each segment of the path, and another when the entire path is done.
box.addComponent(
new utils.FollowPathComponent(
path,
2,
() => {
log('finished moving box')
},
() => {
log('finished a segment of the path')
}
)
)
Toggle
Use the ToggleComponent
to switch an entity between two possible states, running a same function on every transition.
The ToggleComponent
has the following arguments:
startingState
: Starting state of the toggle (ON or OFF)
onValueChangedCallback
: Function to call every time the toggle state changed.
It exposes three methods:
toggle()
: switches the state of the component between ON and OFF
isOn()
: reads the current state of the component, without altering it. It returns a boolean, where true
means ON.
setCallback()
: allows you to change the function to be executed by onValueChangedCallback
, for the next time it's toggled.
The following example switches the color of a box between two colors each time it's clicked.
import utils from '../node_modules/decentraland-ecs-utils/index'
const box = new Entity()
box.addComponent(new BoxShape())
box.addComponent(new Transform())
let greenMaterial = new Material()
greenMaterial.albedoColor = Color3.Green()
let redMaterial = new Material()
redMaterial.albedoColor = Color3.Red()
box.addComponent(
new utils.ToggleComponent(utils.ToggleState.Off, value => {
if (value == utils.ToggleState.On) {
box.addComponentOrReplace(greenMaterial)
} else {
box.addComponentOrReplace(redMaterial)
}
})
)
box.addComponent(
new OnClick(event => {
box.getComponent(utils.ToggleComponent).toggle()
})
)
engine.addEntity(box)
Combine Toggle with Translate
This example combines a toggle component with a move component to switch an entity between two positions every time it's clicked.
import utils from '../node_modules/decentraland-ecs-utils/index'
const box = new Entity()
box.addComponent(new BoxShape())
box.addComponent(new Transform())
let Pos1 = new Vector3(1, 1, 1)
let Pos2 = new Vector3(1, 1, 2)
box.addComponent(
new utils.ToggleComponent(utils.ToggleState.Off, value => {
if (value == utils.ToggleState.On) {
box.addComponentOrReplace(
new utils.MoveTransformComponent(Pos1, Pos2, 0.5)
)
} else {
box.addComponentOrReplace(
new utils.MoveTransformComponent(Pos2, Pos1, 0.5)
)
}
})
)
box.addComponent(
new OnClick(event => {
box.getComponent(utils.ToggleComponent).toggle()
})
)
engine.addEntity(box)
Time
These tools are all related to the passage of time in the scene.
Delay a function
Add a Delay
component to an entity to execute a function only after an n
amount of milliseconds.
This example creates an entity that only becomes visible in the scene after 100000 milliseconds (100 seconds) have passed.
import utils from '../node_modules/decentraland-ecs-utils/index'
const easterEgg = new Entity()
const easterEggShape = new BoxShape()
easterEggShape.visible = false
easterEgg.addComponent(easterEggShape)
easterEgg.addComponent(
new utils.Delay(100000, () => {
easterEgg.getComponent(BoxShape).visible = true
})
)
engine.addEntity(easterEgg)
To delay the execution of a task that isn't directly tied to any entity in the scene, create a dummy entity that only holds a Delay
component.
Delay removing an entity
Add an ExpireIn
component to an entity to remove it from the scene after an n
amount of milliseconds.
This example creates an entity that is removed from the scene 500 milliseconds after it's clicked.
import utils from '../node_modules/decentraland-ecs-utils/index'
const box = new Entity()
box.addComponent(new BoxShape())
box.addComponent(
new OnClick(() => {
box.addComponent(new utils.ExpireIn(500))
})
)
engine.addEntity(box)
Repeat at an Interval
Add an Interval
component to an entity to make it execute a same function every n
milliseconds.
This example creates an entity that changes its scale to a random size every 500 milliseconds.
import utils from '../node_modules/decentraland-ecs-utils/index'
const box = new Entity()
box.addComponent(new BoxShape())
box.addComponent(new Transform())
box.addComponent(
new utils.Interval(500, () => {
let randomSize = Math.random()
box.getComponent(Transform).scale.setAll(randomSize)
})
)
engine.addEntity(box)
To repeat the execution of a task that isn't directly tied to any entity in the scene, create a dummy entity that only holds an Interval
component.
Triggers
The trigger component can execute whatever you want whenever the player's position or the position of a specific entity or type of entity overlaps with an area.
The TriggerComponent
has the following arguments:
shape
: Shape of the triggering collider area, either a cube or a sphere (TriggerBoxShape
or TriggerSphereShape
)
layer
: Layer of the Trigger, useful to discriminate between trigger events. You can set multiple layers by using a |
symbol.
triggeredByLayer
: Against which layers to check collisions
onTriggerEnter
: Callback when an entity of a valid layer enters the trigger area
onTriggerExit
: Callback function for when an entity of a valid layer leaves the trigger area
onCameraEnter
: Callback function for when the player enters the trigger area
onCameraExit
: Callback function for when the player leaves the trigger area
enableDebug
: When true, makes the trigger area visible for debug purposes. Only visible when running a preview locally, not in production.
The following example creates a trigger that changes its position randomly when triggered by the player.
import utils from '../node_modules/decentraland-ecs-utils/index'
const box = new Entity()
box.addComponent(new BoxShape())
box.getComponent(BoxShape).withCollisions = false
box.addComponent(new Transform({ position: new Vector3(2, 0, 2) }))
let triggerBox = new utils.TriggerBoxShape(Vector3.One(), Vector3.Zero())
box.addComponent(
new utils.TriggerComponent(
triggerBox,
0,
0,
null,
null,
() => {
log('triggered!')
box.getComponent(Transform).position = new Vector3(
1 + Math.random() * 14,
0,
1 + Math.random() * 14
)
},
null
)
)
engine.addEntity(box)
Note: The trigger shape can be positioned or stretched, but it can't be rotated on any axis. This is a design decision taken for performance reasons. To cover a slanted area, we recommend adding multiple triggers if applicable.
Dissable a collision component
TriggerComponent
components have an enabled
property, which is set to true
by default when creating it. You can use this property to disable the behavior of the component without removing it.
box.getComponent(utils.TriggerComponent).enabled = false
Set a custom shape for player
You can optionally configure a custom shape and size for the player's trigger area, according to your needs:
utils.TriggerSystem.instance.setCameraTriggerShape(
new utils.TriggerBoxShape(
new Vector3(0.5, 1.8, 0.5),
new Vector3(0, -0.91, 0)
)
)
Trigger layers
You can define different layers (bitwise) for triggers, and set which other layers can trigger it.
The following example creates a scene that has:
- food (cones)
- mice (spheres)
- cats (boxes)
Food is triggered (or eaten) by both cats or mice. Also, mice are eaten by cats, so a mouse's trigger area is triggered by only cats.
Cats and mice always move towards the food. When food or mice are eaten, they respawn in a random location.
import utils from '../node_modules/decentraland-ecs-utils/index'
const foodLayer = 1
const mouseLayer = 2
const catLayer = 4
let triggerBox = new utils.TriggerBoxShape(Vector3.One(), Vector3.Zero())
const food = new Entity()
food.addComponent(new ConeShape())
food.getComponent(ConeShape).withCollisions = false
food.addComponent(
new Transform({
position: new Vector3(1 + Math.random() * 14, 0, 1 + Math.random() * 14)
})
)
food.addComponent(
new utils.TriggerComponent(
triggerBox,
foodLayer,
mouseLayer | catLayer,
() => {
food.getComponent(Transform).position = new Vector3(
1 + Math.random() * 14,
0,
1 + Math.random() * 14
)
mouse.addComponentOrReplace(
new utils.MoveTransformComponent(
mouse.getComponent(Transform).position,
food.getComponent(Transform).position,
4
)
)
cat.addComponentOrReplace(
new utils.MoveTransformComponent(
cat.getComponent(Transform).position,
food.getComponent(Transform).position,
4
)
)
}
)
)
const mouse = new Entity()
mouse.addComponent(new SphereShape())
mouse.getComponent(SphereShape).withCollisions = false
mouse.addComponent(
new Transform({
position: new Vector3(1 + Math.random() * 14, 0, 1 + Math.random() * 14),
scale: new Vector3(0.5, 0.5, 0.5)
})
)
mouse.addComponent(
new utils.TriggerComponent(triggerBox, mouseLayer, catLayer, () => {
mouse.getComponent(Transform).position = new Vector3(
1 + Math.random() * 14,
0,
1 + Math.random() * 14
)
mouse.addComponentOrReplace(
new utils.MoveTransformComponent(
mouse.getComponent(Transform).position,
food.getComponent(Transform).position,
4
)
)
})
)
const cat = new Entity()
cat.addComponent(new BoxShape())
cat.getComponent(BoxShape).withCollisions = false
cat.addComponent(
new Transform({
position: new Vector3(1 + Math.random() * 14, 0, 1 + Math.random() * 14)
})
)
cat.addComponent(new utils.TriggerComponent(triggerBox, catLayer))
mouse.addComponentOrReplace(
new utils.MoveTransformComponent(
mouse.getComponent(Transform).position,
food.getComponent(Transform).position,
4
)
)
cat.addComponentOrReplace(
new utils.MoveTransformComponent(
cat.getComponent(Transform).position,
food.getComponent(Transform).position,
4
)
)
engine.addEntity(food)
engine.addEntity(mouse)
engine.addEntity(cat)
Conversions
This library includes a number of helpful functions for common value conversions.
Clamp
Use the clamp()
function to easily clamp possible values between a maximum and a minimum.
The clamp()
function takes the following arguments:
value
: Input number to convert
min
: Minimum output value.
max
: Maximum output value.
The following example limits an incoming value between 5 and 15. If the incoming value is less than 5, it will output 5. If the incoming value is more than 15, it will output 15.
let input = 200
let result = utils.clamp(input, 5, 15)
log(result)
Map
Use the map()
function to map a value from one range of values to its equivalent, scaled in proportion to another range of values, using maximum and minimum.
The map()
function takes the following arguments:
value
: Input number to convert
min1
: Minimum value in the range of the input.
max1
: Maximum value in the range of the input.
min2
: Minimum value in the range of the output.
max2
: Maximum value in the range of the output.
The following example maps the value 5 from a scale of 0 to 10 to a scale of 300 to 400. The resulting value is 350, as it keeps the same proportion relative to the new maximum and minimum values.
let input = 5
let result = utils.map(input, 0, 10, 300, 400)
log(result)
World position
If an entity is parented to another entity, or to the player, then its Transform position will be relative to its parent. To find what its global position is, taking into account any parents, use getEntityWorldPosition()
.
The getEntityWorldPosition()
function takes a single argument:
entity
: The entity from which to get the global position
The function returns a Vector3
object, with the resulting position of adding the given entity and all its chain of parents.
The following example sets a cube as a child of the player, and logs its true position when clicked.
const cube = new Entity()
cube.addComponent(new Transform({ position: new Vector3(0, 0, 1) }))cube.addComponent(new BoxShape())
engine.addEntity(cube)
cube.setParent(Attachable.FIRST_PERSON_CAMERA)
cube.addComponent(
new OnPointerDown(() => {
log(getEntityWorldRotation(myCube))
}))
World rotation
If an entity is parented to another entity, or to the player, then its Transform rotation will be relative to its parent. To find what its global rotation is, taking into account any parents, use getEntityWorldRotation()
.
The getEntityWorldRotation()
function takes a single argument:
entity
: The entity from which to get the global rotation
The function returns a Quaternion
object, with the resulting rotation of multiplying the given entity to all its chain of parents.
The following example sets a cube as a child of the player, and logs its true rotation when clicked.
const cube = new Entity()
cube.addComponent(new Transform({ position: new Vector3(0, 0, 1) }))cube.addComponent(new BoxShape())
engine.addEntity(cube)
cube.setParent(Attachable.FIRST_PERSON_CAMERA)
cube.addComponent(
new OnPointerDown(() => {
log(getEntityWorldRotation(myCube))
}))
Send requests
Use the sendRequest()
function to easily send HTTP requests to APIs.
The sendRequest()
function has a single required argument:
url
: The URL to send the request
async function request() {
let response = await utils.sendRequest(
'https://events.decentraland.org/api/events/?limit=5'
)
log(response)
}
NOTE: The sendRequest() function is asynchronous. To access its response, you must use it within an async
block, together with an await
.
The sendRequest()
function also lets you use the following arguments, for sending more advanced requests:
method
: The HTTP method to use. GET
is the default, other common options are POST
, PUT
, and DELETE
.
headers
: The HTTP headers of the request, as a JSON object.
body
: The body of the request, as a JSON object.
async function request() {
let response = await utils.sendRequest(
'https://jsonplaceholder.typicode.com/posts',
'POST',
{
'content-type': 'application/json',
},
{
content: 'My test JSON',
}
}
Labels
Add a text label floating over an entity using addLabel()
.
The addLabel()
function has just two required arguments:
text
: The string of text to display
parent
: The entity to set the label on
const cube = new Entity()
cube.addComponent(new Transform({ position: new Vector3(8, 1, 8) }))cube.addComponent(new BoxShape())
engine.addEntity(cube)
utils.addLabel('random cube', cube)
The addLabel()
function also lets you set the following:
billboard
: If true, label turns to always face player.
color
: Text color. Black by default.
size
: Text font size, 3 by default.
textOffset
: Offset from parent entity's position. By default 1.5 meters above the parent.
Tip: The addLabel()
function returns the created entity, that you can then tweak in any way you choose.
Debug helpers
Debug cube
Render a simple clickable cube to use as a trigger when debugging a scene with addTestCube()
.
NOTE: The test cube is only shown in preview, unless configured to appear also in production.
The addTestCube()
function has just two required arguments:
pos
: The position, rotation and/or scale of the cube, expressed as a TransformConstructorArgs object, as gets passed when creating a Transform
component.
triggeredFunction
: A function that gets called every time the cube is clicked.
myCube = await utils.addTestCube({ position: new Vector3(0, 0, 1) }, () => {
log('Cube clicked')
})
The addTestCube()
function also lets you set the following:
label
: An optional label to display floating over the cube
color
: A color for the cube's material.
sphere
: If true, it renders as a Sphere instead of a cube.
noCollider
: If true, the cube won't have a collider and will let players walk through it.
keepInProduction
: If true, it will be visible for players in-world once the scene is deployed. Otherwise, the cube is only present when previewing he scene locally.
Tip: The addTestCube()
function returns the created entity, that you can then tweak in any way you choose.
Action sequence
Use an action sequence to play a series of actions one after another.
IAction
The IAction
interface defines the actions that can be added into a sequence. It includes:
hasFinished
: Boolean for the state of the action, wether it has finished its execution or not.
onStart()
: First method that is called upon the execution of the action.
update()
: Called on every frame on the action's internal update.
onFinish()
: Called when the action has finished executing.
Action Sequence Builder
This object creates action sequences, using simple building blocks.
The SequenceBuilder
exposes the following methods:
then()
: Enqueue an action so that it's executed when the previous one finishes.
if()
: Use a condition to branch the sequence
else()
: Used with if() to create an alternative branch
endIf()
: Ends the definition of the conditional block
while()
: Keep running the actions defined in a block until a condition is no longer met.
breakWhile()
: Ends the definition of the while block
Action Sequence System
The action sequence system takes care of running the sequence of actions. The ActionsSequenceSystem
exposes the following methods:
startSequence()
: Starts a sequence of actions
setOnFinishCallback()
: Sets a callback for when the whole sequence is finished
isRunning()
: Returns a boolean that determines if the sequence is running
stop()
: Stops a running the sequence
resume()
: Resumes a stopped sequence
reset()
: Resets a sequence so that it starts over
Full example
The following example creates a box that changes its scale until clicked. Then it resets its scale and moves.
import utils from '../node_modules/decentraland-ecs-utils/index'
import { ActionsSequenceSystem } from '../node_modules/decentraland-ecs-utils/actionsSequenceSystem/actionsSequenceSystem'
let boxClicked = false
const box = new Entity()
box.addComponent(new BoxShape())
box.addComponent(new Transform({ position: new Vector3(14, 0, 14) }))
box.addComponent(new OnClick(() => (boxClicked = true)))
engine.addEntity(box)
class ScaleAction implements ActionsSequenceSystem.IAction {
hasFinished: boolean = false
entity: Entity
scale: Vector3
constructor(entity: Entity, scale: Vector3) {
this.entity = entity
this.scale = scale
}
onStart(): void {
const transform = this.entity.getComponent(Transform)
this.hasFinished = false
this.entity.addComponentOrReplace(
new utils.ScaleTransformComponent(
transform.scale,
this.scale,
1.5,
() => {
this.hasFinished = true
},
utils.InterpolationType.EASEINQUAD
)
)
}
update(dt: number): void {}
onFinish(): void {}
}
class MoveAction implements ActionsSequenceSystem.IAction {
hasFinished: boolean = false
entity: Entity
position: Vector3
constructor(entity: Entity, position: Vector3) {
this.entity = entity
this.position = position
}
onStart(): void {
const transform = this.entity.getComponent(Transform)
this.entity.addComponentOrReplace(
new utils.MoveTransformComponent(
transform.position,
this.position,
4,
() => {
this.hasFinished = true
}
)
)
}
update(dt: number): void {}
onFinish(): void {}
}
const sequence = new utils.ActionsSequenceSystem.SequenceBuilder()
.while(() => !boxClicked)
.then(new ScaleAction(box, new Vector3(1.5, 1.5, 1.5)))
.then(new ScaleAction(box, new Vector3(0.5, 0.5, 0.5)))
.endWhile()
.then(new ScaleAction(box, new Vector3(1, 1, 1)))
.then(new MoveAction(box, new Vector3(1, 0, 1)))
engine.addSystem(new utils.ActionsSequenceSystem(sequence))