Hello! Please choose your
desired language:
Dismiss

This is our second tutorial working with the new SDK. If you haven’t already, check out the first tutorial to get familiar with the basics.

We’re going to build an example scene over a two part series. The first installment will introduce how to use glTF animations and then explore different ways of moving entities around the scene. The second installment will show you how to get user position data, dig deeper into glTF animations, and show you why it’s useful to have multiple independent systems.

For this example, we’ll work with a 3D model of a character that we purchased on Bitgem. This particular model includes a nice collection of built-in animations that we’ll use to make things a little more lively. The model is named “skeleton king” by its creators, but I decided to affectionately name him “Gnark”. I don’t know why, he just has one of those faces that go well with “Gnark”.

While reading through this article, you can either create your own scene and mimic each step, or you can find the final code in this repository. For the code we’re building in part one, be sure to check the release 1.0.

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:

  • Make sure you have the latest version of the Decentraland SDK. Run the following command on a command prompt:
npm install -g decentraland
  • Create an empty folder to create the project, then navigate to that directory.
  • Run the following command to create some boilerplate code:
dcl init
  • Open game.ts and delete all of the default content in that file
  • Download the assets for the scene from this link. Extract it and place the /models folder at root level in the scene’s directory.
  • This scene takes over four parcels of land. For the preview to support that, you need to open the scene.json file and specify four parcel coordinates that are adjacent to each other. For example:
"scene": {
   "parcels": [
     "0,0", "0,1", "1,1", "1,0"
   ],
   "base": "0,0"
 }

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

Add 3D models and animations

We’ll start by adding the following code to our game.ts file to place Gnark and his temple into our scene:

// Create temple
const temple = new Entity()
temple.add(new GLTFShape('models/Temple.gltf'))
temple.add(new Transform({
    position: new Vector3(10, 0, 10)
}))

// Add temple to engine
engine.addEntity(temple)

// Create Gnark
let gnark = new Entity()
gnark.add(new GLTFShape('models/gnark.gltf'))
gnark.add(new Transform({
    position: new Vector3(5, 0, 5),
    scale: new Vector3(0.75, 0.75, 0.75)
}))

// Add Gnark to engine
engine.addEntity(gnark)

Now let’s add an animation to make Gnark walk around! The gnark.gltf file already comes with a few embedded animations we can use. To play them, we need to know what they’re named in the file. If you’re not sure what animations are included in a .glTF or what they’re named, you can either:

  • Download the glTF tools extension for Visual Studio Code and use it to open the file. You will be able to see a list of all animations and preview them.
  • Open the contents of the .glTF file with a text editor and scroll down till you find the animation names (these files tend to be very lengthy, but it can be done).
const walkClip = new AnimationClip('walk')
gnark.get(GLTFShape).addClip(walkClip)
walkClip.play()

Here we created an AnimationClip object to handle the walking animation. This object keeps track of the progress along the animation sequence and it’ll determine what to display on every frame. Note that we’re then adding this object to the GLTFShape component, not to the entity.

You don’t need any systems to handle the animation of 3D models. AnimationClip objects take care of updating the model on every frame as they progress over the animation.

If we run the scene preview with just this code, we can see Gnark moonwalking in place. Kind of cool in its own way, but not quite what we had in mind. We also want him to move forward as he walks!

Movement baked into an animation

We could edit the walk animation that’s stored in our 3D model so that it moves the character’s mesh away from a fixed location as it walks. If we did that, we’d already be done with part 1 of this tutorial!

That’s okay for some use cases, but it’s very limiting if you want to allow any player interaction with the animated entity. For example, in part two we’ll have Gnark react when a user stands too close to him. If the walk animation included the changes in position as he walks around the scene, then the entity’s position value (stored in the Transform component) would remain unchanged the whole time. Without that value changing, we wouldn’t be able to tell how far away the user is standing from him.

How movement changes in the new SDK

Older versions of our SDK used Transition settings, that described the speed and way in which an entity moves each time that its position changes.

Version 5.0 of the SDK gets rid of that abstraction. We instead use a more traditional approach that relies on a game loop and incremental frame by frame changes. When moving an entity, we’re using a system to change the position field of the Transform component by small increments on every frame of the scene.

If you’ve worked with more traditional game engines before, such as Unity or Unreal, chances are you’ll find this new mechanic a lot more familiar.

Move using Translate

The easiest way to move an entity is to create a system that calls the translate() function on each frame.

export class GnarkWalk {
  update() {
    let increment = Vector3.Forward().scale(0.05)
    gnark.get(Transform).translate(increment)
  }
}

engine.addSystem(new GnarkWalk())

We’re calling the update function once on each frame, moving Gnark 0.05 meters forward each time. Vector3.Forward() creates a new vector with the values (0, 0, 1). We need to scale this vector down so that Gnark doesn’t move a whole meter forward 30 times per second.

If we open a scene preview now, we can actually see Gnark walking forward like a boss. Great! We’re moving in the right direction, no pun intended.

Make movement smoother

Imagine a scenario where the user of your scene is struggling to keep up with the frame rate. That could result in the movement appearing jumpy, as not all frames are evenly timed, but each frame is still moving Gnark forward by the same amount.

You can compensate for this uneven timing by using the dt (delay time) parameter to adjust the scale of the movement. This parameter is always included as part of the update() function, and its value is equal to the time it took to process the last frame. Its value is measured in seconds, so its value is always a very small number.

Let’s modify our system so that it takes dt into account.

export class GnarkWalk {
  update(dt: number) {
    let increment = Vector3.Forward().scale(dt * 1.5)
    gnark.get(Transform).translate(increment)
  }
}

Since dt tends to be a 1/30th of a second, we’re moving Gnark forward at roughly the same speed as before, but the size of each increment is affected by the value of dt in case a frame is delayed.

Use linear interpolation

There’s an elephant in the room we’ve been ignoring so far: when Gnark hits the boundaries of our scene, he just keeps walking off into the horizon, without a care in the world.

A simple way to fix this would be to add an if statement to make him stop at a given position. A much nicer way to do this, however, is to use linear interpolation.

Linear interpolation, or lerp for short, is a very popular tool in game development. It allows you to easily find an intermediate point that exists somewhere in the path between point A and point B.

The lerp() function takes three parameters:

  • The vector for the origin position
  • The vector for the target position
  • The fraction: a value from 0 to 1 that represents a position somewhere between point A and point B.

For example, if the origin position is (0, 0, 0) and the target position is (10, 0, 10):

  • A fraction of 0 returns (0, 0, 0)
  • A fraction of 0.3 returns (3, 0, 3)
  • A fraction of 1 returns (10, 0, 10)

To implement the lerp() function in our scene, we’re going to have to store data about where Gnark is coming from, where he’s going, and how far along that path he’s walked so far. We could store these values as simple variables somewhere in the scene script, and that would be fine if you don’t plan to scale the complexity of your scene any further.

However, if you want to keep the code clean and its parts reusable, I highly recommend that you handle this information through a custom component.

Tip: If the information you want to keep track of is in direct relation to a specific entity in the scene, chances are that it will pay off to store this information in a custom component.

@Component('lerpData')
export class LerpData {
  origin: Vector3 = new Vector3(5, 0, 5)
  target: Vector3 = new Vector3(5, 0, 15)
  fraction: number = 0
}

We also need to add this component to the gnark entity:

gnark.add(new LerpData())

Finally, we need to rewrite our system so that it uses a lerp to move Gnark. We want to do the following on each frame of the scene:

  1. Increment the value of fraction just a little, and in proportion to dt as we did before
  2. Execute a lerp function using fixed coordinates for the origin and target positions and the new value of fraction
  3. Set Gnark’s new position to the value that our lerp function returns
export class GnarkWalk {
  update(dt: number) {
    let transform = gnark.get(Transform)
    let lerp = gnark.get(LerpData)
    if (lerp.fraction < 1) {
      lerp.fraction += dt / 6
      transform.position = Vector3.Lerp(lerp.origin, lerp.target, lerp.fraction)
    } else {
      walkClip.pause()
    }
  }
}

Now when Gnark reaches his destination, he stops walking. We’re explicitly pausing the walkClip animation too, remember that the walking animation is a separate matter from the actual movement of the entity.

Move along a path

What we really want is to have Gnark walking continuously in circles along a fixed path, patrolling his temple. Firstly, let’s store the path we want him to walk along as a set of coordinates in an array.

const point1 = new Vector3(5, 0, 5)
const point2 = new Vector3(5, 0, 15)
const point3 = new Vector3(15, 0, 15)
const point4 = new Vector3(15, 0, 5)
const path: Vector3[] = [point1, point2, point3, point4]

We will use a lerp function just as we did before, but once he reaches the end of a lerp, we don’t want him to stop, we want him to switch his target to the next coordinate in the path array.

To implement this, we’re going to have to make some changes to that data stored on our LerpData component. First, we’ll store a reference to the path array. Secondly, instead of storing the coordinates for the origin and target, we’ll just store their indexes in the array.

@Component('lerpData')
export class LerpData {
  array: Vector3[] = path
  origin: number = 0
  target: number = 1
  fraction: number = 0
}

Finally, we’ll modify our system so that it cycles over the path array:

export class GnarkWalk {
  update(dt: number) {
    let transform = gnark.get(Transform)
    let path = gnark.get(LerpData)
    path.fraction += dt / 6
    if (path.fraction < 1) {
      transform.position = Vector3.Lerp(
        path.array[path.origin],
        path.array[path.target],
        path.fraction
      )
    } else {
      path.origin = path.target
      path.target += 1
      if (path.target >= path.array.length) {
        path.target = 0
      }
      path.fraction = 0
      transform.lookAt(path.array[path.target])
    }
  }
}

The first part of the update() function remains identical. What changes is that when Gnark is done lerping to a position, we move over to the next point in the path array and set the target and origin fields to new values. If we reach the end of the path array, we return to the beginning to start another round.

We’re also rotating Gnark using the lookAt() function of the Transform component each time. This makes him face the next position in the path before walking in that direction.

If you were just reading along and still want to jump in to experience the final scene, you can find it here].

Final thoughts

We’re done for today! Enter the preview and see how Gnark patrols the area, making sure no one comes in to desecrate his ancient temple.

On part two of this tutorial, we’ll give him the power to react when he finds an intruder! We’ll also make him turn more smoothly when he reaches each corner by playing a separate animation.

You can already look at the code for achieving these steps in the GitHub repository if you want to get a head start!

To see other examples that make use of the new SDK, 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