The LAND auction has started, participate and buy LAND before it ends! Learn More
Hello! Please choose your
desired language:
Dismiss

In this article, we’ll build a scene that animates a swarm of hummingbirds that fly randomly around the scene. I chose hummingbirds not just because they’re adorable, but also out of convenience: they can fly in place and maneuver very agilely. This makes it easier to program their behavior, as you’ll see later.

My name is Nicolas Earnshaw, and I take care of the Decentraland documentation. That means everything you can find in docs.decentraland.org. If you have any feedback on our docs, please make a pull request or create an issue on the docs GitHub repo!

In my previous article on building interactive scenes, we went over some fundamentals of using the SDK, including how scene states and events are handled. In this article I want to go a little bit deeper and tackle two very important concepts that I personally found quite mysterious when I first started to use the Decentraland SDK. We’ll create a sample scene that doesn’t just look pretty but also showcases:

  1. How to animate a 3D model
  2. How to handle an undetermined amount of entities

A lot of how the SDK works is heavily inspired by React.js. If you’re already a React master, handling multiple entities will probably come easily to you. If not, don’t worry, you don’t need any familiarity with React in order to follow along with this tutorial.

This is how our scene will look when we’re done:

You can find the code and models for this tutorial in this repo. Or, you can build your own scene as you follow along. If you ever get stuck along the way or want to skip ahead to another section, you can download the code for the scene at one of its intermediate stages by clicking on the releases tab and selecting one of the releases. Each release matches up with one of the sections of this document.

Find and animate a 3D model

I found this very nice 3D model of a hummingbird that we’ll use from Poly by Google, licensed under CC BY. The model has no animations of its own, so we will have to animate it ourselves. For that, we must download it as an .obj file, and import it into Blender.

The first thing you need to do is build an armature to represent the different movable parts of the model. Keep it simple, you don’t need to represent the bird’s entire skeleton, just the parts that you intend to move.

Then you need to link the armature to the model by making the armature a child element of the bird mesh. Once matched, move the parts of the armature around to check that the mesh follows it in ways that look natural. If the model gets deformed in weird ways, then you can use weight paint to change what parts of the model are affected by each bone in the armature.

Next, you need to set the armature into a series of poses over time to create the actual animations. The transition between each pose occurs smoothly by default. If the animation will be looped in your scene, make sure the final pose is identical to the starting pose to avoid jumpiness.

To create several animations, select the Dope-Sheet view, and open the Action Editor. You can also use the Dope-Sheet view to edit the frames, like adjusting the time between two frames or making a frame store only the position of certain bones of the armature.

I created three animations with this model: fly, look, and shake. Note that fly only moves the bird’s wings whilst look and shake only move the bird’s head.

To export a model from Blender in glTF format, you need to first install an add-on. There are several out there, but we have found that some have issues with animations. In our experience, this add-on by Kupomar works best.

Add a single bird and animate it

Create a new Decentraland scene by following the steps in create your first scene. For the scene type, be sure to select Interactive, the second option.

Once the scene is created, create a new models folder inside the scene folder and add the glTF file you exported from Blender.

Now open the scene’s scene.tsx file and delete what’s in the render() function to replace it with this:

async render() {
  return (
    <scene>
      <gltf-model
        src="models/hummingbird.gltf"
        position={{ x: 5, y: 1, z: 5 }}
        skeletalAnimation={[
          { clip: "Bird_fly" , playing: true, loop: true },
          { clip: "Bird_shake" , playing: true, loop: true }
        ]}
      />
    </scene>
  )
}

The gltf-model entity we’re adding here uses two of the animations we created in Blender: Bird_fly and Bird_shake. If you’re not sure how the animations are named in the glTF file, you can easily check them by opening the file. We can run two animations at the same time because they don’t overlap: Bird_fly only contains information about wing positions and Bird_shake only contains information about head positions. If your animations do overlap, you can always use the weight property to calculate a weighted average of what each animation tells it to do.

This is a good time to check our scene to make sure our little friend is looking and behaving as we expected.

WHAT IS THAT THING, IT’S TERRIFYING! At least it works and is moving just as we expected, but it’s unnaturally huge. Let’s resize our model for Decentraland.

Change the hummingbird’s position over time

Besides shrinking the hummingbird’s size, we want it to fly randomly around the scene. To do this, we need to represent its position as a variable in the scene state, update this variable regularly, and then reference the value of this variable whenever we render the scene.

We will first create a new scene state and add our own birdPos variable.

state: IState = {
  birdPos: { x: 5, y: 1, z: 5 }
}

Also change the contents of the IState interface to reflect this.

export interface IState {
  birdPos: Vector3Component
}

Note that our code editor underlines Vector3Component in red, that’s because we must first import this class to our file.

import { Vector3Component } from 'decentraland-api'

Then we need to initiate a time-based loop in sceneDidMount() function, calling a simple function that creates a random coordinate and sets it as the new value of birdPos.

sceneDidMount() {
  setInterval(() => {
    this.newBirdPos()
  }, 4000)
}

newBirdPos() {
  const newPos = {
    x: Math.random() * 10,
    y: Math.random() * 2 + 1,
    z: Math.random() * 10
  }
}

The final step is to make the position of the bird take its value from birdPos.

Let’s also please, please make it smaller so I don’t jump out of my chair again when we preview the scene!

async render() {
  return (
    <scene>
      <gltf-model
        src="models/hummingbird.gltf"
        scale={0.2}
        position={this.state.birdPos}
        transition={{
          position:{ duration: 500, timing: "ease-in-out" }
        }}
        lookAt={this.state.birdPos}
        skeletalAnimation={[
          { clip: "Bird_fly" , playing: true, loop: true },
          { clip: "Bird_shake" , playing: true, loop: true }
        ]}
      />
    </scene>
  )
}

We added four new things to the gltf-model entity:

  • scale is now 0.2. this makes the bird smaller. Thank goodness.
  • position references birdPos instead of a fixed value. Each time this variable changes, the render() function is called again to produce a new output.
  • transition handles changes in the bird’s position. Without it, we would just see it teleport from one position to the next. We set timing to ease-in-out, which I think looks quite natural as the bird builds up speed and then slows down gradually. Note that the duration of the transition is fixed at 500 ms, this means that it will vary its speed depending how far the next random point is. I first thought of this as a temporary solution, but I liked how it looked. Let’s keep it like that.
  • lookAt makes the bird always turn to face a specific point in space, in this case its next position. When using lookAt, you don’t need to think of rotation in terms of angles, the SDK calculates them for you. Although it’s a known fact that hummingbirds are remarkably good fliers and are capable of flying backwards and sideways, it would look kind of silly if they kept their orientation fixed as they flew around.

Here’s how the scene looks now:

This works quite well. A lot less scary. And I don’t know about you, but I feel that the way it moves around the scene is quite convincing.

Switch animations randomly

I don’t like that the bird keeps shaking its head in a constant loop, it becomes too predictable and mechanic, so let’s randomize it a little to make it seem more natural and realistic. So, we’ll create a variable in the scene state that indicates which animation we want it to perform. We always want it to keep flapping its wings, of course, so that won’t change, but we created two different animations for its head, so let’s use them. The variable will have three possible values:

  • Null will keep its head still
  • Looking will activate the Bird_look animation
  • Shaking will activate the Bird_shake animation

We can create a custom type for this variable that only accepts these values.

export type BirdAction = null | 'looking' | 'shaking'

We can then assign this type to a variable when defining it in the scene state, just like you assign string or number in other cases.

export interface IState {
  birdPos: Vector3Component,
  birdAction: BirdAction,
}
// (...)
state: IState = {
  birdPos: { x: 5, y: 1, z: 5 },
  birdState: null
}

We want to change the value of this new variable regularly. Let’s add it to the same loop that changes the bird’s location.

sceneDidMount() {
  setInterval(() => {
    this.newBirdPos()
    this.newBirdAction()
  }, 4000)
}

newBirdAction() {
  let newAction: BirdAction
  const ranNum = Math.random()

  if (ranNum < 0.6) {
    newAction = null
  } else if (ranNum < 0.8) {
    newAction = 'looking'
  } else {
    newAction = 'shaking'
  }

  this.setState({ birdAction: newAction })
}

To change how our little friend behaves, the last thing we need to do is reference birdAction when we render the model.

async render() {
  return (
    <scene >
      <gltf-model
        src="models/hummingbird.gltf"
        scale={0.2}
        position={this.state.birdPos}
        transition={{
         position: { duration: 500, timing: "ease-in-out" }
        }}
        lookAt={this.state.birdPos}
        skeletalAnimation={[
          {
            clip: "Bird_fly",
            playing: true,
            loop: true
          },
          {
            clip: "Bird_look",
            playing: this.state.birdAction == 'looking'
          },
          {
            clip: "Bird_shake",
            playing: this.state.birdAction == 'shaking'
          }
        ]}
      />
    </scene>
  )
}

{% raw %}

When the statement this.state.birdAction == 'looking' evaluates to true, then the playing property of Bird_look is also true. The same applies for Bird_shake.

If we try our scene now, you’ll note that the bird sometimes keeps its head still, sometimes it looks around, and sometimes it shakes in what looks like a cute little sneeze.

Supporting multiple hummingbirds

This is where we get to the fun part.

We have variables that represent the current state of a single bird, but what if we want the same variables to represent the state of multiple birds? We use arrays, that’s what we do!

{% raw %}

export interface IState {
  birdPositions: Vector3Component[],
  birdActions: BirdAction[]
}
// (...)
state: IState = {
  birdPositions: [
    { x: 5, y: 1, z: 5 },
    { x: 5, y: 1, z: 5 },
    { x: 5, y: 1, z: 5 }
  ],
  birdActions: [ null, null, null ],
}

We’re adding three sets of values to our arrays for now, and each will produce a separate bird in our scene. Although they all start at the same location, they will soon move apart from each other randomly.

To keep the render() function clean, we’ll move the rendering of the hummingbird entities away into its own separate function and call it from render like this.

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

The new renderBirds() function is very similar to what render used to do, but it goes over the birdPositions array using a map() operation, creating a new entity for each element in the array. As we iterate through the array, the value stored in the current array position can be referenced through pos and the current index number through birdNum.

renderBirds() {
  return this.state.birdPositions.map( (pos, birdNum) =>
    <gltf-model
      src="models/hummingbird.gltf"
      scale={0.2}
      key={birdNum.toString()}
      position={this.state.birdPositions[birdNum]}
      lookAt={this.state.birdPositions[birdNum]}
      transition={{
        position: { duration: 500, timing: "ease-in-out" },
      }}
      skeletalAnimation={[
        {
          clip: "Bird_fly",
          playing: true,
          loop: true
        },
        {
          clip: "Bird_look",
          playing: this.state.birdActions[birdNum] == 'looking'
        },
        {
          clip: "Bird_shake",
          playing: this.state.birdActions[birdNum] == 'shaking'
        }
      ]}
    />
  )
}

Notice that now, when we reference variables in the scene state, we refer to a specific indexes in the arrays. We also included a key on each bird entity, this helps the engine keep track of which bird moved where. Without this key, the engine would have to guess how the new list of rendered entities corresponds to the one from the last render. That might result in some transitions occurring between positions that were meant to belong to separate birds.

We should always change the scene state through the setState() function. So to update a single bird we need to set the values for the entire array, even the parts that haven’t changed. Here are our newBirdPos() and newBirdAction() functions, altered to handle arrays:

newBirdAction(bird: number) {
  let newActions: BirdAction[] = this.state.birdActions.slice()
  const ranNum = Math.random()

  if (ranNum < 0.6) {
    newActions[bird] = null
  } else if (ranNum < 0.8) {
    newActions[bird] = 'looking'
  } else {
    newActions[bird] = 'shaking'
  }

  this.setState({ birdActions: newActions })
}

newBirdPos(bird: number) {
  let newPositions = this.state.birdPositions.slice()
  newPositions[bird] = {
    x: Math.random() * 10,
    y: Math.random() * 2 + 1,
    z: Math.random() * 10
  }
  this.setState({ birdPositions: newPositions })
}

Note that in both functions we’re making a temporary copy of an array from the scene state, then we change one of the elements in this copied array and finally we pass the whole array as a new value for the variable. When copying the arrays, we use the .slice() method to ensure that we’re copying the array’s values into a new array, rather than just creating a pointer.

Note that both functions now accept a parameter called bird, this determines which of the birds in the arrays to work on. We now need to include a value for this parameter each time we call these functions. Actually, let’s go nuts and call both functions a bunch of times manually, just to see how it looks if we have a handful of birds at the same time.

sceneDidMount() {
  setInterval(() => {
    this.newBirdPos(0)
    this.newBirdAction(0)
    this.newBirdPos(1)
    this.newBirdAction(1)
    this.newBirdPos(2)
    this.newBirdAction(2)
    this.newBirdPos(3)
   this.newBirdAction(3)
  }, 4000)
}

Now it’s time to see if all our changes worked by previewing our scene:

Looking good! The biggest problem we have now is that all birds shift positions at the same time. It looks too choreographed, like straight out of a Pixar musical number. We will later be initiating them at different times, which will make it look a lot more natural.

Handling a varying number of birds

Now let’s make things a lot more dynamic!

We want a new bird to appear in our scene each time you click on a tree. Each time we create a new bird, we want to add a new element to the birdPositions and birdActions arrays and start a new time-based loop that updates these values periodically.

The birds start at what seems like a pretty arbitrary set of coordinates, but that’s where we’ll put our tree.

async createBird() {
  this.setState({
    birdPositions: [
      ...this.state.birdPositions,
      { x: 4, y: 2, z: 8 }
    ]
  })
  this.setState({
    birdActions: [
      ...this.state.birdActions,
      null
    ]
  })
  const bird = this.state.birdPositions.length - 1
  setInterval(() => {
    this.newBirdPos(bird)
    this.newBirdAction(bird)
  }, 3000 + Math.random() * 2000)
}

We’re setting the new values of the state variables by passing the entire array to setState, including the values that already existed and didn’t change. We’re then creating a new loop that will keep calling the newBirdPos and newBirdAction functions for the new bird.

We’re setting the looping interval with a random number to keep the movements less predictable. Since each bird is created from a user click, we don’t have to worry about the birds moving in unison as we saw before. However, if they all looped over the same interval, a predictable repeating pattern would emerge.

There’ll be no birds in the scene when users first enter it. For this we need to get rid of the initial values that we had manually added to the arrays in the scene state, both should now start empty.

state: IState = {
  birdPositions: [],
  birdActions: []
}

Let’s now get rid of what we put in sceneDidMount() and listen for clicks from the user. When the tree entity is clicked, we will call the createBird() function.

sceneDidMount() {
  this.eventSubscriber.on('tree_click', () => {
    const bird = this.state.birdPositions.length
    if (bird > 10) return
    this.createBird()
    console.log("new bird")
  })
}

Each time the user clicks the tree, we’re checking the current length of the birdPositions array to make sure we don’t have too many birds on our scene. In theory, we could keep adding birds forever, but we need to avoid hitting the triangle limit of our scene.

To stand in for a tree, we will just add a box entity and set its id to tree. We’ll have to pretend that looks like a tree for now.

async render() {
  return (
    <scene >
      {this.renderBirds()}
      <box
        id="tree"
        position={{ x: 4, y: 2, z: 8 }}
      />
    </scene>
  )
}

Adding scenery and animating the tree

On this project, I was lucky to get some help from Shibu, our art director here at Decentraland. He built this lovely little landscape and a proper tree to go with it.

We’ll copy both the tree and the rest of the background as gltf models in the /models folder of our scene and add two new gltf-model entities to the render() function to use the ground and the tree.

We also added an animation to the tree model so that it shakes a little when clicked. This makes it a bit more obvious that something is happening and that the bird is appearing because of the user’s action. We added an armature to the tree to do this, just like the hummingbird. Trees with bones… weird, but it works.

To activate this animation we must add a new boolean variable to the scene state, we’ll call it treeShake. Each time the user clicks the tree, we want to set this variable to true and then back to false an instant later. We’ll create a new function named shakeTree() and call it each time the user clicks the tree, right before calling createBird().

async shakeTree() {
  this.setState({ treeShake: true })
  setTimeout(() => {
    this.setState({ treeShake: false })
  }, 2000)
}

Here we’re using a setTimeout() function to delay the action of setting treeShake back to false by 2000 milliseconds.

Coding tip of the day: never set a scene state variable directly

In this example you’ve seen that we always made changes to our scene state through setState(), even when passing a whole array. While it’s possible to set a variable or one of its array elements directly, and you might get away with it, try to avoid this at all costs.

In our scene, there were a number of times where it would have been tempting to just write this.state.birdPositions[bird] = newPos. The trouble is that this could potentially mess up how the scene object keeps track of changes, especially when there are multiple threads running in parallel that each have an effect on the scene state.

Using the setState() function to change variables also has the added bonus that it calls the render() function automatically to display the scene in its new state. Remember that unlike most game engines out there, scenes in Decentraland aren’t rendered in a continuous loop. They are only rendered once something in scene is known to have changed.

Final thoughts

As we’ve learned today, you don’t need to explicitly define every single entity in your scene as a literal XML tag in the render function. You can build entities from an array if you use them cleverly.

Play around with your array lengths and values to completely transform your scene over time. This idea is essential for building games, where you often have to handle similar arrays for enemies, obstacles, collectible items, features of a dynamic scenery, etc. You’ll also find that many other interactive experiences in addition to games can benefit from this kind of mechanic. Feel free to play with any of the code we’ve included in the example in GitHub!

Thanks for following along, and stay tuned for our next Developer Tutorial!

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