Palagpat Coding

Fun with JavaScript, game theory, and the occasional outbreak of seriousness

Saturday, February 13, 2010

Cloning Zelda: Fixing Chrome

Chrome displaying a 'broken' tab

This week, an epic snowfall in the mid-Atlantic states kept me home from work for four straight days, giving me lots of spare time to play with my kids, and work on some side projects. One of these was addressing a long-standing problem with Canvassa: namely, why it was broken in Google's Chrome browser.

The initial impetus for this task came from a comment left by Gaby de Wilde on a recent blog entry, in which he asked if I knew why the project wasn't working on Chrome. I was a bit embarrassed to admit that I wasn't sure, so it drove me to look into why.

Step 1: Get My Feelings Hurt

First stop was to fire up Chrome and open the JavaScript Console. Then I loaded up the main Canvassa URL and examined the error console. Sure enough, after loading several of my class files, it barfed on Game.js. Unfortunately, due to the nature of Dojo's on-demand script loader, the console didn't actually tell me WHY that class didn't load.

Given that the error console wasn't being very forthcoming, my next stop was Douglas Crockford's JSLint code-validation tool. Needless to say, it lived up to its billing and "hurt my feelings," at least a little bit. It turns out I was cutting quite a few corners that Crockford argues shouldn't be cut: omitting semicolons, defining my variables wrong, and not filtering blocks seem to be my most common errors. Firefox and IE don't care about this sort of thing; they just blithely ignore it and carry on with what you intended to say, instead of making a scene in front of the entire classroom, going on about how "rules are for everyone, including you, Mister Potter."


Step 2: Learn Something About Chrome's Canvas Implementation

Once JSLint had issued me 50 demerits and was satisfied with the quality of my Game.js class file, I uploaded the fixed copy to my web server and went back to Chrome to reload the main page. Success... for a few minutes anyway. After playing the game for a few minutes, I began to notice that the game would, at seemingly random times, appear to "freeze up" and stop drawing its sprites. After a few seconds, though, they would all come back and the game would resume. Opening the JavaScript console again, I noticed that the freezeup occurred every time the app generated a flood of these errors:

Error drawing game sprites: DOMCoreException [in game_drawSprites(default)]

I added some additional logging to the game_drawSprites() function, and was able to determine that the exception was occurring in two cases: when Leevers or Zolas went under the ground/water, and when Link killed monsters. Looking for commonalities in those two cases, I found the answer: in both the "digger"-type monsters' animations, I defined the "under water/ground" animation — which should be invisible to the player — by specifying the animation frame's sprite coordinates as outside the boundaries of the source image. Looking in monster/_base.js, the base class for all monsters, I saw that I was doing this same thing in the default monster-death animation:

// /loc/monster/Zola.js: 'underwater' animation definition:
  this._stateDefs[4] = { name: 'underwater', ... anim: [
     [ {x:900,y:900,t:120} ]

// note that the monsters.png image is 160x304 pixels, so (900,900) is off the edge

// /loc/monster/_base.js: 'die' animation definition:
  { name: 'die', ... anim: [
    [ {x:64,y:16,t:6},{x:80,y:16,t:3},{x:200,y:16,t:20} ]

// again, (200,16) is beyond the 160x304 edge of monsters.png

When I originally set up the sprite animation system, I thought I was being terribly clever to define "invisible" animation frames in this way. My good buddy Firefox just ignored these calls, or at the very least failed quietly, so no sprite would be displayed for that animation tick. Chrome, however, doesn't see this as clever at all: if you try to pass invalid x,y coordinates to Canvas's 2d context object's drawImage() function, it throws a big, loud exception.

Of course, my approach had one other problem as well: if I ever expanded the monster sprite tile image to include more sprites, these supposedly "invisible" animations could suddenly be showing frames of some other sprite's animations! Yeah... bad idea.

Step 3: Go With What I Know

As with my last Canvassa update, the solution to this problem came to me by way of my MUGEN experience. With that game engine, the way you define an "invisible" animation frame is by using a sprite index of -1 (note that MUGEN's animation frame format is spriteX, spriteY, offsetX, offsetY, time):

; teleport: disappear and reappear behind my opponent
[Begin Action 1200]
1200,0, 0,0, 5
1200,1, 0,0, 5
1200,2, 0,0, 5
-1,-1, 0,0, 20  ; invisible for 20 ticks before re-appearing
1200,3, 0,0, 5
1200,4, 0,0, 5
1200,5, 0,0, 5

So, adopting this convention, I made the following, small changes to my code:

// /loc/Sprite.js: added this single line to draw():
   if (cut.x === -1 || cut.y === -1) { return; } // (-1 means "don't draw me")

// /loc/monster/Zola.js: changed the 'underwater' animation definition to:
  this._stateDefs[4] = { name: 'underwater', ... anim: [
     [ {x:-1,y:-1,t:120} ]

// /loc/monster/_base.js: 'die' animation definition:
  { name: 'die', ... anim: [
    [ {x:64,y:16,t:6},{x:80,y:16,t:3},{x:-1,y:-1,t:20} ]

Once I made those changes and uploaded them to my server, I reloaded the game url in Chrome, and it ran without a hitch. Go on, see for yourself.

So MUGEN saves the day (again). I'm always pleasantly surprised when experience in one realm helps me solve problems in another, but considering how often it happens, I really shouldn't be. Experience is experience... no matter what language or platform it comes from.

Labels: , , ,


Post a Comment

Subscribe to Post Comments [Atom]

<< Home