Give Your Avoider Game Some Life… Points

by Michael James Williams on May 7, 2009 · 18 comments

in Avoider Game Extras,Tutorial

Delicious 1-UPs
Photo by adselwood.

HP. Lives. Mans. Credits. 1-UPs. All ways of giving the player an extra chance to play, rather than ending the game after a single mistake. These are also ways of rewarding the player for obtaining a certain number of points, reaching a particularly tricky spot, or inserting another coin.

As before, FrozenHaddock was way ahead of me in adding this feature to his game. Let’s play catch-up, and look at different ways of measuring health.

Click the image below to try the game out:

Snapshot_O07.png
Click to try it out!

As usual, if you followed the AS3 Avoider Game Tutorial you should be able to fit this into your game, even if you’ve made improvements. I’m going to base this tutorial on the game you end up with after completing the base tutorial. If you didn’t follow the tutorial, you can follow along with me using the files in this zip file, here.

Whatever you’re using, open the FLA, and let’s get started.

Three Strikes, You’re Out

So at the minute, the game goes straight to the game over screen as soon as the avatar hits an enemy. A simple lives system changes this so that, when the avatar hits an enemy:

  • If the player has at least one life left, he loses one life
  • Otherwise, Game Over.

Wording it like this makes it fairly obvious what we’ve got to change. Open AvoiderGame.as, and find the piece of code that deals with an enemy hitting the avatar:

?View Code ACTIONSCRIPT3
179
180
181
182
183
if ( avatarHasBeenHit )
{
	bgmSoundChannel.stop();
	dispatchEvent( new AvatarEvent( AvatarEvent.DEAD ) );
}

Based on our simple pair of rules (above), it’s easy to rewrite this:

?View Code ACTIONSCRIPT3
179
180
181
182
183
184
185
186
187
188
189
190
if ( avatarHasBeenHit )
{
	if ( playerLives > 0 )
	{
		playerLives = playerLives - 1;
	}
	else
	{
		bgmSoundChannel.stop();
		dispatchEvent( new AvatarEvent( AvatarEvent.DEAD ) );
	}
}

Of course, we’ll need to create this new variable playerLives. Make it a class-wide variable of type int. (An int is just a whole number, i.e. it has no fractions and no decimal point.)

?View Code ACTIONSCRIPT3
12
13
14
public class AvoiderGame extends MovieClip 
{
	public var playerLives:int;

We need to give this a value, so do that in the constructor:

?View Code ACTIONSCRIPT3
33
34
35
public function AvoiderGame() 
{
	playerLives = 3;

I’ve picked three lives — for some reason, this is a fairly common number. Test this change and you’ll find that when you touch an enemy, whoops, the game freezes.

Snapshot_O01.png

That’s my fault; I forgot we stopped the tick timer as soon as the avatar touched an enemy, rather than when we checked what to do. To fix this, we just need to move the line gameTimer.stop(); from here:

?View Code ACTIONSCRIPT3
169
170
171
172
173
if ( PixelPerfectCollisionDetection.isColliding( avatar, enemy, this, true ) ) 
{
	gameTimer.stop();
	avatarHasBeenHit = true;
}

…to the piece of code we were editing earlier:

?View Code ACTIONSCRIPT3
180
181
182
183
184
185
186
187
188
189
190
191
192
if ( avatarHasBeenHit )
{
	if ( playerLives > 0 )
	{
		playerLives = playerLives - 1;
	}
	else
	{
		gameTimer.stop();
		bgmSoundChannel.stop();
		dispatchEvent( new AvatarEvent( AvatarEvent.DEAD ) );
	}
}

If we test this out now, we’ll find that, er… it doesn’t seem to work. We get Game Over as soon as we touch an enemy:

Snapshot_O02.png

What’s going on? We need a better look. It’s time for a visual approach.

Counter, Our Old Friend

Remember the Counter class? Both the Clock and Score classes extend it to keep track of their internal numbers. Let’s create a lives counter.

So, first create a new symbol of type Movie Clip. Call it Lives and export it for ActionScript. Put a dynamic text field inside, with an instance name of livesDisplay. You can decorate it as well, if you like:

screenshot

Now create a new AS file, and save it as Lives.as in your Classes folder. The code is almost identical to that for the Score class, so I’ll just paste it here:

?View Code ACTIONSCRIPT3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package
{
	import flash.text.TextField;
	public class Lives extends Counter
	{
		public var livesDisplay:TextField;
 
		public function Lives()
		{
			super();
		}
 
		override public function updateDisplay():void
		{
			super.updateDisplay();
			livesDisplay.text = currentValue.toString();
		}
	}
}

There’s the basic code, now we have to connect this to the play screen. Open the PlayScreen symbol and drag in an instance of Lives from the Library.

screenshot

Give this an instance name of gameLives.

Now, because we turned off “Automatically declare stage instances” back in Part 12, we need to declare this object as a class-wide variable in AvoiderGame.as:

?View Code ACTIONSCRIPT3
12
13
14
public class AvoiderGame extends MovieClip 
{
	public var gameLives:Lives;

…and now we can tie this in to our existing code, based around playerLives, without much of a change:

?View Code ACTIONSCRIPT3
34
35
36
37
public function AvoiderGame() 
{
	playerLives = 3;
	gameLives.setValue( playerLives );
?View Code ACTIONSCRIPT3
182
183
184
185
186
187
188
189
190
191
192
193
194
195
if ( avatarHasBeenHit )
{
	if ( playerLives > 0 )
	{
		playerLives = playerLives - 1;
		gameLives.setValue( playerLives );
	}
	else
	{
		gameTimer.stop();
		bgmSoundChannel.stop();
		dispatchEvent( new AvatarEvent( AvatarEvent.DEAD ) );
	}
}

Notice here that we’re not removing the playerLives variable and replacing it with this new gameLives, we’re merely using gameLives to represent the playerLives value. Why? Well, it lets us separate the internal values with the external display of those values. If we wanted, we could add a second display object to show the number of lives in a different way. It’s up to you, as the programmer, to decide whether this is worth it.

Anyway, run the game:

Snapshot_O03.png

We certainly start with three lives, but as soon as we hit an enemy this very rapidly counts down to zero. Let’s experiment by setting the initial number of lives to 99 (do this in the AvoiderGame constructor function):

Snapshot_O04.png

Now it’s easy to see the source of the problem (apologies to anyone reading that understood this several paragraphs ago ;) ). The player doesn’t lose a life just at the point they touch an enemy; they lose a life for every tick during which they are touching an enemy!

We could leave the game like this, change the display from a number to a health bar, and let the number of “lives” (HP, really) increase every tick the player wasn’t touching an enemy. Feel free to do that, if it suits your game. However, I’m going to go with the FrozenHaddock design of making the enemy disappear once it touches the avatar, taking one of the player’s lives with it.

An Eye For An Eye

This isn’t actually going to require much of a change to our code. Take a look through and see if you can figure out what’s going to need to change before reading on.

The answer lies in this part:

?View Code ACTIONSCRIPT3
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
while ( i > -1 )
{
	enemy = army[i];
	enemy.moveABit();
	if ( PixelPerfectCollisionDetection.isColliding( avatar, enemy, this, true ) ) 
	{
		avatarHasBeenHit = true;
	}
	if ( enemy.y > 350 )
	{
		removeChild( enemy );
		army.splice( i, 1 );
	}
	i = i - 1;
}

All we need to do is remove the enemy at the point it hits the avatar. Our existing code will take care of the rest. The simplest way of doing this is like so:

?View Code ACTIONSCRIPT3
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
while ( i > -1 )
{
	enemy = army[i];
	enemy.moveABit();
	if ( PixelPerfectCollisionDetection.isColliding( avatar, enemy, this, true ) ) 
	{
		avatarHasBeenHit = true;
		removeChild( enemy );
		army.splice( i, 1 );
	}
	if ( enemy.y > 350 )
	{
		removeChild( enemy );
		army.splice( i, 1 );
	}
	i = i - 1;
}

The new lines here are 174-175. Hopefully you’re biting your lip at the sight of duplicate code at lines 179-180 — if so, you’re right to whimper. Really, we should avoid such duplication. I shall leave it up to you to clean up this small mess I have created ;)

Time to test!

Snapshot_O06.png

Oops, I forgot to change the number of lives back to three. Well, never mind; it actually plays pretty well like this, I think. As always, you should adjust this to fit your game.

Challenges

You won’t be surprised to hear me suggest adding sound effects to accompany a lost life. And I already mentioned the idea of changing the lives counter to a health bar, like in beat-em-up games. What else can we do with this?

Well, for a start, you could combine lives AND a health bar: run out of HP and you lose a life. HP automatically regenerates; lives don’t. Frantic uses a system like this.

We don’t have any way of rewarding the player with an extra life right now. You could do this at the start of each level, or when the player gets a certain number of points. Alternatively, how about mixing this with the Collectibles tutorial, and creating an item that gives the player a 1-UP? Similarly, you could add a “shield” power-up that stops the player taking damage for a while.

Speaking of other tutorials, Mushyrulez’s Enemies tutorial is perfect for this. Perhaps the bigger enemies could do more damage, taking two or three lives at a time, while the smaller enemies actually regenerate your health?

Let me know what you come up with :)

Wrapping Up

You can download the zip of all the files from this tutorial here.

Hey, I managed to get all the way through that without making a lame “get a life” joke. I think that’s pretty impressive.

{ 17 comments… read them below or add one }

Snurre May 7, 2009 at 9:21 am

Great. I made it to 11720. But hey, remember to embeded the fonts, mine became times new roman.

Other challenges could be:
- Add an animation to the enemies when they disappear.
- Make the ‘sheild’ active on the player when he is losing one life, and make a blinking animation on him.

MichaelJWilliams May 7, 2009 at 11:36 am

D’oh, school boy error on my part :(

Nice suggestions!

Ardavanski May 30, 2009 at 1:41 pm

Thank you, its really awesome, and your right 100 Lives is perfect :)

Ardavanski May 30, 2009 at 2:10 pm

Do i actually HAVE to do that garbage collecting in part 12 to make it work?
Anoyingly, it dont show how many lives i got. And when i put that new var in, it says duplicate…something.
Do i have to do that?

Michael Williams May 30, 2009 at 2:44 pm

That “duplicate” error is probably due to having “automatically declare stage variables” unchecked, rather than the actual garbage collection. If you haven’t checked that, then removing the public var gameLives:Lives; line should get rid of that error.

Ardavanski May 30, 2009 at 2:46 pm

Fixed:) Thank you. Wow, you know Michael, just following those 12 tutorials, i have learned a lot of AS3! I had like 80% more errors that i asked you for help about, but i solved them all by myself and how? – Remembering what you told us! Your tutorials are GREAT, seriously if there was Tutorial awards, my vote would definently go to you! :)

Michael Williams May 30, 2009 at 2:50 pm

Good good — and thanks :)

Have you read my series on debugging?

Ardavanski May 30, 2009 at 2:55 pm

I have :) And i laughed so much at that
Sometimes it’s best to give your brain time to figure things out on its own, without your interference. So if it feels like you’ve been BANGING YOUR HEAD TO A BRICK WALL and getting nowhere solving your problem

Funny because its so true! same as this;

What a lot of new programmers do here is sit and stare at the above lines of code, trying to figure out just where the heck a problem could be occurring, and getting annoyed because it all looks fine.

The up is what i did :<

Michael Williams May 30, 2009 at 2:59 pm

Hopefully not any more! ;)

bluehell905 January 11, 2010 at 2:30 pm

Is this regarding visual basics?

Michael Williams January 11, 2010 at 5:10 pm

Actually it’s about ActionScript 3.0, a scripting language for Flash.

Graham (Tsolron) May 23, 2010 at 4:58 am

I’ve completed this, but I’m not sure what’s wrong…
I’ve commented out stuff and tested what stuff was before the tutorial to figure out that the location of the problem is the setValue part.

playerLives = 3;
gameLives.setValue( playerLives);

What happens is it compiles the file without any errors or warnings, but then when I click the start button I get the following in the output:

TypeError: Error #1009: Cannot access a property or method of a null object reference.
    at AvoiderGame()
    at DocumentClass/onRequestStart()
    at flash.events::EventDispatcher/dispatchEventFunction()
    at flash.events::EventDispatcher/dispatchEvent()
    at MenuScreen/onClickStart()

But if it’s commented out, it works fine (don’t know about the parts in the tutorial after that though).

I’ve checked your .fla and as far as I can tell, everythings the same. I’ve checked: instance name, Lives properties, Lives.as, and the stuff in AvoiderGame.as.
While I do have added features, none should effect this.

PS. I do know the AvoiderGame.as is run properly until it gets to that one line. It doesn’t do anything after (used a trace).

Michael Williams May 29, 2010 at 12:58 am

Hey Graham,

Looks like gameLives isn’t set to anything, for some reason. (I.e. you’re missing a line where you type, for instance gameLives = new Lives() or whatever.

Does that help?

Graham (Tsolron) May 29, 2010 at 6:18 am

I’m not sure what I changed (I went to the past version and redid the tutorial), but I fixed it.

My next goal is to make HP, which is separate from lives. Then, see if I can add in HP for the enemies, and speed up the shooting (I added in the shooting feature that you have the tutorial for).

Michael Williams June 12, 2010 at 1:31 pm

Oh OK, cool :)

HP, good idea! Let me know how that goes.

Dat-THanh December 30, 2010 at 8:40 pm

Hi. My lives bar doesnt work, and i have no lives like before. QWhat is the problem?

package 
{
    import flash.display.MovieClip;
    import flash.utils.Timer;
    import flash.events.TimerEvent;
    import flash.ui.Mouse;
    import flash.events.KeyboardEvent;
    import flash.ui.Keyboard;
    import flash.events.Event;
    import flash.media.SoundChannel;

public class AvoiderGame extends MovieClip 
{
    public var gameLives:Lives;
    public var playerLives:int;
    public var gameClock:Clock;
    public var gameScore:Score;
    public var backgroundContainer:BackgroundContainer;
    public var army:Array;
    public var bullets:Array;
    public var enemy:Enemy;
    public var avatar:Avatar;
    public var gameTimer:Timer;
    public var useMouseControl:Boolean;
    public var downKeyIsBeingPressed:Boolean;
    public var upKeyIsBeingPressed:Boolean;
    public var leftKeyIsBeingPressed:Boolean;
    public var rightKeyIsBeingPressed:Boolean;
    public var spaceBarIsBeingPressed:Boolean;
    public var backgroundMusic:BackgroundMusic;
    public var bgmSoundChannel:SoundChannel;    //bgm for BackGround Music
    public var enemyAppearSound:EnemyAppearSound;
    public var shotSound:ShotSound;
    public var explodeSound:ExplodeSound;
    public var sfxSoundChannel:SoundChannel;    //sfx for Sound FX
    public var currentLevelData:LevelData;
    public var ticksSinceLastShot:int;

public function AvoiderGame() 
{
    playerLives = 3;
    gameLives.setValue( playerLives );
    currentLevelData = new LevelData( 1 );
    setBackgroundImage();
    ticksSinceLastShot = 0;
    currentLevelData = new LevelData( 1 );
    setBackgroundImage();

    backgroundMusic = new BackgroundMusic();
    bgmSoundChannel = backgroundMusic.play();
    bgmSoundChannel.addEventListener( Event.SOUND_COMPLETE, onBackgroundMusicFinished, false, 0, true );
    enemyAppearSound = new EnemyAppearSound();
    shotSound = new ShotSound();
    explodeSound = new ExplodeSound();

    downKeyIsBeingPressed = false;
    upKeyIsBeingPressed = false;
    leftKeyIsBeingPressed = false;
    rightKeyIsBeingPressed = false;
    spaceBarIsBeingPressed = false;

    useMouseControl = false;
    Mouse.hide();
    army = new Array();
    bullets = new Array();

    avatar = new Avatar();
    addChild( avatar );
    if ( useMouseControl )
    {
        avatar.x = mouseX;
        avatar.y = mouseY;
    }
    else
    {
        avatar.x = 200;
        avatar.y = 250;
    }

    gameTimer = new Timer( 25 );
    gameTimer.addEventListener( TimerEvent.TIMER, onTick, false, 0, true );
    gameTimer.start();

    addEventListener( Event.ADDED_TO_STAGE, onAddToStage, false, 0, true );
}

public function onBackgroundMusicFinished( event:Event ):void
{
    bgmSoundChannel = backgroundMusic.play();
    bgmSoundChannel.addEventListener( Event.SOUND_COMPLETE, onBackgroundMusicFinished, false, 0, true );
}

public function onAddToStage( event:Event ):void
{
    stage.addEventListener( KeyboardEvent.KEY_DOWN, onKeyPress, false, 0, true );
    stage.addEventListener( KeyboardEvent.KEY_UP, onKeyRelease, false, 0, true );
}

public function onKeyPress( keyboardEvent:KeyboardEvent ):void
{
    if ( keyboardEvent.keyCode == Keyboard.DOWN )
    {
        downKeyIsBeingPressed = true;
    }
    else if ( keyboardEvent.keyCode == Keyboard.UP )
    {
        upKeyIsBeingPressed = true;
    }
    else if ( keyboardEvent.keyCode == Keyboard.LEFT )
    {
        leftKeyIsBeingPressed = true;
    }
    else if ( keyboardEvent.keyCode == Keyboard.RIGHT )
    {
        rightKeyIsBeingPressed = true;
    }
    else if ( keyboardEvent.keyCode == Keyboard.SPACE )
    {
        spaceBarIsBeingPressed = true;
    }
}

public function onKeyRelease( keyboardEvent:KeyboardEvent ):void
{
    if ( keyboardEvent.keyCode == Keyboard.DOWN )
    {
        downKeyIsBeingPressed = false;
    }
    else if ( keyboardEvent.keyCode == Keyboard.UP )
    {
        upKeyIsBeingPressed = false;
    }
    else if ( keyboardEvent.keyCode == Keyboard.LEFT )
    {
        leftKeyIsBeingPressed = false;
    }
    else if ( keyboardEvent.keyCode == Keyboard.RIGHT )
    {
        rightKeyIsBeingPressed = false;
    }
    else if ( keyboardEvent.keyCode == Keyboard.SPACE )
    {
        spaceBarIsBeingPressed = false;
    }
}

public function onTick( timerEvent:TimerEvent ):void 
{
    ticksSinceLastShot = ticksSinceLastShot + 1;
    gameClock.addToValue( 25 );
    if ( Math.random() &lt; currentLevelData.enemySpawnRate )
    {
        var randomX:Number = Math.random() * 400;
        var newEnemy:Enemy = new Enemy( randomX, -15 );
        army.push( newEnemy );
        addChild( newEnemy );
        //gameScore.addToValue( 10 );
        sfxSoundChannel = enemyAppearSound.play();
    }
    if ( useMouseControl )
    {
        avatar.x = mouseX;
        avatar.y = mouseY;
    }
    else
    {
        if ( downKeyIsBeingPressed )
        {
            avatar.moveABit( 0, 1 );
        }
        else if ( upKeyIsBeingPressed )
        {
            avatar.moveABit( 0, -1 );
        }
        else if ( leftKeyIsBeingPressed )
        {
            avatar.moveABit( -1, 0 );
        }
        else if ( rightKeyIsBeingPressed )
        {
            avatar.moveABit( 1, 0 );
        }
    }

    if ( avatar.x &lt; ( avatar.width / 2 ) )
    {
        avatar.x = avatar.width / 2;
    }
    if ( avatar.x &gt; 400 - ( avatar.width / 2 ) )
    {
        avatar.x = 400 - ( avatar.width / 2 );
    }
    if ( avatar.y &lt; ( avatar.height / 2 ) )
    {
        avatar.y = avatar.height / 2;
    }
    if ( avatar.y &gt; 300 - ( avatar.height / 2 ) )
    {
        avatar.y = 300 - ( avatar.height / 2 );
    }

    if ( spaceBarIsBeingPressed )
    {
        if ( ticksSinceLastShot &gt;= 10 )
        {
            var newBullet = new Bullet();
            newBullet.x = avatar.x;
            newBullet.y = avatar.y;
            addChild( newBullet );
            bullets.push( newBullet );
            ticksSinceLastShot = 0;
            sfxSoundChannel = shotSound.play();
        }
    }

    var j:int = bullets.length - 1;
    var bullet:Bullet;
    while ( j &gt; -1 )
    {
        bullet = bullets[j];
        bullet.moveABit();
        if ( bullet.y &lt; -10 )
        {
            removeChild( bullet );
            bullets.splice( j, 1 );
        }
        j = j - 1;
    }

    var avatarHasBeenHit:Boolean = false;
    var enemyHasBeenHit:Boolean;
    var i:int = army.length - 1;
    var enemy:Enemy;
    while ( i &gt; -1 )
    {
        enemy = army[i];
        enemy.moveABit();
        enemyHasBeenHit = false;
        j = bullets.length - 1;
        while ( j &gt; -1 )
        {
            bullet = bullets[j];
            if ( bullet.hitTestObject( enemy ) )
            {
                enemyHasBeenHit = true;     
                removeChild( bullet );
                bullets.splice( j, 1 );
                gameScore.addToValue( 10 );
                sfxSoundChannel = explodeSound.play();
            }
            j = j - 1;
        }
        if ( enemyHasBeenHit )
        {                       
            removeChild( enemy );
            army.splice( i, 1 );
        }
        else
        {
            if ( PixelPerfectCollisionDetection.isColliding( avatar, enemy, this, true ) ) 
            {
                avatarHasBeenHit = true;
                removeChild( enemy );
                army.splice( i, 1 );
            }
            if ( enemy.y &gt; 350 )
            {
                removeChild( enemy );
                army.splice( i, 1 );
            }
        }
        i = i - 1;
    }

    if ( avatarHasBeenHit )
    {
        if ( playerLives &gt; 0 )
        {
            playerLives = playerLives - 1;
            gameLives.setValue( playerLives );
        }
        else
        {
            gameTimer.stop();
            bgmSoundChannel.stop();
            dispatchEvent( new AvatarEvent( AvatarEvent.DEAD ) );
    }

    if ( gameScore.currentValue &gt;= currentLevelData.pointsToReachNextLevel )
    {
        currentLevelData = new LevelData( currentLevelData.levelNum + 1 );
        setBackgroundImage();
    }
}

public function setBackgroundImage():void
{
    if ( currentLevelData.backgroundImage == "blue" )
    {
        backgroundContainer.addChild( new BlueBackground() );
    }
    else if ( currentLevelData.backgroundImage == "red" )
    {
        backgroundContainer.addChild( new RedBackground() );
    }
}

public function getFinalScore():Number
{
    return gameScore.currentValue;
}

public function getFinalClockTime():Number
{
    return gameClock.currentValue;
}

}

}

paulo September 5, 2011 at 8:53 am

nice tutorial, but something is wrong i think,or i didnt understan some of the steps…xD

Leave a Comment

Writing code? Write <pre> at the start and </pre> at the end to keep it looking neat.

Anti-Spam Protection by WP-SpamFree

{ 1 trackback }

Previous post:

Next post: