HaxeFlixel RPG tutorial: Part 12

posted on Oct 20, 2014 in Haxe, OpenFL, Game design, HaxeFlixel
HaxeFlixel RPG tutorial combat

Last time we made the enemies begin chasing the player when they get too close. This time we'll begin implementing some basic combat mechanics to our RPG.

When the enemy reaches the player and collides with them, both the player and the enemy should stop, and the combat starts.

Today we'll implement a basic state machine, which has 2 states - a walking state and a combat state. Whenever the enemy touches the player which is in a walking state, the state changes to combat, and the player's character becomes unresponsive to any movement commands or collisions with other enemies.

Start by going to Enemy.hx and adding a new condition to the following block:

if (tilemap.ray(startPoint, heroPoint) && pathToHero.length <= 5 && hero.active) {
	wanderTicks = 300;
	path.start(this, pathToHero);
}

This code is located in the update() function of Enemy.hx. The additional condition that I added checks whether the hero is "active" before starting the chase.

The rest of the changes will be made in PlayState.hx.

Go to PlayState.hx now and, before the declaration of the class, add a new enumartor:

enum PlayerAction {
	Walking;
	Combat;
}

Create a new variable in the PlayState class of the type PlayerAction.

private var currentAction:PlayerAction;

Set it to Walking in the init() function. Also, set the "active" property of the hero to true.

currentAction = Walking;
hero.active = true;

Note that the "active" property is part of the FlxSprite class, we don't need to add it manually.

Now go to the update() method and find the block that is responsible for handling mouse clicks. Add a new condition there, checking whether the currentAction is set to Walking:

if (currentAction == Walking && FlxG.mouse.justReleased){

Now add a new collision detection block, which loops through all enemies and checks their collision with the hero. In case of a collision, we want to call a method called onEnemyCollision:

// Collisions
FlxG.overlap(hero, potions, onPotionCollision);
var i:Int;
for (i in 0...enemies.length) {
	FlxG.overlap(hero, enemies[i], onEnemyCollision);
}

The function itself looks like this:

private function onEnemyCollision(hero:FlxSprite, enemy:Enemy):Void {
	if (enemy.exists && hero.exists && hero.active && enemy.active) {
		hero.active = false;
		enemy.active = false;
		currentAction = Combat;
		startCombat();
	}
}

The function checks whether both of the objects exist and are active. These "active" values are then set to false, and currentAction is set to Combat.

After this, no other enemy will start chasing the player, even if they are in the required vicinity. The player will not be able to move the player while in Combat mode, which is what we want.

A startCombat() function is called, which currently does nothing:

private function startCombat():Void {
	trace("Combat starts");
}

Here's the full code:

package ;

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

enum PlayerAction {
	Walking;
	Combat;
}

/**
 * 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;
	public var hero:FlxSprite;
	private var path:FlxPath;
	private var hud:HUD;
	private var potions:FlxTypedGroup<Potion>;
	private var enemies:Array<Enemy>;
	private var currentAction:PlayerAction;
	
	/**
	 * 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, TILE_WIDTH, TILE_HEIGHT);
		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();
		
		enemies = new Array<Enemy>();
		addEnemy(10, 15);
		addEnemy(12, 10);
		addEnemy(15, 6);
		addEnemy(20, 6);
		addEnemy(12, 20);
		
		hud = new HUD();
		add(hud);
		
		currentAction = Walking;
		hero.active = true;
	}
	
	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 addEnemy(x:Int, y:Int):Void {
		var enemy:Enemy = new Enemy(tileMap, hero);
		enemy.x = x * TILE_WIDTH;
		enemy.y = y * TILE_HEIGHT;
		enemies.push(enemy);
		add(enemy);
	}
	
	private function onPotionCollision(hero:FlxSprite, potion:Potion):Void {
		if (potion.exists && hero.exists) {
			potion.kill();
			hud.addHealth(1);
		}
	}
	
	private function onEnemyCollision(hero:FlxSprite, enemy:Enemy):Void {
		if (enemy.exists && hero.exists && hero.active && enemy.active) {
			hero.active = false;
			enemy.active = false;
			currentAction = Combat;
			startCombat();
		}
	}
	
	private function startCombat():Void {
		trace("Combat starts");
	}

	/**
	 * 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);
		var i:Int;
		for (i in 0...enemies.length) {
			FlxG.overlap(hero, enemies[i], onEnemyCollision);
		}
		
		// 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 (currentAction == Walking && 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, TILE_WIDTH, TILE_HEIGHT);
				}else {
					movementMarker.loadGraphic(AssetPaths.marker_stop__png, false, TILE_WIDTH, TILE_HEIGHT);
				}
			}else {
				movementMarker.loadGraphic(AssetPaths.marker_stop__png, false, TILE_WIDTH, TILE_HEIGHT);
			}
			movementMarker.setPosition(tileCoordX * TILE_WIDTH, tileCoordY * TILE_HEIGHT);
		}
	}
}

We'll continue in the next tutorial!

17503