Cloning Zelda, part 3B: Monster-think
As I mentioned last time, part 3 ended up being too much work to describe all in a single post. Here's part B.
When I worked up the map schema in part 2, I had to decide how to handle the non-static portions of the scenery: walls that can be blown up, trees that can be burned, and statues that can get up and walk around. We'll deal with the walls and trees in a subsequent entry, but the statues , called Armos, were better treated as monsters since they can be woken up (), move around, attack, get killed, and respawn when you come back to the screen later (a burned bush or exploded wall, on the other hand, stays that way for the rest of the game). So I figured it was time to decide how to deal with the game's sprites.
As I mentioned in part 2, the original implementation of the game drew the map and Link on a single canvas, which had to be completely repainted for every game tick. One way around this is to use multiple Canvas elements, layered one on top of the other:
The "background" canvas gets painted first, on the bottom, then the game's sprites are all painted transparently onto the sprite canvas above the background, so it truly only needs to be repainted when it changes. Accomplishing this in HTML code is as simple as defining a CSS selector that applies to all canvas tags, like so:
canvas { border: 6px double black; position: absolute; top: 20px; left: 20px; }
The canvas tags themselves are declared one right after the other, bottom layer first then working upwards (this keeps them in the proper z-order):
<canvas id="map" width="512" height="480"></canvas> <canvas id="sprites" width="512" height="480"></canvas>
With the canvas layers properly set up, I modified the game code to paint each layer only when appropriate: redrawing the background layer when it changes, and redrawing the sprite layer on every iteration through the main game loop. That loop, then, becomes responsible for painting Link, the monsters, and any items/loot lying around on the ground. I usually try to do things in an object-oriented way if at all possible, so the map, player, and enemy classes each take care of painting themselves. That makes the main game loop's "redraw" section quite succinct:
this.drawSprites = function(){ ... // draw any enemies if (this.enemies) { for (var i=0; i<this.enemies.length; i++) { this.enemies[i].draw(spriteCanvas); } } // draw link player.draw(spriteCanvas); ... }
(Incidentally, I am indebted to the sprite gallery at ZeldaDungeon.net for most of my enemy sprites, although I did rip a few from an old NES emulator I had lying in a musty old corner of my hard drive). Aside from a few other bits of code to take care of things like the game header, that's the heart of the game.drawSprites() method, which is one-half of the game's main program loop (we'll talk about the other half in a minute).
As I noted in the last paragraph, the game is now structured to include an array of "enemy" objects at all times. This array gets re-initialized whenever Link enters a new screen, based on the data encoded in the gameData structure for each map screen. Here, for example, is part of the entry for overworld map screen 4,3 (the one shown in the first image in this post):
enemies: [ {type:'armos',color:0,position:{x:40,y:72}}, {type:'armos',color:0,position:{x:72,y:72}}, {type:'armos',color:0,position:{x:104,y:72}}, {type:'armos',color:0,position:{x:40,y:104}}, {type:'armos',color:0,position:{x:72,y:104}}, {type:'armos',color:0,position:{x:104,y:104}}, {type:'leever',color:1,position:{x:72,y:40}}, {type:'leever',color:1,position:{x:104,y:40}}, {type:'leever',color:1,position:{x:152,y:40}}, {type:'leever',color:1,position:{x:152,y:72}}, ]
The gameData definition for monsters consists of three parts: a type, a color, and a starting position. The type indicates the type of monster object to be spawned, and color and position are passed along to its constructor so that it knows how and where to draw itself when asked to do so (many enemy types have a a red/orange and a blue "subspecies", as it were; the color attribute specifies which to draw). The Game class contains a large switch() block that uses the type value to determine which Enemy subclass to instantiate.
Yes, enemy subclasses. When you consider all of the different types of monster in the Zelda overworld (and even in the dungeons), there are shared traits. For instance, some monsters (namely Octorocs, Moblins, and Lynels) can throw things at you (rocks, arrows, and swords, respectively): it makes sense to define an abstract base class, let's call it "Thrower", for enemies that throw things. We can then put the specific code that implements the throwing into the base class, and just override the function that draws the sprites for each respective type of enemy derived from that class. (note: I am using the excellent Javascript library Dojo to handle my classes now; it does all of the heavy lifting for me in terms of polymorphism, class inheritance, etc.)
dojo.declare("Enemy", null, { constructor: function(position, color){}, draw: function(ctx){}, canMove: function(direction){}, move: function(direction){}, think: function(){} }); dojo.declare("Thrower", Enemy, { // (moblins, octorocs, lynels) attack: function(){ // todo: launch projectile object } }); dojo.declare("Octoroc", Thrower, { constructor: function(position, color){ // override base class to store correct sprite drawing info, etc } });
I omitted a lot of the implementation details in the above example; indeed, not everything is even implemented yet! Nevertheless, the basic framework is now in place so that enemies appear in their proper places on each map screen, and all know how to move, more or less: I haven't yet fixed the Diggers subclass for Leevers and Zolas so that they go below the ground/water and pop up in a new position, nor have I implemented the pseudo-"jumping" movement that Tectites use. Both of these enemy subtypes just use the base type's movement algorithm at the moment. Likewise, I also haven't implemented the attack() method on the Thrower subclass; that's part of what I'll be doing in Part 4 of this series. But at the very least, all enemies can take advantage of the think() method, which we call from the main game loop (see? Told you I'd talk about the other half), which I can now show in its entirety:
this.main = function() { // update monster positions if (this.enemies) { for (var i=0; i<this.enemies.length; i++) { this.enemies[i].think(); } } // draw everything this.drawSprites(); }
If you'd like to look at the implementation of the Enemy class and its many subclasses, they're all contained in the enemies.js script file (The updated game.js is here)
So, now we have enemies, and they know (kinda-sorta) how to move around. The game is finally (almost) starting to get interesting to play, now: when my son saw it, it engrossed him for 15 minutes or more as he walked around exploring the whole overworld and all of its ecosystems. Feel free to do the same, here.
Next time, we'll finally get around to giving Link a sword and the ability to swing it. He'll need it, because we're also going to implement hit points and the ability to kill (and be killed)!
Labels: clone, Dojo, JavaScript, Zelda
0 Comments:
Post a Comment
Subscribe to Post Comments [Atom]
<< Home