HaxeFlixel RPG tutorial: Part 15

posted on Oct 23, 2014 in Haxe, OpenFL, Game design, HaxeFlixel
HaxeFlixel RPG tutorial tween

In this part we'll add some animation to our combat window using the FlxTween class.

The combat window will not smoothly slide down from the top of the screen to the center when the player enters combat. And it will slide back up when the battle ends.

We need to be wary of tween overlapping, however, because the combat can end and begin again before the window has fully gone up. In this case we need to cancel the first animation.

Begin by creating a new variable in PlayState which will store the "closing" tween animation (when the window slides up, off the screen).

private var combatHide:FlxTween;

Set a value for this variable in the endCombat() method using FlxTween.tween().

The method can receive 4 parameters - the object to move, the properties to move, the duration in seconds, and tweening options (an instance of TweenOptions).

In this case, I'm going to modify combatWindow's y property to -200 in 1 second. I'm going to play the tweening once without looping using the type FlxTween.ONESHOT, eased using FlxEase.quadIn to make the window accelerate slightly. When the animation is completed, I want to call a function called hideCombat.

public function endCombat(enemy:Enemy):Void {
	enemy.kill();
	combatHide = FlxTween.tween(combatWindow, {y: -200 }, 1, { type: FlxTween.ONESHOT, ease: FlxEase.quadIn, complete: hideCombat} );
	hero.active = true;
	currentAction = Walking;
}

The hideCombat function simply hides the window and makes it inactive:

private function hideCombat(tween:FlxTween):Void {
	combatWindow.active = false;
	combatWindow.visible = false;	
}

We have the "slide out" animation ready, let's do the "slide in" one now.

As I said above, we need to check whether the closing animation is playing and cancel it to prevent overlapping.

We already have a reference to that tween - the combatHide variable. Canceling an ongoing tween is as easy as calling its cancel() method.

After that, a new tween is created to slide the window down. All of this is done in startCombat():

private function startCombat(enemy:Enemy):Void {
	combatWindow.active = true;
	combatWindow.visible = true;
	if (combatHide!=null && combatHide.active) {
		combatHide.cancel();
	}
	FlxTween.tween(combatWindow, { y: FlxG.height / 2 - 40 }, 1, { type: FlxTween.ONESHOT, ease: FlxEase.quadOut } );
	combatWindow.y = -200;
	combatWindow.fight(enemy);
}

And that's all - here's the full PlayState.hx code for reference:

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.tweens.FlxEase;
import flixel.tweens.FlxTween;
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;
	public var hud:HUD;
	private var potions:FlxTypedGroup<Potion>;
	private var enemies:Array<Enemy>;
	private var currentAction:PlayerAction;
	private var combatWindow:CombatWindow;
	
	private var combatHide:FlxTween;
	
	/**
	 * 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;
		
		combatWindow = new CombatWindow(this);
		combatWindow.active = false;
		combatWindow.visible = false;
		add(combatWindow);
	}
	
	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;
		enemy.health = 5;
		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(enemy);
		}
	}
	
	private function startCombat(enemy:Enemy):Void {
		combatWindow.active = true;
		combatWindow.visible = true;
		if (combatHide!=null && combatHide.active) {
			combatHide.cancel();
		}
		FlxTween.tween(combatWindow, { y: FlxG.height / 2 - 40 }, 1, { type: FlxTween.ONESHOT, ease: FlxEase.quadOut } );
		combatWindow.y = -200;
		combatWindow.fight(enemy);
	}
	
	public function winCombat(enemy:Enemy):Void {
		endCombat(enemy);
	}
	
	public function endCombat(enemy:Enemy):Void {
		enemy.kill();
		combatHide = FlxTween.tween(combatWindow, {y: -200 }, 1, { type: FlxTween.ONESHOT, ease: FlxEase.quadIn, complete: hideCombat} );
		hero.active = true;
		currentAction = Walking;
	}
	
	private function hideCombat(tween:FlxTween):Void {
		combatWindow.active = false;
		combatWindow.visible = false;	
	}

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

Now, when you enter or end a battle, the window will slowly slide in or out.

It will look like this:

HaxeFlixel RPG tutorial tween

We'll add sound effects next time!

15191

Free newsletter

Get updates and bonus content!