Tactics RPG Ability Area of Effect

To fully select targets for an ability we need a few more steps. We began with specifying a Range. In this lesson we will implent a variety of Area Of Effect components which are applied based off the selection from the range. In addition, each ability may have multiple “effects” – each of which may specify a unique set of valid targets. I will provide a couple of implementations for these as well, so that we can have a completed targeting selection loop in place.

Area Of Effect

Some abilities have a range with a single target, like an archer. Some abilities have a range with a subrange target, like the blast radius of a magic spell. This difference in targeting area is what I will refer to as the Area of Effect. All abilities will need to have this type of component, just like they needed a Range component. By mixing and matching the two components on your GameObjects you can have a nice variety of ways to select the target(s) for your abilities.

Let’s begin by creating the abstract base class. Create a new script called AbilityArea and place it in Scripts/View Model Component/Ability/Area Of Effect.

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

public abstract class AbilityArea : MonoBehaviour
{
	public abstract List<Tile> GetTilesInArea (Board board, Point pos);
}

Much like the Range component, this script returns a list of tiles in its area. These are tiles which need to be highlighted and shown to the player as candidate locations for effect application. Note that in addition to the Board reference, we must also pass a Point parameter to the method. This is used to indicate a selected location within a range from which to determine what tiles to grab.

Unit Ability Area

Our first concrete subclass is called UnitAbilityArea and it simply returns whatever Tile exists at the indicated position. This could be used for implementing the attack ability of an archer.

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

public class UnitAbilityArea : AbilityArea 
{
	public override List<Tile> GetTilesInArea (Board board, Point pos)
	{
		List<Tile> retValue = new List<Tile>();
		Tile tile = board.GetTile(pos);
		if (tile != null)
			retValue.Add(tile);
		return retValue;
	}
}

Specify Ability Area

When you want to target an area of tiles around the cursor’s position, you will use our next subclass, SpecifyAbilityArea. For example, this would be used to implement a black mage’s fire spell which can also hit tiles adjacent to the targeted location.

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

public class SpecifyAbilityArea : AbilityArea 
{
	public int horizontal;
	public int vertical;
	Tile tile;

	public override List<Tile> GetTilesInArea (Board board, Point pos)
	{
		tile = board.GetTile(pos);
		return board.Search(tile, ExpandSearch);
	}

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

Full Ability Area

Our next concrete subclass is called FullAbilityArea, and as the name implies, every tile that is highlighted by an ability’s range is also a potential target for this area of effect. This could be used for a dragoon’s fire breath attack (again I am referencing Final Fantasy Tactics Advance here).

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

public class FullAbilityArea : AbilityArea 
{
	public override List<Tile> GetTilesInArea (Board board, Point pos)
	{
		AbilityRange ar = GetComponent<AbilityRange>();
		return ar.GetTilesInRange(board);
	}
}

Effect Target

As I briefly mentioned in the introduction, an ability can have more than one effect. For example, a Cure spell which normally restores hit points to units might have a secondary effect of damaging the undead. So in this example, the first effect of the ability would only target living units, and the second effect would only target undead units. Even if the ability only has a single effect, it still may require special targeting. The ability to determine what is and is not a valid target will be another component.

Create an abstract base class called AbilityEffectTarget and place it in Scripts/View Model Component/Ability/Effect Target. Use the following implementation:

using UnityEngine;
using System.Collections;

public abstract class AbilityEffectTarget : MonoBehaviour 
{
	public abstract bool IsTarget (Tile tile);
}

Couldn’t be much easier right? The only thing this component does is to determine whether or not the effect applies to whatever may or may not be located at the specified board tile.

Default Ability Effect Target

Let’s create our first concrete subclass called DefaultAbilityEffectTarget. Most ability effects will probably use this – it simply requires that there be something on the tile, and that the something which is there has hit points. Note that it doesn’t necessarily have to be a normal unit – you may or may not wish to include that requirement.

using UnityEngine;
using System.Collections;

public class DefaultAbilityEffectTarget : AbilityEffectTarget 
{
	public override bool IsTarget (Tile tile)
	{
		if (tile == null || tile.content == null)
			return false;

		Stats s = tile.content.GetComponent<Stats>();
		return s != null && s[StatTypes.HP] > 0;
	}
}

KO’d Ability Effect Target

Just for a quick bit of variety I decided to create one more concrete targeter. Add another script called KOdAbilityEffectTarget. This time we are looking for an entity with no hit points. For example, a resurrection skill might require this target type.

using UnityEngine;
using System.Collections;

public class KOdAbilityEffectTarget : AbilityEffectTarget 
{
	public override bool IsTarget (Tile tile)
	{
		if (tile == null || tile.content == null)
			return false;

		Stats s = tile.content.GetComponent<Stats>();
		return s != null && s[StatTypes.HP] <= 0;
	}
}

Turn

Now that we will truly be selecting the targets of an ability, we will store them in the Turn object, so that they are available between multiple battle states. Add a field for a List of Tile called targets:

public List<Tile> targets;

Battle States

We have a few more components to play with, so let’s plug them into the game and see how they work. As is normally the case, we will need to both modify and add new States. I will add a few extra states to help complete the loop of actually using an ability and ending a turn by choosing a facing direction.

Confirm Ability Target State

Once a user has selected a direction for their range, or a location within their range (depending upon the range type of the ability), we will enter a new state called ConfirmAbilityTargetState. This state will highlight a (potentially) new set of tiles which shows the area that can be effected by the ability using the current cursor position or facing angle of the active unit.

When the state enters, we will look thru the effect targeting components attached to the ability and determine what, if any, valid targets fall within the selected area. If we have a target, then we will show it in the secondary stat panel. If we have more than one target, you can use the movement input to cycle through which of the targets is displayed in the stat panel.

Note that in the future, when we determine things like the predicted damage of an ability, hit chance, etc. we will use this state to show this information in the UI to help the player make more informed decisions.

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

public class ConfirmAbilityTargetState : BattleState
{
	List<Tile> tiles;
	AbilityArea aa;
	int index = 0;

	public override void Enter ()
	{
		base.Enter ();
		aa = turn.ability.GetComponent<AbilityArea>();
		tiles = aa.GetTilesInArea(board, pos);
		board.SelectTiles(tiles);
		FindTargets();
		RefreshPrimaryStatPanel(turn.actor.tile.pos);
		SetTarget(0);
	}

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

	protected override void OnMove (object sender, InfoEventArgs<Point> e)
	{
		if (e.info.y > 0 || e.info.x > 0)
			SetTarget(index + 1);
		else
			SetTarget(index - 1);
	}

	protected override void OnFire (object sender, InfoEventArgs<int> e)
	{
		if (e.info == 0)
		{
			if (turn.targets.Count > 0)
			{
				owner.ChangeState<PerformAbilityState>();
			}
		}
		else
			owner.ChangeState<AbilityTargetState>();
	}

	void FindTargets ()
	{
		turn.targets = new List<Tile>();
		AbilityEffectTarget[] targeters = turn.ability.GetComponentsInChildren<AbilityEffectTarget>();
		for (int i = 0; i < tiles.Count; ++i)
			if (IsTarget(tiles[i], targeters))
				turn.targets.Add(tiles[i]);
	}
	
	bool IsTarget (Tile tile, AbilityEffectTarget[] list)
	{
		for (int i = 0; i < list.Length; ++i)
			if (list[i].IsTarget(tile))
				return true;
		
		return false;
	}

	void SetTarget (int target)
	{
		index = target;
		if (index < 0)
			index = turn.targets.Count - 1;
		if (index >= turn.targets.Count)
			index = 0;
		if (turn.targets.Count > 0)
			RefreshSecondaryStatPanel(turn.targets[index].pos);
	}
}

Ability Target State

In order to reach our new state, we need to modify what happens when you use the confirm input during the AbilityTargetState script. Instead of pretending like we just completed an attack, we will first verify that our selection is valid, and if so, enter the ConfirmAbilityTargetState which we just created.

protected override void OnFire (object sender, InfoEventArgs<int> e)
{
	if (e.info == 0)
	{
		if (ar.directionOriented || tiles.Contains(board.GetTile(pos)))
			owner.ChangeState<ConfirmAbilityTargetState>();
	}
	else
	{
		owner.ChangeState<CategorySelectionState>();
	}
}

Perform Ability State

After having selected an ability and a target to apply the ability to, it is time to actually take action. There are a ton of ways this could be implemented, though Mechanim would probably be used since we are focusing on Unity. Ultimately we need some way to have events tied to animation so that we can do something like swing a sword, and then at a specific point in the animation, play a sound and apply the effect of the ability – which in that case would be to reduce the target’s hit points.

This state is sort of a placeholder – I left comments showing potential places for logic to appear. I also added a TemporaryAttackExample method. As the name hopefully implies, this is placeholder code. In a more complete project, I would not directly do the work of an Ability’s Effect in this state. Instead, there would be another class per effect, very much like we did with the Feature component of an item. The real implementation would probably loop through the effects and targets and attempt to apply the effect on each target.

When the animation and application of the ability are complete, we continue onto the next relevant state.

using UnityEngine;
using System.Collections;

public class PerformAbilityState : BattleState 
{
	public override void Enter ()
	{
		base.Enter ();
		turn.hasUnitActed = true;
		if (turn.hasUnitMoved)
			turn.lockMove = true;
		StartCoroutine(Animate());
	}
	
	IEnumerator Animate ()
	{
		// TODO play animations, etc
		yield return null;
		// TODO apply ability effect, etc
		TemporaryAttackExample();
		
		if (turn.hasUnitMoved)
			owner.ChangeState<EndFacingState>();
		else
			owner.ChangeState<CommandSelectionState>();
	}
	
	void TemporaryAttackExample ()
	{
		for (int i = 0; i < turn.targets.Count; ++i)
		{
			GameObject obj = turn.targets[i].content;
			Stats stats = obj != null ? obj.GetComponentInChildren<Stats>() : null;
			if (stats != null)
			{
				stats[StatTypes.HP] -= 50;
				if (stats[StatTypes.HP] <= 0)
					Debug.Log("KO'd Uni!", obj);
			}
		}
	}
}

End Facing State

This state is used to wrap up the end of a turn, instead of simply choosing “Wait” from the ability menu. It allows you a chance to decide which direction you want a unit to face before giving control to the next unit.

In the future we will add some UI here of arrows over the active units head which indicate what you are supposed to be doing.

using UnityEngine;
using System.Collections;

public class EndFacingState : BattleState 
{
	Directions startDir;

	public override void Enter ()
	{
		base.Enter ();
		startDir = turn.actor.dir;
		SelectTile(turn.actor.tile.pos);
	}
	
	protected override void OnMove (object sender, InfoEventArgs<Point> e)
	{
		turn.actor.dir = e.info.GetDirection();
		turn.actor.Match();
	}
	
	protected override void OnFire (object sender, InfoEventArgs<int> e)
	{
		switch (e.info)
		{
		case 0:
			owner.ChangeState<SelectUnitState>();
			break;
		case 1:
			turn.actor.dir = startDir;
			turn.actor.Match();
			owner.ChangeState<CommandSelectionState>();
			break;
		}
	}
}

Command Selection State

If a player chooses Wait from the Ability Menu, let’s go to the EndFacingState instead of immediately ending the turn. Not only does it allow them to face a different direction, but it provides an opportunity for them to “cancel” back out just in case they had chosen to wait by accident.

protected override void Confirm ()
{
	switch (abilityMenuPanelController.selection)
	{
	case 0: // Move
		owner.ChangeState<MoveTargetState>();
		break;
	case 1: // Action
		owner.ChangeState<CategorySelectionState>();
		break;
	case 2: // Wait
		owner.ChangeState<EndFacingState>();
		break;
	}
}

Demo

Expand the Hero prefab’s hierarchy in the project pane and select the Attack object from before. Experiment by adding different combinations and settings for the different ranges, areas, and effect targets (one of each). Then play the scene, and notice that after attacking another unit(s) their hit points will be reduced. Note that you can even change components while the game is playing, so it would be easy to give each of the Units on the board a different configuration.

Summary

In this lesson we wrapped up the selection process for an ability. A user can see how far an ability will reach (range), what area the ability will affect (area of effect), and who within that area will be targeted (effect target). I demonstrated how keeping each of these as separate components makes it easy to have a large number of configurations and adds great diversity to your game.

We also added a few extra battle states to help the process feel more complete. We will need several more UI pieces, and will need to actually create the abilities and their effects themselves, so stay tuned – I plan to get there eventually!

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.

Advertisements

3 thoughts on “Tactics RPG Ability Area of Effect

  1. I’m still loving the series. I had a question about architecture, and this is probably something you are going to get to in a future post (and did cover a bit in your Items post). Say you wanted to be able to equip various abilities at run time (maybe giving 3 random ninja attacks to a ninja from a pool of 6). Each of those abilities would need an AbilityRange, an AbilityEffectTarget, and an AbillityArea component (plus whatever other components we end up adding, like a graphics component and a damage component). Would it be good to store all that information in a spreadsheet (using a string to represent which version of AbilityRange, etc, we wanted on that ability) and then create a factory class that would parse the spreadsheet in the editor to make prefabs that could be called by name? And then store them in something like Resources\Abilities and use Resources.Load to access them when we need them? Or would it be better to have a AbilityHolder GameObject that holds all the prefab abilities we created and returns them based on the name we give it?

    Thanks for this- I am really learning a ton.

    Like

    1. Great question Jordan. As you mentioned, I would probably want to find a clever way to store the data in a spreadsheet and create prefabs from that as we did with Jobs. I would pick “Resources.Load” over a single AbilityHolder which had a reference to all of the prefabs. This is because when you grab a reference to a project asset it actually loads the entire asset into memory. So if you have 100’s of skills each with custom special fx then all of the models, animations, particles, textures, etc would all also be loaded. Even if you had enough memory to spare, it would still increase loading time.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s