Tuesday, October 15, 2013

The Pond

Update:
Read my guest post on Mozilla Hacks

Check out The Pond (Source - GPL) on:

          Google Play             Chrome Web Store          Amazon App Store         Firefox Marketplace

also on clay.io, FB (http), Pokki

Tools used: LightTable IDE, CocoonJS

The Pond - because quality trumps quantity.

Before I go into some of the highlights of The Pond in code, I would like to give credit to both LightTable and CocoonJS for making the experience so enjoyable and awesome.

LightTable is a javascript IDE which started out as a concept video, and turned into a successful Kickstarter.  The most amazing feature of LightTable is it's code injection - as you edit javascript, you can update the javascript interpreted by V8 in the browser in real-time (video), which is an extremely powerful tool when developing a game where dealing with full page reloads can be cumbersome.

CocoonJS is a mobile platform for deploying HTML5 canvas games to mobile. It has implemented the javascript canvas in OpenGL for faster performance on mobile, and it also makes deploying the application to many platforms extremely simple (just tick a few check-boxes and you get a compiled version of the app for 4 different platforms).

One of the most important features of The Pond is that it's completely fluid and dynamic. No matter what screen size, be it a full screen desktop or low resolution mobile, The Pond dynamically adjusts itself flawlessly. Additionally, on mobile it will automatically reduce render quality if it detects a lag in frame rate. It is able to accomplish these things in part because it uses a proper game loop (from this amazing article on game loops):

var MS_PER_UPDATE = 18
var lag = 0.0
var previousTime = 0.0

// main game loop
function draw(time) {
  requestAnimFrame(draw)
  lag += time - previousTime
  previousTime = time

  var MAX_CYCLES = 18
  while(lag >= MS_PER_UPDATE && MAX_CYCLES) {
   
    // user input, movement, and animation calculations
    physics()
    lag -= MS_PER_UPDATE
    MAX_CYCLES--
  }

  // if we exhausted our cycles, the client must be lagging
  if(MAX_CYCLES === 0) {

    // adaptive quality
    lowerQuality()
  }
  
  // if 5 frames behind after update, jump
  if(lag/MS_PER_UPDATE > 75) {
    lag = 0.0
  }

  // draw to canvas
  paint()
}

The key to this game loop is that it uses a fixed interval physics time-step, and renders to the screen whenever possible. This means that all physics calculations are constant and run in predictable time. This means a fast computer will see the same physics as a slower computer (or in this case mobile device). Then, after the physics has been synced properly (at 60FPS), the actual screen drawing is done. This means that a slower computer will have a lower frame-rate for paint updates (30fps vs 60fps for example), which makes sense because the computer should lag but not break (due to physics time-step differences).

Debug Mode
One of the most difficult challenges with this project was dealing with collisions. Unlike other games where a box or a circle can model an object mostly accurately, I needed a way to detect collision between two irregular objects efficiently. Originally I thought about color-based pixel testing (really slow), and also doing Bézier curve collision calculations (extremely difficult and computationally expensive). What I ended up doing was hacking it, and just fit 6 circles within the body to do collision tests for each circle to determine whole body collision.


Fish.prototype.collide = function (fish) {

  // there are 6 circles that make up the collision box of each fish
  // check if they collide
  var c1, c2
  for (var i=-1, l = this.circles.length; ++i<l;) {
    c1 = this.circles[i]

    for (var j=-1, n = fish.circles.length; ++j < n;) {
      c2 = fish.circles[j]

      // check if they touch
      if(distance(c1, c2) <= c2.r + c1.r) {
        return true
      }
    }
  }
  return false
}

Another challenge I faced was dealing with rendering performance on mobile. The most expensive part of the whole painting operation (which was the bottleneck) was drawing the curves for each fish every frame. Normally most application use a sprite sheet to handle character animation (see Senshi), however The Pond has many dynamic elements in terms of color and shape based on rotation speed which make using a sprite sheet extremely difficult. So instead of using a sprite sheet, I draw each fish as a combination of Bézier curves.

Fish.prototype.drawBody = function() {
  var fish = this
  var size = this.size
  var ctx = this.ctx
  var curv = this.curv
  var o = this.oscillation
  ctx.strokeStyle = fish.bodyOutline
  ctx.lineWidth = 4

  for(var i = -1; i < 2; i+=2){
    var start = {
      x: size,
      y: 0
    }
    var c1 = {
      x: size * (14/15),
      y: i*size + size/30*o + curv/3
    }
    var c2 = {
      x: -size/2,
      y: i*size + size/30*o + curv/2
    }
    var end = {
      x: -size*2,
      y: i*size/3 + size/15*o + curv
    }
    ctx.moveTo(start.x, start.y)
    ctx.bezierCurveTo(c1.x, c1.y, c2.x, c2.y, end.x, end.y)
    var c3 = {
      x: -size * 2.5,
      y: i*size/6 + size/10*o + curv
    }
    var c4 = {
      x: -size*3,
      y: i*size/4 - size/15*o + curv/2
    }
    var end2 = {
      x: -size*3,
      y: -size/15*o + curv/3
    }
    ctx.bezierCurveTo(c3.x, c3.y, c4.x, c4.y, end2.x, end2.y)
  }
  ctx.stroke()

}

Now, this code could be optimized slightly by removing the new objects being created ({} generates a new object), however based on testing the biggest performance culprit is the bezierCurveTo() call, and having clean code takes priority over a micro-optimization. `this.oscillation` is based on a sin wave, and `this.curv` is based on distance to current rotation target. Overall, I was quite pleased with the rendering performance of the app. For more details, check out the commit log on github where you can find the commits which made the biggest performance improvements.

Lastly, I had a good bit of trouble figuring out how to get the fish to turn toward a target direction in the most efficient manner (take the shortest path around the unit circle). I eventually came up with this:

function directionTowards(a, b) {
  return Math.atan2(a.y-b.y, a.x-b.x)
}    
    
var dir = this.dir
var targetDir = directionTowards(MOUSE, this)
var arcSpeed = this.arcSpeed

// should it turn left or right? (based on shortest distance)
var arcDirection = 1

// if the arc distance is greater than 180deg, turn the other way
if(Math.abs(dir-targetDir)>Math.PI) {
  arcDirection = -1
}

// prevent over-turning 
var moveDistance = Math.min(arcSpeed, Math.abs(dir-targetDir))

// do the actual rotation
this.dir += (dir > targetDir ? -1 : 1) * arcDirection * moveDistance

// normalize dir within range( -PI < dir < PI )
if(this.dir>Math.PI) {
  this.dir = this.dir - Math.PI*2
} else if(this.dir<-Math.PI) {
  this.dir = this.dir + Math.PI*2
}

Bonus: I tried to integrate a metaballs effect into the game, but it didn't work out. Check out this jsfiddle for a great example of metaballs (blog post).

3 comments:

  1. This game is astounding! No other word for it. I wrote a review about it on my Firefox OS Gaming blog: http://firefoxosgaming.blogspot.com/2013/11/the-pond-game-review.html. All I can say is ... more! more! more!

    ReplyDelete
  2. Well done. Not being a dick at all, but thought I'd point out it is spelled "oscillation." Again, job well done.

    ReplyDelete