It’s time to finish this game up by adding a “win” condition. Fortunately we already have all the pieces we need to pull this off: collectables that run customizable bits of code when collected.
The plan here is simple: After randomly generating the maze we will also randomly place several collectables. When collected the collectables will run a piece of code that checks how many the player has picked up. When the player gathers the last collectable the game will display a “You Win!” message.
One Week Too Late For An Easter Bunny Joke
Let’s ignore the collectable counting logic for now and just focus on randomly placing collectables throughout the maze. At first glance this seems like an easy task: Pick a random X and Y coordinate, put a collectable there, repeat as necessary.
The trick is that we don’t want to ever accidentally put two objects on the same space. Not only would this be bad game design, it would probably crash our code because of a slight mistake we made in the way we remove collected objects from our global collectables list? (Extra Credit: Find the bug and fix it!).
Not really much of a trick though. All it means is that our object placing code needs to go inside of a loop that keeps choosing random spaces until it finds one that is empty.
function createCollectiblesList(number, grid){
var collectables = [];
var width = grid[0].length;
var height = grid.length;
for(var i=0; i < number; i++){
var x;
var y;
var unique = false; //Set loop flag up so the loop runs at least once
while(!unique){
x = Math.floor(Math.random()*width);
y = Math.floor(Math.random()*height);
unique = true; //Assume the random coordinate is empty
collectables.forEach(function(collectable){
if(collectable.x == x && collectable.y == y){
unique = false; //Oops! Square already has a collecatble. Reset flag so we loop again
}
});
}
//If we've gotten here the loop must have found an empty square and exited
collectables.push({
x:x,
y:y,
action:function(){alert("You picked up a collectable");}
});
}
return collectables;
}
How I learned To Stop Worrying And Love Semi-Nondeterministic Code
I’ll be honest: Every time I write a loop that depends on finding a unique random value I find myself worrying “But what if the computer, in a huge coincidence, keeps generating the same random numbers again and again for hours on end and so it never breaks out of the loop.”
This is silly. The odds of a computer generating the same set of random coordinates trillions of times in a row is so phenomenally low that your user is more likely to be killed by random meteorite strike than they are to get stuck for even a single minute while waiting for your code to finish filling the map with collectables.
In fact, just for fun, I made a 20 x 20 maze and filled it with 400 collectables. That means our random algorithm should constantly be bumping into it’s own filled slots and having to guess again. The last collectable in particular should take a long time to place since there is only a 1 in 400 chance of the computer randomly finding the last remaining open space. This seems like the perfect chance for slow code!
The maze still generated itself almost immediately.
It’s easy to forget just how crazy fast computers have become. Sure, industrial code that deals with millions or billions of records still has to be carefully designed with speed in mind but if you’re only juggling a few thousand pieces of data around you can pretty much do whatever you want with no worries. Odds are good it will run plenty fast and if it doesn’t you can always fix it later. It’s a waste of valuable developer time to overly obsess up front.
Weren’t We Programming Something?
Let’s get back to our game. With that edit made why not update your maze building code with something like var collectables = createCollectiblesList(5, mazeGrid); and see what happens?
Just make sure the total number of collectables you place is smaller than the maze. Trying to randomly find unique spaces for 30 collectables in a maze with only 25 spaces WILL lock you into an infinite loop. All things considered we probably should update createCollectiblesList to throw an error when given impossible input or maybe automatically adjust the total collectables to a min or zero and a max of 1 per grid space.
Who’s Keeping Score?
Our final task is simultaneously one of the easiest but most difficult parts of this entire Let’s Program.
In order to let the player “Win” all we need to do is:
1) Keep track of how many collectables there are in total
2) Create a “collectables collected” variable that starts at zero
3) Update the custom code inside each collectable to update the “collected” variable by one
4) When the “collected” variable equals the “total” variable we display a message.
Super simple, right? All we need is a ++ and a == and we’re basically done.
But where should all the variables live?
Probably the best choice would be to make them global. That way not only could the collectables reference them but we could also use them to print the players progress onto the screen (collectables: 2/5).
But since this is a semi-educational blog let’s take avoid the obvious solution and instead take this opportunity to learn about a nifty but somewhat obscure programming technique: closures.
What is a closure? Well, the technical explanation can get awfully confusing but the core idea is pretty simple. A closure is a trick that takes a temporary variable and makes it permanent by placing it, or “enclosing” it, inside of another piece of longer lived code. As long as that wrapper code is still alive the temporary variable will stick around even if it’s way past it’s expiration date. Admittedly this only works in languages with automatic memory management but that’s pretty much everything except C and C++ these days.
This means we could create totalCollectibles and collectablesCollected variables directly inside of our createCollectiblesList function, insert them into the action code that runs when a collectable is collected and then rest assured that those two variables would hang around until the end of our game even though normal Javascript logic suggests they should have been deleted as soon as createcollectiblesList finished running.
function createCollectiblesList(number, grid){
var collectables = [];
var width = grid[0].length;
var height = grid.length;
var totalCollectibles = number;
var collectablesCollected = 0;
for(var i=0; i < number; i++){
var x;
var y;
var unique = false;
while(!unique){
x = Math.floor(Math.random()*width);
y = Math.floor(Math.random()*height);
unique = true;
collectables.forEach(function(collectable){
if(collectable.x == x && collectable.y == y){
unique = false;
}
});
}
collectables.push({
x:x,
y:y,
action:function(){
collectablesCollected++;
alert("You have picked up "+collectablesCollected+" out of "+totalCollectibles+" collectables");
if(collectablesCollected == totalCollectibles){
alert("Congratulations! You won the game! Refresh the page to play again.");
}
}
});
}
return collectables;
}
Give it a shot. Picking up collectables should now properly reflect your running total as well as the actualy total of collectables. And we didn’t need to use a global variable or object oriented anything to do it.
Like I said earlier, this probably wasn’t the best way to solve this problem. Turning our collectable account into a closure variable makes it invisible to everything except the function inside each collectable objects. So if it ever turns out we need that data for something else we’d be plain out of luck and have to rewrite the closure to use something more traditional like global data or object references.
But closures are cool and, used properly, can achieve a lot of the same results as Object Oriented Programming. Just without the objects, which might be exactly what you need. And even if you personally never need closures it’s still useful to recognize how they work in case you have to maintain code from somebody else who used them.
What Next?
I’m happy with where we’ve ended up here. We have some basic 3D graphics, a nice grid based movement system and dungeon that randomly rebuilds itself every time the game is run. I still need to clean the code up a little and post the finished product for everyone to look at but aside from that I’m done.
But maybe you still want more? Understandable. Here’s some ideas:
1) Improve the graphics. We’ve been using nothing but solid colored rectangles and cubes. Three.js can do so much more than that. Read the documentation and see if you can add some sort of brick or stone texture to the wall. Maybe add a dirt floor or a wooden ceiling. Then see if you can replace the collectables with a more interesting 3D object, maybe even one loaded from a 3D file instead of just using built-in geometry.
2) Improve the random maze. We used literally the simplest maze building algorithm out there and it shows in the low quality of our random mazes. Why not try to improve things by researching some alternative, higher-quality, maze algorithms and using them instead?
3) Improve the game experience. Popping up alert messages to tell the player when they have won is kind of boring. Why not make it more like a real game? Show the player’s collectable count up in the corner, display an on-screen win message when they find everything and include an option to restart the game from within the game instead of forcing them to reload the page.
4) Improve the event system. Right now we have one array of collectables that all run the same code when touched. Make things more flexible by adding in new collectables with different behavior. Try to figure out an easy way to create, track and render all sorts of different objects all at the same time. Some ideas might include a teleporter that moves the player randomly or a bomb that resets the player’s collectable count to zero and re-randomizes the dropped objects.
Any one of those would be a great experience for an up and coming 3D game programmer. Finish all four and you probably understand enough about dungeon crawling programming to take a shot at building your own complete engine in whatever language you want.