HaxeFlixel RPG tutorial: Part 7

posted on Oct 15, 2014 in Haxe, OpenFL, Game design, HaxeFlixel
HaxeFlixel RPG tutorial HUD

In this part we will create an UI overlay that displays the player's health.

There are several ways to manage a HUD in HaxeFlixel. The requirement is that it always stays on the screen and doesn't move like everything captured by the camera does.

You can create a separate camera for all your UI needs and display that as an overlay. You can also update the HUD's coordinates with every frame to make sure it matches up with the camera.

In this tutorial I'll show you a much easier way to have static HUD on the screen.

Start by introducing these variables:

private var overlay:FlxSpriteGroup;
private var healthDisplay:FlxText;
private var health:Int;
private var maxHealth:Int;

The overlay FlxSpriteGroup object will act as the container for all of our UI elements, in this case we only have 1 - healthDisplay FlxText.

Add a new method, which updates the text value of the healthDisplay object:

public function updateHealth():Void {
	healthDisplay.text = "Health: " + health + "/" + maxHealth;
}

Instantiate the object in init() function and call the updateHealth method right away to display the text:

healthDisplay = new FlxText(2, 2);
health = 5;
maxHealth = 10;
updateHealth();

Now we'll add the overlay group. Add the existing updateHealth object to this group using the add() method.

After that, set the scrollFactor.x and scrollFactor.y properties to 0:

overlay = new FlxSpriteGroup();
overlay.add(healthDisplay);
overlay.scrollFactor.x = 0;
overlay.scrollFactor.y = 0;
add(overlay);

The scrollFactor point determines the speed that the object scrolls at. Since we don't want to move our HUD at all, set it to 0.

The scrollFactor value can be used to set different scrolling speeds for different entities to provide an illusion of depth. This can be used in parallax backgrounds, where the background consists of multiple layers and the background moves slower than the foreground when the camera is moved.

This object is a FlxPoint instance, so you can set different values for x and y axes.

Since we used a FlxSpriteGroup object as a wrapper container for our text field, we can use the same group in the future when new UI elements have to be added. Just add them to this group and they will always have a static position.

That is all there is to it, here's the full PlayState.hx code:

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.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;
	/**
	 * Function that is called up when to state is created to set it up.
	 */
	override public function create():Void
	{
		super.create();
		
		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);
		
		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);
	}
	
	public function updateHealth():Void {
		healthDisplay.text = "Health: " + health + "/" + maxHealth;
	}

	/**
	 * 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();
		
		// 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 will cover collision detection by creating collectible potions in the next part!

18653