← All posts
May 6, 2013

Game development in HTML5 canvas and CoffeeScript

In the past a few months, I’ve tried to rebuild the classical FC tank game in CoffeeScript.

Ben Cao · 5 min read · 0 comments

In the past a few months, I’ve tried to rebuild the classical FC tank game in CoffeeScript.

The final work is here.

FC Tank gameplay: the player's yellow tank defends the eagle base among brick, steel and bush tiles while enemy tanks attack

It’s an interesting journey, I would like to summarize things I learned from it.

Game Basics

The law of frame

The most fundamental thing before developing a game is to understand FPS — frame per second, and how the game world is running in a frame-by-frame law.

Before each frame can be rendered, the game program needs to calculate the position, direction, speed, and other attributes for each moving objects(display object) in the screen. The higher the FPS is, the smoother the users feel. The typical refresh rate of an LCD screen is 60Hz, so it’s good enough to set 60 FPS as our goal.

This set a great challenge for game developers because some parts may sometimes take long CPU time. That’s why a great game needs great performance optimization.

Here’s the core loop of Tank timeline:

start_time_line: () ->
  last_time = new Date()
  @timeline = setInterval(() =>
    current_time = new Date()
    # assume a frame will never last more than 1 second
    delta_time = current_time.getMilliseconds() - last_time.getMilliseconds()
    delta_time += 1000 if delta_time < 0
    _.each(@map.missiles, (unit) -> unit.integration(delta_time))
    _.each(@map.gifts.concat(@map.tanks), (unit) -> unit.integration(delta_time))
    _.each(@map.missiles, (unit) -> unit.integration(delta_time))
    last_time = current_time
    @frame_rate += 1
  , parseInt(1000/@fps))
  # show frame rate
  @frame_timeline = setInterval(() =>
    @frame_rate_label.setText(@frame_rate + " fps")
    @frame_rate = 0
  , 1000)

Pathfinding

In many kinds of games, enemies have some degree of AI. They need to move across certain paths or towards some targets. Terrains will impact the result and can be changing all the time, so we have to do frequent calculations in order to find out the best path.

There are many pathfinding algorithms, the most common ones are: Dijikstra and A star, while A star is a slightly improved Dijikstra by adding a weight based on the “distance” of the current position to target position.
Here’s a wonderful site demonstrate the Pathfinding algorithm very well: PathFinding.js.

Enemy AI

AI differs in different kind of games.

For an enemy tank AI, here is the list I considered to be important:

  • Where to move
  • When to fire
  • Adjustable difficulty

Scenes

A game can be divided into several scenes, these scenes should be easily switchable and be properly designed to have its own preparation and cleaning functions.

Each scene should be designed separately, while the global storage must be there to keep shared data by different scenes.

For Example, the tank game is divided into

  • welcome scene
  • stage scene
  • game scene
  • report scene
  • hi_score scene

Each scene has start/stop methods which do prepare/clearing stuff. The global storage is the instance of game. Take the stage scene as an example:

class StageScene extends Scene
  constructor: (@game) ->
    super(@game)
    @init_stage_nodes()

  start: () ->
    super()
    @current_stage = @game.get_config('current_stage')
    @update_stage_label()
    if @game.get_config('stage_autostart')
      setTimeout((() => @game.switch_scene('game')), 2000)
    else
      @enable_stage_control()

  stop: () ->
    super()
    @disable_stage_control()
    @prepare_for_game_scene()

Stage design and Tiled map

It’s possible to design a stage by a few lines of code. But regular users cannot easily do that.

I started to think whether a GUI tool exists. And I found Tiled map.

Tiled map is an ecosystem which contains a 2D sprite-based map format standard and a set of cross-platform GUI tools and cross-language map readers and writers.

Its JSON definition is so easily readable that I directly read the data from the JSON export from a tiled output.

Use tiled made adding new stage easy and possible for non-programmers to get involved.

Designing a stage in the Tiled map editor, with stage layers and a terrains tileset of brick, steel and grass tiles

User input handling

The challenge is to transfer simulated user input signals to digital signals.

A variety of user input devices are available now. The most frequently used devices today are keyboard, mouse and touch screen.

I chose the keyboard as input for the tank game. There are 3 types of keyboard events, keyup, keydown, keypress.

Here’s the code logic I used to handle keyboard input:

class UserCommander extends Commander
  constructor: (@map_unit, key_setting) ->
    super(@map_unit)
    @key_map = {}
    for key, code of key_setting
      @key_map[code] = key
    @key_status = {
      up: false,
      down: false,
      left: false,
      right: false,
      fire: false
    }
    @reset_input()

  reset_input: () -> @inputs = { up: [], down: [], left: [], right: [], fire: [] }
  is_pressed: (key) -> @key_status[key]
  set_pressed: (key, bool) -> @key_status[key] = bool

  next: ->
    @handle_key_up_key_down()
    @handle_key_press()

  handle_key_up_key_down: () ->
    for key, types of @inputs
      continue if _.isEmpty(types)
      switch (key)
        when "fire"
          @fire()
        when "up", "down", "left", "right"
          if @direction_changed(key)
            @turn(key)
            break
          keyup = _.contains(@inputs[key], "keyup")
          keydown = _.contains(@inputs[key], "keydown")
          if keydown
            @start_move()
          else
            @stop_move() if keyup
    @reset_input()

  handle_key_press: () ->
    for key in ["up", "down", "left", "right"]
      if @is_pressed(key)
        @turn(key)
        @start_move()
    if @is_pressed("fire")
      @fire()

  add_key_event: (type, key_code) ->
    return true if _.isUndefined(@key_map[key_code])
    key = @key_map[key_code]
    switch type
      when "keyup"
        @set_pressed(key, false)
        @inputs[key].push("keyup")
      when "keydown"
        @set_pressed(key, true)
        @inputs[key].push("keydown")

Tunning Numbers

Most number settings that are obvious were borrowed from origin FC tank game, such as tank life, missile power, terrain properties and so on.

Others such as enemy tank IQ are defined according to feelings. At first, there’s no enemy IQ concept and I found enemies always rush to me in shortest paths, what a nightmare!

So I have to import an IQ concept which is actually the hundred percent rate that they choose the right target — user’s home, otherwise, they rush to a random area in the map.

Enemies are becoming smarter and smarter along with user entering into later stages.

Tunning Performance

Drawing Library is the most important factor to final performance

I’ve tried Rapheal, oCanvas and Kinetic. Kinetic was my final choice because it’s based on HTML5 canvas, its API is programmer friendly and its performance was much better than oCanvas.

Improve pathfinding using an advance data structure

I can’t find an existing implementation of Binomial Heap in Javascript, so I implemented one. The new data structure reduced average pathfinding time from around 700ms to less than 50ms.

Why I love CoffeeScript more than Javascript

  • easy and intuitive class inheritance
  • simpler array/hash iteration
  • @ instead of this
  • shorthand for function definition

Unit Test and Continuous Integration

Travis-CI is excellent. It supports running unit tests in a variety of environments and is already seamlessly integrated with Github.

The unit test tool I used is QUnit.

Some Possible improvements

  • Larger and custom map
  • Multi-user online game

Originally published at blog.bencao.it on May 6, 2013.

No comments yet. Be the first.

Optional. Leave it blank to post as Anonymous.