Sign up for our upcoming Game Jam! Over $250,000 USD in prizes in MANA and LAND.
X
Hello! Please choose your
desired language:
Dismiss

Welcome back! In part 1 of this two-part series, we created a scene where a character named Gnark patrols his ancient temple, walking in circles along a fixed path. Today, we’re going to make this scene more interesting in two ways. First, we’ll get him to turn the corners more smoothly by playing a turning animation. Second, we’ll make him react when a user stands just a little too close for comfort.

If you followed along in part 1, you can pick up right where you left off. You can also download the assets used in this sceene here. Otherwise, you can see the code for the final version of the scene in the repo and read along without having to run any of the code yourself.

Note: The code in this tutorial has been migrated to version 5.1 of the SDK.

Add extra animations

As we mentioned in part 1, our gnark.gltf file comes embedded with a couple of animations. In part 1 we only used the walk animation, but in this installment we’re also going to take advantage of the animations that are saved in the model as turnRight and raiseDead. Before we can do that, we need to create AnimationState objects to handle each.

const turnRClip = new AnimationState('turnRight')
turnRClip.looping = false
gnarkAnimator.addClip(turnRClip)
const raiseDeadClip = new AnimationState('raiseDead')
gnarkAnimator.addClip(raiseDeadClip)

When creating an animationState object, we can configure the object’s properties, which include looping, speed, and weight.

All clip objects loop their animations by default, but we’re configuring the clip object for the turnRight animation so that it only plays the animation once when activated.

Create a Timer Component

We want Gnark to stop moving forward for as long as the turnRight animation is playing. We’ll create a new component to keep track of the passage of time while this happens.

To make this component more generic and easily reusable on other projects, we won’t hardcode a fixed amount of time. Instead, each time that the component is created, we’ll be able to set the value of the timeLeft field. To do this, we give the component a constructor() function that accepts a numerical argument.

@Component("timeOut")
export class TimeOut {
    timeLeft: number
    constructor( time: number){
         this.timeLeft = time
    }
}

We’ll use this component a little differently from how we’ve used components before. Our gnark entity won’t start the scene with an instance of this component that stays there all along. Instead, we’ll only create a new instance when it’s needed (each time that Gnark turns a corner) and we’ll remove the component when we’re done with it.

We’ll also create a component group that lists every entity with a TimeOut component. The gnark entity will be jumping in and out of this group as instances of the TimeOut component are added and removed.

export const paused = engine.getComponentGroup(TimeOut)

Create a Timer System

We’ll now add a specialized system to our scene to handle the TimeOut component.

export class WaitSystem {
  update(dt: number) {
    for (let ent of paused.entities){
      let time = ent.getComponentOrNull(TimeOut)
      if (time){
        if (time.timeLeft > 0) {
          time.timeLeft -= dt
        } else {
          ent.removeComponent(TimeOut)
        }
      }
    }
  }
}

engine.addSystem(new WaitSystem())

Our system is simple and well-contained; it just subtracts the time that has gone by since the last frame (dt) from the TimeOut component. If the time runs out, it removes the component.

The system uses the component group we created to iterate over every entity that has a TimeOut component. We don’t really need to iterate over a list like this, since we know that the gnark entity is the only one that’s relevant to this system in our scene. Nevertheless, it’s nice to write code that can later be easily reused in other projects.

About handling multiple systems

We could simply add the code of our timer to the GnarkWalk system that our scene already had, and that would be ok. However, as projects grow in size it’s often useful to keep independent behaviors in separate systems that are clearly delineated. If you design your scene well, you can make complex behaviors emerge from simple decoupled systems that act upon the same entities independently.

An example use case for this kind of separate behavior would be building an AI system that decides where an entity will walk to next, and another gravity system that pulls all entities down. If the AI system makes an entity walk off a cliff, the gravity system will do its thing without the AI system even noticing. Likewise, the gravity system doesn’t need to know the direction in which the character was moving or where it was going before it fell, it just pulls things down.

Modify the walk system

We now need to modify the GnarkWalk system so that it incorporates the rotation animation and the TimeOut component we just created. We took the code from part 1 of this tutorial and made a few changes so that the system now looks like this:

export class GnarkWalk {
  update(dt: number) {
    if (!gnark.hasComponent(TimeOut)){
      let transform = gnark.getComponent(Transform)
      let path = gnark.getComponent(LerpData)
      walkClip.playing = true
      if (path.fraction < 1) {
        path.fraction += dt/12
        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])
        walkClip.pause()
        turnRClip.play()
        gnark.addComponent(new TimeOut(TURN_TIME))
      }
    }
  }
}

You’ll notice that most of this code is no different from what we wrote in part 1. Refer to part 1 if you need to refresh your memory, but here’s what we just changed:

  • The entire update() function is enclosed in a condition that checks if the gnark entity has a TimeOut component added to it. If the component exists, the system does nothing.
  • We set walk.playing to true. This is used right after a TimeOut component expires and Gnark needs to start walking again. It will have no effect on the frames where the animation was already playing.
  • When Gnark’s done lerping over a segment of the path, we pause the walk animation and play the turnRight animation. Remember that the turnRight animation is non-looping, so it’s only played once. We also add a TimeOut component to the gnark entity. When this component is added, Gnark stops moving until the WaitSystem removes it.

Note: We’re keeping the line that turns Gnark to face his next target position using LookAt. This is executed in the same frame where we start to play the turnRight animation. This is because the turnRight animation happens to start with the model looking away and it then turns to face the front. Other models are likely to have animations that start facing forward and then turn to look away, in those cases you would need to change the rotation of the model at the end of the animation instead.

Try out the scene preview now! You should see Gnark performing the turnRight animation on every corner of his path!

Get user position

If we want to get the distance between Gnark and the user, we first need to know where the user is. For that, we’ll instance a camera object.

const camera = Camera.instance

Once this object is created, we can get the user’s current position simply by calling camera.position.

Calculate distances

As we all learned in school (while wondering if this knowledge would ever be useful), you get the distance between two points via a simple trigonometric formula: √a² + b²

This trigonometric formula is meant for 2D spaces, but it works well for our scene because we can assume that both points are at ground level.

We can save some processing power by not calculating the square root. This optimization is a fairly standard practice in game development. Finding a square root is a demanding computation and we can easily spare it. For example, instead of checking that the result of the whole formula is less than 4, we can simply check that the result of a² + b² is less than 16.

function distance(pos1: Vector3, pos2: Vector3): number {
 const a = pos1.x - pos2.x
 const b = pos1.z - pos2.z
 return a * a + b * b
}

Reacting to proximity

Let’s add yet another specialized system to our scene so that Gnark is able to respond when the user gets too close to him.

export class BattleCry {
  update() {
    let transform = gnark.getComponent(Transform)
    let path = gnark.getComponent(LerpData)
    let dist = distance(transform.position, camera.position)
    if ( dist < 16) {
      raiseDeadClip.playing = true
      walkClip.playing = false
      turnRClip.playing = false
      transform.lookAt(camera.position)
    }
    else if (raiseDeadClip.playing){
      raiseDeadClip.pause()
      transform.lookAt(path.array[path.target])
    }
  }
}

engine.addSystem(new BattleCry())

In this system, we’re using the camera object to get the user’s position, then we’re using the distance() function we created to find the distance between Gnark and the user.

If this distance is less than 4 meters (remember our distance formula doesn’t calculate the square root, so we use 16 that equals to 4 squared), Gnark plays the raiseDead animation. As he does this, he also pauses all other animations, and turns to face the user.

If the distance becomes greater than 4 meters and he’s still playing the raiseDead animation, he stops the animation and turns back to the next target in his path.

There’s one last thing we need to do. We need to go back to the GnarkWalk system and add another condition at the start of the update() function. We need to make sure that the raiseDead animation isn’t currently playing before moving Gnark forward. By adding this, he’ll remain still while the animation is playing, and resume walking when it stops.

if (!gnark.hasComponent(TimeOut) && !raiseDeadClip.playing ){
(...)

Final thoughts

We’re done! Let’s run the preview one more time. This time, be brave and walk right up to Gnark to see how he reacts to your proximity!

If you were just reading along and still want to jump in to experience the final scene, you can find it here. To see other examples that make use of the new SDK, check out the Example Scenes page in the documentation!

Explore the metaverse in style
Be one of the first to get your own avatar, and gain early access to Decentraland.
Learn More