Let’s keep working on our 3D runner game and use event handlers to move our ship!
This article is also available on Medium.
In the last episode, we talked about spawning objects to populate our level with obstacles and bonuses, and we saw how object pooling can help optimise memory management.
Today, we are going to work on our event handlers to finally get some control over our ship and have it steer left or right. We’ll upgrade our grid shader to make it handle both the Z and the X axes. And finally, we’ll implement a very basic math lerping module to create basic animations.
This tutorial is available either in video format or in text format — see below 🙂
Moving the ship with the arrow keys
Ok so: we want to move the ship along the X axis depending on the keyboard keys that we are currently pressing – or not pressing. So, for example, if I press the left arrow key, I want to glide towards the left; if I press the right arrow key, I want to fly to the right and if I release all keys I want to go straight, just like in my initial idle state.
Step 1: Trying a basic implementation
To begin with, let’s do a very naive implementation of this logic and see what it does.
I’ll simply go to my constructor and add another speed variable for our movement along the X axis, the
this.speedX, that is initially set to zero:
This variable will take one of three values: either -1 if I’m moving to the left, 0 if I’m moving straight or 1 if I’m moving right.
To react to my “pressing key events”, I just need to go to my
_keydown() event handler (the one we defined a while ago in the second tutorial of this series), and add a little if-check.
Remember that this callback is only called when a key has been pressed. So what we want to do here is check which key was pressed: if I pressed the left arrow key, my new speed along the X axis will be -1; else, it will be 1. In the end, I can assign this new value as my current
Similarly, in the
_keyup() handler, I just have to reset the
this.speedX to 0:
Ok – now that I have a speed, I can use it to compute my current translation along the X axis. Basically, I’ll want to define another variable in my constructor, the
this.translateX variable (that is initially zero, too):
Then, in my update loop, my current speed along the X axis just increments or decrements this value. This is the basic Newtonian physics concept of speed being the derivative of the position: the speed is the rate of change of the position, so you simply need to add your current speed to your current position each render frame to “apply” this velocity to your object.
Also, because my speed is -1 or 1, and so it acts more like a direction than an actual speed value, I need to multiply this
this.speedX by a factor to avoid my ship moving too fast:
Note that my factor is negative – this is just because, as with the movement along the Z axis, I will not really be moving the ship but rather the world around it, so it has to be reversed to get the produce the right effect.
However, if I save this, you’ll see that – nothing happens!
Step 2: Getting it right!
And that’s because, at the moment, we are not actually applying this x-translation to anything!
Remember how, for the forward movement along the Z axis, we used the shader on our grid to animate it and simulate movement? Well, now, we need to do the same for the X axis as well 🙂
To fix this issue, let’s go back to our
_createGrid() method and improve our grid object – and its shader – a bit.
First, we have to create another set of buffer attributes for the vertices that are moveable along the X axis. So, let’s create a second array,
moveableX – and fill it with the vertical edges:
Now, we can pass a new property to our shader, the
this.translateX value, and use it in our vertex shader to move the vertical lines across the X axis. Overall, the code is pretty similar to the other if-statement, except that we check for the
moveableX buffer attribute, we use the
translateX property and we are computing the wrapped X position:
This time, my formula for the
xDist is just:
xDist = translateX. Contrary to the
zDist, the current time of the scene has no impact here. So we won’t have something like
translateX * time, it wouldn’t make any sense 😉
The only thing left to do is to actually update this value as our
this.translateX variables are updated. Once again, we’ll do this in the
_updateGrid() method. Similarly to our code for the Z direction, we simply update the
this.translateX property of our grid shader and the X position of the
And if we save this, we can now glide left and right and the entire scene moves consistently!
Adding animations using basic interpolation
Ok – we’ve successfully implemented a basic event handling logic that lets us control the ship along the X axis.
To make it a bit more pleasing to the eye, though, we can add a basic animation of the ship slightly rolling on its side whenever it changes direction. This will give a better feedback to the player that the action was indeed recorded and that the ship is steering.
What I’ve done for Hyperspeed is basically the following:
- if the ship is going straight and I press the left or right arrow key, then it starts gradually rotating from its current (null) rotation to a slight angle around the Z axis
- then, as long as I keep the same key pressed, if the ship has reached its maximum rotation, it stays a bit on the side but doesn’t continue rotating
- if I release the key, the ship goes back to its straight null rotation
- and if I press the other key, then the ship gradually rotates to match the other angle around the Z axis
In my original code, I used TweenJS to compute this gradual lerping between two rotations. But when revisiting the topic, I actually realised that we can do it by hand and discover how to create a very basic lerping module ourselves!
Note: if you want to check out the TweenJS library, though: it’s really cool for whenever you want to lerp between 2 values (they call it “tween” because they compute the “in-between” values). You can use for 3D with Three.js but also for 2D or just for your data. So make sure to have a look if you’re interested in that sort of things! 😉
We’ll go through this in two parts: first, we’ll create a new
Lerp class that does a generic lerping between two values; and then we’ll see how to apply it to our specific use case: the rotation of our ship.
Creating our lerping module
To begin with, let’s create a new JS file called
lerp.js and import it in our
Now, inside of this file, we’ll create a brand new class:
Designing the class
The basic idea is that our lerp objects will be instances of this class. They will each have given
to points, that correspond to the initial and target values that we want the lerp object to reach, and an associated
delay. Then, we’ll have an internal ticker that will be regularly updated in our game update loop to make this lerp object move forward in its interpolation.
This class, just like our
Game class, will have two main entry points: the constructor and the
update() method. But this update method will also take in the time delta between the current call and the previous call to be able to update the internal clock of the lerp object.
In the constructor, we’ll get our
delay parameters. We’ll also initialise the internal
time variable and the current
value to the
Computing the linearly interpolated value
Then, in the
update() method, we’ll want to do a linear interpolation between those two points. A linear interpolation can be written pretty easily in parametric form, meaning that you first define a t-value that goes from 0 to 1 and then describe your interpolated value based on this t-value.
In our case, the t-value is the ratio of the current time over the total delay of the lerping: this will be 0 at the beginning, 1 at the end of the move and various floating point values in the 0-1 range all throughout the animation.
Then, a linear interpolation is simply written like this:
If you’re not too familiar with math interpolations, think of this as pouring water from one glass to another.
At the very beginning, all your water is in the first glass –
t equals 0, so
1 - t equals 1 and 100% of the water is on the
from glass side. Then, as time goes by, our t-value starts to increase and you start pouring water to the second glass.
Notice that the total quantity of water stays exactly the same all throughout the process, you don’t add or remove any – it’s just about moving it to the second glass. At any point in the process, you can say that you have a portion of your water in the first glass, and the rest in the second glass.
So, going back to our math formula, that means that the current value of your variable is a mix of the
Finishing up the update
Of course, for this work, we have to update our internal clock with the
timeDelta variable that is passed from the game update loop for next time!
And finally, if we’ve gone over our delay, we just delete the object to stop its process:
But we have to be careful: for now, our interpolation has a fixed speed as if all delays were always 1 second. This means that, for example, if I create a lerp object with a delay of half a second, for now, it will only lerp half the way before reaching the end condition. To avoid this, we have to compute a
lerpSpeed and multiply our
timeDelta by this value in the
This will re-adjust the speed of the lerp object so that it really does go from the
from point to the
to point in the given delay.
Adding some callback hooks
The final thing we want to do is add some specific callbacks to execute at critical points of the process. Here, more specifically, we’ll want to do something every time the value updates (namely, we’ll want to update the actual ship rotation) and when the lerp is finished (we’ll want to clean up some variables).
To make it easy to hook up our callbacks, let’s define two functions: the
Those simply assign a callback passed as a parameter to the instance so that, in the
update() method, we can check for those and call them if they exist at the right time:
onupdate(), we’ll send back the updated value so that the optional callback function, if it exists, can access directly.
To wrap this up, a little trick we can benefit from is to make all of these functions return the instance itself at the end – this will enable chaining these methods and create our lerp object in just one sweep go instead of multiple lines 😉
Lerp class in our game
Ok so – time to go back to Hyperspeed! For our project, we want to tween between two rotations for our ship.
We’ll be using our
Lerp class to create a lerp object that does exactly that – that changes the ship rotation on update and is regularly updated in our game update loop.
Before we actually create a lerp object, let’s first prepare a little variable in our
Game constructor to hold any reference to such an object. We’ll call it
this.rotationLerp and initialise it to a null value:
Now, in our
update() method, we can actually extract the
timeDelta computation to a local variable, and this way if we happen to have a valid
this.rotationLerp object at that time (i.e. if it’s not null), we’ll be able to call its
update() method with our freshly computed
Ok! We’re ready to really use all of this for our ship.
So let’s create a new util function in our
Game class called
_rotateShip() that takes in a target rotation as well as the delay for the animation. This method will modify the transform of our ship to get it from its current rotation to the target rotation in the given delay.
The function will simply store a new lerp object in our
this.rotationLerp variable and initialise it with the given parameters.
onUpdate() method, we’ll want to take the updated value of the lerp object and use it for the Z rotation of the ship. For the
onFinish() method, we’ll just clean up our
this.rotationLerp variable and reset it to null:
We have to be careful, though, because at that point, our code will error with some null references. And that’s once again an issue with using
this and getting the right context. For now, this
this variable doesn’t refer to our game instance, so for example
this.ship doesn’t exist.
A common way of working around this is to “bake” the
this variable at the beginning of the
_rotateShip() function, and then pass this safe and fixed value to our inner callbacks. I’ll store it in a variable called
$this; and this way, the
$this variable does refer to our game instance and the code will run properly 🙂
All that’s left to do is call this
_rotateShip() function in our
First, let’s go back to our
_keydown() handler and change a bit the end. Rather than simply assigning the new
this.speedX value, we want to first check if the new speed is different from the current one; and only if it is, we re-assign it and we start a little rotation animation:
Our target rotation depends on the side we are steering to – as usual, those are values that I’ve tested and that I know work for my scene but you might have to adjust these depending on your game! Also, remember that our delays are given in seconds, so here I’m saying the animation will last 800 milliseconds.
We can do the same in the
_keyup() function to reset our rotation to 0; I’ll give a bit shorter of a delay to make it reset quicker:
Now, if I save this and I press the left and right arrow keys, I see that my ship gradually rotates to the side! Whenever I change the direction, it shifts to the other rotation; and if I let go of my key, it goes back to its idle rotation!
A little fix: getting the proper reference X position
Before we end this tutorial, there is a little fix that we should do in the logic we implemented last time for object pooling.
When we compute our reference X position, instead of using the
this.ship.position.x variable, we should actually look at the
this.translateX variable. And, more precisely, we need to take the opposite of this value because remember that our grid translate is the opposite of our virtual ship position:
This will make sure that the objects do spawn in front of us in the distance 😉
Now that we can actually steer this ship, we’re ready to add some difficulty to the game! In the next episode, we’ll see how to handle collisions with the objects on the terrain – both the obstacles and the bonuses…