Tactics RPG Ability Range

Abilities such as attacking or using magic are part of a large and complex system. In this lesson we will take the first step toward this implementation by providing several classes that can be used to control the range of any particular one.

Every ability must have a component which defines its range. The ranges for some abilities might be merely distance based, but others will take on some sort of pattern, such as a line or a cone. When an ability is selected, tiles will also be highlighted according to the distance and or pattern specified by this component. In this way an ability can show the valid target(s) which are within its reach.

Abstract Base Class

Begin by creating the abstract base class named AbilityRange and organize it in the following path Scripts/View Model Component/Ability/Range. All of the concrete subclasses which we create in this lesson should also be saved there.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public abstract class AbilityRange : MonoBehaviour 
{
	public int horizontal = 1;
	public int vertical = int.MaxValue;
	public virtual bool directionOriented { get { return false; }}
	protected Unit unit { get { return GetComponentInParent<Unit>(); }}

	public abstract List<Tile> GetTilesInRange (Board board);
}

This class is super simple – only two fields, two properties, and an abstract method signature. The first field, horizontal, defines the number of tiles away from the user which can be reached (think X & Z axis). The second field, vertical, defines the height difference between the user’s tile and the target tiles which are within reach (think Y axis).

The first property, directionOriented should be true when the range is a pattern like a cone or line. When it is true, we will use the movement input buttons to change the user’s facing direction so that the effected tiles change. When the directionOriented property is false, you may move the cursor to select tiles within the highlighted range.

To illustrate the difference, consider a black mage ability like Fire from Final Fantasy Tactics Advance. When you select the spell, you have a range of tiles highlighted around the caster. You then select a location within the range to actually cast the spell by moving the cursor. In contrast, a Dragoon has Fire Breath which highlights tiles in a cone in front of the unit in the same direction the unit faces. When you use the movement input, you simply change the direction the dragoon faces and in this way are able to target different sets of tiles.

The final property, unit, crawls up through the hierarchy chain to find the Unit component. Some ranges will need to know the current user’s location in order to determine what tiles are reachable.

Every concrete subclass will be required to implement the GetTilesInRange method, which will return a List of Tile(s) which can be reached by the selected ability. This is how we will know what tiles to highlight on the board, and in the future will be used to determine if there are targets within reach.

Constant Ability Range

The frist concrete subclass we will make will probably be one of the most frequently used types of ranges. The use case for this type of class is when an ability always has the same pre-specified amount of range, i.e. Spell “X” has “Y” reach in terms of tiles. This class would be similar to a class where the range is based off of the range of an equipped weapon, except that the horizontal and vertical fields would need to be adjusted first to match.

Create a script named ConstantAbilityRange. The implementation is below:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class ConstantAbilityRange : AbilityRange 
{
	public override List<Tile> GetTilesInRange (Board board)
	{
		return board.Search(unit.tile, ExpandSearch);
	}

	bool ExpandSearch (Tile from, Tile to)
	{
		return (from.distance + 1) <= horizontal && Mathf.Abs(to.height - unit.tile.height) <= vertical;
	}
}

The code here should look familiar – we used something very similar when we implemented the Movement components. I am using the board’s search ability to retrieve the list of tiles within range of the user’s tile, and I use a delegate which only allows the search to continue as long as we are within the range specified by the horizontal and vertical fields.

Self Ability Range

Some abilities may only effect the user. In this case we don’t need to perform a search on the board, we simply return the user’s tile. Create another script named SelfAbilityRange and see the implementation below:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class SelfAbilityRange : AbilityRange 
{
	public override List<Tile> GetTilesInRange (Board board)
	{
		List<Tile> retValue = new List<Tile>(1);
		retValue.Add(unit.tile);
		return retValue;
	}
}

Infinite Ability Range

If you want to reach any or all targets no matter where they are on the board, then you will need a new type of range. Add a new script called InfiniteAbilityRange. Like before, we wont need to do a search here, we can simply return a list which directly copies all of the board’s tiles.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class InfiniteAbilityRange : AbilityRange 
{
	public override List<Tile> GetTilesInRange (Board board)
	{
		return new List<Tile>(board.tiles.Values);
	}
}

Cone Ability Range

All of the range types until now have been ones which were not dependent upon the facing direction of the user. Now we need a special case where the direction does matter. We will be selecting tiles which extend in a cone shape from the location of the user out.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class ConeAbilityRange : AbilityRange 
{
	public override bool directionOriented { get { return true; }}

	public override List<Tile> GetTilesInRange (Board board)
	{
		Point pos = unit.tile.pos;
		List<Tile> retValue = new List<Tile>();
		int dir = (unit.dir == Directions.North || unit.dir == Directions.East) ? 1 : -1;
		int lateral = 1;

		if (unit.dir == Directions.North || unit.dir == Directions.South)
		{
			for (int y = 1; y <= horizontal; ++y)
			{
				int min = -(lateral / 2);
				int max = (lateral / 2);
				for (int x = min; x <= max; ++x)
				{
					Point next = new Point(pos.x + x, pos.y + (y * dir));
					Tile tile = board.GetTile(next);
					if (ValidTile(tile))
						retValue.Add(tile);
				}
				lateral += 2;
			}
		}
		else
		{
			for (int x = 1; x <= horizontal; ++x)
			{
				int min = -(lateral / 2);
				int max = (lateral / 2);
				for (int y = min; y <= max; ++y)
				{
					Point next = new Point(pos.x + (x * dir), pos.y + y);
					Tile tile = board.GetTile(next);
					if (ValidTile(tile))
						retValue.Add(tile);
				}
				lateral += 2;
			}
		}

		return retValue;
	}

	bool ValidTile (Tile t)
	{
		return t != null && Mathf.Abs(t.height - unit.tile.height) <= vertical;
	}
}

This code is a bit longer, but hopefully not too hard to understand. Basically, I differentiate between a North / South axis and an East / West axis. The loops are swapped on iterating over “X” or “Y” first based on which axis we are facing.

For example, if the unit is facing north, then the outer loop will increment on “Y” for as many tiles as is specified by the horizontal field. Then an inner loop on “X” beginning with a lateral offset of 1 and incrementing by 2 (so you get odd numbers like 1, 3, 5 etc) is used to determine the sideways spread of the cone at each step away from the user.

It might be a little confusing why I iterate over the “Y” with the horizontal reach. The axis is actually treated as “Z”, but I use “Y” because the Point class used to represent a tile’s position uses the fields “X” and “Y”.

Line Ability Range

The final sample for this lesson is another direction oriented pattern – a line starting from the user and extending to the edge of the board. Before I can show you the implementation though, we need to add a bit of code to the Board class so that it can tell us its bounding area. First add the following Fields / Properties to the Board script:

public Point min { get { return _min; }}
public Point max { get { return _max; }}
Point _min;
Point _max;

Also in the Board script, we will initialize the min and max backers at the beginning of the Load method, and then update them with “better” values as we actually load each tile:

public void Load (LevelData data)
{
	_min = new Point(int.MaxValue, int.MaxValue);
	_max = new Point(int.MinValue, int.MinValue);

	for (int i = 0; i < data.tiles.Count; ++i)
	{
		GameObject instance = Instantiate(tilePrefab) as GameObject;
		Tile t = instance.GetComponent<Tile>();
		t.Load(data.tiles[i]);
		tiles.Add(t.pos, t);

		_min.x = Mathf.Min(_min.x, t.pos.x);
		_min.y = Mathf.Min(_min.y, t.pos.y);
		_max.x = Mathf.Max(_max.x, t.pos.x);
		_max.y = Mathf.Max(_max.y, t.pos.y);
	}
}

Now when the board is loaded we can quickly know the minimum and maximum positions that a tile could possibily appear within.

Go ahead and create another script named LineAbilityRange and use the following:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class LineAbilityRange : AbilityRange 
{
	public override bool directionOriented { get { return true; }}

	public override List<Tile> GetTilesInRange (Board board)
	{
		Point startPos = unit.tile.pos;
		Point endPos;
		List<Tile> retValue = new List<Tile>();

		switch (unit.dir)
		{
		case Directions.North:
			endPos = new Point(startPos.x, board.max.y);
			break;
		case Directions.East:
			endPos = new Point(board.max.x, startPos.y);
			break;
		case Directions.South:
			endPos = new Point(startPos.x, board.min.y);
			break;
		default: // West
			endPos = new Point(board.min.x, startPos.y);
			break;
		}

		while (startPos != endPos)
		{
			if (startPos.x < endPos.x) startPos.x++;
			else if (startPos.x > endPos.x) startPos.x--;

			if (startPos.y < endPos.y) startPos.y++;
			else if (startPos.y > endPos.y) startPos.y--;

			Tile t = board.GetTile(startPos);
			if (t != null && Mathf.Abs(t.height - unit.tile.height) <= vertical)
				retValue.Add(t);
		}
	
		return retValue;
	}
}

Battle States

We have created all of the Range types I will make for this lesson, but I want to go ahead and plug them into the game so you can see everything working. To accomplish this we will need to modify one of our Battle States and add another new one.

In addition we will add one more extension method to DirectionsExtensions shown below:

public static Directions GetDirection (this Point p)
{
	if (p.y > 0)
		return Directions.North;
	if (p.x > 0)
		return Directions.East;
	if (p.y < 0)
		return Directions.South;
	return Directions.West;
}

and we will add a reference in the Turn script which lets us know which ability has been selected through the Ability Menu.

public GameObject ability;

Ability Target State

Add a new script called AbilityTargetState in the Scripts/Controller/Battle States folder. This state should become active after selecting “Attack” from the Ability Menu. It will find an AbilityRange component on the user (we will simply attach one to the hero prefab as a demonstration) and then highlight the tiles it can reach.

If the range component is directional, then using movement input will cause the unit to change facing directions and the selected tiles will be changed. Otherwise, it will allow you to move the cursor around the board so you can select tiles from within the area. In addition, whoever the cursor highlights will be displayed in the secondary stat panel.

using System.Collections;
using System.Collections.Generic;

public class AbilityTargetState : BattleState 
{
	List<Tile> tiles;
	AbilityRange ar;

	public override void Enter ()
	{
		base.Enter ();
		ar = turn.ability.GetComponent<AbilityRange>();
		SelectTiles ();
		statPanelController.ShowPrimary(turn.actor.gameObject);
		if (ar.directionOriented)
			RefreshSecondaryStatPanel(pos);
	}

	public override void Exit ()
	{
		base.Exit ();
		board.DeSelectTiles(tiles);
		statPanelController.HidePrimary();
		statPanelController.HideSecondary();
	}

	protected override void OnMove (object sender, InfoEventArgs<Point> e)
	{
		if (ar.directionOriented)
		{
			ChangeDirection(e.info);
		}
		else
		{
			SelectTile(e.info + pos);
			RefreshSecondaryStatPanel(pos);
		}
	}
	
	protected override void OnFire (object sender, InfoEventArgs<int> e)
	{
		if (e.info == 0)
		{
			turn.hasUnitActed = true;
			if (turn.hasUnitMoved)
				turn.lockMove = true;
			owner.ChangeState<CommandSelectionState>();
		}
		else
		{
			owner.ChangeState<CategorySelectionState>();
		}
	}

	void ChangeDirection (Point p)
	{
		Directions dir = p.GetDirection();
		if (turn.actor.dir != dir)
		{
			board.DeSelectTiles(tiles);
			turn.actor.dir = dir;
			turn.actor.Match();
			SelectTiles ();
		}
	}

	void SelectTiles ()
	{
		tiles = ar.GetTilesInRange(board);
		board.SelectTiles(tiles);
	}
}

Category Selection State

We moved the code from the Attack method into the confirm branch of OnFire in AbilityTargetState – another step closer to its final resting place, but we have a few more states in between to add. In the meantime, we now need to use the old Attack method to call the new state:

void Attack ()
{
	turn.ability = turn.actor.GetComponentInChildren<AbilityRange>().gameObject;
	owner.ChangeState<AbilityTargetState>();
}

Note that this is still placeholder code. The attack ability should be defined in another script somewhere, like the job component (in case the type of attack is different per job type) or ideally in some sort of a skill-set manager component.

Demo

Drag the hero prefab into the scene. Add a child gameobject called “Attack” and add one of the range components to it. Apply the changes to the prefab, delete the instance, and then play the scene. When you choose “Attack” from the Ability menu you should see tiles light up on the board.

Repeat this process for each of the different types of ranges. Experiment to make sure you can change directions on the line and cone patterns, but can move the cursor in the other types.

What’s Next?

In the next lesson we will add the next step of the process – the Area of Effect. This is somewhat similar to Range, but serves a slightly differnet purpose. For example, imagine two different actions, one with an archer who has a long range and only a single tile target, vs another with a black mage who has a medium range with a splash area target for his spell. In both of these cases you could begin the process by using the ConstantRangeAbility but after moving the cursor and confirming its placement, another state will appear showing the “real” target area which must again be confirmed. It may sound repetitive, but during this state we have the ability to show a variety of other useful information to the player such as the chance of the ability hitting, and the stats of the attacked target etc.

Summary

In this lesson we began the first of several posts which are geared to the implementation of using abilities. We implemented a variety of range types which can be attached to an ability, and then added a new battle state so we could see that it selected tiles correctly.

Don’t forget that the project repository is available online here. If you ever have any trouble getting something to compile, or need an asset, feel free to use this resource.

8 thoughts on “Tactics RPG Ability Range

  1. Other great tutorial!! You’re a genius, man!!

    One Question in this part Category Selection State, I don’t get to do. Look in the repository of your project, but are not yet this tutorial files. I will wait for the upload.

    Thanks!!

  2. Hi Jon, I have a few questions that you might have covered in the next step, but I need to get my head around this first.

    I don’t quite get how the line and cone range work. I mean, I understand the code in specifying the range, but I can’t get an image on how the two range implementation would act in a battle, mainly because the tile selector doesn’t move (we instead move the unit’s orientation). I honestly haven’t seen FFT, and the one tactical RPG I’ve played is Wakfu, which display all available tiles in range regardless of unit orientation.

    On another note, say that I want to have two abilities; one with range of a n-tile cross and the other has 4 diagonal lines from the unit’s position. Do you create two new different scripts for the ranges or could you modify it as a component?

    Thanks in advance, and great step as always!

    1. The line and cone ranges are used in area based attacks, rather than specific tile location based attacks. So for example, on a cone based attack you might imagine a dragon’s fire breath coming from the dragon and fanning out in a cone for a certain distance. Anything that is located within that area is subject to the effects.

      For the new area range types of a cross or diagonal lines I would create a new component which followed the basic pattern of the ones I have already setup.

      Glad you are enjoying it 🙂

  3. Quick question about the implementation of the lateral integer:

    So, from my current understanding, it’s used to make it so the min/max each decrease/increase by 1 every loop by dividing the current lateral value by 2, but wouldn’t it do the same thing to merely initialize the lateral value at zero, and increment it by 1 for each loop so that min = -lateral and max = lateral for each step?

    Could be missing something so I figured I’d ask, just in case.

      1. Mkay. I think I can see where you got the current implementation, though, since each step is the width of the cone at that point, which is neat. Thanks for the clarification.

Leave a Reply

Your email address will not be published. Required fields are marked *