Warning: The magic method __toString() must have public visibility and cannot be static in /home/darrent/bostongamejams.com/wp-content/plugins/inline-google-docs/inc/gelement.php on line 129
Akihabara Tutorial, Part 3: Basic Mapping — Boston Game Jams

Akihabara Tutorial, Part 3: Basic Mapping

by Darius Kazemi on June 14th, 2010

This is the third tutorial in a multi-part tutorial series where we will teach you how to make an 8-way shooter in HTML5 and JavaScript using the Akihabara framework. Akihabara is a set of Javascript libraries that take advantage of some of HTML5’s unique features to facilitate game creation. One of the best things about writing a game in HTML5 is that it will run in any browser that supports HTML5 on any platform. This includes Chrome, Firefox, Safari, and WebKit browsers on iPhone/iPad, WebOS devices, and other mobile platforms.

In this tutorial, we will teach you how to create a map that serves as the visual background of your game and that also provides collidable “walls” through which our moving sprite from Part 2 cannot move. One of Akihabara’s best features for rapid game development is how easy it makes the process of basic map-making. As a result, this will be a shorter tutorial, but we’ll still take our time familiarizing ourselves with the core concepts of basic mapping. We’ll also add in collision, so the player object actually hits the walls.

The final product

What you should have at the end of this lesson is something like this. Press Z to advance past the title screen and use the arrow keys to move. Notice how you collide with walls!

Defining the map

With Akihabara it’s incredibly easy to define a map. You just draw it out in ASCII using the help.asciiArtToMap function! We’re going to write a function called load_map that goes right after your main function. It looks like this:

function loadMap() {
  return help.asciiArtToMap([
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"x                  xx                  x",
"x                  xx                  x",
"xxxxxxxx      x    xx                  x",
"x             x    xxxxxxxxxx          x",
"x             x                        x",
"x             x                        x",
"x     xxxx  xxxxxxxxx           xxxxxxxx",
"x                  xx                  x",
"x                  xx                  x",
"xxxx               xx                  x",
"x      xxxxxxxxx   xx                  x",
"x                  xx                  x",
"x                  xx        x         x",
"xxxxxxx  xxxxxxxxxxxx        x         x",
"xxxxxxx  xxxxxxxxxxxx        x         x",
"x                  xx      xxxx        x",
"x                  xx        x         x",
"xxxxxxxx      x    xx        x         x",
"x             x    xx                  x",
"x             x    xx                  x",
"x             x    xx                  x",
"x     xxxx  xxxxxxxxx                  x",
"x                  xx                  x",
"xxxx                                   x",
"x                                      x",
"x      xxxxxxxxx   xx                  x",
"x                  xx                  x",
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    ], [ [null, ' '], [0, 'x'] ])
}

The help.asciiArtToMap function takes two parameters. First we pass in a giant array of characters representing our map — you can see above that we’ve essentially drawn our map using “x” and ” ” (space) characters. Then we pass in a translation array, which is an array of arrays, formatted like

[ [null, char1], [0, char2], [1, char3] ]

You need at least a null entry, followed by one numerical entry for each tile type you want to render. The null entry maps char1 (in this case the ” ” (space) character) to empty space. The following mappings correspond to elements in the map tilesheet, which we explain in the next section. In the example above, we map “x” to the 0th (first) tile in the tilesheet. We only have one tile for this demo so we don’t bother defining further tiles.

Loading the map tiles

To the Akihabara engine, the map is simply another object in our game world, like our moving sprite from Part 2. Also like our moving sprite object, this one renders an image, so we’ll start by loading the image that we’ll use to represent our wall pieces. The image we’re using is called map_pieces.png, which you should download and place in the same directory as your index.html.

function loadResources() {
...
  // ** This code goes right before the gbox.addFont() call **
  // Add our map spritesheet, which gives us our "wall" pieces
  gbox.addImage('map_spritesheet', 'map_pieces.png');

  gbox.addTiles({
    id:      'map_pieces',
    image:   'map_spritesheet',
    tileh:   16,
    tilew:   16,
    tilerow: 1,
    gapx:    0,
    gapy:    0
  });

Changes to the main() method

Next, we update our main method. The first thing we need to do is modify the gbox.setGroups call in our main function to create a new rendering group for the map itself. We’ll call this group ‘background’ and put it at the front of the array declaration, which means it will be the first thing rendered. This is desirable because we want the ‘player’ and ‘game’ (player sprite and UI element) groups to render on top of the walls of the level.

function main() {
  // ** For Part 3 we're adding 'background' to the next line **
  gbox.setGroups(['background', 'player', 'game']);

Next we scroll down to our maingame.initializegame function. Right after the addPlayer call, we put an addMap call. We haven’t defined this function yet, but just like addPlayer adds a player object to our world, addMap is going to add the map. Because this runs inside maingame.initializegame it occurs once, when the game loads for the first time.

  maingame.initializeGame = function() {
    // From Part 2...
    addPlayer();

    // Here we create a background object that will draw the map onto the 'background' layer each time our game world is drawn
    addMap();
  };

Right after the maingame.initializeGame function definition (just before we call gbox.go), we define our map itself. Our map is a structure that consists of three pieces of data: tileset, which is the name of the map tileset we defined in loadResources; map, which actually calls our load_map function above that returns map data translated from the ASCII art; and then a tileIsSolid function, which defines for the built-in collision code which tiles are solid and which are not. For example, a grass tile wouldn’t be solid but a wall tile would.

  // Here we define the map, which consists of a tileset, the actual map data, and a helper function for collision
  map = {
    tileset: 'map_pieces', // Specify that we're using the 'map_pieces' tiles that we created in the loadResources function

    // This loads an ASCII-definition of all the 'pieces' of the map as an array of integers specifying a type for each map tile
    // Each 'type' corresponds to a sprite in our tileset. For example, if a map tile has type 0, then it uses the first sprite in the
    //  map's tile set ('map_pieces', as defined above) and if a map tile has type 1, it uses the second sprite in the tile set, etc.
    // Also note that null is an allowed type for a map tile, and uses no sprite from the tile set
    map: loadMap(),

    // This function have to return true if the object 'obj' is checking if the tile 't' is a wall, so...
    tileIsSolid: function(obj, t) {
      return t != null; // Is a wall if is not an empty space
    }
  };

(As a side note: it may seem weird that we’re defining our map after we call addMap: keep in mind that the addMap call is just a function we’re defining! It does not actually run until gbox.go is called at the end of the main function. So chronologically speaking: we’re telling the game to get ready to call addMap, then we define the map, then addMap actually runs.)

Right after we define map, we call help.finalizeTilemap, which calculates and sets the map.h and map.w properties, the height and width of the map.

  // this function calculates the overall height and width of the map by counting the number of tiles.
  map = help.finalizeTilemap(map);

Then we run gbox.createCanvas, passing it the map.h and map.w values to make a canvas the size of our map. The canvas is now a drawing surface the exact dimensions of our map, so we then call gbox.blitTilemap to actually draw our map onto the canvas. Important note: don’t confuse the ‘map_canvas’ that we’re creating with the screen itself. Think of this as a temporary canvas where we draw the image of our map, off to the side somewhere. Later on we’ll take this canvas and draw it on the main canvas that serves as the ‘screen’ of our game.

  // Since finalizeMap has calculated the height and width, we can create a canvas that fits our map. Let's call it "map_canvas".
  gbox.createCanvas('map_canvas', { w: map.w, h: map.h });

  // This function grabs the map from the "map" object and draws it onto our "map_canvas". So now the map is in the rendering pipeline.
  gbox.blitTilemap(gbox.getCanvasContext('map_canvas'), map);

  gbox.go();
}

Then we close out the main function with gbox.go as always.

Adding our map object

Let’s define our addMap function right above our addPlayer function. The addMap function is what adds our map to the game world. Earlier in the code, we told Akihabara that it will be called when the game initializes, right after the player object is added. Here’s what addMap looks like:

// This is our function for adding the map object -- this keeps our main game code nice and clean
function addMap() {
  gbox.addObject({
    id:    'background_id', // This is the object ID
    group: 'background',    // We use the 'backround' group we created above with our 'setGroups' call.

    // The blit function is what happens during the game's draw cycle. Everything related to rendering and drawing goes here.
    blit: function() {
      // First let's clear the whole screen. Blitfade draws a filled rectangle over the given context (in this case, the screen)
      gbox.blitFade(gbox.getBufferContext(), { alpha: 1 });

      // Since we blitted the tilemap to 'map_canvas' back in our main function, we now draw 'map_canvas' onto the screen. The 'map_canvas' is
      // just a picture of our tilemap, and by blitting it here we're making sure that the picture re-draws every frame.
      gbox.blit(gbox.getBufferContext(), gbox.getCanvas('map_canvas'), { dx: 0, dy: 0, dw: gbox.getCanvas('map_canvas').width, dh: gbox.getCanvas('map_canvas').height, sourcecamera: true });
    }
  });
}

It’s very simple compared to our player object. We define the object’s id, we assign it to the ‘background’ group so it renders before anything else, and we define our blit function, which clears the screen and draws the map. The blit function is drawing the contents of map_canvas — if you remember, we stored an image of our map onto the map_canvas back in our main function. So we’re just making sure that we redraw that image every frame. And that’s it for the map object!

Fixing a, um, problem

Since we’re talking about clearing the screen, we have a confession to make. We made a mistake in tutorial 2. As a matter of convenience, we had you clear the screen inside the player object’s blit function. This is totally wrong. As you can see above, the background is the appropriate place for us to clear the screen. This is because it’s the first thing that is rendered, so we want to clear the previous screen, then render the background and then everything else on top of it. Otherwise we’d be clearing out the background (that is, the map) from inside the player object! We wouldn’t want that.

We need to delete these two lines of code from the old player object code from Part 2:

      // Clear the screen.
      gbox.blitFade(gbox.getBufferContext(), {});

Sorry about that!

Adding collision

Thankfully for us, Akihabara makes adding collision to our player object pretty simple. The first thing we need to do is redefine the collision box, which is the invisible box around our player object that all the collisions are calculated on. The new code goes right after our tileset definition.

function addPlayer() {
  gbox.addObject({
    id:      'player_id',    // id refers to the specific object
    group:   'player',       // The rendering group
    tileset: 'player_tiles', // tileset is where the graphics come from

    // ** New code for Part 3 **
    // We're overriding the default colh value for the object. "colh" stands for collision height, and it's the height of our collision box.
    colh:gbox.getTiles('player_tiles').tileh,

The colh property is a default property in any topview object. It stands for “collision height,” and it’s the height of our collision box. The object also automatically has values for colw (collision box width) and colx and coly (the x/y offset of the collision box).

We’re overriding colh from its default because by default in the toys.topview object, colh is set to half the height of the sprite. This is because topview is normally used for Zelda-style games where the hitbox is considered to be the bottom half of the sprite so the top half can “overlap” scenery that’s “behind” it. In this case we’re just setting colh to the default tile height, so our collision box can be imagined as a square which just barely contains our circle inside it.

In our initialize function, we set the starting position for our player to (20,20) instead of the default (0,0). We do this because there’s a tile at (0,0), so if the player started there they’d be stuck forever!

    initialize: function() {

      // Here we're just telling it to initialize the object, in this case our player.
      toys.topview.initialize(this, {});

      // ** New code for Part 3 **
      // And we set the starting position for our player.
      this.x = 20;
      this.y = 20;
    },

Finally, in the first function, we tell it to check for collisions after forces are applied to the object.

    first: function() {
      // Toys.topview.controlKeys sets the main key controls. In this case we want to use the arrow keys which
      //  are mapped to their english names. Inside this function it applies acceleration values to each of these directions
      toys.topview.controlKeys(this, { left: 'left', right: 'right', up: 'up', down: 'down' });

      // This adds some friction to our accelerations so we stop when we're not accelerating, otherwise our game would control like Asteroids
      toys.topview.handleAccellerations(this);

      // This tells the physics engine to apply those forces
      toys.topview.applyForces(this);

      // ** New code for Part 3 **

      // We're setting up a collision bounding box here based on our colx, coly, colh, and colw parameters. We're setting the tolerance to 6
      // because our sprite is round. A tolerance of about 6 gives us a good feeling of rounded corners to our object without making the object
      // feel too jello-like on corners. We arrived that the particular number through trial and error -- generally speaking tolerance should
      // be somewhere between 0 and half your sprite width or height.
      toys.topview.tileCollision(this, map, 'map', null, { tolerance: 6, approximation: 3 });
    },

The reason we put the collision check after forces are applied to the object is because what the tileCollision function does is check to see if the object is overlapping with a solid tile on the map. If it is, it snaps the object back away from the tile. This basically undoes the applied forces, which ends up looking and feeling like we expect.

The tileCollision function itself gets passed the player object, the map, a literal string called ‘map’ (which normally would let you change around how your map object is defined, but it doesn’t quite work, so until it gets fixed in a future Akihabara version, you should ALWAYS send ‘map’),  a null value that you shouldn’t worry about too much, and then values for tolerance and approximation.

The tolerance parameter informs the collision detection algorithm of how strict it should be. Essentially tolerance defines how “rounded” the edges of the collision box are. We just played around with a bunch of numbers until we came up with 6.

The approximation parameter tells the collision detection algorithm how precise it should be. Generally speaking, the smaller this number is, the more precise your collision checking will be. Watch out though: the more precise the algorithm, the more resources it’ll take up. Again, we arrived at 3 by trial and error. Note that an approximation of 0 or less will crash your game!

Bumping into walls: a video game tradition since Breakout

You can now save your HTML file and load it in a compatible web browser; you should see something like this. You’ll see the title screen from part 1. Press Z on your keyboard to get past the title screen and the blue circle will appear on the screen, surrounded by a beautiful beautiful maze. Control the circle with the arrow keys on your keyboard, enjoy as you collide with the walls!

Akihabara Tutorial Series

Written by Darius Kazemi and Darren Torpey

{ 9 comments… read them below or add one }


Fatal error: Cannot assign by reference to overloaded object in /home/darrent/bostongamejams.com/wp-content/themes/thesis_18/lib/classes/comments.php on line 176