Wednesday, February 2, 2011

Project Jumper Part 11: They're Everywhere!

A new rivalry is born!

You know, there's a great bit play area here; it's way to much for even the most epic of duels between helmutguy and Skel-monsta. We need to add a little more danger to the world. To that end, it's time to give Skel-monsta some friends.

Oh, also, helmutguy and Skel-monsta are fired. It's about time I stopped using 'borrowed' sprites from commercial games, even though my own replacements are kind of terrible. Whatever. This game is now the saga of lizardhead versus the Spikemonstas. Yes, they both still just crumble when killed, because I'm still too lazy for proper sprite animated deaths.

Respawning enemies
This isn't really related to the whole multiple enemies thing, but it'll be useful and it's quick, so I'm just throwing it in. I'd like for the Spikemonstas to respawn a while after they're destroyed. It's just a few lines of code, and a quick override function.

First, some quick variables to keep track of things. We'll just need 3 Numbers, _startx, _starty, and _spawntimer. In the constructor, initialize the _start numbers to X and Y, and initialize _spawntimer to 0.

In enemy.update() we want to tell the enemy to respawn if it's been dead for 3 seconds.
if (dead)
{
    _spawntimer += FlxG.elapsed;
    if (_spawntimer >= 3)
        {
            reset(_startx,_starty);
        }
return; // Don't need to worry about the rest of the function if this is dead.
}

That should go first thing in update(), so that if the enemy IS dead it will skip all the living code that follows it.

reset() is an inherited function that sets the object back to life at whatever coordinates you tell it. We need just a bit more out of it, though, so we need a quick override.
override public function reset(X:Number, Y:Number):void 
{
    super.reset(X, Y);
    health = 3;
    _spawntimer = 0;
}

There's one thing missing still. See, when an object is killed, flixel stops bothering to execute its update() function. But we want it to execute its update() function. The solution here is to tweak the kill() function, and add this AFTER super.kill():
exists = true;
visible = false;
//Shove it off the map just to avoid any accidents before it respawns
x = -10;
y = -10;

There, a neverending supply of Spikemonsta. You're welcome. Diversion over.

Grouping Enemies
Obviously, we need more than one Spikemonsta. In fact, we want bunches. Well, one bunch anyway. The exact way to do this will depend on what logic we want. Random placement would be pretty easy, just pick some random x and y values, check to make sure it's legal, and go. But I'd like to keep the same coordinates from game to game, so it's going to be a little more work.
I could just manually create each creature with the location I want, but that's big and ugly and doesn't teach me anything, so no easy way out for me there.
I could use DAME and create a sprite layer and use the flixel export script. But I haven't taken the time to figure that out yet, and right now I'm focusing on flixel itself. I'll just put a pin in this; it's something I'll want to learn later, but not right now.
Here's what I'm going to do: Make a csv file with all of the coordinates of the monsters I want to place. Basically, I'm going to make something a little like how FlxTilemap works, except I'm making it my own self. Here's monstacoords.csv
144,416
256,528
432,432
688,416
112,640
1120,688
1392,512
1200,320
1120,144
1152,112
1184,80
848,112
944,304
912,464
1504,464
336,288

And I've embedded this into PlayState as monstaList:Class the same way I did the tilemap files.
Now we need to actually apply those coordinates to some enemies. Instead of making each monster one by one, we'll automate the process. First, we'll make a FlxGroup called Enemies to hold them all. Then in PlayState.create() we add this little blurb:
// Set up the enemies here
_enemies = new FlxGroup;  // Set up the group
placeMonsters(new monstaList, Enemy);  // Initialize each creature
add(_enemies); // Display them

Make sure this is after you created the player, since we'll need to pass the player on to the monsters as they're made. Obviously, placeMonsters() isn't a built in function, so we have to make it.
private function placeMonsters(MonsterData:String, Monster:Class):void
{
var coords:Array;
var entities:Array = MonsterData.split("\n");   // Each line becomes an entry in the array of strings
for (var j:int = 0; j < entities.length; j++)
{
coords = entities[j].split(",");  //Split each line into two coordinates
_enemies.add(new Monster(uint(coords[0]), uint(coords[1]), player, _mongibs));
}
}

So, there's a few interesting things here. For one, notice that I passed along the actual class Enemy, not any enemy object in particular. This means that I could make multiple different classes of enemy, and use the same function to set each of them up. This will be helpful when I get tired of SpikeMonstas. The rest of the function is just putting the data into arrays where we can use them, and then using them.

We can't stop here, though. All of our collision detection and stuff needs to be adjusted to refer to _enemies instead of skelmonsta or spikemonsta or whatever I left it at.
override public function update():void
{
super.update();
player.collide(map);
_enemies.collide(map);
_gibs.collide(map);
_bullets.collide(map);
 //<--snip-->
//Check for impact!
/* if (player.overlaps(_enemies))     // this doesn't actually work, that's why I commented it out.
{
player.kill(); // This should probably be more interesting
}*/
FlxU.overlap(player, _enemies, hitPlayer);  // The new collision check
FlxU.overlap(_bullets, _enemies, hitmonster);
if (_restart) FlxG.state = new PlayState;
}
private function hitPlayer(P:FlxObject,Monster:FlxObject):void
{
P.kill();
}

Sad but true, player.overlaps(_enemies) won't do the trick. That's because it only compares to the group itself, not to the members of the group. Yes, the group itself has an x and y and a bounding box. No, there's not much use for that. No worries, though, I just made another function to handle lizardhead's horrible explodey death.

Multiple Enemy Types
It's a little boring to just have SpikeMonstas to deal with, so let's add at least one other kind of enemy. This would be easier if I had planned a little better, but whatever. First off, just for the sake of convenience, I'm going to make a generic enemy template class. It will never be directly used, it will just force some consistency in how I make my classes. Basically, I take the existing Enemy class and extract only the lines that will always be used:
package com.chipacabra.Jumper 
{
import org.flixel.FlxSprite;
/**
* ...
* @author David Bell
*/
public class EnemyTemplate extends FlxSprite 
{
protected var _player:Player;
protected var _startx:Number;
protected var _starty:Number;
public function EnemyTemplate(X:Number, Y:Number, ThePlayer:Player) 
{
super(X, Y);
_startx = X;
_starty = Y;
_player = ThePlayer;
}
override public function kill():void 
{
if (dead) { return };
super.kill();
//We need to keep updating for the respawn timer, so set exists back on.
exists = true;
visible = false;
//Shove it off the map just to avoid any accidents before it respawns
x = -10;
y = -10;
}
}
}

And then the equivalent lines from the Enemy class get commented out. Also, I change Enemy so that it extends EnemyTemplate instead of FlxSprite, and change super(X,Y); to super(X,Y,ThePlayer); to match EnemyTemplate's constructor. All that work, and no visible change when the game is running. But now I can add a new class:
package com.chipacabra.Jumper 
{
import com.chipacabra.Jumper.Player;
import org.flixel.FlxG;
/**
* ...
* @author David Bell
*/
public class Lurker extends EnemyTemplate 
{
[Embed(source = '../../../../art/lurkmonsta.png')]public var imgLurker:Class;
[Embed(source = '../../../../sounds/monhurt2.mp3')]public var sndHurt:Class;
[Embed(source = '../../../../sounds/mondead2.mp3')]public var sndDead:Class;
protected static const RUN_SPEED:int = 30;
protected static const GRAVITY:int = 300;
protected static const HEALTH:int = 2;
protected static const SPAWNTIME:Number = 45;
protected static const JUMP_SPEED:int = 60;
protected var _spawntimer:Number;
public function Lurker(X:Number, Y:Number, ThePlayer:Player) 
{
super(X, Y, ThePlayer);
_spawntimer = 0;
loadGraphic(imgLurker, true, true);
drag.x = RUN_SPEED * 9;
drag.y = JUMP_SPEED * 7;
acceleration.y = GRAVITY;
maxVelocity.x = RUN_SPEED;
maxVelocity.y = JUMP_SPEED;
health = HEALTH;
}
override public function update():void 
{
if (dead)
{
_spawntimer += FlxG.elapsed;
if (_spawntimer >= SPAWNTIME)
{
reset(_startx,_starty);
}
return;
}
if (velocity.y == 0) { acceleration.y = -acceleration.y }
if (x != _startx)
{acceleration.x = ((_startx - x)); }
var xdistance:Number = _player.x - x;
var ydistance:Number = _player.y - y;
var distancesquared:Number = xdistance * xdistance + ydistance * ydistance;
if (distancesquared < 45000)
{
shoot(_player);
}
super.update();
}
override public function reset(X:Number, Y:Number):void 
{
super.reset(X, Y);
health = HEALTH;
_spawntimer = 0;
}
override public function hurt(Damage:Number):void 
{
if (x > _player.x)
velocity.x = drag.x * 4;
else
velocity.x = -drag.x * 4;
flicker(.5);
FlxG.play(sndHurt,1,false,50)
super.hurt(Damage);
}
private function shoot(P:Player):void 
{
// Bullet code will go here
}
override public function kill():void 
{
if (dead) { return; }
super.kill();
exists = true;
visible = false;
velocity.x = 0;
velocity.y = 0;
x = -10;
y = -10;
}
}
}

Obviously, I intend to do more with this, but right now I just want it up and running. In any case, you'll notice some pronounced similarities to the regular enemy, but I can also change how it moves, what it does, what it looks like, etc. With these tools, I can populate the world with any number of different creatures. One little subtlety: Because Enemy and Lurker don't have the same constructors, we need to use different commands to create them. A simple if statement in placeMonsters will do fine here:
if (Monster == Enemy)
{_enemies.add(new Monster(uint(coords[0]), uint(coords[1]), player, _mongibs)); }
else
{ _enemies.add(new Monster(uint(coords[0]), uint(coords[1]), player));}

Source and flash file are coming soon, I want to do a little more work on the graphics.
Oh, just as a closing note: '/n' is not the same as '\n', and "true" is not the same as "false." It's always the silly little things like that that make debugging so frustrating.

edit: Source and flash file are up now. I've actually made a bunch of little changes. Nothing really new, but it's probably worth taking a look at. In particular, I've tweaked how collision works when climbing a ladder to work better.

No comments:

Post a Comment