Isometric Game Development in HTML 5 Canvas Part 1

Last modified: 07 Feb 2018

Summary

Loads a level that was created in Tiled and have a hero move around using Cocos2D-Javascript.

Thanking

With the help of a few really nice developers on the internet I manage to put their hard work together and came out with this project. They are:

  1. Ryan Williams for his Cocos2D-Javascript, port of Cocos2D iOS. At this moment it’s the only Javacsript library I know of that support Tiled’s TMX file format

  2. Clint Bellanger for his Free Libre Action Roleplaying Engine. I have to say he is one multi talented developer who also does most the artwork on his own game engine and whole heartedly making all his art work free for public use. Really rare kind. We will be using a lot of his graphic work here - specifically, the TMX file, the tileset and our hero sprite.

  3. Thorbjørn Lindeijer for his general purpose tile map editor which is responsible in creating the levels.

Code is hosted in Github. Demo you can access here.

Start

Cocos2D-Javascript requires NodeJS and NPM once you have installed these correctly and Cocos2D-Javascript, we can now proceed starting first on an overview of our graphic assets used in the game.

Graphic Assets

Our Tiled TMX file:

tmx editor

The map will have a total size of 60x60 tiles with each tile having a base size of 64x32.This would give us a total of 3840x1920 pixels for our hero to move around.

It will also have 2 layers - the floor/background layer, and the object/wall layer. Both layer will share one tileset:

dungeon tileset

This is our dungeon tileset broken into 64x128. Our hero will have 2 animations with 8 directions each. 1 for idle and 1 for walking. Each animation will be on its own file:

idle frames walk frames
Idle Animation Frames Walk Animation Frames

The idle animation will only have 4 frames but when we reach the last frame we will play it in reverse order. The walk animation will be looped. Each line of the of the animation file will have one direction. Starting from W moving clock-wise to SW.

Loading the map

After you create your project using ‘cocos new <project name>’ create a folder name ‘resources’ inside the ‘/src’ folder. This is where our game data will live. Now open ‘src/main.js’ and right on the top put this shorthand to the TMXTiledMap class:

1
, TMXTiledMap = nodes.TMXTiledMap

On our game’s main constructor, this is where we will do most of our game’s initialization code.

Now we proceed to load the map and center it on our canvas:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Get size of canvas
var s = Director.sharedDirector.winSize

// load our tmx map
tmxMap = new TMXTiledMap({ file: '/resources/atrium.tmx' })

// get dimension properties from our tmx map
var ms = tmxMap.mapSize
, ts = tmxMap.tileSize
, ws = geo.sizeMake(ms.width * ts.width, ms.height * ts.height)

// calculate the center spot for the map
var mapCenterX = -ms.width * ts.width / 2
var mapCenterY = -ms.height * ts.height / 2
// center our map in our canvas
tmxMap.position = ccp(mapCenterX + s.width / 2, mapCenterY + s.height / 2)
// move it to where we want the area to show first to our player
tmxMap.position.x += 900
tmxMap.position.y += 350

// add the map to our main layer
this.addChild(tmxMap)

Look at visually how this looks like before and after centering our map:

centering tiles

Loading our hero

We will be using a number of classes here to correctly load our hero sprite and animate it while its standing still or walking. In a nutshell it goes like this:

  1. Load the sprite sheet file into a Texture2D
  2. Extract the frames out of the texture with SpriteFrame
  3. Create Animation for each of the direction - that will be 8
  4. Create a Sprite object, use a single frame from #2 for its initial representation

So that we don’t end up with a big ball of mud, we will create a set of classes to handle our sprite and the animations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Entity.js
use "strict"  // Use strict JavaScript mode
var cocos  = require('cocos2d')   // Import the cocos2d module

function Entity(opts) {
  Entity.superclass.constructor.call(this)
  this.position   = opts.position
}

Entity.inherit(cocos.nodes.Node, {
  sprite          : null, // cocos.nodes.Sprite
  tmxMap          : null  // holds a reference to our current tmx map
})

module.exports = Entity

Our Entity class is pretty much bare - at least for now.

Our AnimationSet class will contain these properties:

1
2
3
4
5
6
7
8
9
10
11
12
AnimationSet.inherit(Object, {
  texture            : null, // cocos.Texture2D

  frameWidth      : null, // frame width and height from the sprite sheet
  frameHeight     : null,
  numFrames      : null, // number of frames in the image sprite

  animations        : null,  // cocos.Animations
  animaionIndex   : 0,     // current running animation
  animationDelay  : .04,
  startingFrame    : null, // the starting frame for this sprite
})

In the constructor we will accept 5 properties - the filename to the sprite sheet, the frame width and height, how fast the animation will play, and if we need to rewind the animation while playing. With these properties we will be able to load the sprite sheet, extract all the frames and create a cocos.Animation for all 8 directions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function AnimationSet(opts) {
  // store the properties on this object
  this.animationDelay = opts.animationDelay
  this.frameWidth   = opts.frameWidth
  this.frameHeight  = opts.frameHeight

  // load our sprite sheet file
  this.texture    = new Texture2D({ file: opts.textureFile })

  // calculate the number of frames for each row and column, we will use this to loop through all the frames
  var numFramesX  = this.texture.size.width / this.frameWidth
  var numFramesY  = this.texture.size.height / this.frameHeight

  // prepare an array to store each frame
  var animFrames = new Array()
  // loop through all the rows
  for(var y = 0; y &lt; numFramesY; y++) {
    // loop through all the columns
    for(var x = 0; x &lt; numFramesX; x++) {
      // using the frameWidth, frameHeight, x and y, we will be able to extract each individual frame from the sprite sheet
      animFrames.push(new SpriteFrame({ texture: this.texture, rect: new Rect(this.frameWidth * x, this.frameHeight * y, this.frameWidth, this.frameHeight) }))
    }

    // if this animation plays backward then we will push the same number of frames again to the array, starting from the last
    if(opts.backward) {
      var length = animFrames.length
      for(var z = 1; z &lt; numFramesX + 1; z++) {
        animFrames.push(animFrames[length - z])
      }
    }
  }

  this.startingFrame = animFrames[0]

  // group all the frames into their own animation
  this.animations = new Array()
  for(var i = 0; i &lt; 8; i++) {
    // the array splice method will remove items from the array and returns it
    // in effect, we are returning all the frames for each row on every loop
    this.animations.push(new Animation({ frames: animFrames.splice(0, opts.backward ? numFramesX * 2 : numFramesX), delay: this.animationDelay }))
  }
}

The Agent class inherits from the Entity class and will create and store 2 animation sets - 1 for idle, and 1 for walking. It is also responsible in creating our sprite object.