Tactics RPG A.I. Part 1

I’ve decided to split the A.I. implementation into two parts. This first part will cover the “what” as in “what ability do I want to use?”. This is the easier of the two problems. The next portion on “how” as in “how do I know where to move and aim to best use the ability?” is rather complex. Since this bit is easier, I will also include the code that ties the A.I. into the battle states, as well as adding another user interface view which shows the name of the ability that a computer controlled unit has chosen to use.

The A.I. for this tactics project will select abilities to use based on a sequential list concept, much like the Final Fantasy 1 example from the previous post. In this lesson, we will be creating a system which can cycle through this sort of attack sequence. We will be deciding what to use and who or what to use it on (in a general sense, not a specific target).

Targets

In order to be able to apply certain abilities to certain unit types, we will need some way to “flag” what a valid target for an ability will be. This is sort of like the way the Gambits in Final Fantasy 12 had both a target and an action. For this I created a new enum called Targets in the Scripts/Enums folder:

using UnityEngine;
using System.Collections;

public enum Targets
{
	None,
	Self,
	Ally,
	Foe,
	Tile
}

Alliance

Of course, the target types such as ally and foe are subject to the unit’s perspective. In other words, the foe of a monster would be a hero and vice versa. So to use the Targets enum we will update the Alliance component:

public bool confused;

public bool IsMatch (Alliance other, Targets targets)
{
	bool isMatch = false;
	switch (targets)
	{
	case Targets.Self:
		isMatch = other == this;
		break;
	case Targets.Ally:
		isMatch = type == other.type;
		break;
	case Targets.Foe:
		isMatch = (type != other.type) && other.type != Alliances.Neutral;
		break;
	}
	return confused ? !isMatch : isMatch;
}

The confused field isn’t used by anything currently, and I haven’t actually tested it. I included it to hint at what could be the target of a nasty ability effect. It is used to flip whether or not the unit believes something is a match.

Plan Of Attack

As soon as it is determined that it is the computer’s turn to make a move, we will need to formulate a plan of attack. This means I decide what ability to use, who or what to use the ability on, where I move to on the board, and where I cast the ability or which direction I face while casting the ability. All of this data is stored in a simple object which will be populated by various steps of the AI process.

In part one we will fill out the “ability” and “target” fields. I will add a placeholder code for the implementation of the other fields just to give you an idea of how everything will work, but the “real” work will be presented in part two. Create a new script named PlanOfAttack and place it in a new directory Scripts/View Model Component/AI:

using UnityEngine;
using System.Collections;

public class PlanOfAttack 
{
	public Ability ability;
	public Targets target;
	public Point moveLocation;
	public Point fireLocation;
	public Directions attackDirection;
}

Base Ability Picker

The first step in the AI process is determining what Ability to use and who or what to use it on. For this I created a new component called the BaseAbilityPicker which I put in a Scripts/View Model Component/AI/Ability Picker directory.

using UnityEngine;
using System.Collections;

public abstract class BaseAbilityPicker : MonoBehaviour
{
	#region Fields
	protected Unit owner;
	protected AbilityCatalog ac;
	#endregion

	#region MonoBehaviour
	void Start ()
	{
		owner = GetComponentInParent<Unit>();
		ac = owner.GetComponentInChildren<AbilityCatalog>();
	}
	#endregion

	#region Public
	public abstract void Pick (PlanOfAttack plan);
	#endregion
	
	#region Protected
	protected Ability Find (string abilityName)
	{
		for (int i = 0; i < ac.transform.childCount; ++i)
		{
			Transform category = ac.transform.GetChild(i);
			Transform child = category.FindChild(abilityName);
			if (child != null)
				return child.GetComponent<Ability>();
		}
		return null;
	}

	protected Ability Default ()
	{
		return owner.GetComponentInChildren<Ability>();
	}
	#endregion
}

This abstract base class indicates that we simply want to be able to pick an ability to use in a turn, but it doesn’t care how or why the ability is chosen. The abstract Pick method must be implemented by concrete subclasses and in that step will update the “Plan of Attack” object which had been passed along as a parameter.

For convenience sake, we added a method which can find an ability which matches a given name (as a string). This was important since our units are created dynamically and we can’t directly link from the picker to the ability since it wont have been created yet.

Fixed Ability Picker

The primary “picker” used by this first implementation will be one which says exactly what we want to use. This goes back to the sequential list idea where I specify something like “Attack, Fire, Heal” and it will use those specific abilities.

using UnityEngine;
using System.Collections;

public class FixedAbilityPicker : BaseAbilityPicker
{
	public Targets target;
	public string ability;

	public override void Pick (PlanOfAttack plan)
	{
		plan.target = target;
		plan.ability = Find(ability);

		if (plan.ability == null)
		{
			plan.ability = Default();
			plan.target = Targets.Foe;
		}
	}
}

We accomplish the job by exposing two fields, the name of an ability and who or what we want to target with that ability. Then in the Pick method, we use the base class’s Find method to get the actual ability of the same name and then update the plan of attack with our decisions. If the named ability can’t be found, we grab the first ability it can find instead (which would be Attack).

Random Ability Picker

Because I had mentioned that it would be nice to add a little variety to the attack sequence, we can help break up the pattern a little by adding some randomly chosen abilities. Note that instead of maintaining pairs of ability names and targets, I simply refer to a list of other ability pickers. In most cases those will be FixedAbilityPickers, but we could also nest other complex types of pickers if we wanted to. Then we randomly grab one of the pickers and return the value that the selected picker holds.

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

public class RandomAbilityPicker : BaseAbilityPicker
{
	public List<BaseAbilityPicker> pickers;

	public override void Pick (PlanOfAttack plan)
	{
		int index = Random.Range(0, pickers.Count);
		BaseAbilityPicker p = pickers[index];
		p.Pick(plan);
	}
}

Attack Pattern

Next we need a component which can organize all of our pickers into the actual sequential list and keep track of its own position within that list. For this I added a new script named AttackPattern to the Scripts/View Model Component/AI folder.

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

public class AttackPattern : MonoBehaviour 
{
	public List<BaseAbilityPicker> pickers;
	int index;
	
	public void Pick (PlanOfAttack plan)
	{
		pickers[index].Pick(plan);
		index++;
		if (index >= pickers.Count)
			index = 0;
	}
}

At this point you can begin creating resources of “Attack Pattern” prefabs. The basic idea is that you create an empty game object with the attack pattern script attached. Then add children gameobjects, one for each ability picker. Whenever I create one of the Random Ability Picker types, I first would create all of the Fixed types which it would refer to. By keeping each picker on its own gameobject it is very easy to drag and drop them in the inspector to the compound picker types and/or to the attack pattern list in the root.

This is still prototype level work, so I think it’s good to just manually create what we want to work with. When I have verified that the system works well and is sufficiently flexible, it would be nice to come up with a way to create them by simpler “recipes” and allow a factory to automatically generate them like we do with ability catalogs.

An example image of an attack pattern setup appears below. I have added a few of these prefabs to the project in my repository, and they will be loaded in the Unit factory as part of the unit creation process.

WarriorAttackPattern_zpsufucoyzq

To the UnitRecipe class I added a string name for this prefab called “strategy”…

public string strategy;

… and then I used that name to load the prefab in the UnitFactory with the following method

static void AddAttackPattern (GameObject obj, string name)
{
	Driver driver = obj.AddComponent<Driver>();
	if (string.IsNullOrEmpty(name))
	{
		driver.normal = Drivers.Human;
	}
	else
	{
		driver.normal = Drivers.Computer;
		GameObject instance = InstantiatePrefab("Attack Pattern/" + name);
		instance.transform.SetParent(obj.transform);
	}
}

Note that I also added a few new abilities because I wanted to verify that I could target different types, ranges, etc. and make sure that everything worked well. Feel free to grab them from my repository since they are not listed here.

Drivers

In a lot of Final Fantasy games, you might lose control of your own units (such as through Charm or Berserk), but you might also gain control of enemy units with special Abilities of your own. Because the control of a unit can change between human control and computer control, I added a new set of flags to indicate who should be “driving” at the beginning of a turn.

using UnityEngine;
using System.Collections;

public enum Drivers
{
	None,
	Human,
	Computer
}

In addition, I created a component to hold that flag called Driver in the Scripts/View Model Component/Actor directory:

using UnityEngine;
using System.Collections;

public class Driver : MonoBehaviour 
{
	public Drivers normal;
	public Drivers special;

	public Drivers Current
	{
		get
		{
			return special != Drivers.None ? special : normal;
		}
	}
}

The “normal” flag indicates how a unit was loaded initially, and the “special” flag could indicate that the default behavior is overridden, perhaps by a status ailment, etc. I haven’t implemented any of these kinds of abilities yet, but I put the code there as a hint to how it might be handled.

Computer Player

Now we need a sort of overall manager for the A.I. components. This class will be responsible for tying all of these new classes together so that they don’t all need to be coupled together themselves. Basically the Computer Player script will be responsible for creating the plan of action object as well as either directly implementing logic to fill-out the plan, or it can find and make use of other components which can handle the relevant task.

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

public class ComputerPlayer : MonoBehaviour 
{
	BattleController bc;
	Unit actor { get { return bc.turn.actor; }}

	void Awake ()
	{
		bc = GetComponent<BattleController>();
	}

	public PlanOfAttack Evaluate ()
	{
		// Create and fill out a plan of attack
		PlanOfAttack poa = new PlanOfAttack();

		// Step 1: Decide what ability to use
		AttackPattern pattern = actor.GetComponentInChildren<AttackPattern>();
		if (pattern)
			pattern.Pick(poa);
		else
			DefaultAttackPattern(poa);

		// Step 2: Determine where to move and aim to best use the ability
		PlaceholderCode(poa);

		// Return the completed plan
		return poa;
	}

	void DefaultAttackPattern (PlanOfAttack poa)
	{
		// Just get the first "Attack" ability
		poa.ability = actor.GetComponentInChildren<Ability>();
		poa.target = Targets.Foe;
	}

	void PlaceholderCode (PlanOfAttack poa)
	{
		// Move to a random location within the unit's move range
		List<Tile> tiles = actor.GetComponent<Movement>().GetTilesInRange(bc.board);
		Tile randomTile = (tiles.Count > 0) ? tiles[ UnityEngine.Random.Range(0, tiles.Count) ] : null;
		poa.moveLocation = (randomTile != null) ? randomTile.pos : actor.tile.pos;

		// Pick a random attack direction (for direction based abilities)
		poa.attackDirection = (Directions)UnityEngine.Random.Range(0, 4);

		// Pick a random fire location based on having moved to the random tile
		Tile start = actor.tile;
		actor.Place(randomTile);
		tiles = poa.ability.GetComponent<AbilityRange>().GetTilesInRange(bc.board);
		if (tiles.Count == 0)
		{
			poa.ability = null;
			poa.fireLocation = poa.moveLocation;
		}
		else
		{
			randomTile = tiles[ UnityEngine.Random.Range(0, tiles.Count) ];
			poa.fireLocation = randomTile.pos;
		}
		actor.Place(start);
	}

	public Directions DetermineEndFacingDirection ()
	{
		return (Directions)UnityEngine.Random.Range(0, 4);
	}
}

This script is only partially implemented, and a good chunk of it is only placeholder code which we will replace in Part 2. The important parts are that one of the Battle States will tell this script to Evaluate on a computer controlled unit’s turn, and at that time we will create and return a filled out PlanOfAttack so that the other Battle States will know what to show.

Inside the Evaluate method we grab a reference to the AttackPattern component (assuming there is one) and let it fill out a portion of the plan of attack. If the component is missing we have a fallback which uses a simple attack instead.

The placeholder code shouldn’t occupy much of your attention, but if you are curious, it simply grabs relevant components to know what the move range of a unit is, picks a random location from that list, then determines the range of the selected ability based on that random location and picks another random tile as an aiming location. We also provide a random attack direction, because some abilities require a direction rather than an aim location.

In addition I added a DetermineEndFacingDirection method which will be needed in the final version, but which is also a placeholder implementation. In the real version we will use that to cause the unit to turn and face the nearest opponent rather than leaving its back exposed. For now, I just want to see everything move.

Battle Message Controller

We haven’t connected any sort of animations or special fx to our usage of abilities, so at the moment there is no visual way to see what ability our new A.I. is actually picking. You could simply add a Debug.Log and print its name, but I thought we should go ahead and add something a little nicer to the interface. We will add a black bar across the top of the screen with a label in it that shows the name of the ability. Add a new script named BattleMessageController to the Scripts/Controller directory:

using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections;

public class BattleMessageController : MonoBehaviour 
{
	[SerializeField] Text label;
	[SerializeField] GameObject canvas;
	[SerializeField] CanvasGroup group;
	EasingControl ec;

	void Awake ()
	{
		ec = gameObject.AddComponent<EasingControl>();
		ec.duration = 0.5f;
		ec.equation = EasingEquations.EaseInOutQuad;
		ec.endBehaviour = EasingControl.EndBehaviour.Constant;
		ec.updateEvent += OnUpdateEvent;
	}

	public void Display (string message)
	{
		group.alpha = 0;
		canvas.SetActive(true);
		label.text = message;
		StartCoroutine(Sequence());
	}

	void OnUpdateEvent (object sender, EventArgs e)
	{
		group.alpha = ec.currentValue;
	}

	IEnumerator Sequence ()
	{
		ec.Play();

		while (ec.IsPlaying)
			yield return null;

		yield return new WaitForSeconds(1);

		ec.Reverse();

		while (ec.IsPlaying)
			yield return null;

		canvas.SetActive(false);
	}
}

In the past I have used extension methods to tie an animation feature to a component. That would have been perfectly acceptable here as well – perhaps even better since it would be reusable, but the code was already so simple I didn’t bother. All I am doing is fading the UI element in, letting it stay on screen for a second, and fading it back out.

The prefab for this object is also pretty easy. It has a root GameObject with this script attached. Its first child is the Canvas (keeping it separated from the GameObject with the script lets me disable the Canvas without also disabling the controller script). The canvas has a sliced image using the ActionNameBacker sprite, and then a child label is attached to the image. See if you can build it yourself, if not, refer to the one on my repository.

Battle Controller

Add references to the new BattleMessageController and ComputerPlayer to our overal BattleController script:

public BattleMessageController battleMessageController;
public ComputerPlayer cpu;

Don’t forget to attach the Computer Player component to the same gameobject. The Battle Message Controller will be on its own prefab (parent it to the Battle Controller though) and then connect the references in the inspector.

Battle State

Now we have pretty much everything we need, we just need to tie it into the game itself. We will do this through the battle states. I considered making separate states for the A.I. but since most of the code would be the same whether or not the A.I. or the player was driving I ended up leaving them together.

Open the base state, BattleState and add a protected field which will show who the current driver of a Unit is. We will assign the value in the Enter method. Plus, I added a condition that I only listen to the Input events if the driver is the Player. This way, whenever the A.I. is driving I don’t need to worry about input from the player interfering with anything.

protected Driver driver;

public override void Enter ()
{
	driver = (turn.actor != null) ? turn.actor.GetComponent<Driver>() : null;
	base.Enter ();
}

protected override void AddListeners ()
{
	if (driver == null || driver.Current == Drivers.Human)
	{
		InputController.moveEvent += OnMove;
		InputController.fireEvent += OnFire;
	}
}

Base Ability Menu State

A lot of the Battle States show menus to allow the player to select an ability, but we don’t need to show them for the computer AI to pick an ability. In fact, we specifically don’t want to show them, or else it would be confusing and the player might think they were supposed to do something. So add an if statement just before the call to LoadMenu to verify that it is the Human player who is driving the current unit:

public override void Enter ()
{
	// ... old code up here... 
	
	// Add this condition
	if (driver.Current == Drivers.Human)
		LoadMenu();
}

Command Selection State

The first state for a unit’s turn is the Command Selection State. It is in this state that we will need to let the A.I. create the Plan of Attack. When the state Enters, we will call a new method to handle this whenever the A.I. is driving:

// Add this at the end of the Enter method
if (driver.Current == Drivers.Computer)
	StartCoroutine( ComputerTurn() );
	
IEnumerator ComputerTurn ()
{
	if (turn.plan == null)
	{
		turn.plan = owner.cpu.Evaluate();
		turn.ability = turn.plan.ability;
	}

	yield return new WaitForSeconds (1f);

	if (turn.hasUnitMoved == false && turn.plan.moveLocation != turn.actor.tile.pos)
		owner.ChangeState<MoveTargetState>();
	else if (turn.hasUnitActed == false && turn.plan.ability != null)
		owner.ChangeState<AbilityTargetState>();
	else
		owner.ChangeState<EndFacingState>();
}

The Command Selection State becomes active at a few points during a Units turn, but we only want to generate the code to fill out a plan of attack the first time. So we check for null and generate a plan only then.

I put all of the code in a coroutine and added a 1 second delay before continuing to the next state. This isn’t necessary for the computer, but by having some delays it allows a human observer to better track what is happening. For example, at this point a new unit has been selected and the camera will be animating to center over it. By waiting for the full second, the observer will realize (hopefully) that it is a computer controlled unit which has been selected and that now it is just time to continue watching.

After the brief pause, we allow one of three other states to take over. If we haven’t moved and need to, then we continue down that path. Otherwise if we haven’t acted and need to, then we continue down that path. If there are no move or action steps to take, we complete the turn with the end facing state.

Of course for this code to compile I added a field to hold the PlanOfAttack as a field on the Turn class. I also set the field to null whenever the Change method is called.

// Add this field
public PlanOfAttack plan;

// Add this to the end of the Change method
plan = null;

Move Target State

When a human is driving, this state allows the cursor to be moved around the board and for a tile to be selected using input (from a keyboard etc). We can mimic this behavior for the A.I. and it will help the human observer understand what is about to happen. When this state is entered we check whether or not the A.I. is driving in the Enter method. If so, we call the computers method. All we do is loop with a small wait between each step until the cursor has reached the move location field which had been provided in the plan of attack.

// Add this to the end of the Enter method
if (driver.Current == Drivers.Computer)
	StartCoroutine(ComputerHighlightMoveTarget());
	
IEnumerator ComputerHighlightMoveTarget ()
{
	Point cursorPos = pos;
	while (cursorPos != turn.plan.moveLocation)
	{
		if (cursorPos.x < turn.plan.moveLocation.x) cursorPos.x++;
		if (cursorPos.x > turn.plan.moveLocation.x) cursorPos.x--;
		if (cursorPos.y < turn.plan.moveLocation.y) cursorPos.y++;
		if (cursorPos.y > turn.plan.moveLocation.y) cursorPos.y--;
		SelectTile(cursorPos);
		yield return new WaitForSeconds(0.25f);
	}
	yield return new WaitForSeconds(0.5f);
	owner.ChangeState<MoveSequenceState>();
}

Ability Target State

Again, we have the same basic idea as we did with the Move Target State. The only real difference would be when using an ability which is based on direction – then we just turn the unit to face that direction. Otherwise we animate the cursor moving to the correct spot when it is the A.I.’s turn:

// Add to the end of the Enter method
if (driver.Current == Drivers.Computer)
	StartCoroutine(ComputerHighlightTarget());
	
IEnumerator ComputerHighlightTarget ()
{
	if (ar.directionOriented)
	{
		ChangeDirection(turn.plan.attackDirection.GetNormal());
		yield return new WaitForSeconds(0.25f);
	}
	else
	{
		Point cursorPos = pos;
		while (cursorPos != turn.plan.fireLocation)
		{
			if (cursorPos.x < turn.plan.fireLocation.x) cursorPos.x++;
			if (cursorPos.x > turn.plan.fireLocation.x) cursorPos.x--;
			if (cursorPos.y < turn.plan.fireLocation.y) cursorPos.y++;
			if (cursorPos.y > turn.plan.fireLocation.y) cursorPos.y--;
			SelectTile(cursorPos);
			yield return new WaitForSeconds(0.25f);
		}
	}
	yield return new WaitForSeconds(0.5f);
	owner.ChangeState<ConfirmAbilityTargetState>();
}

Confirm Ability Target State

When a human was driving for this state, we had shown a UI element that indicated the hit chance and damage prediction of an ability. It would provide an opportunity for a user to make an educated decision about which ability would be most effective against certain opponents. Whenever the computer is driving, we will simply use it to display the name of the ability that had chosen.

public override void Enter ()
{
	// ... old code up here...
	
	if (turn.targets.Count > 0)
	{
		// Only show this UI for Human controlled units
		if (driver.Current == Drivers.Human)
			hitSuccessIndicator.Show();
		SetTarget(0);
	}
	
	// Only show this UI for AI controlled units
	if (driver.Current == Drivers.Computer)
		StartCoroutine(ComputerDisplayAbilitySelection());
}

IEnumerator ComputerDisplayAbilitySelection ()
{
	owner.battleMessageController.Display(turn.ability.name);
	yield return new WaitForSeconds (2f);
	owner.ChangeState<PerformAbilityState>();
}

End Facing State

For the final state, we allow the computer to decide an end facing direction. After a short pause we apply it.

// Added to the end of the Enter Method
if (driver.Current == Drivers.Computer)
	StartCoroutine(ComputerControl());
	
IEnumerator ComputerControl ()
{
	yield return new WaitForSeconds(0.5f);
	turn.actor.dir = owner.cpu.DetermineEndFacingDirection();
	turn.actor.Match();
	owner.facingIndicator.SetDirection(turn.actor.dir);
	yield return new WaitForSeconds(0.5f);
	owner.ChangeState<SelectUnitState>();
}

Demo

There are a few little polish items I added this time, but didn’t discuss since they are not relevant to the A.I. For example I had the board’s tiles be parented to itself so the scene hierarchy looks a little cleaner. I also added a few new abilities which caused me to add another ability effect and ability effect target. Check out the repository and you can see EnemyAbilityEffectTarget and EsunaAbilityEffect. I modified the Line Ability Range so that it would only extend as far as the horizontal distance field indicated. This way I could easily make a normal attack ability that simply targets the first tile in front of the user. Be sure to check out the repository commit in order to see the full list of changes.

As one last little optional tidbit, feel free to add some placeholder code to the TurnOrderController script which skips the turns of human controlled units. Why would I do that? Well I wouldn’t – at least not for the final game, but as a debug helper it can be nice to just watch the A.I. for awhile and make sure it is acting like I expect. I didn’t commit this code to the repo, but it could be handy in this post as well as Part 2.

bool CanTakeTurn (Unit target)
{
	// OPTIONAL === Add this bit to skip the player turns so you can just watch
	Alliance a = target.GetComponentInChildren<Alliance>();
	if (a.type == Alliances.Hero)
		return false;
	// === END OPTIONAL

	BaseException exc = new BaseException( GetCounter(target) >= turnActivation );
	target.PostNotification( TurnCheckNotification, exc );
	return exc.toggle;
}

When you run the project you should see something which looks like the following video:

Summary

This lesson was content rich, but mostly with easy to understand concepts. The main focus of this lesson was showing the process of how an A.I. unit will choose an ability to use on any given turn. The decision is made by use of a sequential attack pattern. In addition, we took the time to plug the A.I. into all of the battle states and allow the A.I. decisions to be presented to the player by animating the cursor movements and by showing the name of the chosen ability.

At this point we have fully autonomous agents, although they don’t look incredibly smart. This is because even though they can move around the board and use abilities, they are not moving or aiming with any real purpose. That will be the focus of Part 2, so stay tuned!

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.

Leave a Reply

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