Let’s Program A 3D Browser Game Part 5: Before You Can Learn To Walk You Must Learn To Dungeon Crawl

If we want to actually control our dungeon experience of just spinning around in automated circles the first thing we’re going to need is a way to get user input. Fortunately we already know how to do that in javascript.

For those of you who don’t feel like reading the linked article we basically create a global javascript object that stores what buttons are currently being pushed. We then create a pair of functions, one designed to keep track of keys being pushed and one to keep track of keys being released. Finally we link those two functions to the webpages natural keyDown and keyUp events.

In other words, put this somewhere convenient, like at the top of your script before the runMaze function:

var playerInput = new Object();

function doKeyDown(event){
   var keynum;

   if(window.event){ //Browser is IE
      keynum = event.keyCode;
   }
   else{
      keynum = event.which;
   }

   if(keynum == 37){
      playerInput.left = 1;
   }
   else if(keynum == 38){
      playerInput.up = 1;
   }
   else if(keynum == 39){
      playerInput.right = 1;
   }
   else if(keynum == 40){
      playerInput.down = 1;
   }
}

function doKeyUp(event){
   var keynum;
   
   if(window.event){ //Browser is IE
      keynum = event.keyCode;
   }
   else{
      keynum = event.which;
   }

   if(keynum == 37){
      playerInput.left = 0;
   }
   else if(keynum == 38){
      playerInput.up = 0;
   }
   else if(keynum == 39){
      playerInput.right = 0;
   }
   else if(keynum == 40){
      playerInput.down = 0;
   }
}

Then be sure to update your body definition like so:

<body onload="runMaze();" onkeydown="doKeyDown(event);" onkeyup="doKeyUp(event);">

And voila! We can now track the arrow keys. Let’s see if we can do anything cool with that.

For instance, what if we were to wrap our automatic camera rotation code inside of an if statement?

var render = function () {
   requestAnimationFrame( render );

   if(playerInput.left){
      camera.rotation.y += 0.01;
   }
   else if(playerInput.right){
      camera.rotation.y -= 0.01;
   }

   renderer.render(scene, camera);
};

And now we can spin the camera IN BOTH DIRECTIONS by holding down the left and right arrows.

Wrong Genre?

Of course, if you’ve played a dungeon crawler before you might notice that something here doesn’t feel quite right. Specifically, we shouldn’t be able to turn just a few degrees at a time. It’s much more traditional to lock the player to the four cardinal directions and force them to slowly rotate through a full 90 degrees on each keypress.

Ironically limiting the player’s control like this is actually going to be harder than just letting them do whatever they want. No more directly reacting to button presses on each frame. Instead we need a multi-step workflow like this:

1) Put the player into a TURNING state that blocks new input

2) While TURNING rotate the player a few degrees per frame

3) When the player has turned a full 90 degrees remove the TURNING state and wait for more input

That means we’re going to need some constants (which Javascript finally sort-of supports) to list out our possible states and a variable for keeping track of which state the player is currently in.

const WAITING = 1;
const TURNING_RIGHT = 2;
const TURNING_LEFT = 3;

var state = WAITING;

We’re also going to want to keep track of which way the camera is currently pointing as well as how far the player has already turned.

var currentDirection = 0;
var turningArc = 0;

With all that we can now setup our loop to start managing state for us by putting this in our render function.

if(state == WAITING){
   if(playerInput.left){
      state = TURNING_LEFT;
   }
   else if(playerInput.right){
      state = TURNING_RIGHT;
   }
}

And now we can use that state to slowly (over the course of 60 frames) move the player through a 90 degree (half PI radian) arc by getting rid of our old turning code and replacing it with something like this:

if(state == TURNING_LEFT){
   turningArc += Math.PI/2 / 60;
   if(turningArc >= Math.PI/2){
      turningArc = Math.PI/2;
      currentDirection = currentDirection + turningArc;
      turningArc = 0;
      state = WAITING;
   }
   
   camera.rotation.y = currentDirection + turningArc;
}

if(state == TURNING_RIGHT){
   turningArc += Math.PI/2 / 60;
   if(turningArc >= Math.PI/2){
      turningArc = Math.PI/2;
      currentDirection = currentDirection - turningArc;
      turningArc = 0;
      state = WAITING;
   }

   camera.rotation.y = currentDirection - turningArc;
}

Now when we are TURNING_LEFT (or RIGHT) we start each frame by adding one 60th of a turn to our turningArc. We then adjust the camera by setting its y rotation to the combination of our turningArc to our currentDirection. So if the player is facing north (0 radians) and has turned 30 degrees the camera will point North-North East.

We also check whether or not we’ve turned a full 90 degrees. When we have that means the turn is finished and several things happen:

1) We set our turningArc to exactly 90 degrees to avoid overshooting out goal. We don’t want the player accidentally turning 91 degrees or eventually they’ll be pointing in the completely wrong direction.

2) We update our currentDirection to reflect the completed turn.

3) We reset the turningArc so it’s ready for our next turn

4) We change the state to waiting so the game will accept input again

Really the only strange thing here is the fact that we have to ADD turning angles when turning left and SUBTRACT them when turning right, which seems backwards. This is just because of the orientation of the default y axis of our engine. If you really cared I’m sure you can swap it around by turning the game’s main camera completely upside down before moving the player or building any walls but I’m just going to live with it.

Time To Move On

We have a nice basic framework for turning around now, but it’s hard to call this a dungeon crawler while the player is stuck in just one place. The next obvious step is to add some actual movement through some code pretty similar to what we just wrote. Sadly there’s no quite enough room to include that all right here so I guess I’ll see you next time!