Palagpat Coding

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

Wednesday, October 14, 2009

Cloning Zelda: Adding a Title Screen

Legend of Zelda title screen

First, a note on the server move: you may have noticed that my domain was offline for a few days last week; this was because I flubbed the domain transfer to my new server. Anyway, all was eventually straightened out, and I've successfully migrated all of my content from the old server to the new one. If you see anything here on the site that's broken, post a comment and let me know.

Now, on to today's subject: in trying to add several new states to the Game class for Canvassa, I've learned something interesting about Javascript's in operator: it doesn't really work for arrays! More specifically, it does in fact work, just not the way one would expect.

I do a lot of work in Python. In that language, you can do something like the following, and have a reasonable expectation of success:

>>> myList = [5,10,15,20]
>>> print (10 in myList)
True

So in a nutshell, you can define a list of values, and then check a reference value for membership in that list. Simple, and useful. I was doing this in Canvassa's Game class to test for the existence of a caller-specified game state before actually changing to the new state:

    this.constants.states = { overworld: 0, inventory: 1, 
                              dungeon: 2, map: 3, 
                              dying: 4, gameover: 5, cheat: 99, 
                              enum: [0,1,2,3,4,5,99] };
    ...
    changeState: function game_changeState(newState) {
        if (newState in this.constants.states.enum) {
            this.currentState = newState;
            this.drawBG();
        } else {
            console.log("Game.changeState() -- invalid state:", newState);
        }
    }

So basically I was using game.constants.states.enum as a list to keep track of the valid state values that changeState() should accept. Since the states I was using were all 0-based and sequential (except for the "cheat" state, which I haven't tested since Part 2 or so), this worked just fine. But in truth, it wasn't actually doing what I thought it was doing, as I was about to find out (dun-dun-DUN!)

I've been bothered for a while now about how I was loading the quest data (which handles map layouts, item and monster placement, and so on). Basically I was using the dojo.require() function to lazy-load the _quest1.js file, which doesn't actually contain a dojo class definition, but simply assigned the quest data to a namespaced variable, loc.gameData, which my classes could then refer to as a global. All kinds of poor, hackish behavior going on there. Plus, I'm building this game engine to support multiple (and, eventually, user-created) quests, so hard-coding one into the game bootstrapping logic is stupid and short-sighted. In order to correct this egregious flaw in the least destructive way possible, I reasoned, I should add several new states to the Game class, using negative integers to represent these meta-states that exist outside the regular game loop:

    this.constants.states = { title: -1, menu: -2, loading: -3,
                              overworld: 0, inventory: 1, 
                              dungeon: 2, map: 3, 
                              dying: 4, gameover: 5, cheat: 99, 
                              enum: [-3,-2,-1,0,1,2,3,4,5,99] };

In theory, the game engine could now start in the title state, and when the player presses start, change to the menu state, then allow quest selection from there. (for now, I'm simply XHR-loading _quest1.js when the user presses start, and will add the menu state later). But instead of working as I expected, this is when everything started to fall apart. I made the above change and a few others that would direct the game to load the necessary image resources, and then call game.changeState(game.constants.states.title) to display the title screen. BOOM! JavaScript error:

Game.changeState() -- invalid state: -1

Spending some time testing various expressions in the Firebug console, I eventually figured it out: the 'in' operator, when applied to JavaScript arrays, tests for the existence of a given index, not value! Observe this little exchange from my Firebug console:

>>> game.constants.states.enum
[-3, -2, -1, 0, 1, 2, 3, 4, 5, 99]
>>> -1 in game.constants.states.enum
false
>>> 8 in game.constants.states.enum
true

Like I said on Twitter when I discovered this: Freaky! I spent a few minutes looking at ways to get around this apparent misbehavior on JavaScript's part (I've since come to terms with why it works the way it does, but it's totally counter-intuitive to my way of thinking), and found this method, suggested by JavaScript rockstar Jonathan Snook.

In JavaScript, there's an in operator that tests whether a property is in an object. We can actually use this to mimic the PHP function in_array.
if(name in {'bobby':'', 'sue':'','smith':''}) { ... }
If the value of name matches any of the keys in the object literal, it returns true.

Jonathan then offers a refinement via a small function that takes a string array as input, and returns an object literal that can then be tested for membership via in. A reasonable approach, but not really one I wanted to pursue, if something simpler would work. This is what I came up with:

    this.constants.states = { overworld: 'overworld',
                              inventory: 'inventory',
                              dungeon: 'dungeon',
                              map: 'map',
                              dying: 'dying',
                              gameover: 'gameover',
                              title: 'title',
                              menu: 'menu',
                              loading: 'loading',
                              cheat: 'cheat' };
    ...
    changeState: function game_changeState(newState) {
        if (newState in this.constants.states) {
            this.currentState = newState;
            this.drawBG();
        } else {
            console.log("Game.changeState() -- invalid state:", newState);
        }
    }

Et, voila! Simple and elegant. I still have my pseudo-enumeration of valid game states, but I've eliminated the unnecessary "enum: [1,2,3...]" construction, and changed the state values from meaningless integers into strings that matched their own keys. Now my code can still refer to states via their game.constants.states.* enumeration values, and in works as expected, since those strings resolve as properties of the game.constants.states object literal.

There was a little more code cleanup necessary to support this change (for instance, I had to define a list of "unpausable" states so that the user wouldn't be able to put the game in pause mode from the title screen), and there are still a few lingering, unrelated problems in the 0.5 codebase, but we now have a title screen, and the ability to add new game states and check for their existence. All in all, a good night's work.

Labels: , , ,

0 Comments:

Post a Comment

Subscribe to Post Comments [Atom]

<< Home