Hello! Please choose your
desired language:
Dismiss

TypeScript tips

The Decentraland SDK is meant to be used via TypeScript (.tsx) files. This section introduces a number of tips and tricks you can take advantage of when building your scene. What’s discussed here isn’t directly related to the features of the SDK, but rather about ways in which you can use the TypeScript language and context to make the most out of it.

Log to console

You can log messages to the JavaScript console of the browser while viewing a scene.

You don’t need to import any additional libraries to do this, simply write console.log() in any part of your scene.tsx file. You can use log messages, for example, to confirm the occurrence of events, or to verify that the value of a variable changes in the way you expected.

this.subscribeTo("pointerDown", e => {
  console.log("click")
})

To view logged messages while running a preview of your scene, look at the JavaScript console, which you can open in the developer settings of your browser.

You can also trace the whole list of commands that were executed leading to a point in the code using console.trace().

this.subscribeTo("pointerDown", e => {
  console.trace()
})

The console.trace() command prints the list of functions that were called in order before this line was executed.

Create a global constant

You can define global constants at the root level of a .tsx file. Once defined, they can be referenced throughout the entire file.

This is useful for values that are used multiple times in your scene and need to have consistency. This makes it easier to maintain your code, as you only need to change one line.

import { createElement, ScriptableScene } from "decentraland-api"

const updateRate = 300
const myColors = [
  "#3d9693",
  "#e8daa0",
  "#968fb7",
  "#966161",
  "#879e91",
  "#66656b",
  "#6699cc"
]

export default class myScene extends ScriptableScene {
  state = {
    interval: updateRate,
    boxColor: myColors[1],
    doorColor: myColors[4]
  }

  // (...)
}

Define an enum

TypeScript allows you to define custom string enums. They are useful for assigning to variables that are only allowed to hold certain specific values in your scene.

Using string enums makes your code more readable. If you’re working with an advanced code editor like Visual Studio Code or Atom, it also makes writing your code easier, since the code editor provides autocomplete options and validation.

For example, the example below defines a custom enum with three possible values. It then uses the enum as a type for a variable when defining an interface for the scene state.

export enum Goal {
  Idle,
  Walk,
  Sit
}

export interface IState {
  dogGoal: Goal
  dogPosition: Vector3Component
  catGoal: Goal
  catPosition: Vector3Component
}

Each time you want to refer to a value in an enum, you must write it as <enum name>.<value>. For example, Goal.Walk refers to the Walk value, from the Goal enum.

  if (this.state.dogGoal == Goal.Walk){
   this.setState(catGoal: Goal.Sit)
  }

Your code editor suggests the possible values for the enum as soon as you refer to it, making your scene easier to write.

Define custom data types

Defining a custom type has similar advantages to defining an enum, but is a bit less verbose and you might find its syntax more familiar, depending on what coding languages you’re more experienced in using.

export type characterState = "walking" | "won" | "falling"

The custom type defined above can only hold the three possible values listed above. You can then use it, for example, for a variable in the scene state.

state = {
  characterNow: (characterState = "walking")
}

Scene state interfaces

The scene state object can be defined as a type of its own. This ensures that the state object always has the right variables and that they all have valid values for their corresponding types. If you’re working with an advanced code editor like Visual Studio Code or Atom, defining a state interface helps your editor provide type validation and smart auto-completes.

interface IState {
  doorState: boolean
  boxes: number
  userPos: Vector3Component
}

Once you defined an interface, you can pass it to the custom scene class as the second argument, which sets the type for the scene’s state object.

export default class ArtPiece extends ScriptableScene<any, IState> {
  state: IState = {
    pedestalColor: "#3d30ec",
    dogAngle: 0,
    donutAngle: 0
  }

  // (...)
}

Import external libraries

You can import most any JavaScript library to your scene.tsx file. Use external libraries to help you with advanced mathematical operations, call APIs, run predefined AI scripts or whatever your scene needs.

For example, this line imports quaternion and vector types from Babylonjs.

import { Vector3, Quaternion } from "babylonjs"

For most 3D math operations, we recommend importing Babylonjs, because much of Decentraland’s SDK uses it, making compatibility safer. Other libraries with similar capabilities are available too and can also be imported.

Before a library can be used by your scene, it must be installed with npm in the scene’s folder. This is important because when the scene is deployed to Decentraland, all of its dependencies must be uploaded as well.

When importing from large libraries like Babylonjs, we recommend only importing the elements that you need for your scene, instead of importing the entire library.

Vector math operations

Vectors in decentraland are of type Vector3Component, this type is very lightweight and doesn’t include any methods.

To avoid doing vector math manually, we recommend importing the Vector3 type from Babylonjs. This type comes with a lot of handy operations like scaling, subtracting and more.

To use this, you must first install babylonjs in the project folder. Run the following command from a terminal in the scene’s main folder.

npm install babylonjs

You can then import elements of the Babylon js library into your scene’s .tsx files.

import { Vector3 } from "babylonjs"

Once imported to your .tsx file, you can assign the Vector3 type to any variable. Variables of this type will then have access to all of the type methods.

The example below deals with Vector3 variables and a few of the functions that come with this type.

moveToGoal(){
  const delta = this.state.goalPosition.subtract(this.state.dogPosition)
  delta = delta.normalize().scale(.015)
  this.setState(dogPosition: this.state.dogPosition.add(delta))
}

Entities in decentraland accept variables of type Vector3 for setting position, rotation and scale. There’s no need to convert a variable to Vector3Component type when applying it to an entity.

Keep in mind that some events in a Decentraland scene, like the positionChanged event, have attributes that are of type Vector3Component. If you wish to use methods from Vector3 on this information, you must first change its type.

Handle animated 2D sprites

You can add 2D animations in your scene as a way to save on triangle amounts, or as a chosen aesthetic.

You’ll generally want to apply sprite animations to a plane entity that’s configured to behave as a billboard. Setting an entity’s billboard component makes it rotate to always face the user, learn more about this in Entity positioning.

We recommend using the Decentraland sprite helpers node package. To install it run the following:

npm i --save dcl-sprites

Then import the library into your scene:

import { createSpriteSheet } from "dcl-sprites"

Read the library’s documentation for further instructions on how to use it.

Access data across objects

When your scene reaches a certain level of complexity, it’s convenient to break the code out into several separate objects instead of having all of the logic inside the scriptableScene class.

The downside is that information becomes harder to pass on. If all of your logic occurs inside the scriptableScene class, you can keep track of all information using the scene state and scene properties. But if that’s not the case, then you must keep in mind that you can’t reference the scene state or scene properties from outside the scriptableScene class.

You can either:

  • Pass information from the main scriptableScene class as properties of child objects.
  • Use a library like Redux to create a univesal data store that can be referenced from anywhere.

See scene state for more details.

Execution timing

TypeScript provides various ways you can control when parts of your code are executed.

The scriptableScene object comes with a number of default functions that are executed at different times of the scene life cycle, for example sceneDidMount() is called once when the scene starts and render() is called each time the that the scene state changes. See scriptable scene for more information.

Entities can include a transition component to make any changes occur gradually, this works very much like transitions in CSS. See Entity positioning for more information.

Start a time-based loop

The setInterval() function initiates a loop that executes a function repeatedly at a set interval

setInterval(() => {
  this.setState({ randomNumber: Math.random() })
}, 1000)

This sample initiates a loop that sets a randomNumber variable in the scene state to a new random number every 1000 milliseconds.

End a loop

The setInterval() function returns an id for the loop, you can terminate the execution of this loop by running the clearInterval() function, passing it the loop’s id.

  let count = 0
  const loopId = setInterval(() => {
    count += 1
    console.log(count)
    if (count === 5) {
      clearInterval(loopId)
    }
  }

This example iterates over a loop until a condition is met, in which case clearInterval() is called to stop the loop.

Delay an execution

setTimeout()

The setTimeout() function delays the execution of a statement or function.

setTimeout(f => {
  console.log("you'll have to wait for this message")
}, 3000)

The setTimeout function requires that you pass a function or statement to execute, followed by the amount of milliseconds to delay that execution.

await sleep()

A more elegant way to delay execution is by creating a helper sleep function and calling it. The benefit is that if you need to pause execution several times in a process, you don’t need to nest multiple statements one inside the other.

Add the following function to your scene.tsx file:

export function sleep(ms: number = 0) {
  return new Promise(r => setTimeout(r, ms))
}

Then you can call the sleep() function from any asyncronous function in your scene as shown below.

  async updateAnimation(){
    this.setState({playAnimation1:true, playAnimation2:false})
    await sleep(5000)
    this.setState({playAnimation1:false, playAnimation2:true})
    await sleep(5000)
    this.updateAnimation()
  }

Note: for the code above to work, your TypeScript file must include or import a definition for the sleep() function.

Freeze till complete

Adding await at the start of a statement stops all execution of the current thread until that statement returns a value.

await this.runImportantProcess()

In this example, execution of the thread is delayed until the function runImportantProcess() has returned a value.

When needing to store the value of the return statement, the await goes after the equals sign like this:

const importantValue = await this.runImportantProcess()

await can only be used within the context of an async function, as otherwise it would freeze the main thread of execution of the scene, which is never desirable.

If you’re familiar with C# language, you’ll see that asyncrhonous functions in TypeScript behave just the same. Functions run synchronously by default, but you can make them run asynchronously by adding async before the name when defining them.

Tip: If you want to understand the reasoning behind JavaScript promises, async and await, we recommend reading this article.

Handle arrays in the scene state

There are a number of things you need to take into account when working with arrays that belong to the scene state of a Decentraland scene.

Since you must always update the scene state through the method .setState(), you can’t just use array methods like .push() or pop() that would change this variable directly. You must call setState() to pass it the full array you want to have after implementing the change.

If you’re making a copy of an array that’s meant to be modified, make sure you’re cloning it entirely and not merely referencing its values. Otherwise changes to that array will also affect the original. To copy an array’s values rather than the array itself, use the spread operator (three dots).

const newArray = [...this.state.myArray]

Add to an array

This example adds a new element at the end of the array:

this.setState({ myArray: [...this.state.myArray, newValue] })

This example adds a new element at the at the start of the array:

this.setState({ myArray: [newValue, ...myArray.state.list] })

Update an element on an array

This example changes the value of the element that’s at valueIndex:

this.setState({
  myArray: [
    ...this.state.myArray.slice(0, valueIndex),
    newValue,
    ...this.state.myArray.slice(valueIndex + 1)
  ]
})

Remove from an array

This example pops the first element of the array, all other elements are shifted to fill in the space.

const [_, ...rest] = this.state.list
this.setState({ list: [...rest] })

This example removes the last element of the array:

const [...rest, _] = this.state.list
this.setState({ list: [...rest] })

This example removing all elements that match a certain condition. In this case, that their id matches the value of the variable toRemove.

this.setState({ myArray: ...myArray.state.list.filter(x => x.id === toRemove) })

The map operation

There are two array methods you can use to run a same function on each element of an array separately: map() and forEach(). The main difference between them is that map() returns a new array without affecting the original array, but forEach() can overwrite the values in the original array.

The map() operation runs a function on each element of the array, it returns a new array with the results.

renderLeaves(){
  return this.state.fallingLeaves.map((leaf, leafIndex) =>
    <plane
      position={{ x: leaf.x , y: leaf.y, z:leaf.z }}
      scale={0.2}
      key={"leaf" + leafIndex.toString()}
    />
  )
}

This example goes over the elements of the fallingLeaves array running the same function on each. The original array is of type Vector3Component so each element in it has values for x, y and z coordinates. The function that runs for each element returns a plane entity that uses the position stored in the array and has a key based on the array index.

Combine with filter

You can combine a map() or a forEach() operation with a filter() operation to only handle the array elements that meet a certain criteria.

renderLeaves(){
  return this.state.fallingLeaves
    .filter(pos => pos.x > 0)
    .map( (leaf, leafIndex) =>
      <plane
        position={{ x: leaf.x , y: leaf.y, z: leaf.z }}
        scale={0.2}
        key={"leaf" + leafIndex.toString()}
      />
    )
}

This example is like the one above, but it first filters the fallingLeaves array to only handle leaves that have a x position greater than 0. The fallingLeaves array is of type Vector3Component, so each element in the array has values for x, y and z coordinates.

The forEach operation

The forEach() operation runs a same function on every element of the array.

renderLeaves() {
  var leaves: ISimplifiedNode[] = []

  this.state.fallingLeaves.forEach( (leaf,leafIndex) => {
    leaves.push(
      <plane
        position={{ x: leaf.x, y: leaf.y, z: leaf.z }}
        scale={0.2}
        key={"leaf" + leafIndex.toString()}
      />
    )
  })

  return leaves
}

Like the example used to explain the map operator above, this example goes over the elements of the fallingLeaves array running the same function on each. The original array is of type Vector3Component so each element in it has values for x, y and z coordinates. The function that runs for each element returns a plane entity that uses the position stored in the array.

The function performed by the forEach() function doesn’t have a return statement. If it did, it would overwrite the content of the this.state.fallingLeaves array. Instead, we create a new array called leaves and push elements to it, then we return the full array that at the end.

As you can see from comparing this example to the prevous ones , it’s a lot simpler to use map() to render entities from a list.

Note: Keep in mind that when dealing with a variable from the scene state, you can’t change its value by setting it directly. You must always change the value of a scene state variable through the this.setState() operation.

Make the render function dynamic

The render() function draws what users see in your scene. In its simplest form, its return statement contains what resembles a literal XML definition for a set of entities with fixed values. An essential part of making a scene interactive is to have the render function change its output in response to changes in the scene state.

Although what’s typically in the return statement of render() may resemble XML, everything that goes in between { } is being processed as TypeScript. This means that you can interrupt the tag and attribute syntax of XML with curly brackets to add JSX expressions anywhere you choose.

The end result of all the expressions in your return statement must always be a set of nested isSimplifiedObject entities, nested inside a scene entity.

Note: You are free to add TypeScript expressions to make up the return value, but you can’t add statements. The difference is that expressions always have a return value, but statements might not. You can’t use if/else or switch/case because those are statements, but you can call functions that do the same.

Add or remove entities

Instead of telling the engine what actions to take to reach a desired state, you tell the engine what new state you want to render, and the engine figures out how to get there. If you’re familiar with the React framework, you’ll note that the Decentraland API is designed around the same ideas.

Because of this, there is no action to add or remove entities from the scene. Instead, this is implicitly done when you call the render() function telling it to render a different set of entities.

  • If the new set includes an entity that wasn’t rendered before, it’s implicitly added.
  • If an entitiy is missing from the new set, then it’s implicitly removed.

Reference variables from render

The simplest way to change how something is rendered is to reference the value of a variable from the value of one of the XML attributes.

async render() {
  return (
    <scene>
      <box
        color= {this.state.boxColor}
        scale={this.state.boxSize}
      />
    </scene>
  )
}

Add conditional logic to render

Another simple way to make render() respond to changes in variables is to add conditional logic.

async render() {
  return (
    <scene>
      {this.state.boxOrSphere == sphere
        ?<sphere />
        :<box />
      }
    </scene>
  )
}

In the example above, the render function either returns a box or a sphere depending on the value of a boxOrSphere variable. Note that we needed to wrap the entire conditional expression in { } for it to be processed correctly as TypeScript.

async render() {
  return (
    <scene>
      <box
          position={{x: 2, y: this.state.liftBox ? 5:0 , z:1}}
          transition={{ position:
            { duration: 300, timing: this.state.bounce? "bounce-in" : "linear" }
          }}
      />
    </scene>
  )
}

In this second example, the y position of the box is determined based on the value of liftBox and the timing of its transition is based on the value of bounce. Note that both of these conditional expressions were added in parts of the code that were already being processed as TypeScript, so no additional { } were needed.

Note: In these examples we’re able to add conditional logic through the use of an ? / : expression. You can’t use an if and else in this context, because those are statements, and statements aren’t supported as part of the return value.

Render entities from an array

For scenes where the number of entities isn’t fixed, use an array to represent these entities and their attributes and then use a map() operation within the render() function.

async render() {
  return (
    <scene>
      { this.state.sequence.map(num =>
        <box
          position={{ x: num * 2, y: 1, z: 1 }}
          key={"box" + num.toString()}
        />
      }
    </scene>
  )
}

This function uses a map() operation to create a box entity for each element in the sequence array, using the numbers stored in this array to set the x coordinate of each of these boxes. This enables you to dynamically change how many boxes appear and where by changing the sequence variable in the scene state.

A few best practices when rendering entities from a list:

  • Use array.map to go over the list
  • Don’t use a for loop
  • Give each entity a unique key
  • Avoid using the array index as the entity key

Render entities from an object

When you want to keep track of multiple pieces of information about each entity in a collection, it’s useful to store the entities as an object, where each attribute of the object represents one of the entities.

We recommend defining a custom type for the object, to validate that data is being stored in a consistent way.

export type boxes = {
  [key: string]: [Vector3Component, Vector3Component, boolean]
}

The following code example renders a collection of boxes from a sequence variable in the scene state.

 async render() {
    return (
      <scene>
        { this.renderBoxes()}
      </scene>
    )
  }

  renderBoxes(){
    let boxModels: any[] = []
    for (var box in this.state.sequence) {
      boxModels.push(
        <box
          key={"box"}
          position={this.state.sequence[box][0]}
          rotation={this.state.sequence[box][1]}
          visible={this.state.sequence[box][2]}
         />
      )
      return boxModels
  }
}

While iterating through the attributes in the object, box refers to the attribute key, and you can use it to access the values under that key via this.state.sequence[box].

Keep the render function readable

The output of the render() function can include calls to other functions. Since render() is called each time that the scene state is updated, so will all the functions that are called by render().

Doing this keeps the code in render() more readable. In simple scenarios it’s mostly recommendable to define all the entities of the scene within the render() function, but when dealing with a varying number of entities or a large number that can be treated as an array, it’s often useful to handle this behavior in a function outside render().

async render() {
  return (
    <scene>
      {this.renderObstacles()}
      {this.renderFruit()}
      {this.status.difficulty === 'hard' ?
        this.renderHardEnemies()
        : this.renderEasyEnemies()
      }
    </scene>
  )
}

The functions that are called as part of the return satement must, of course, return values that combine well with the rest of what’s being rendered to produce a valid XML output. In the example above, the renderObstacles() function can contain the following:

renderObstacles() {
  return this.state.sequence.map(num =>
    <box
      position={{ x: num * 2, y: 1, z: 1 }}
    />
  )
}

The this operator

Most often, when you use this in a scene it refers to the instance of the scriptable scene object that is running the scene.

However, the meaning of this in a function is relative to where a function is being called from, not to where the function was defined. The same function could assign different meanings to this depending on where it was called from. If a function is defined as part of the scene, but it’s called by the onClick component of an entity, any uses of the operator this in the function refer to the function itself, not to the scriptable scene object.

It can sometimes be a problem if you need to refer to the scene state or to other functions in the scene. To avoid this problem, you can either define the function as a lambda or call the function through a lambda defined in the onClick value.

import * as DCL from "decentraland-api"

export interface IState {
  clickCounter: number
}

export default class clickTest extends DCL.ScriptableScene<any, IState> {
  state: IState = {
    clickCounter: 0
  }

  // is defined as a lambda
  clickBox = () => {
    this.setState({ clickCounter: (this.state.clickCounter += 1) })
    console.log(this.state.clickCounter)
  }

  // called via a lambda in the onClick
  clickBox2() {
    this.setState({ clickCounter: (this.state.clickCounter += 1) })
    console.log(this.state.clickCounter)
  }

  // this function is called in a way where 'this' doesn't refer to the scene object
  clickBox3() {
    console.log(this)
  }

  async render() {
    return (
      <scene>
        <box
          id="function defined as lambda"
          position={{ x: 3, y: 1, z: 1 }}
          onClick={this.clickBox}
        />
        <box
          id="lambda in onClick"
          position={{ x: 1, y: 1, z: 1 }}
          onClick={() => this.clickBox2}
        />
        <box
          id="`this` doesn't refer to the scene object"
          position={{ x: 5, y: 1, z: 1 }}
          onClick={this.clickBox3}
        />
      </scene>
    )
  }
}