Let’s Program A 3D Browser Game Part 8: Dungeon Maintanence

So our little dungeon crawling engine has all the basics down now including: walls, moving, and walls that prevent you from moving. Cool. Less cool is the fact that 95% of our code exists inside of one giant function: runMaze. Not really a problem for just playing with code but it does hurt readability and could make things harder to change and experiment with as we go on.

So let’s clean things up a little by moving as many variables and functions as we can out of our mega runMaze function and into the global top level of code.

Now at this point quite a few of your are probably pointing out that having a bunch of global functions and variables isn’t much better. Real modern game code should use object oriented design and name spaces and so on.

That’s all technically true. But also true is the fact that this isn’t a serious game engine. So our only real goal here is to do the bare minimum of clean up required to make the last experiments of this Let’s Program possible. Turning that end result into a full fledged professional grade game engine will be left as an exercise to the most ambitious among you.

First off let’s move our maze creation code outside of our mega-function. Instead we’ll put it in it’s own function like so:

function createMazeGrid(){
   function MazeCell(northWall, eastWall, southWall, westWall){
      this.northWall = northWall;
      this.eastWall = eastWall;
      this.southWall = southWall;
      this.westWall = westWall;
   }

   var mazeGrid = [Array(2), Array(2)];

   mazeGrid[0][0] = new MazeCell(true, false, false, true);
   mazeGrid[0][1] = new MazeCell(true, true, true, false);
   mazeGrid[1][0] = new MazeCell(false, true, true, true);
   mazeGrid[1][1] = new MazeCell(false,false,false,false);

   return mazeGrid;
}

We can also move the code we use for setting up the wall graphics into it’s own function like so:

function placeWallGraphics(scene, mazeGrid){
   var wallGeometry = new THREE.PlaneGeometry( 1, 0.5 );
   var wallMaterial = new THREE.MeshStandardMaterial( );

   mazeGrid.forEach(function(mazeRow, rowCount){
      mazeRow.forEach(function(mazeCell, colCount){
         if(mazeCell.northWall)
           placeWall(colCount, rowCount, 'n');
         if(mazeCell.eastWall)
           placeWall(colCount, rowCount, 'e');
         if(mazeCell.southWall)
            placeWall(colCount, rowCount, 's');
         if(mazeCell.westWall)
            placeWall(colCount, rowCount, 'w');
      });
   });

   function placeWall(x,y,direction){
      var wall = new THREE.Mesh( wallGeometry, wallMaterial );
      wall.position.z = y*1;
      wall.position.x = x*1;
      if(direction == 'n'){
         wall.position.z -= 0.5;
      }
      else if(direction == 'e'){
         wall.position.x += 0.5;
         wall.rotation.y = -Math.PI/2;
      }
      else if(direction == 's'){
         wall.position.z += 0.5;
         wall.rotation.y = Math.PI;
      }
      else if(direction == 'w'){
         wall.position.x -= 0.5;
         wall.rotation.y = Math.PI/2;
      }
      else{
         return false;
      }

      scene.add(wall);
   }
}

We still have a function nested inside a function here but that’s fine for now. At least the whole thing fits on a single screen. Barely. Depending on your screen and font size.

HOWEVER! This new function has changed our work flow a little bit. When the code for adding walls to the scene was inside of runMaze it had direct access to the local scene object. Now that it’s inside its own function it doesn’t have anywhere to actually put walls.

The easy solution to this is to make scene a required argument to the function. When we call placeWallGraphics the first thing we will do is show it what scene we want to add the walls to.

After pulling out those two big chunks of code our runMaze function starts like this now:

function runMaze(){
   var mazeCanvas = document.getElementById("mazeCanvas");

   var scene = new THREE.Scene();
   var renderer = new THREE.WebGLRenderer({ canvas: mazeCanvas });
   var camera = new THREE.PerspectiveCamera( 75, mazeCanvas.width/mazeCanvas.height, 0.1, 1000 );

   //Big chunk of code replaced with two clean function calls
   var mazeGrid = createMazeGrid();
   placeWallGraphics(scene, mazeGrid);

   //Code as usual from here on out

That really helps simplify our mega function and makes it obvious what these two chunks of code doing: They help set up the game by making the maze and then building graphics for it. Also be sure to note the placeWallGraphics function call. See how we’re passing it the scene so it knows where to put all the 3D objects it creates?

Give And Take

This is actually an interesting little demonstration of the fact that breaking one big function into multiple smaller functions isn’t always “free”. On the one hand we did indeed make our individual functions easier to read, understand and maintain. On the other hand we cut them off from local variables meaning we now have to explicitly pass them variables that they used to just have direct access to.

This is a general trend in programming. Big functions are hard to understand but let you use the same variables again and again and again. Smaller functions are easier to work with but require you to do a lot more variable passing and reference juggling. This is especially obvious in 3D programming where it’s not unusual for a function to require half a dozen references to things like image buffers and translation matrices and cameras and so on.

Now on the whole the benefit you get from splitting a thousand line mega-function into a dozen sub-hundred line helpers is bigger than the small cost you incur in the form of having to pass more variables. But it’s still valuable to be consciously aware that you are making a trade off and that in rare instances keeping a chunk of code local might be easier to understand than creating a long chain of functions that call functions, passing variables all along the way.

Still A Little Cleaning To Do

Anyways, with those two big chunks of code the only mega problem left with our mega function is the fact that we defined the entire validMove function right inside of it. Nesting functions like this is not only messy and kind of weird, it limits our ability to use that function elsewhere. For example, we might have an AI that also needs to know what are and aren’t valid moves and it can’t do that if this logic is locked up inside of runMaze. Let’s pull it out in the name of readability and reuse.

There is a tiny complication. validMove depends both on our constant definitions of NORTH, EAST, SOUTH, and WEST as well as on having access to the mazeGrid. So get this to work we’ll need to

1) Pull the constants out to global scope so that both the main function and our new helpers can access them

2) Pass mazeGrid as an argument to the new stand alone validMove function

So first thing to do is grab those constants and put them somewhere convenient, like all the way up at the top of our script:

var playerInput = new Object();

const NORTH = 100;
const EAST = 101;
const WEST = 102;
const SOUTH = 103;

Then we can pull the validMove function up to the top level, making sure to add a new argument for passing the local mazeGrid object to it now that it doesn’t live in the same scope as the maze.

function validMove(mazeGrid, 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;
}

And finally we go to the one line that depends on validMove and update it with by adding the mazeGrid to the call. Basically instead of just asking “Can the player move forward?” like we used to we’re now saying “Here’s what our maze looks like. Based on that, can the player move forward?”

else if(playerInput.up && validMove(mazeGrid, player.gridX, player.gridY, player.direction)){
   walkingDistance = 0;
   startX = camera.position.x;
   //etc...

Another possible advantage to splitting things out like this is that we can now use the same function on multiple mazes. That’s pretty useless to us since we only have one maze and one player… but imagine a more complex game with multiple floors and players and maybe even wandering monsters. You might wind up needing to ask simultaneously: “Can Bob move forward on Map A?”, “Can Sally move forward on Map C?”, “Can the monster move forward on Map G?”.

To be perfectly honest I’m still not very happy with this code. A lot of our functions are still poorly organized and overly long. But at least we’ve isolated the map creation code, which was my main objective here because that will really pave the way for the last two features I want to add to this little test game: Collectibles and Random Maps.