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

Somebody Keeps Leaving Their Collectables Lying Around The Dungeon

While our game engine has basic movement it’s honestly still pretty boring, mostly because it’s pointless. You walk around a little maze, look at the walls and then eventually give up and close the browser window. There is no goal to work towards, no way to win or to tell if you are doing good or bad.

That’s why real dungeon crawlers have monsters and treasure chests and traps and special events and also why even the most amateur of rural corn mazes makes sure to include something like an exit to look for.

Now obviously we’re not going to try and build an entire RPG framework in a single Let’s Program, but we can at least try to add one interesting feature: some collectibles that the player can hunt for while exploring the maze.

Our requirements are simple: We want floating cubes that we can place on our map. When the player enters the same square as a cube “something” should happen, like a message being displayed. The floating cube should then disappear.

Like all non-trivial programming problems there are roughly a zillion different ways to accomplish this and I almost got sucked down an infinite rabbit hole of trying to figure out the best way to build a collectible system that could later be expanded into a full on custom event system.

That was dumb. Carefully planning out code is important but at some point you have to just give things your best shot and write some code. If it isn’t perfect you can always rewrite it.

So here’s the coding plan:

– We will have an array of “collectible” objects.

– Each collectible will keep track of it’s x location, y location, and what code to run when triggered.

– While setting the maze up we will loop through the “collectible” array, add a 3D cube to the scene for each collectible and then store a reference to that object inside the collectible.

100% Hand Crafted Artisanal Collectables

Let’s get started by writing a standalone function that returns an array of collectible objects. This way we can expand or change how this works without cluttering up our main dungeon code. Since this is just our prototype we will manually build an array and then manually push two manually built anonymous collectable objects onto it:

function createCollectiblesList(){
   var collectables = [];

   collectables.push(
      {
         x:0,
         y:1,
         action:function(){alert("You picked up a collectable");},
      });

   collectables.push(
      {
         x:1,
         y:0,
         action:function(){alert("You picked up a different collectable");},
      });

   return collectables;
}

Now that we have a sample collectable array let’s write a second function that can use that array to add some collectables to the map. Collectables that will be represented as dark blue cubes 1/4th as wide as our 1 unit maze cells. Only tricky part here is that because I did a horrible job of lining our 3D space up with our with our map grid our collectable’s X position will map to Z in the 3D plane and it’s Y will map to X.

function placeCollectableGraphics(scene, collectables){
   var collectableGeometry = new THREE.BoxGeometry( 0.25, 0.25, 0.25 );
   var collectableMaterial = new THREE.MeshStandardMaterial( {color: 0x000088} );

   collectables.forEach(function(collectable){
      var collectableObject = new THREE.Mesh(collectableGeometry, collectableMaterial);
      collectableObject.position.z = collectable.x;
      collectableObject.position.x = collectable.y;
      scene.add(collectableObject);
   });
}

Wow. I really need to get around to rotating 3D space so it matches grid space better. But for now let’s just give our prototype a test run by inserting these functions into the setup portion of our runMaze function.

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 );

   var mazeGrid = createMazeGrid();
   placeWallGraphics(scene, mazeGrid); 
   
   //New lines
   var collectables = createCollectiblesList();
   placeCollectableGraphics(scene, collectables);

   //Rest of function as normal

Look around the maze now and you should see a blue box to the east and south of the starting location.

Looks a bit flat but it really was made with 3D graphics. Honest!

Hmm.. that’s a little bit boring. Let’s spice things up by making them spin. All we have to do is add a bit of code near the end of our constantly repeating render function. Put this right after all the switch statements based on player state but before the recursive call to render:

//Make our collectables spin
collectables.forEach(function(collectable){
   var collectableObject = collectable.objectRef;
   collectableObject.rotation.x += 2 * deltaTime/1000;
   collectableObject.rotation.y += 2 * deltaTime/1000;
});

まわる まわる

Now for our last bit of functionality: When the user enters a block with a collectable the collectable’s function should trigger.

To implement this we are going to modify the code that runs when the player is MOVING_FORWARD. When this code detects the player has fully entered the new square it should cross reference the collectables list and look for matches before returning the player to the WAITING loop, which we can do with something like this:

if(state == MOVING_FORWARD)
{
   walkingDistance += 1 * deltaTime/1000;

   if(walkingDistance >= 1){
      walkingDistance = 1;
      state = WAITING;
      processCollectableCollisions(player.gridX, player.gridY,collectables,scene);
   }

   //Rest of the function stays as is

Pretty easy, mostly because we tossed all the hard work onto the processCollectableCollisions function. All we have to do is tell it what X and Y location the player is looking at along with what list of collectables and scene it should reference and it does all the hard work for us! Or at least it will after we write it.

Fortunately it’s not that hard to write. We just loop through the provided collectables and see if any match the provided player location. If we find a match we trigger the code linked to that collectable and then remove the collectable from both the 3D scene and from the list of collectables. Don’t want the player picking the same object up twice!

function processCollectableCollisions(x, y,collectables,scene){
   collectables.forEach(function(collectable,index){
      if(collectable.x == x && collectable.y == y){
         collectable.action(); //Run the object's event
         scene.remove(collectable.objectRef); //Remove graphics from scene
         collectables.splice(index,1); //Remove collectable from list
      }
   });
}

A little bit messy and the functional programmer in me hates the way the function deletes items from the array given to it… but it works! More or less. I think the way we’re using splice might break down if we ever have two collectables with the same X and Y location but for a rough draft prototype this will get the job done.

Beyond Collectables

Our only goal was to program a couple of collectables but we actually managed to prototype a pretty powerful event system. This is because we can put literally any JavaScript we want inside of collectable.action. Right now it just triggers an alert but there’s no reason we couldn’t have it increment a score or teleport the player across the map or add an item to their inventory or trigger a cutscene.

OK, we don’t have an inventory to add to or any cutscenes to trigger but if we took the time to program those sorts of things we could link them to our collectables pretty easily.

And what if instead of making all the collectables cubes we made it possible to assign them each different symbols? Treasure chests and warning signs and keys and silhouettes. Maybe even an invisible option for when we want something to happen on a certain square without the player seeing it ahead of time.

Now you have a flexible system for making any sort of location based event you can imagine. And while that’s not quite enough for building a complete dungeon crawler it would be a pretty good start.