Last time we improved our engine to let us make nice clean 90 degree turns, a definite requirement of any proper dungeon crawler. The next logical step is to let us also move forward in whatever direction we happen to be facing so that we can actually explore our 3D maze instead of just admiring it from one spot.
The logic for moving forward is very similar for the logic of making a 90 degree turn.
- Wait for the user to press the up key
- Enter a “moving” state that blocks new player input
- For every frame spent moving move the player a small distance in the direction they are facing
- Once the user has moved an entire dungeon square distance reset their state and wait for the next key press
The only tricky bit here is keeping track of which direction the player needs to move in. With turning we could always just add (or subtract) 90 degrees from our current position and trust things would work out fine. But with movement sometimes we will need to add to the player’s x position and sometimes to their z position.
Obviously we need some way to keep track of which axis the player should move along when they press the move key. While we could technically calculate this information based off of the player’s current degree of rotation I think it makes for easier and cleaner code to just explicitly keep track of whether the player is facing north, east, south or west. And of course that means we’re going to need new constants and a new player variable.
const NORTH = 100; const EAST = 101; const WEST = 102; const SOUTH = 103; var direction = NORTH;
And then to keep track of our actual movement we’re going to need a few more:
const MOVING_FORWARD = 4; var walkDistance = 0; var startX = 0; var startZ = 0;
Now let’s put those to work, first by updating our turning code to also properly update the direction the player is facing. Because the player is locked into a full turn as soon as they press the key we might as well do things the easy way and update their direction as soon as they press left or right; no need to wait until the end of the turn. So find the if statements inside of the waiting state and add to them like so:
if(playerInput.left){ state = TURNING_LEFT; switch(direction){ case NORTH: direction = WEST; break; case EAST: direction = NORTH; break; case SOUTH: direction = EAST; break; case WEST: direction = SOUTH; break; } } else if(playerInput.right){ state = TURNING_RIGHT; switch(direction){ case NORTH: direction = EAST; break; case EAST: direction = SOUTH; break; case SOUTH: direction = WEST; break; case WEST: direction = NORTH; break; } }
Now that we know what direction the player is facing we can safely move them forward. To start let’s add some detection code for the up key inside of our waiting state block. This else if should go immediately after the playerInput.right else if we just updated:
else if(playerInput.up){ walkingDistance = 0; startX = camera.position.x; startZ = camera.position.z; state = MOVING_FORWARD; }
Pretty simple. If the player presses the up button we put them in the MOVING_FORWARD state and initialize the data we need to make the move succeed. In particular we need to mark down where the player is currently standing in startX and startZ. We also reset the walkingDistance variable to 0 since we’re going to need to use that data to figure out when we should stop moving.
Of course, being in the MOVING_FORWARD state doesn’t do us any good unless there is code for it. So make some space at the end of your render function, right before the recursive call to render, and add this:
if(state == MOVING_FORWARD) { walkingDistance += 1 /30; if(walkingDistance >= 1){ walkingDistance = 1; state = WAITING; } switch(direction){ case NORTH: camera.position.z = startZ - walkingDistance; break; case EAST: camera.position.x = startX + walkingDistance; break; case SOUTH: camera.position.z = startZ + walkingDistance; break; case WEST: camera.position.x = startX - walkingDistance; break; } }
This logic should look familiar since it’s just our 90 degree turn code straightened into a line.
Basically for every frame we spend MOVING_FORWARD we extend our walking distance by a fraction of a square length (which for us is 1). We then look at our current direction to figure out which way we need to move and update the camera to a new position based off our starting location and the distance we have moved so far. As walkingDistance increases the camera gets moved farther and farther from where it started.
When walking distance exceeds 1 whole unit we force it back to a perfect 1 and reset our state to waiting for input. By not including a return statement we also get to have one final round of camera updates based off of the perfect 1 which should properly place the camera at the desired end point, ready for any future turns or moves. This is important because missing our target by even a tiny fraction can add up over the course of several hundred moves and leave the camera in the completely wrong place.
If everything went well you can now load your code and walk around our tiny three square maze. Of course, the lack of any sort of collision detection also means you can ghost through the walls and walk right out of the maze but solving that is a problem for another day.
Still, we have a little bit of time left over so let’s make a quick improvement to the code.
At the moment our turn and move code both move the player a set amount per frame. This is an OK approach, especially since the requestAnimationFrame function we’re using does its best to maintain a constant 60 or so frames per second.
But that frame rate is not guaranteed! If your game is really complicated or the user has too many tabs open that frame rate can start to drop and a quick turn that was supposed to take a single second can drag on and on and on.
Alternatively changes in computer technology might lead to a much higher frame rate and suddenly that carefully planned one second turn flashes by in half that time.
Even pro games have issues like this. For example, Dark Souls 2 originally ran at 30 frames per second and calculated weapon durability based on that fact. When it got ported to a new generation of faster machines they boosted the frame rate to 60 and suddenly all the weapons were breaking half twice as fast. Woops.
The solution to these problems is to, whenever possible, base game calculations not off of frames but off of actual time passed. That way you can program game events to take exactly one second regardless of whether that second lasts for thirty frames or three frames or three hundred frames.
First off we’re going to need a variable to keep track of what time it is. Let’s toss it up by the rest of our contsants and global variables:
var last_update = Date.now();
It’s default value is whatever time it is when the program gets initialized.
Now near the top of our render function we can add:
var now = Date.now(); var deltaTime = now - last_update; last_update = now;
These three lines get the current time and compare it to our last stored time in order to figure out how long our last frame lasted. We then reset our update time so we can start counting our next frame.
Now if we want to turn once per second we can:
turningArc += Math.PI/2 * deltaTime/1000;
And if we want to walk forward one square in one second:
walkingDistance += 1 * deltaTime/1000;
We are now frame-rate independent!