Sunday, January 9, 2011

Project Jumper Part 7: Lock and Load

Well, I think that helmutguy has been defenseless long enough. Let's give him some way to fight back. I'm going to go with two different abilities; that should establish some basic principles.
  1. Pew pew lasers
  2. STOMP
This is really only 5 pixels tall, but it looks pretty sweet zoomed in.

Projectile attacks


I threw together this cute little sprite to be helmutguy's projectile. If I wanted to be fancy, I'd make it two or more frames of animation. If you haven't noticed yet, I'm not fancy.

We're probably going to want to have more than one bullet on the screen at a time. The way to handle groups of similar things like this is with the Flixel class named, strangely enough, FlxGroup. It handles all the redundant stuff for you, so you don't have to have things like bullet0, bullet1, etc. or have to muck about with handling arrays or vectors yourself.

There are a few extra steps involved in setting up a group. First, we need to tell our playstate to expect them. We'll add

protected var _bullets:FlxGroup;
to PlayState.as to define the group, and then

_bullets = new FlxGroup;
inside of PlayState.create() to actually create the group.

Next, we need to make the individual instances of the bullets. There's a couple ways to do this. We could do the good recycling method of only making bullets as they're needed and reclaiming used bullets for later. That would be ideal if we want to let helmutguy have any arbitrary number of bullets onscreen at a time. But helmutguy is from Mega Man, and everyone knows that world has bullet limits. I'm going to confine helmutguy to having 4 bullets at a time. Since we know exactly how many bullets there can possibly be, we can just make 4 bullets and reuse them as needed.

(I'm going to save the other method for later, so I'm not just being lazy here.)

The command to add something to a group is the cunningly named FlxGroup.add() function. So let's just add

for (i = 0; i < 4; i++)    // Allow 4 bullets at a time
    _bullets.add(new Bullet());
to PlayState.create(). Nope, I sure haven't created a Bullet class yet. I'll get to that later.While we're at it, it's probably a good idea to include

add(_bullets);
in there. I put it right next to the add(_gibs); line. Just so long as it's after all the background stuff, so it gets drawn on top.
OK, let's actually make that Bullet class now. A neat trick with FlashDevelop is that you can click on Bullet() and type ctrl-shift-1, and it will pop up a menu letting you make the class. Now we just have to fill in the blanks to get this working. Each bullet is a sprite just like the player or a monster, just with its own physics, so we're going to have it extend FlxSprite.

We know the basics of this right now, and it's a fairly simple class I'm making, so here's all of it in one go,with the few new concepts commented on.

package com.chipacabra.Jumper 
{
    import org.flixel.FlxObject;
    import org.flixel.FlxSprite;

    public class Bullet extends FlxSprite 
    {
        [Embed(source = '../../../../art/bullet.png')]public var ImgBullet:Class;
              
        public function Bullet() 
        {
            super();
            loadGraphic(ImgBullet, false);
            exists = false; // We don't want the bullets to exist anywhere before we call them.
        }
        
        override public function update():void 
        {
            if (dead && finished) //Finished refers to animation, only included here in case I add animation later
                exists = false;   // Stop paying attention when the bullet dies. 
            else super.update();
        }
        
        //We want the bullet to go away when it hits something, not just stop.
        override public function hitSide(Contact:FlxObject,Velocity:Number):void { kill(); }
        override public function hitBottom(Contact:FlxObject,Velocity:Number):void { kill(); }
        override public function hitTop(Contact:FlxObject,Velocity:Number):void { kill(); }

        // We need some sort of function other classes can call that will let us actually fire the bullet. 
        public function shoot(X:int, Y:int, VelocityX:int, VelocityY:int):void
        {
            super.reset(X, Y);  // reset() makes the sprite exist again, at the new location you tell it.
            solid = true;
            velocity.x = VelocityX;
            velocity.y = VelocityY;
        }
    }
}

Now it's a matter of letting the player actually shoot the bullets. Just like we had to tell the player about the gibs, we have tell it about the bullets. Now, Mode passes the group of bullets to the player as an array, rather than leaving it as a FlxGroup. I'm going to try keeping it as a FlxGroup to see what happens.
Over in PlayState.as, change the line to

add(player = new Player(1000, 640,_gibs,_bullets));

And accordingly over in Player.as change the declaration to

public function Player(X:int,Y:int,Gibs:FlxEmitter,Bullets:FlxGroup):void
Just for the sake of good form, I went ahead and added a

protected var _bullets:FlxGroup;
to Player.as, and
_bullets = Bullets;
to Player().
So what do we do with this? Well, let's take this a step at a time. We want to be able to push a button to fire a shot (Yeah, yeah, this is obvious stuff. Work with me here) so over in the update() function where we have the movement code, I add in

//Shooting
if (FlxG.keys.X)
{
    shootBullet();  //Let's put the shooting code in its own function to keep things organized
}

Yep, just passing the buck here. But here's the neat FlashDevelop trick again. Put the cursor on shoot() and hit the magic ctrl-shift-1, and you can have it automatically insert a function for you to fill out.

After a bit of experimentation, I came up with this:

protected var _blt:Bullet;  // this is a placeholder variable that goes back with the other declarations earlier in the class

private function shoot():void 
{
    // Prepare some variables to pass on to the bullet
    var bulletX:int = x;
    var bulletY:int = y+4; // nudge it down a bit to look nice
    var bXVeloc:int = 0;
    var bYVeloc:int = 0;
            
    if (_blt = _bullets.getFirstAvail() as Bullet)
    {
        if (facing == LEFT)
        {
            bulletX -= _blt.width; // nudge it a little to the side so it doesn't emerge from the middle of helmutguy
            bXVeloc = -BULLET_SPEED;
        }
        else
        {
            bulletX += width; // start on the right side of helmutguy
            bXVeloc = BULLET_SPEED;
        }
        _blt.shoot(bulletX, bulletY, bXVeloc, bYVeloc);
    }
}


Well, it's not quite perfect, but it works! There's a few flaws, though. One is that when you push the button, it fires all four shots at the same time. Also, if the shots go off the edge of the screen, they never get marked  as dead, so you can't shoot again.

For the first problem, we need some sort of delay before helmutguy can shoot again. We'll use a variable to track the time between shots called _cooldown, and a const to set how long between shots called GUN_DELAY.

protected static const GUN_DELAY:Number = .4;
protected var _cooldown:Number;

In the constructor, we set

_cooldown = GUN_DELAY;

so that helmutguy can start shooting right away.
Then we need to tweak the shoot() function to check for the delay, and reset the timer after each shot.

if (_blt = _bullets.getFirstAvail() as Bullet)
{
     if (facing == LEFT)
    {
        bulletX -= _blt.width; // nudge it a little to the side so it doesn't emerge from the middle of helmutguy
        bXVeloc = -BULLET_SPEED;
    }
    else
    {
        bulletX += width;
        bXVeloc = BULLET_SPEED;
    }
    _blt.shoot(bulletX, bulletY, bXVeloc, bYVeloc);
    _cooldown = 0; // reset the shot clock
}

Finally, in update() we need to add one more line to actually have the counter count time.

_cooldown += FlxG.elapsed;
FlxG.elapsed will always give you the time, in seconds, between one update and the next. That means our GUN_DELAY time is in seconds.

OK, next step is handling the bullets that escape. We could just have them survive a set amount of time, or kill them if they leave the screen. For this demonstration, I think I'll have them die if they travel too far off the screen.It's a super easy addition. In Bullet.update() just add the line

if (getScreenXY().x < -64 || getScreenXY().x > FlxG.width+64) { kill();} // If the bullet makes it 64 pixels off the side of the screen, kill it
getScreenXY is a function that all FlxSprites have. It returns an FlxPoint, which has an x and a y property that we can access. Now the bullets are doomed to die sooner or later, so helmutguy is never out of ammo for too long.

It's time for the payoff: making the shot actually kill the target. Let's use the FlxU.overlap() function for this. Over in PlayState.update()  (Yes, I know we jump around a lot. Sorry!) we add in:

FlxU.overlap(_bullets, skelmonsta, hitmonster)
What we're looking at here is a function that compares two things, in this case _bullets and skelmonsta, and if they're overlapping then it trys to run the function hitmonster(_bullets, skelmonsta). Which I haven't written yet. I should do that right now.

private function hitmonster(Bullet:FlxObject, Monster:FlxObject):void
{
     Bullet.kill();
     Monster.hurt(1);
}
Then we change classes AGAIN and we tell Enemy how to deal with being hurt. First, in update(), we set

health=3;
Since we're hurting him with hurt(1), each hit reduces his health by 1, so he'll take three hits to kill. I'd also like to add a special effect when he's hurt, so I override the hurt() function thusly:

override public function hurt(Damage:Number):void 
{
    if (facing = RIGHT) // remember, right means facing left
        velocity.x = drag.x * 4; // Knock him to the right
    else if (facing = LEFT) // Don't really need the if part, but hey.
        velocity.x = -drag.x * 4;
    flicker(.5);  //guess what this does
    super.hurt(Damage);
}
There's a small bug, though. After you kill the monster, if you go touch the spot where he was, you still die. This is because Player.overlaps() doesn't check if the target is alive. So one more quick override over in Player.as:

override public function overlaps(Object:FlxObject):Boolean 
{
    if (!(Object.dead))
        return super.overlaps(Object);
    else
        return false;
}


At this point, all that's left to do for a perfect serviceable attack is to add some sound effects and some sort of animation when Skelmonsta is killed. There's nothing new there, so I'm just skip past it.
In fact, now that I look at it, this post is longer than I expected it would be, so I'm going to cut it in half here. Next I'll do the stomp on head style attack, but for now I'm going to sleep.

Finally spruced up the sound and graphics a bit. Source code is here, flash file is here.

12 comments:

  1. it's work perfect!

    i use hitRight, hitLeft instead of hitSide()
    (for Flixel - stable version)

    in playstate i define variable "i"
    (protected var i:Number;)

    how i can auto reBorn Skelmonsta after his dead?

    Big thanx!

    ReplyDelete
  2. i did it!
    add new variable "reborn" in enemy.as:

    public var reborn:Boolean;

    and then "kill" function :

    public override function kill():void
    {
    reborn = true;
    }

    in playstate.as update function add:

    if (skelmonsta.reborn == true)
    {
    add(skelmonsta = new Enemy(1260, 640, gibs, player));
    }

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
  4. How i can add ladders on map?
    i have problem with GRAVITY...

    ReplyDelete
  5. Congratz on your blog. It's really helping me out.

    ReplyDelete
  6. I'll put ladders on the list of stuff to get to!

    ReplyDelete
  7. I had been waiting on you to post your source, because I could not get this lesson to work. I figured it was something I did wrong. But, when I test yours, I get the same problem. I get:

    "[Fault] exception, information=TypeError: Error #1009: Cannot access a property or method of a null object reference."

    on this part of the shoot() function:

    if (_blt = _bullets.getFirstAvail() as Bullet)
    {
    if (facing == LEFT)
    {
    bulletX -= _blt.width; // nudge it a little to the side so it doesn't emerge from the middle of helmutguy
    bXVeloc = -BULLET_SPEED;
    }
    else
    {
    bulletX += width;
    bXVeloc = BULLET_SPEED;
    }
    _blt.shoot(bulletX, bulletY, bXVeloc, bYVeloc);
    _cooldown = 0; // reset the shot clock
    }

    I am using 2.35 stable build

    ReplyDelete
  8. Nevermind, the problem was in my Playstate. I went through yours line by line against mine. I honestly don't know what was wrong, though. I did move some things around for the sake of organization. So, either something was out of order, or I added something that wasn't there before.

    ReplyDelete
  9. At a guess, maybe you forgot to change the line to add(player = new Player(1000, 640,_gibs,_bullets));

    If you don't pass along the _bullets parameter, it just ends up left as a null object, which will piss flash right off when you try to do _bullets.getFirstAvail()

    ReplyDelete
  10. _bullets.getFirstAvailable()

    instead of

    _bullets.getFirstAvail()

    ReplyDelete
  11. This comment has been removed by the author.

    ReplyDelete
  12. I have really problems implementing the _cooldown flag.

    Bullets always came out together.

    So, I make a little change when X key is detected:

    if (FlxG.keys.X) {
    if (this._cooldown >= GUN_DELAY) {//adding this if, the thing really work nice
    this.shootBullet();
    }
    }
    this._cooldown += FlxG.elapsed;

    instead of

    if (FlxG.keys.X) {
    this.shootBullet();
    }
    this._cooldown += FlxG.elapsed;

    I don't know if this is the wrong way, but at least the code is now working for me.

    ReplyDelete