Let’s Program a 3D Browser Game: Index and Code

Did you know that modern browser support full 3D graphics?

Most people don’t. Probably because most websites don’t use it. Probably because it’s not really a useful feature for the average news site or online shopping center.

But it’s still a really cool technology and definitely something to keep in mind in case you ever find yourself needing to offer users a degree of interactivity or visualization above and beyond 2D text and images. I can imagine all sorts of scenarios where being able to offer a 3D model viewer or simple simulation would go a long way to making a complex topic understandable.

So let’s dive into the topic by building a simple 3D maze based off of old school first person dungeon crawlers. We’ll be using a library called three.js to handle the nitty gritty details and will cover enough of the basics of user input and game loop logic to at least give you a starting point for any interactive 3D websites you may find yourself needing to build.

Show Me The Finished Code

You can find the final product here. Play around with it to get a feel for what we’re going to build or save the page to your local machine to look at the source code. It’s all Javascript so need for compilers; the whole program is right in the <script> tags of that single page (although you’ll need to download a copy of the three.js library as well to get it working locally).

Show Me The Articles

Let’s Program A 3D Browser Game Part 1: You Can Do That?!

Let’s Program A 3D Browser Game Part 2: Three.js To The Rescue

Let’s Program A 3D Browser Game Part 3: Feeling Boxed In

Let’s Program A 3D Browser Game Part 4: A Different Kind Of Software Architect

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

Let’s Program A 3D Browser Game Part 6: Forward To Tomorrow!

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

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

Let’s Program A 3D Browser Game Part 9: Look, Shiny

Let’s Program A 3D Browser Game Part 10: Sometimes I Surprise Myself

Let’s Program A 3D Browser Game Part 11: Hunting For Treasure

Can We Buy The Internet Back From Advertisers?

Recently people have become a little disturbed by how much of their personal information is being collected and processed by big Internet companies like Google and Facebook.

But the only reason those companies care about collecting all that personal data is because it’s the most efficient way to sell advertising.

And the only reason they care about advertising is because selling ads is the most effective way to make money off of a free service.

And the only reason their services are free is because no one is willing to pay a monthly fee for access to social media and search engines.

Or are they?

It’s true that when these technologies first went mainstream people weren’t really sure what to do with them. They were reluctant to spend real money on virtual products. As a result giving access away for free was pretty much the only way to get any users.

But now? People know how valuable an effective search engine is. People have seen how helpful social media is for keeping up with friends and family (and arguing with strangers). People believe the Internet is a genuine necessity of modern life ranking only slightly behind food and water.

And now that people understand how useful these services are, maybe they are also finally willing to directly pay for them. Maybe even pay enough money to make ads unnecessary and make data mining less lucrative.

The big question is: How much would Google or Facebook or Whatever Inc. have to charge to survive without any ads? And how many people would be willing to pay that?

Casual research (a.k.a. some random articles I found on the web) suggests that the average user provides Google with $200 worth of ad revenue per year, and about half that much to social media companies like Facebook and Twitter.

That’s… really not that bad. Less than phone service and about the same as other major software as service platforms. With that in mind I bet a lot of people would gladly pay $200 a year for a “pro” level Google account with no ads or $100 to keep in touch with family on some sort of genuinely “privacy-first” social media platform.

In some ways I think this transition is already happening. Netflix has made paying for software as a service a mainstream idea and a lot of other websites already offer the choice to remove ads with a paid membership. Now that data privacy is also becoming a mainstream concern I really wouldn’t be surprised to naturally see more and more “free (with ads)” products offering privacy enhanced “pro” membership options.

So while a lot of people are worried we’re rapidly hurtling towards some sort of distopian corporate panopticon it’s also very possible that we’ve reached peak digital insecurity and that things are going to start getting better as more and more companies shy away from unpopular advertising tactics and instead tap into the growing wave of customers who place a premium on responsible data handling.

Let’s Program A 3D Browser Game Part 11: Hunting For Treasure

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.