HaxeFlixel RPG tutorial: Part 8

posted on Oct 16, 2014 in HaxeFlixel
HaxeFlixel RPG tutorial collision

Today we'll add collectible potions that replenish the player's health when picked up.

The player will be able to pick up an item by walking over it. This means collision detection.

So far we actually have no collision detection whatsoever. The "collision" with the walls we have right now is not actual collision, but avoidance, since we use a pathfinding algorithm to move our character.

Let's start by creating a new FlxSprite subclass called Potion.

Create a new class called Potion.hx:

package ;
import flixel.FlxSprite;

class Potion extends FlxSprite
{

	public function new() 
	{
		super();
		loadGraphic("assets/images/potion.png", false, 16, 16);
	}
	
}

This is the graphic I'm using:

HaxeFlixel RPG tutorial collision

Now back to PlayState.hx, introduce a new variable.

private var potions:FlxTypedGroup<Potion>;

This FlxTypedGroup instance will allow us to group all potions together to create a common entry point for collision detection. Instantiate this object in the init() function and add it to the state:

potions = new FlxTypedGroup<Potion>();
add(potions);

Then call spawnPotion() a few times to create a few collectible potions.

spawnPotion(5, 5);
spawnPotion(6, 5);
spawnPotion(3, 10);
spawnPotion(4, 10);
spawnPotion(1, 10);

The spawnPotion() function creates a Potion instance, adds it to the group, and positions it according to the parameter values:

private function spawnPotion(x:Int, y:Int):Void{
	var potion:Potion = new Potion();
	potion.x = x * TILE_WIDTH;
	potion.y = y * TILE_HEIGHT;
	potions.add(potion);
}

Since all of our potions are bundled together in a group, we only need to add 1 line to the update() function to handle collision between the hero and all potions in this group.

// Collisions
FlxG.overlap(hero, potions, onPotionCollision);

The first two parameters represent the objects or groups being tested, the third parameter is the callback function to call when a collision is detected.

Here's the code to the collision handler:

private function onPotionCollision(hero:FlxSprite, potion:Potion):Void {
	if (potion.exists && hero.exists) {
		potion.kill();
		addHealth(1);
	}
}

You can see that I first check whether both of these entities exist, using their "exists" property. This value is set to false when I call the kill() method of a sprite. The same method also removes the sprite from the screen.

The addHealth() method adds health and updates the text field:

public function addHealth(num:Int):Void {
	health += num;
	if (health > maxHealth) {
		health = maxHealth;
	}
	updateHealth();
}

Since our map is bigger than the screen, we need to expand the world bounds to support collision on all of the map's area. Add this to the init() function:

FlxG.worldBounds.width = TILE_WIDTH * LEVEL_WIDTH;
FlxG.worldBounds.height = TILE_HEIGHT * LEVEL_HEIGHT;

Now here's the full code to PlayState.hx:

package ;

import flixel.FlxCamera;
import flixel.FlxG;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.FlxState;
import flixel.group.FlxGroup;
import flixel.group.FlxSpriteGroup;
import flixel.group.FlxTypedGroup;
import flixel.text.FlxText;
import flixel.tile.FlxTilemap;
import flixel.util.FlxColor;
import flixel.util.FlxPath;
import flixel.util.FlxPoint;
import openfl.Assets;

/**
 * A FlxState which can be used for the actual gameplay.
 */
class PlayState extends FlxState
{
	private var tileMap:FlxTilemap;
	public static var TILE_WIDTH:Int = 16;
	public static var TILE_HEIGHT:Int = 16;
	public static var LEVEL_WIDTH:Int = 50;
	public static var LEVEL_HEIGHT:Int = 50;
	public static var CAMERA_SPEED:Int = 8;
	private var camera:FlxCamera;
	private var cameraFocus:FlxSprite;
	private var movementMarker:FlxSprite;
	private var hero:FlxSprite;
	private var path:FlxPath;
	
	private var overlay:FlxSpriteGroup;
	private var healthDisplay:FlxText;
	private var health:Int;
	private var maxHealth:Int;
	private var potions:FlxTypedGroup<Potion>;
	/**
	 * Function that is called up when to state is created to set it up.
	 */
	override public function create():Void
	{
		super.create();

		FlxG.worldBounds.width = TILE_WIDTH * LEVEL_WIDTH;
		FlxG.worldBounds.height = TILE_HEIGHT * LEVEL_HEIGHT;
		
		tileMap = new FlxTilemap();
		tileMap.loadMap(Assets.getText("assets/data/map.csv"), "assets/images/tileset.png", TILE_WIDTH, TILE_HEIGHT, 0, 1);
		tileMap.setTileProperties(0, FlxObject.ANY);
		tileMap.setTileProperties(1, FlxObject.ANY);
		tileMap.setTileProperties(2, FlxObject.NONE);
		add(tileMap);
		
		cameraFocus = new FlxSprite();
		cameraFocus.makeGraphic(1, 1, FlxColor.TRANSPARENT);
		add(cameraFocus);
		
		camera = FlxG.camera;
		camera.follow(cameraFocus, FlxCamera.STYLE_LOCKON);
		
		movementMarker = new FlxSprite();
		movementMarker.visible = false;
		add(movementMarker);
		
		hero = new FlxSprite(16, 16);
		hero.loadGraphic("assets/images/hero.png", true, 16, 16);
		hero.animation.add("down", [0, 1, 0, 2]);
		hero.animation.add("up", [3, 4, 3, 5]);
		hero.animation.add("right", [6, 7, 6, 8]);
		hero.animation.add("left", [9, 10, 9, 11]);
		add(hero);
		
		potions = new FlxTypedGroup<Potion>();
		add(potions);
		spawnPotion(5, 5);
		spawnPotion(6, 5);
		spawnPotion(3, 10);
		spawnPotion(4, 10);
		spawnPotion(1, 10);
		
		hero.animation.play("down");
		
		path = new FlxPath();
		
		healthDisplay = new FlxText(2, 2);
		health = 5;
		maxHealth = 10;
		updateHealth();
		
		overlay = new FlxSpriteGroup();
		overlay.add(healthDisplay);
		overlay.scrollFactor.x = 0;
		overlay.scrollFactor.y = 0;
		add(overlay);
	}
	
	private function spawnPotion(x:Int, y:Int):Void{
		var potion:Potion = new Potion();
		potion.x = x * TILE_WIDTH;
		potion.y = y * TILE_HEIGHT;
		potions.add(potion);
	}
	
	private function onPotionCollision(hero:FlxSprite, potion:Potion):Void {
		if (potion.exists && hero.exists) {
			potion.kill();
			addHealth(1);
		}
	}
	
	public function updateHealth():Void {
		healthDisplay.text = "Health: " + health + "/" + maxHealth;
	}
	
	public function addHealth(num:Int):Void {
		health += num;
		if (health > maxHealth) {
			health = maxHealth;
		}
		updateHealth();
	}

	/**
	 * Function that is called when this state is destroyed - you might want to
	 * consider setting all objects this state uses to null to help garbage collection.
	 */
	override public function destroy():Void
	{
		super.destroy();
	}

	/**
	 * Function that is called once every frame.
	 */
	override public function update():Void
	{
		super.update();
		
		// Collisions
		FlxG.overlap(hero, potions, onPotionCollision);
		
		// Animation
		if (!path.finished && path.nodes!=null) {
			if (path.angle == 0 || path.angle == 45 || path.angle == -45) {
				hero.animation.play("up");
			}
			if (path.angle == 180 || path.angle == -135 || path.angle == 135) {
				hero.animation.play("down");
			}
			if (path.angle == 90) {
				hero.animation.play("right");
			}
			if (path.angle == -90) {
				hero.animation.play("left");
			}
		} else {
			hero.animation.curAnim.curFrame = 0;
			hero.animation.curAnim.stop();
		}
		
		// Camera movement
		if (FlxG.keys.anyPressed(["DOWN", "S"])) {
			cameraFocus.y += CAMERA_SPEED;
		}
		if (FlxG.keys.anyPressed(["UP", "W"])) {
			cameraFocus.y -= CAMERA_SPEED;
		}
		if (FlxG.keys.anyPressed(["RIGHT", "D"])) {
			cameraFocus.x += CAMERA_SPEED;
		}
		if (FlxG.keys.anyPressed(["LEFT", "A"])) {
			cameraFocus.x -= CAMERA_SPEED;
		}
		
		// Camera bounds
		if (cameraFocus.x < FlxG.width / 2) {
			cameraFocus.x = FlxG.width / 2;
		}
		if (cameraFocus.x > LEVEL_WIDTH * TILE_WIDTH - FlxG.width / 2) {
			cameraFocus.x = LEVEL_WIDTH * TILE_WIDTH - FlxG.width / 2;
		}
		if (cameraFocus.y < FlxG.height / 2) {
			cameraFocus.y = FlxG.height / 2;
		}
		if (cameraFocus.y > LEVEL_HEIGHT * TILE_HEIGHT - FlxG.height / 2) {
			cameraFocus.y = LEVEL_HEIGHT * TILE_HEIGHT - FlxG.height / 2;
		}
		
		// Mouse clicks
		if (FlxG.mouse.justReleased){
			var tileCoordX:Int = Math.floor(FlxG.mouse.x / TILE_WIDTH);
			var tileCoordY:Int = Math.floor(FlxG.mouse.y / TILE_HEIGHT);
			
			movementMarker.visible = true;
			if (tileMap.getTile(tileCoordX, tileCoordY) == 2) {
				var nodes:Array<FlxPoint> = tileMap.findPath(FlxPoint.get(hero.x + TILE_WIDTH/2, hero.y + TILE_HEIGHT/2), FlxPoint.get(tileCoordX * TILE_WIDTH + TILE_WIDTH/2, tileCoordY * TILE_HEIGHT + TILE_HEIGHT/2));
				if (nodes != null) {
					path.start(hero, nodes);
					movementMarker.loadGraphic(AssetPaths.marker_move__png, false, 16, 16);
				}else {
					movementMarker.loadGraphic(AssetPaths.marker_stop__png, false, 16, 16);
				}
			}else {
				movementMarker.loadGraphic(AssetPaths.marker_stop__png, false, 16, 16);
			}
			movementMarker.setPosition(tileCoordX * TILE_WIDTH, tileCoordY * TILE_HEIGHT);
		}
	}
}

As the result, you'll be able to pick up potions by walking over them:

HaxeFlixel RPG tutorial collision

We'll add a separate HUD with a level counter and an experience bar in the next tutorial!

19351