Let’s Program A 3D Browser Game Part 7: You Shall Not Pass

Looks Nice But Doesn’t Do Much

If we’re being perfectly honest at this point we haven’t programmed a dungeon crawler so much as a dungeon viewer. We can walk around and look at our walls but there’s no actual game logic preventing us from walking right off the border of the map and wandering off towards infinity (which is a lot more boring than it sounds).

So let’s start fixing things by making walls a bit more solid.

Traditionally video game walls act like walls thanks to collision detection. Basically once every frame you check whether the player’s geometry is mathematically overlapping with any wall geometry and then move/stop/kill the player as appropriate.

But for us that would be too overkill.

Because our maze is a grid we don’t have to worry about actual collisions. Instead we can just look up which square the player is standing in and see if it has a wall in the direction the player wants to move. If there is no wall we run the move logic like normal. If there is a wall no movement happens (although we could shake the screen a bit to give the player some feedback on their key press if we were feeling fancy). This is nice because it’s a much easier problem to solve than full object collision and is also a lot less demanding on the processor.

Where Am I?

At the moment we keep track of the player’s location entirely in terms of where the camera is. Unfortunately the camera exists in 3D graphic land and doesn’t help us a lot when it comes to figuring out where we are in 2D map land. That means we’re going to need to expand our code to also keep track of the player’s location in terms of his X and Y location in the grid.

Warning: Keeping track of these two bits of data separately introduces the risk of the player’s grid location getting out of sync with his camera location leading to all sorts of weirdness. Preventing this from happening will be one of our primary goals today.

Anyways, to help keep track of new variables let’s create a player object to hold player related game data.

var player = {};
player.gridX = 0;
player.gridY = 0;
player.direction = NORTH;

By default the player is in grid square [0, 0] and facing NORTH. This matches where our camera starts. If you ever decide to change the starting location or direction make sure to update it in both locations. In the future we could avoid this by setting the camera’s start position based on the player’s logical start location (or the other way around) but for our current experiment we can just personally promise ourselves to make sure they always match up.

Next step in making sure the player and the camera match up is to make sure they always point in the same direction. Fortunately we already have code that determines what direction the camera should point after each turn so all we have to do is update that code to also keep the player up to date.

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;
   }
   player.direction = direction; //Sync player and camera
}

See that new line near the end where we update the player’s direction? Make that same change in the playerInput.right block.

Next up is making sure the player’s grid location properly updates on forward movement.

else if(playerInput.up){
   walkingDistance = 0;
   startX = camera.position.x;
   startZ = camera.position.z;
   state = MOVING_FORWARD;
   switch(direction){
      case NORTH:
         player.gridX--;
         break;
      case EAST:
         player.gridY++;
         break;
      case SOUTH:
         player.gridX++;
         break;
      case WEST:
         player.gridY--;
         break;
   }
 }

The only tricky part here is making sure you match X and Y up properly with NORTH-SOUTH and EAST-WEST. Which pairing is right depends on how we define the axis of our map array. If having X represent NORTH and SOUTH feels weird to you then try playing with the code that defines how the original maze was constructed.

Now that we feel confident our player’s position on the map matches up with the camera’s position in the 3D maze we can pretty easily check for valid vs invalid moves. We’re going to write a helper function to figure this out, so don’t just dump it in the render function like everything else. Instead maybe include it after all the wall placing functions in the first half of the script.

function validMove(x, y, direction){
   if(direction == NORTH){
      return !mazeGrid[x][y].northWall;
   }
   else if(direction == EAST){
      return !mazeGrid[x][y].eastWall;
   }
   else if(direction == SOUTH){
      return !mazeGrid[x][y].southWall;
   }
   else if(direction == WEST){
      return !mazeGrid[x][y].westWall;
   }
   return false;
}

Super simple, right? We pick which cell in the maze we want to look at and what direciton we want to move. We then see if there is NOT a wall there. No wall means we can move. Yes wall means we can’t.

With that handy function all set up we can now finally prevent the player from ghosting through walls. It’s as simple as updating our if(playerInput.up) code to also keep track of whether or not the player can move forward.

else if(playerInput.up && validMove(player.gridX, player.gridY, player.direction))

And that’s it! Load up your code now and you’ll be properly stuck in our little 3 cell “maze”. Exciting, if a little claustrophobic.

That’s really it as far as core dungeon crawling code goes. We move in nice predictable compass directional steps and we can tell when we hit a wall. Everything else is just icing.

But before you go off and start modding this code with traps and doors and keys and whatnot let’s spend a little time cleaning up our experimental code to make it a little more professional, understandable and extendable.