HaxeFlixel RPG tutorial: Part 9

posted on Oct 17, 2014 in Haxe, OpenFL, Game design, HaxeFlixel
HaxeFlixel RPG tutorial FlxBar

In this part we'll add a separate class for managing everything related to HUD. We'll also add a level counter and an experience bar.

The experience bar is made using HaxeFlixel's FlxBar class.

We'll first remove all the health related code from the PlayState.hx class and later move it to the new HUD class.

The new HUD.hx class will extend FlxSpriteGroup, which will make it possible to add children sprites to it.

Below is the full code to this class with the explanation.

package ;
import flixel.group.FlxSpriteGroup;
import flixel.text.FlxText;
import flixel.ui.FlxBar;

/**
 * RPG HUD.
 * @author Kirill Poletaev
 */
class HUD extends FlxSpriteGroup
{
	private var healthDisplay:FlxText;
	private var levelDisplay:FlxText;
	private var expBar:FlxBar;
	
	private var hp:Int;
	private var maxHp:Int;
	private var exp:Int;
	private var maxExp:Int;
	private var level:Int;
	
	public function new() 
	{
		super();
		scrollFactor.x = 0;
		scrollFactor.y = 0;
		
		healthDisplay = new FlxText(2, 2);
		hp = 5;
		maxHp = 10;
		add(healthDisplay);
		
		levelDisplay = new FlxText(2, 12);
		level = 1;
		add(levelDisplay);
		
		maxExp = 10;
		exp = 3;
		expBar = new FlxBar(4, 25, FlxBar.FILL_LEFT_TO_RIGHT, 100, 4);
		expBar.createFilledBar(0xFF63460C, 0xFFE6AA2F);
		add(expBar);
	}
	
	override public function update() {
		healthDisplay.text = "Health: " + hp + "/" + maxHp;
		levelDisplay.text = "Level: " + level;
		expBar.currentValue = exp;
		expBar.setRange(0, maxExp);
	}
	
	public function addHealth(num:Int):Void {
		hp += num;
		if (hp > maxHp) {
			hp = maxHp;
		}
	}
	
}

You can see that I introduced new variables, and that I renamed the "health" variable to "hp". This is because all FlxSprite objects have a "health" property that I am not going to use this time.

The FlxBar object that we're using to display the player's experience can be skinned using various methods, in this casae I use createFilledBar() to generate a simple bar with a solid fill.

The FlxBar constructor has several optional parameters which allow you to tell the bar where to take its values from by providing a variable name. But in this case it's not the ideal approach, since the maximal experience value will increase with every new level.

All the updates are now handled in HUD's own update() method. The currently displayed value of the FlxBar can be set using the currentValue property, and the minimum/maximum values can be set using the setRange() method.

Now we just need to add the HUD to PlayState and remove all the previous health-related code from it.

We'll end up with a PlayState.hx class that looks like this:

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.ui.FlxBar;
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 hud:HUD;
	
	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(TILE_WIDTH * 7, TILE_HEIGHT * 3);
		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();
		
		hud = new HUD();
		add(hud);
	}
	
	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();
			hud.addHealth(1);
		}
	}

	/**
	 * 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);
		}
	}
}

We'll add randomly wandering enemies next time.

16855