Hello! Please choose your
desired language:
Dismiss

Version 5.0 of the SDK introduces a new architecture based on three important factors: entities, components, and systems (ECS). We realize that this might take some getting used to, especially if you’ve already been building scenes with the old SDK.

We want to make sure that your transition to version 5.0 is as smooth and enjoyable as possible. Or, if you’re new to Decentraland, we want to get you up and running with the latest and greatest iteration of our developer tools. So dive into this tutorial for a quick intro to ECS, and how it can be used to create interactive, 3D content for Decentraland.

What this tutorial will cover

For this tutorial, we’ll create a simple scene using version 5.0 of the Decentraland SDK. The scene we’ll be working with is quite simple; it just features a couple of spinning wheels that you can… spin around, but this is a great place to start getting familiar with a lot of the essential concepts that are new to the SDK.

We’ll see how components are used for adding primitive shapes, materials, 3D models, and click events, we will also cover some of the trickier concepts, including how to create custom components, how systems work and how to create component groups.

While reading through this article, you can either create your own scene and mimic each step, or take a look at the final code in this repository.

Create a blank scene

If you wish to create your own version of this scene as you read along, you must first do the following:

  1. Make sure you have the latest version of the Decentraland SDK. Run the following command on a command prompt: npm install -g decentraland
  2. Then, create an empty folder, navigate to that directory, and run the following command to create some boilerplate code: dcl init
  3. Open game.ts and delete all of the default content in that file.
  4. Finally, download the assets for the scene from this link. Extract it and place the /materials and /models folders at root level in the scene’s directory.

Tip: We recommend using a source code editor like VS Code while working on Decentraland scenes. It makes writing code a lot easier by providing smart autocompletes, and more.

Adding entities and basic components

Let’s start our scene by adding some static content. Paste the following code into the game.ts file.

let stage = new Entity()
stage.add(new GLTFShape("models/Theatre.gltf"))
stage.add(new Transform({
 position: new Vector3(5, 0, 5),
 rotation: Quaternion.Euler(0, 90, 0)
}))
engine.addEntity(stage)

Note: Unlike in previous versions of the SDK, you don’t need to wrap any of this code in a class or in render or sceneDidMount methods. None of those layers of abstraction exist anymore. Now the scene’s main code is just executed like a script from start to finish.

In the code above, we’re creating a stage entity that has a GLTFShape component that loads a .glTF 3D model. This 3D model includes all the fixed things in our scene, that’s everything except for the two wheels. This 3D model also includes embedded collider geometry.

We also add a Transform component to the stage entity, this determines the entity’s position, rotation and scale.

Finally, we add the stage entity to the engine. This is an important step that tells the scene that this entity and its components aren’t just something to deal with abstractly in the background, we want them to be rendered and active in the scene. If you don’t add entities to the engine, they might as well not exist at all.

Let’s paste some more code into our scene:

// Define a reusable Cylinder shape component
let CylinderWCollisions = new CylinderShape()
CylinderWCollisions.withCollisions = true

Here we define a CylinderShape component and we configure it so that it also behaves as a collider that doesn’t allow users to walk through it. Components store information, but if this information doesn’t have a context, then it doesn’t mean anything. If we want an actual “thing” to exist in our scene that has the shape that is described by our component, we need to create an entity and add our component to it.

// Create the first wheel entity
let wheel1 = new Entity()
wheel1.add(CylinderWCollisions)
wheel1.add(new Transform({
 position: new Vector3(3, 2, 6),
 rotation: Quaternion.Euler(90, 0, 0),
 scale: new Vector3(1, 0.05, 1)
}))
engine.addEntity(wheel1)

// Create a second wheel entity
let wheel2 = new Entity()
wheel2.add(CylinderWCollisions)
wheel2.add(new Transform({
 position: new Vector3(7, 2, 6),
 rotation: Quaternion.Euler(90, 0, 0),
 scale: new Vector3(1, 0.05, 1)
}))
engine.addEntity(wheel2)

In the code above, we create two new entities and we add the CylinderWCollisions component to both. We’re also giving each a Transform component to set their positioning.

Note: Another way to do this is to create a separate instance of the CylinderShape component for each wheel, both ways work just as well. However, reusing components over multiple entities is often a great way to save processing resources.

You may have noticed that when passing rotation values, we’re referencing something called Quaternion. The Transform component stores rotation angles in quaternion format. Quaternion angles are weird four-numbered things that are very practical to use in calculations but quite hard to understand conceptually. Thankfully, the SDK includes a number of helper functions to save you from having to think in terms of quaternions, like for example: Quaternion.Euler(). This helper allows us to create a quaternion object by passing it the values of an Euler angle instead. Euler angles, by the way, are the normal angles that we all know and love, those that go from 0 to 360 and have x, y, and z axis.

We can now run a preview of the scene by opening a command prompt to the scene’s root directory and running dcl start. You’ll see that the scene is already being rendered! That’s all it really takes to bring 3D content into the world!

Materials

Our wheels look white, because we haven’t given them a material. To be able to tell when our spinning wheels are actually spinning – and to make them more fun – we’ll give them a texture. Textures are applied to entities by adding a Material component to them:

// Create material
let SpiralMaterial = new Material()
SpiralMaterial.albedoTexture = "materials/hypno-wheel.png"

// Add material to wheels
wheel1.add(SpiralMaterial)
wheel2.add(SpiralMaterial)

In the snippet above, we’re defining a new component of type Material, then we’re adding that same component to both our wheel entities.

We configured the albedoTexture field on our material. An albedo texture is simply a texture that responds to varying lighting conditions, so it will be more or less lit depending on the direction it’s facing.

Custom components

Here’s where things start to get more interesting! Let’s start by deciding what information we’ll need to keep track of as we spin each of our wheels:

  • A true/false boolean to know if the wheel should be spinning or not
  • A number for the spin speed
  • A vector for the spin direction

To store this information, we’re going to define a custom component and call it WheelSpin.

@Component('wheelSpin')
export class WheelSpin {
 active: boolean = false
 speed: number = 30
 direction: Vector3 = Vector3.Up()
}

Once defined as a class, our custom component can be added to entities just like the other components we’ve been using in our scene. We’re going to add this component to both our wheel entities.

Note that we wrote two names for our component in its definition, wheelSpin (with a lowercase) and WheelSpin (with an uppercase). The first is an internal name that we won’t be caring much about in this tutorial. The second name, the one with the uppercase, is the one we’ll use when referring to the component.

wheel1.add(new WheelSpin())
wheel2.add(new WheelSpin())

Each wheel holds a separate instance of the WheelSpin class, that stores information that’s specifically about that individual wheel and that can change over time. To illustrate this more clearly, let’s make our wheels hold slightly different information in their WheelSpin components: let’s change the direction value on wheel2. wheel1 will remain with the default direction (Vector3.Up()), as we’re not altering its value.

wheel2.get(WheelSpin).direction = Vector3.Down()

Component groups

Component groups are a new tool included in the SDK that can be very powerful when used correctly. A component group is nothing more than an array that keeps track of every entity in the scene that matches a certain criteria.

In our case, we’re interested in keeping track of all the entities that have a WheelSpin component, since we can assume that every entity with a WheelSpin component is meant to behave like a spinning wheel. You’ll later see how this is useful.

The line below creates a component group that lists entities with a WheelSpin component.

const wheels = engine.getComponentGroup(WheelSpin)

The nice thing about component groups is that the engine keeps updating them every time an entity is added or removed, and every time that an entity is given new components or has them removed. In our scene, the component group will automatically list both wheel1 and wheel2, without us having to explicitly add them.

Click behaviors

We want the wheels to start spinning when you click them, and then to go faster as you keep clicking them again.

Click behaviors, as everything in the ECS, are determined by a specialized component. We’ll add an OnClick component to each wheel, and write a function into this component that will be executed each time the entity is clicked.

wheel1.add(
 new OnClick(e => {
   let spin = wheel1.get(WheelSpin)
   if (!spin.active){
     spin.active = true
   } else {
     spin.speed += 20
   }
   //log("speed: ", spin.speed)
 })
)

wheel2.add(
 new OnClick(e => {
   let spin = wheel2.get(WheelSpin)
   if (!spin.active){
     spin.active = true
   } else {
     spin.speed += 30
   }
   //log("speed: ", spin.speed)
 })
)

The function that’s executed by the OnClick component changes the values stored in the wheel’s WheelSpin component.

So far, changing values in the WheelSpin component doesn’t have any effect on what users can see in the scene, it goes completely unnoticed. We still need to do one last thing before these values truly mean something.

Before we do that, let’s experience a little instant gratification from what we’ve done so far. We can at least log the values in the WheelSpin component as they change. To do that, let’s erase the // in the last line of each function to uncomment the log() statement, that way we can see this variable change in console.

Run the scene preview again, open the javascript console (in Chrome you do that by going to View > Developer > Javascript Console) and click on the wheels a few times. You should see the console print out numbers that keep getting higher every time you click.

Making things happen

Now it’s time to get into the really fun part! Let’s make the spinning wheels actually spin for real!

This is where we introduce the remaining part of the entity component system architecture: systems. Remember that components are here to store data, and entities are here to group components into something concrete that has an identity of its own. Systems are here to change the data in the components as time goes by. If you want anything in a scene to change over time, chances are you’re going to need a system to do that.

All systems have an update() function, this function is called once on every frame of the scene, so about 30 times per second.

In our system, we want the update function to perform the following task 30 times per second, for each wheel in the scene:

  • Check if the wheel’s state is active
  • If so, make it turn just a little, in proportion to its current speed

Here’s the code for a system that takes care of that:

export class RotatorSystem implements ISystem {
 update(dt: number) {
   for (let wheel of wheels.entities) {
     let spin = wheel.get(WheelSpin)
     let transform = wheel.get(Transform)
     if (spin.active){
       transform.rotate(spin.direction, spin.speed * dt)
     }
   }
 }
}

The system’s update() function iterates over each wheel in wheels, the component group we created. For each wheel entity, it creates temporal shortcuts to the entity’s relevant components and it then carries out the steps we mentioned above: it checks if the wheel is active, and if so, it rotates it.

We’re rotating the wheel using the .rotate() method of the Transform component. This method requires that you pass it a direction (expressed as a 3D Vector) and an amount to turn. As we’re calling this function 30 times per second, the amount that we turn the wheel should be very small if we want to actually see the spinning as it occurs frame by frame.

One thing that might have called your attention about the system’s code is the dt parameter. dt stands for delay time, it represents how long it took the last frame to get processed. Ideally, we should have 30 frames per second, in which case dt equals 1/30. But if the user’s device is struggling to compute and render the scene, then some frames could take longer, and the value of dt would be larger.

In our code, we’re multiplying how much we rotate the wheel in each frame by dt. If a frame takes longer than usual to be processed, our wheel will also be turning a little more on that frame to compensate. Making movements relative to dt is a great way to keep movement smooth even if things get tough.

After pasting the code above to define the system as a class, we need to add an instance of the system to the engine so that it’s actually used. You do that by adding this line to the scene:

engine.addSystem(new RotatorSystem())

Final thoughts

We’re done, and our scene should now be working! If you were just reading along and still want to play with the final scene, you can find it here.

Enter the preview and click on the wheels to make them spin. If you keep spinning them, they start going faster and faster! Notice that, as they rotate in different directions, we get different optical illusions from each.

This is actually quite enjoyable to interact with! Just for fun, try this out: get one of the wheels to spin at a good speed by clicking it several times, then stare at it up close up for about 30 seconds, and quickly look at the palm of your hand. Do the same with the other wheel and see how the optical illusion changes! :)

If you want to grasp the concepts explained in this tutorial more firmly, I suggest you try this bit of homework:

Add more wheels to the scene. Notice how they behave just like the others. Each has its own instance of the WheelSpin component and is treated individually by the RotatorSystem system. Extra credit: Add another entity to serve as a button. Each time the user clicks this button, create a new spinning wheel in a random location.

Note how, even as the number of wheels changes over time, the wheels component group keeps track of them and allows the RotatorSystem system to handle them all.

To see more advanced examples in practice, check out the Example Scenes page in the documentation!

Start Building!
Our SDK provides everything you need to start developing games and applications.
Get Started