HaxePunk shooting game tutorial: Part 8

posted on Oct 03, 2014 in Haxe, OpenFL, Game design, HaxePunk
HaxePunk shooting tutorial health

Today we'll add player's life counter to the shooter game.

This entity will display a heart icon next to the number of lives left, when that number reaches 0 - the game should end.

A life is lost when the player's ship collides with an oncoming enemy ship, which means that we will need to handle that collision detection.

Up until now I showed how to display various screen elements (like pictures and text) by applying them directly to the "graphic" property of an Entity. Today I'll show you how to put multiple graphics into a single Entity.

You can add more than one graphics to an Entity using the addGraphic() method. In the LifeCounter.hx code below I use this approach to add both the heart icon and the text field to the entity:

package ;
import com.haxepunk.Entity;
import com.haxepunk.graphics.Text;

/**
 * Player life counter.
 * @author Kirill Poletaev
 */
class LifeCounter extends Entity
{
	private var lives:Int;
	private var txt:Text;

	public function new(icon:Dynamic) 
	{
		super();
		lives = 0;
		layer = -4;
		
		txt = new Text("", 30);
		txt.color = 0xff0000;
		txt.size = 24;
		
		addGraphic(icon);
		addGraphic(txt);
		
		updateLives(3);
	}
	
	public function updateLives(num:Int):Void {
		lives += num;
		txt.text = lives + "";
	}
	
}

You can see that I also added an updateLives() method to this class, which lets us update the actual life number and the text field's value at the same time.

The MainScene.hx class is also updated. I added a new LifeCounter instance, which receives a heart icon as the only constructor's parameter.

I also pass a reference to the LifeCounter object to each EnemyShip instance:

package ;

import com.haxepunk.graphics.atlas.TextureAtlas;
import com.haxepunk.graphics.Image;
import com.haxepunk.HXP;
import com.haxepunk.Scene;
import com.haxepunk.utils.Input;
import com.haxepunk.utils.Key;

class MainScene extends Scene
{
	private var player:PlayerShip;
	private var spawnInterval:Int;
	private var enemyGraphic:Image;
	private var explosion:Explosion;
	private var score:Score;
	private var paused:Bool;
	private var pausedText:PausedText;
	private var lifeCounter:LifeCounter;
	
	public function new()
	{
		super();
		Input.define("up", [Key.UP, Key.W]);
		Input.define("down", [Key.DOWN, Key.S]);
		Input.define("left", [Key.LEFT, Key.A]);
		Input.define("right", [Key.RIGHT, Key.D]);
		
		var atlas:TextureAtlas = TextureAtlas.loadTexturePacker("atlas/atlas.xml");
		enemyGraphic = new Image(atlas.getRegion("enemyShip"));
		
		player = new PlayerShip(atlas);
		add(player);
		spawnInterval = Math.round(Math.random() * 50) + 50;
		
		explosion = new Explosion(atlas);
		add(explosion);
		
		score = new Score();
		add(score);
		
		paused = false;
		pausedText = new PausedText();
		add(pausedText);
		pausedText.visible = false;
		
		lifeCounter = new LifeCounter(new Image(atlas.getRegion("heart")));
		add(lifeCounter);
		lifeCounter.moveTo(10, 10);
	}
	
	override public function update() {	
		if (Input.pressed(Key.P)) {
			togglePause();
		}
		
		if (paused) return;
		
		super.update();
		spawnInterval--;
		if (spawnInterval == 0) {
			var enemy = new EnemyShip(enemyGraphic, explosion, score, lifeCounter);
			add(enemy);
			enemy.x = Math.round(Math.random() * (HXP.width-64));
			enemy.y = -50;
			spawnInterval = Math.round(Math.random() * 20)+30;
		}
	}
	
	public function togglePause() {
		paused = !paused;
		pausedText.visible = !pausedText.visible;
	}
	  
 }

We need to receive that reference in the EnemyShip.hx class, and add a new piece of code to the update() method, which will check for collision with the player's ship.

package ;
import com.haxepunk.Entity;
import com.haxepunk.HXP;
import com.haxepunk.graphics.Emitter;
import com.haxepunk.Sfx;

/**
 * Enemy ship entity.
 * @author Kirill Poletaev
 */
class EnemyShip extends Entity
{
	private var speed:Int;
	private var health:Int;
	private var explosion:Explosion;
	private var score:Score;
	private var sfx_explosion:Sfx;
	private var sfx_hit:Sfx;
	private var lifeCounter:LifeCounter;

	public function new(g:Dynamic, explosion:Explosion, score:Score, lifeCounter:LifeCounter) 
	{
		super();
		graphic = g;
		this.explosion = explosion;
		this.score = score;
		this.lifeCounter = lifeCounter;
		speed = Math.ceil(Math.random() * 3);
		setHitbox(64, 48, 0, 0);
		health = 5;
		
		sfx_explosion = new Sfx("audio/explosion.wav");
		sfx_hit = new Sfx("audio/hit.wav");
	}
	
	override public function update() {
		this.y += speed;
		if (this.y > HXP.height) {
			scene.remove(this);
		}
		
		var collidedEntity = collide("bullet", x, y);
		if (collidedEntity != null) {
			health--;
			scene.remove(collidedEntity);
			explosion.explode(x, y);
			sfx_hit.play(1, (x/HXP.width)*1.6 - 0.8);
		}
		
		collidedEntity = collide("player", x, y);
		if (collidedEntity != null) {
			health = 0;
			lifeCounter.updateLives(-1);
			sfx_hit.play(1, (x/HXP.width)*1.6 - 0.8);
		}
		
		if (health == 0) {
			var i:Int = 0;
			while(i<3){
				explosion.explode(x, y);
				i++;
			}
			score.add(1);
			sfx_explosion.play(1, (x/HXP.width)*1.6 - 0.8);
			scene.remove(this);
		}
	}
	
}

When the ships have collided, this enemy ship will instantly explode, and the player's life count will decrease by 1.

Of course, the PlayerShip.hx class also needs to be updated, since we are now handling collisions with it and all the enemy ships. Set the entity type to "player" and create a hitbox, both of these things are done in the constructor of the class:

type = "player";
setHitbox(64, 48, 0, 0);

If you test the game now, you should be able to run into enemies and have them explode. But you will also lose a life each time you touch an enemy.

Currently, the life count can go to negative numbers, because we do not stop the game if the player has no lives left.

We will handle that in the next tutorial!

9024