Tactics RPG A.I. Part 2

In A.I. Part 1, we added autonomous agents. Our enemy units could randomly move around the board and pick and use abilities with random targets. Now it is time to make them move and aim with a purpose. We need our enemies to be smart enough to hit multiple targets, attack from the best angles, and avoid friendly fire, etc. They should always act intelligently from the options which are available.

Overview

Before we dive into the code, I want to describe the overall process. By understanding the whole picture, the smaller details will hopefully make better sense. To begin we must understand that there are a lot of options which might be available during a turn:

  • No targets in range
  • One or more allies in range
  • One or more foes in range
  • One or more of both allies and foes in range

The algorithm we will implement is a brute-force search of every possible move location and fire location or direction based on that move. Each of those entries will then be scored based on two factors:

  1. The number of desired targets minus the number of undesired targets
  2. The summed chance of hitting desired targets minus the summed chance of hitting undesired targets

In scenarios where either no targets are found, or the only targets that are within range are not desired targets, then the A.I. wont use an ability. Instead, I have the A.I. pathfind to the nearest foe and move in that direction. Otherwise, we will pick randomly from the options with the highest score and update the plan of attack accordingly.

Ability Component Updates

I want to be able to refer to general truths of the components of our abilities rather than have to manually check the type of each. So we will add a few convenient properties to help facilitate this idea.

Hit Rate

First, I want to know if the hit rate will be calculated based on the angle of attack or not. For this, add the following property to the base class:

public virtual bool IsAngleBased { get { return true; }}

I assumed that more of the hit rate subclasses would be angle based than not, so we made the default “true”. However, we will need to override this in the FullTypeHitRate and set it to false:

public override bool IsAngleBased { get { return false; }}

Ability Range

Likewise, I want to know whether or not the range an ability can reach is dependent on the casters position. Again, I assume that this is the most common case, so I default the property to true:

public virtual bool positionOriented { get { return true; }}

This time we have two subclasses which need to be overriden and set to false, both the InfiniteAbilityRange and the SelfAbilityRange:

public override bool positionOriented { get { return false; }}

Computer Player

Open up the ComputerPlayer script. Go ahead and delete the method PlaceholderCode. Then add a couple new fields:

Alliance alliance { get { return actor.GetComponent<Alliance>(); }}
Unit nearestFoe;

The “alliance” field is just a convenience wrapper property to get the alliance of the selected unit. The other field, “nearestFoe”, will be determined at a few different points within a turn. Regardless, it searches the board for the first foe it can find. Note that “Foe” in this case is subject to the perspective of the active unit. In other words, the foe of a monster is a hero, and vice versa.

Next we need to change the Evaluate method as follows:

public PlanOfAttack Evaluate ()
{
	PlanOfAttack poa = new PlanOfAttack();
	AttackPattern pattern = actor.GetComponentInChildren<AttackPattern>();
	if (pattern)
		pattern.Pick(poa);
	else
		DefaultAttackPattern(poa);
	
	if (IsPositionIndependent(poa))
		PlanPositionIndependent(poa);
	else if (IsDirectionIndependent(poa))
		PlanDirectionIndependent(poa);
	else
		PlanDirectionDependent(poa);

	if (poa.ability == null)
		MoveTowardOpponent(poa);
	
	return poa;
}

We have replaced the call to the place holder method with a conditional which will fill out the plan based on what kind of ability is intended to be used. Some abilities do not care where you stand on the board when you use them, such as if you were targeting everyone, or targeting only yourself. Other abilities are cast based on selecting a fire location, but they don’t care what direction you are facing when you cast it. Finally, some abilities require you to be in a particular place and face a particular direction in order to hit the right targets. Each of these scenarios is handled in its own method and will potentially update where a unit will move to, cast at, and or turn to face a direction.

It’s entirely possible that we are unable to use whichever ability we selected. For example, if our attack sequence came up as “Attack” but there were no nearby foes, then our planning phase would indicate this by resetting the ability in our plan to null. When this occurs, I have decided to simply move toward the nearest opponent so that targeting shouldn’t be a problem in the future.

bool IsPositionIndependent (PlanOfAttack poa)
{
	AbilityRange range = poa.ability.GetComponent<AbilityRange>();
	return range.positionOriented == false;
}

void PlanPositionIndependent (PlanOfAttack poa)
{
	List<Tile> moveOptions = GetMoveOptions();
	Tile tile = moveOptions[Random.Range(0, moveOptions.Count)];
	poa.moveLocation = poa.fireLocation = tile.pos;
}

Here we have the methods which check whether or not “position” matters or not when deciding how to use an ability. When it is determined that an ability is position independent, then I simply move to a random tile within the unit’s move range, because position didn’t matter. There is room to polish this up – perhaps the unit would rather move toward or away from its foes during this time. If you want more specific behavior like that it shouldn’t be hard to add. I’ll show an example of moving toward the nearest foe soon.

bool IsDirectionIndependent (PlanOfAttack poa)
{
	AbilityRange range = poa.ability.GetComponent<AbilityRange>();
	return !range.directionOriented;
}

void PlanDirectionIndependent (PlanOfAttack poa)
{
	Tile startTile = actor.tile;
	Dictionary<Tile, AttackOption> map = new Dictionary<Tile, AttackOption>();
	AbilityRange ar = poa.ability.GetComponent<AbilityRange>();
	List<Tile> moveOptions = GetMoveOptions();
	
	for (int i = 0; i < moveOptions.Count; ++i)
	{
		Tile moveTile = moveOptions[i];
		actor.Place( moveTile );
		List<Tile> fireOptions = ar.GetTilesInRange(bc.board);
		
		for (int j = 0; j < fireOptions.Count; ++j)
		{
			Tile fireTile = fireOptions[j];
			AttackOption ao = null;
			if (map.ContainsKey(fireTile))
			{
				ao = map[fireTile];
			}
			else
			{
				ao = new AttackOption();
				map[fireTile] = ao;
				ao.target = fireTile;
				ao.direction = actor.dir;
				RateFireLocation(poa, ao);
			}

			ao.AddMoveTarget(moveTile);
		}
	}
	
	actor.Place(startTile);
	List<AttackOption> list = new List<AttackOption>(map.Values);
	PickBestOption(poa, list);
}

The next case is where the position matters, but the facing angle does not. For example, casting a spell such as “Fire” or “Cure” can target different units based on where you move the aiming cursor. Because you can move before firing, a unit can actually reach targets in a larger radius than just the range of the ability by itself. Because of this I iterate through a nested loop, where an outer loop considers every possible position a unit can move to, and an inner loop considers every tile within firing range of that move location.

Remember that even after considering movement range and ability range, we still have an area of effect on the ability itself. This would need to be considered next. However, there are likely to be a lot of “overlapping” entries here. For example, whether I move one space to the left or one space to the right, I can still fire one space in front of the original location with most ranged abilities. Therefore, I added a dictionary which mapped from a selected tile to an object which records notes on that location such as which targets fall within range of the area of effect. I only create and evaluate this note object the first time I determine that a tile is within firing range. Otherwise, I simply refer to the notes I had already taken and indicate that another tile is also a valid place to fire from.

Before I start going through the loops I recorded the tile the actor was originally placed on. Before the method exits, I move the unit back to the original position. It’s important not to forget this step or the game would be out of sync with the visuals in the game every time the AI took a turn.

Finally, I pass the list of options we have built up to this point to a method which can pick the best overall option for our turn.

void PlanDirectionDependent (PlanOfAttack poa)
{
	Tile startTile = actor.tile;
	Directions startDirection = actor.dir;
	List<AttackOption> list = new List<AttackOption>();
	List<Tile> moveOptions = GetMoveOptions();
	
	for (int i = 0; i < moveOptions.Count; ++i)
	{
		Tile moveTile = moveOptions[i];
		actor.Place( moveTile );
		
		for (int j = 0; j < 4; ++j)
		{
			actor.dir = (Directions)j;
			AttackOption ao = new AttackOption();
			ao.target = moveTile;
			ao.direction = actor.dir;
			RateFireLocation(poa, ao);
			ao.AddMoveTarget(moveTile);
			list.Add(ao);
		}
	}
	
	actor.Place(startTile);
	actor.dir = startDirection;
	PickBestOption(poa, list);
}

This last case depends both on a unit’s position on the board and the direction the unit faces while using the selected ability. It should look pretty similar to the “PlanDirectionIndependent” variation. The main difference here is that instead of grabbing the Ability Range component and looping through targeted tiles, we instead loop through each of the four facing directions. Every single entry generated will have a unique area of effect – there is no overlap or need for the dictionary as I had last time. We can simply track each entry in a list directly.

List<Tile> GetMoveOptions ()
{
	return actor.GetComponent<Movement>().GetTilesInRange(bc.board);
}

This method simply returns the list of tiles which the current actor can reach.

void RateFireLocation (PlanOfAttack poa, AttackOption option)
{
	AbilityArea area = poa.ability.GetComponent<AbilityArea>();
	List<Tile> tiles = area.GetTilesInArea(bc.board, option.target.pos);
	option.areaTargets = tiles;
	option.isCasterMatch = IsAbilityTargetMatch(poa, actor.tile);

	for (int i = 0; i < tiles.Count; ++i)
	{
		Tile tile = tiles[i];
		if (actor.tile == tiles[i] || !poa.ability.IsTarget(tile))
			continue;
		
		bool isMatch = IsAbilityTargetMatch(poa, tile);
		option.AddMark(tile, isMatch);
	}
}

As we were creating each Attack Option (a note on the effect area of using an ability), we needed a way to rate it, so we could sort them later and pick the best one. We accomplish this by looping through the area that the ability could reach from a given firing location. Any tile which is a “legal” target for an ability gets a “mark” – for example you can “Attack” any unit whether friend or foe, so any tile with a unit would be marked by the attack ability. However, we also indicate whether or not the tile is determined to be a “match” (the desired target type for the given ability). In the example before, any given unit would consider a tile with an ally is not a match, but tiles with a foe are a match for the attack ability.

By tracking all of the marks, but also specifying which ones are matches or not, we can better rate a move. For example, if my attack would hit exactly one foe and one ally by targeting tile ‘X’, and exactly one foe but no allies by targeting tile ‘Y’, then the second option is better. I can tally up a score such that marks which are matches incremenet the score and marks that are not a match decrement the score.

Note that I intentially skip the tile on which the caster is currently standing, because that may not be the unit’s location when it moves before firing. We will need to adjust scores based on the caster’s location at a later point.

bool IsAbilityTargetMatch (PlanOfAttack poa, Tile tile)
{
	bool isMatch = false;
	if (poa.target == Targets.Tile)
		isMatch = true;
	else if (poa.target != Targets.None)
	{
		Alliance other = tile.content.GetComponentInChildren<Alliance>();
		if (other != null && alliance.IsMatch(other, poa.target))
			isMatch = true;
	}

	return isMatch;
}

This method shows how to determine which marks are a match or not. An ability which targets a tile is simply marked as true (I havent actually implemented any such abilities, so I might change this logic later). Otherwise, I use the alliance component to determine whether or not the target type is a match.

void PickBestOption (PlanOfAttack poa, List<AttackOption> list)
{
	int bestScore = 1;
	List<AttackOption> bestOptions = new List<AttackOption>();
	for (int i = 0; i < list.Count; ++i)
	{
		AttackOption option = list[i];
		int score = option.GetScore(actor, poa.ability);
		if (score > bestScore)
		{
			bestScore = score;
			bestOptions.Clear();
			bestOptions.Add(option);
		}
		else if (score == bestScore)
		{
			bestOptions.Add(option);
		}
	}

	if (bestOptions.Count == 0)
	{
		poa.ability = null; // Clear ability as a sign not to perform it
		return;
	}

	List<AttackOption> finalPicks = new List<AttackOption>();
	bestScore = 0;
	for (int i = 0; i < bestOptions.Count; ++i)
	{
		AttackOption option = bestOptions[i];
		int score = option.bestAngleBasedScore;
		if (score > bestScore)
		{
			bestScore = score;
			finalPicks.Clear();
			finalPicks.Add(option);
		}
		else if (score == bestScore)
		{
			finalPicks.Add(option);
		}
	}
	
	AttackOption choice = finalPicks[ UnityEngine.Random.Range(0, finalPicks.Count)  ];
	poa.fireLocation = choice.target.pos;
	poa.attackDirection = choice.direction;
	poa.moveLocation = choice.bestMoveTile.pos;
}

This is the method that actually provides a score for each of the attack options. It goes through two “passes” of analyzing our options. On the first pass, it scores each attack option based on having more marks which are matches than marks which are not matches.

Whenever I find a new “best” score, I track what the score was, and add the ability to a list of the options which I consider to be the best. This list will be cleared if I should find a better score, but if I find additional options with a tied score then I will also add them to the list.

When all of the options have been scored, it is actually possible that I wont have any entries in my best options list. This would be the case where an ability could technically be used, but the effect would be detrimental to the user’s party. For example, if the only option an AI unit had to attack was one of its allies, then it would be better not to do anything than to actually perform the ability. In these cases, I mark the plan’s abilty as null so that it wont be performed.

In the cases where I do have some beneficial options to pick from, I will then run another pass to help trim down the options even further. There are multiple reasons for this. For example, lets say I can attack a target unit from multiple different move locations. Some of those locations may be from the front, while others may be from the back. If I can pick, I would want to pick an angle from the back so that my chances of the attack hitting are greater.

By the end of this second “pass” I should have one or more options which were added to the final picks. Because they all share the same score, I pick any of them at random and assign the relevant details to our plan of attack.

void FindNearestFoe ()
{
	nearestFoe = null;
	bc.board.Search(actor.tile, delegate(Tile arg1, Tile arg2) {
		if (nearestFoe == null && arg2.content != null)
		{
			Alliance other = arg2.content.GetComponentInChildren<Alliance>();
			if (other != null && alliance.IsMatch(other, Targets.Foe))
			{
				Unit unit = other.GetComponent<Unit>();
				Stats stats = unit.GetComponent<Stats>();
				if (stats[StatTypes.HP] > 0)
				{
					nearestFoe = unit;
					return true;
				}
			}
		}
		return nearestFoe == null;
	});
}

There are a few different times in a turn when I might want to know where the nearest foe is to a given unit. For example, whenever the turn had no good targets to use the selected ability on – it assumes that the reason is that it wasn’t close enough to the enemy, so I figure out where the nearest foe is and attempt to move toward it. I determine this by using the board’s search method and passing along an anonymous delegate. If it finds a tile with a unit that is a Foe (according to the alliance component) and also isn’t KO’d, then I have found a target worth moving toward.

void MoveTowardOpponent (PlanOfAttack poa)
{
	List<Tile> moveOptions = GetMoveOptions();
	FindNearestFoe();
	if (nearestFoe != null)
	{
		Tile toCheck = nearestFoe.tile;
		while (toCheck != null)
		{
			if (moveOptions.Contains(toCheck))
			{
				poa.moveLocation = toCheck.pos;
				return;
			}
			toCheck = toCheck.prev;
		}
	}

	poa.moveLocation = actor.tile.pos;
}

Whenever a board search has been run, there will be tracking data left over in the tiles. I can iterate over the “path” in reverse until I find a tile which happens to be included in the movement range of the unit. This will allow me to move as close as possible to the foe who is nearest.

public Directions DetermineEndFacingDirection ()
{
	Directions dir = (Directions)UnityEngine.Random.Range(0, 4);
	FindNearestFoe();
	if (nearestFoe != null)
	{
		Directions start = actor.dir;
		for (int i = 0; i < 4; ++i)
		{
			actor.dir = (Directions)i;
			if (nearestFoe.GetFacing(actor) == Facings.Front)
			{
				dir = actor.dir;
				break;
			}
		}
		actor.dir = start;
	}
	return dir;
}

After we have moved and used an ability, we need to determine an end facing direction. For this I find the nearest foe again. Note that it is important to do this a second time, because the foe who was nearest before you moved is not necessarily the foe who is nearest after you have moved. Next I loop through each of the directions until I find a direction which has me face the foe from the front. This way the foe is less likely to be able to attack me from the back.

Attack Option

The “notes” I take on any given location from which to evaluate the usage of an ability is also a somewhat lengthy class. A lot of its functionality I talked through while covering the ComputerPlayer script, but just to be clear I will break it down as well. Add a new script named AttackOption to the same AI folder that our other scripts are located within.

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

public class AttackOption 
{
	class Mark
	{
		public Tile tile;
		public bool isMatch;
		
		public Mark (Tile tile, bool isMatch)
		{
			this.tile = tile;
			this.isMatch = isMatch;
		}
	}
	
	// Add other code here
}

I created another class inside of AttackOption called Mark to hold a pair of data. This is a little cleaner and less error prone than maintaining two separate lists (one for the tiles and one for whether or not the tile was a match). The Mark class is private to the implementation of the AttackOption class, and is only used for convenience and readability. If other classes needed to know about it or use it, then I would probably stick it in its own file.

public Tile target;
public Directions direction;
public List<Tile> areaTargets = new List<Tile>();
public bool isCasterMatch;
public Tile bestMoveTile { get; private set; }
public int bestAngleBasedScore { get; private set; }
List<Mark> marks = new List<Mark>();
List<Tile> moveTargets = new List<Tile>();

There are several fields to fill out here. The “target” and “direction” fields are treated slightly differently (or not at all) based on the type of ability being used. For example, “target” would represent either the tile which we highlighted to use as a firing location, or the tile we would want to move to in order to fire, and direction may or may not apply.

The list of marks grows based on the number of legal targets which are within the area of effect for an ability given the target tile and facing direction.

The list of move targets indicates which locations the unit can move to in order to cast at the indicated target location. Direction oriented abilties will only ever have one move target in this list.

The area targets are the list of tiles which fall within an ability’s area of effect, regardless of if they currently have a target or not. This can be important in cases where I had overlap because I might initially “score” an attack based on the user being in one position, but could actually “pick” an alternate move location later. If the ability were intended for foes, then I would want to make sure I didn’t pick an option where the ability would also hit the caster. On the other hand, if the ability is intended for allies, then allowing the abilty to include the caster would be a nice bonus. The “isCasterMatch” field tracks whether or not one of the move target locations would actually be good to move into or not.

The “bestMoveTile” and “bestAngleBasedScore” fields indicate a tile which provides the best score to attack from. This score would be based on the idea that attacking from behind gives a greater chance of a hit actually connecting. Attacks from the front are easier for an enemy to dodge, so we want to naturally pick ones from the rear.

public void AddMoveTarget (Tile tile)
{
	// Dont allow moving to a tile that would negatively affect the caster
	if (!isCasterMatch && areaTargets.Contains(tile))
		return;
	moveTargets.Add(tile);
}

public void AddMark (Tile tile, bool isMatch)
{
	marks.Add (new Mark(tile, isMatch));
}

Here we have a few of the methods which we called from the ComputerPlayer script while planning a turn based on an ability which was position dependent (or in other words, pretty much any ability except the infinite-range and self-range ones. The AddMoveTarget method is called to build up the list of locations which are in firing range of the current tile. Note that I don’t actually include options that would be bad for the caster, for example I wouldn’t want to move within the blast radius of my own attack.

The AddMark method creates an instance of the class we defined above and adds it to a list. Remember that a mark indicates a target (good or bad) can be hit by whatever fire location was chosen for this AttackOption instance.

public int GetScore (Unit caster, Ability ability)
{
	GetBestMoveTarget(caster, ability);
	if (bestMoveTile == null)
		return 0;

	int score = 0;
	for (int i = 0; i < marks.Count; ++i)
	{
		if (marks[i].isMatch)
			score++;
		else
			score--;
	}

	if (isCasterMatch && areaTargets.Contains(bestMoveTile))
		score++;

	return score;
}

Here we will provide a score by which we can sort the various options available during a turn. Before calculating a score it also looks for the best tile to move to in order to use the ability. The best move target will be the one which has the best score based on the second factor (the angle of attack) – such as attacking a unit from behind gives more points than attacking from the front. In the event that there are no good locations, then the overall score is returned early as zero.

Assuming that a good move tile is found, the score is then tallied based on how many of the marks are a match or not. If the caster is a match for the ability then an extra point is awarded, because we can potentially move to a location where the ability will include it.

void GetBestMoveTarget (Unit caster, Ability ability)
{
	if (moveTargets.Count == 0)
		return;
	
	if (IsAbilityAngleBased(ability))
	{
		bestAngleBasedScore = int.MinValue;
		Tile startTile = caster.tile;
		Directions startDirection = caster.dir;
		caster.dir = direction;

		List<Tile> bestOptions = new List<Tile>();
		for (int i = 0; i < moveTargets.Count; ++i)
		{
			caster.Place(moveTargets[i]);
			int score = GetAngleBasedScore(caster);
			if (score > bestAngleBasedScore)
			{
				bestAngleBasedScore = score;
				bestOptions.Clear();
			}

			if (score == bestAngleBasedScore)
			{
				bestOptions.Add(moveTargets[i]);
			}
		}
		
		caster.Place(startTile);
		caster.dir = startDirection;

		FilterBestMoves(bestOptions);
		bestMoveTile = bestOptions[ UnityEngine.Random.Range(0, bestOptions.Count) ];
	}
	else
	{
		bestMoveTile = moveTargets[ UnityEngine.Random.Range(0, moveTargets.Count) ];
	}
}

There are two main types of abilities to consider, ones which the angle from the attacker to the target makes a difference and ones which do not care about the angle. For the abilities where angle does matter, we use a simple algorithm that looks a lot like what you have seen a few times now in the Computer Player script where we sort attack options. The basic idea is that we loop through all of the tiles we can move to, move the unit there, calculate the angles between the caster and the targets and use those angles to generate a score. When a score is greater than the previous best score, we reset the list of best options, and when we have considered all options, we pick at random from tiles with the highest score. When the angle is irrelevant, we can simply return any tile at random.

bool IsAbilityAngleBased (Ability ability)
{
	bool isAngleBased = false;
	for (int i = 0; i < ability.transform.childCount; ++i)
	{
		HitRate hr = ability.transform.GetChild(i).GetComponent<HitRate>();
		if (hr.IsAngleBased)
		{
			isAngleBased = true;
			break;
		}
	}
	return isAngleBased;
}

This method determines whether or not an ability’s hit rate is determined based on the angle of the attacker to the defender (or caster and target if that makes more sense).

int GetAngleBasedScore (Unit caster)
{
	int score = 0;
	for (int i = 0; i < marks.Count; ++i)
	{
		int value = marks[i].isMatch ? 1 : -1;
		int multiplier = MultiplierForAngle(caster, marks[i].tile);
		score += value * multiplier;
	}
	return score;
}

Here we get the angle from the caster to each of the mark locations. The resulting score is either incremented or decremented based on whether or not the mark was an intended match. The amount the score goes up or down is based on the angle (front, side or back).

void FilterBestMoves (List<Tile> list)
{
	if (!isCasterMatch)
		return;

	bool canTargetSelf = false;
	for (int i = 0; i < list.Count; ++i)
	{
		if (areaTargets.Contains(list[i]))
		{
			canTargetSelf = true;
			break;
		}
	}

	if (canTargetSelf)
	{
		for (int i = list.Count - 1; i >= 0; --i)
		{
			if (!areaTargets.Contains(list[i]))
				list.RemoveAt(i);
		}
	}
}

After we have built a list of the best places to move to (scored based on the number of good marks and the angle based scores) we can potentially further optimize the remaining selection. If the caster is not a match (such as when we are using an offensive ability intended for an opponent) then we wont do anything. Then we need to decide whether or not the caster can move to one of the locations which is also included in the area of effect. If so, we remove any tile from the best choices list which isn’t that good.

int MultiplierForAngle (Unit caster, Tile tile)
{
	if (tile.content == null)
		return 0;

	Unit defender = tile.content.GetComponentInChildren<Unit>();
	if (defender == null)
		return 0;

	Facings facing = caster.GetFacing(defender);
	if (facing == Facings.Back)
		return 90;
	if (facing == Facings.Side)
		return 75;
	return 50;
}

This method helps to score move target options based on the angle of attack. I could have picked any number I wanted to balance the preferences, but I chose numbers which currently match the general percent chance of hitting from that angle. This means that in general I favor attacks from behind, but having a chance to hit two units from the front could still be better than targeting one unit from behind.

Demo

With this code in place, our computer controlled units finally start looking intelligent. They will move toward their targets, attack from angles giving good hit chances, fire in locations that maximize the number of targets, etc. Compare the sample video from this week against last weeks video to see how far we have come.

Summary

Although we didn’t add many classes, the ones we did add were very long and complex. We broke them down step by step and covered the “How” as in “How will a computer controlled unit know where to move and aim in order to best use the selected ability?” With this step completed, we have also finished the prototype impelementation of our A.I. I feel that it looks pretty good!

At this point I would like feedback from anyone who is still following along. I am considering moving on to another project since this one is basically feature complete. If there are topics you want me to cover that I have missed then leave a request below. If you have suggestions for other projects you would love to see, I would like to hear that too. Either way I will probably take a much needed break for the holidays and potentially to get a head start on whatever my next series will be.

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.

42 thoughts on “Tactics RPG A.I. Part 2

  1. Looks great! I’m happy to see a tactics RPG tutorial actually complete on the web 🙂

    You have a great tutorial writing style and if you were to move on to another project all I can request is that you document it. I’m still in the early stages of this one so I do not have any requests for this project at the time.

    However, you may want to edit your post with ideas on how to improve the codebase as a whole, such as what you would refactor such as the item management system if you think it would need a rework for a project moving to production. Any other concluding thoughts would be nice as well 🙂

  2. Staying with this project I think a tutorial on how to set up and initiate a battle from a different Scene would be good. Right now there would be some tricky resource loading involved.

    I’d also like to see some one-off posts on how you’d go about implementing other aspects of a tactics RPG game, like a full inventory or job progression system, or a world map. Obviously you don’t need to make a full game just for tutorial purposes, but as this series has been quite a bit more technical and advanced than many Unity tutorials I’ve seen it would be interesting to see what your approach would be.

  3. This has been one awesome experience guy.

    While it’s a shame to see it end, it does so on a high note – kudos on actually completing it!

    I agree with Stu on the other stuff, at least on a high level view, but that’s cool- I’ll probably start chewing on these next month, seeing as what’s already here is pretty advanced.

    Thanks Jon, because this is exactly what I’ve been looking for over the last… ten years?

  4. Thank you for this amazing series! I learned a lot about higher level (to me) coding practices that are useful beyond the scope of this project as well. If you wanted to continue with the series, it would be interesting to see a bit more about data management between battles and the overall game structure / world map / what ever allows you to move between battles.

    Other than that, I would be interested in more turn based type games- card games, rogue likes, heroes of might and magic types, Final fantasy, etc. I will probably read any series you write however- just for the code tips!

    Enjoy the break and the holidays!

  5. This series has been really amazing and i learned alot. Been using bits and pieces for my own little project.

    Looking forward whatever your next project is going to be!

  6. This is an amazing tutorial. Thanks for a fantastic demonstration of a well-structured and organized project. Even as somebody with a lot of programming experience, I find that working through projects like this really helps me improve as an engineer. In particular, I appreciate the detailed descriptions you provided for why you made each of the design decisions that you did.

    As far as material I’d love to see you talk about in future projects… one area that I’ve been struggling with recently is setting up a networked multiplayer game using Unity 5.x networking. While I’m comfortable with networked game architecture and understand how UNET works at the big-picture level, there are a million small details that I find difficult: not starting an instance of the game until all players have finished loading, validating players’ moves on the server and reporting failures back to the client, etc. I’ve never managed to put together an architecture for this that didn’t feel like a kludge in a lot of places, so I’d love to see an example of it done as well as you handled this series.

    I think it might be interesting to add networked multiplayer to your the tactics RPG, but since you didn’t start the project with networking in mind, it might be too much of a rework to add it at this stage. I’d personally be excited to see a tutorial like this for a Hearthstone-like card game, but ultimately I’m sure I’d learn a great deal by following along no matter what type of game you decide to go with.

    Thanks for your hard work, and I’m looking forward to your next project!

  7. Amazing amazing work there John, been stuck to the computer for the past 4 days, reading over and over. the structure of the project is pretty amazing as it show so many diffrent scopes and how to manipulate the Inspector interface so well. the tutorial is very instuctive and I felt like I actually learned alot, Finite State Machine structure, Objects Entries using a queue and so much more…

    I think that it can be realy amazing if you can actually show us how to make this game work as a Multiplayer, and maybe just a simple implementation of the Battle State in a bigger scope, such as world map Heroes of Might and Magic style when 2 mapped units collide the Battle State interface get into action, Scenes changes and management.

    1 last thing I would like to add that there is a few small problems in the final product on the repo, I’m using unity’s latest version 5.3.X and a few methods have been deprecated which I’ve managed to resolve but there seems to be a reference to a Null object of the Jobs/ +name method prefabs which I cant seem to resolve (Would realy appreciate a solution).

    Thank you very much for this Tutorial John, waiting for the next lesson.

    1. You’re very welcome Daniel. Just as a heads up, if you download the repo and try to run it from the end as-is, you will run into null refs like this. From the file menu choose “Pre Production->Parse Jobs”. This will create the assets that you need automatically. I mention it in the post where jobs are created, but anyone who skips ahead wouldn’t have known.

      If you’ve already done that step and are still having trouble I will need a bit more info to help you trouble shoot.

  8. Thanks for the amazing tutorial, I’m really glad I found it. You did a great job at covering the basics while still giving enough to tweak to our liking.

    I agree with Stu when it comes to seeing other RPG Tactics material such as a world map or a job progression system. Also, a more in-depth level creating guide that talks about things like board decorations or different tile types and how they can affect gameplay would be fun to see and work through.

  9. Thanks for the turorial,but i download the project by SourceTree and click run in untiy editor,It shows a error “No Prefab for name:Jobs/Warrior”,and i cant find any folder named Jobs under the project,Can you help me?Thanks!

  10. Hi, I am following the project. It’s a nice guide which help me to learn about States, ScriptableObjects and Poolers.

    I am completing or adding new features but i found a wall that deny me to continue.

    Player vs IA:
    If the players wins, the conversation “OutroSceneWins” works fine.
    If instead, the CPU wins, the conversation is lock in his first message and the player is unable to follow the conversation. I used the debugger tool from MonoDevelop but i can’t found the error,

    I think, once the player lose, he is unable to use any command or take actions so their clicks are not registered and that produces a “freeze” in the “lost conversation”.

    1. Looks like you found a bug in my project! It turns out the problem lies in a change I made to “BattleState.cs” where I added a condition on whether or not to add listeners for the input events. It turns out that when the enemy wins, there is a driver which isn’t the player so input isn’t detected. There are a variety of ways to solve this problem, but an easy fix for now would be to add an additional check so that the condition becomes:
      if (driver == null || driver.Current == Drivers.Human || IsBattleOver())

      I haven’t done a thorough check to see if that may cause any other problems, but it should be enough to get you started. Good catch by the way!

      1. Thanks a lot. I was sure it was about the listeners but the bell didn’t ring for me.

        I discover the cpu use an ability withou caring about his mana but i will test myself.

        1. The mana for the A.I. units I fixed in the pick function in A.I., checking the ability chosen against the unit mana.

          1. I apologize, I mis-spoke, it was the Plan function that I used to check the caster mana. NEXT I am going to try and dig into the movement so that the A.I. can and will go around a cliff to reach the player. Right now they just walk to the closest edge then get stuck.

  11. Thanks for doing these tutorials! I really appreciate you showing the reasoning behind these implementations (and being very prompt with replying to questions/requests). While I would like to see you continue on with Tactics RPG tutorials, I’ll save my ideas for further topics (aka stuff I need to implement) to solve on my own.

    1. My pleasure Duncan, I hope your own project goes smoothly, but if you ever need to bounce ideas off of someone I’d be happy to browse the threads on my forums of course 🙂

  12. I would love to see a post on your thoughts about implementing a gambit system. Also, what was mentioned above about loading in the battle scene from say, a map or menu scene.

    1. Thanks for the suggestions – I’ll keep them in mind as potential topics to explore. But so you don’t have to wait as long I would encourage you to try the gambit system yourself – it may be easier than you think because it really is just an ordered list of paired conditions and actions: the first condition to pass causes the paired action to be the one to execute. Of course, a tactics RPG makes this kind of A.I. a bit more complex because of the way you can both move and attack on a single turn. Some of the conditions are going to be relevant to where you could move to, like you might want one unit to heal another but that other unit might be out of range even if you spent your turn moving toward it. I think FFXII was better for this because your units all stayed together so the conditional part of the logic was easier.

      Also, regarding loading a battle scene from a map or menu, the trick is just figuring out how to determine what enemies you will face there or what quest objectives etc you might need to face. All you need is some sort of system that lives outside of the scene. If you haven’t looked at my C# tutorial series, “Saving Data”, you might want to check that one out. It covers a few patterns that work for this.

      1. Awesome, I’ll definitely try this out. Here is another question that may be related to gambits. How would you, or I guess would you even, go about taking the AI away from being a central “Enemy player controlling all of their units”, and making it more unit based. What I am thinking is a way to make some units smarter than others. Perhaps a monster would just blindly chase and attack you, but a human opponent would retreat when injured or try to help its allies. I get that you could do this to some extent by giving the units different abilities to choose from, then implementing the gambit conditional pairs. But would that be the best way?

        1. A.I. is a pretty subjective topic. A lot of players assume the A.I. gets smarter simply by raising its stats and thus making it harder to defeat. You could handle something being truly more intelligent with something like a gambit system, but that also takes a bit away from the random feeling of true intelligence and can make a battle too predictable or frustrating if not balanced well.

          I could see having a mixture of a state machine with unique attack patterns. For example, when a units hp is low it might focus on escaping and healing tactics, or possibly suicidal attacks that do more damage. I think this approach would be easier than a full gambit system because the condition for switching states and therefore tactics could be something as simple as checking its own hit points.

  13. I also just noticed some interesting behavior. If you surround an enemy and it can’t move anywhere, it just waits, it does not attack.

    1. That’s interesting. It could be a result of the “Attack Pattern” being on something other than Attack and deciding that whatever move it was going to do wasn’t beneficial because it would hit itself, or was out of magic points, etc. Shouldn’t be too hard to add an extra check before selecting a wait move to see if it could attack.

  14. Hey thejon2014

    Hey men do you know what my problem could be if my carakter just walk and not teleport or Fly. But the enmey works well. I also thought about my Attacks because they dont work like they should.
    The text after the choosen attack skill isnt right for e.x. 100% chance to make 40 Damage but it just reduce it 2 points or somthing similar. A lot of times you tell us to go in your Repository my Problem is that your whole projeckt cant load the skripts. It s really hard to find out what you mean, because i cant watch your values in your project. is it possible that you could send me a example or would you rather have our Project to have a look ?

    Thank for all your Support and what you did in this whole Tutorial. Its amazing and if im finish with it i wanna show you 🙂

    1. The method of traversal a unit uses (walk, fly or teleport) is based on the components that are attached. In my project, all of the hero units were walking types. You are free to examine the assets in the project and modify them to your liking.

      The chance to hit and damage dealt should work, but this is complex enough that it is possible the problem is from a bad setup of the project assets or something you missed in the script. It really could be just about anywhere.

      Have you ever used version control (like git) before? I am not sure what you mean when you say that my whole project can’t load scripts. I have opened it on multiple different computers and multiple versions of Unity – most recently on version 5.5.1. So I am not sure what you are (or aren’t) seeing. After downloading the repository (assuming you know how to complete that step) you can use either the master or develop branch, just check out the most recent commit. Then once you’ve opened the project in Unity use the file menu “Pre Production->Parse Jobs” which creates some of the assets you need. Finally play the Battle scene. If any of those steps don’t make sense I would be happy to try and clarify, just let me know.

      I’m glad you enjoyed the tutorial series, and I really look forward to seeing your own version when you finish it!

  15. Hey theJon2014

    Since my last post from January 31 i tryed to change the InputController from Arrows to Ui Buttons. I trieeeeeed soooo maaany things but damn. this tutorial is really not for beginners. The last 3 Months i learned alot about C# but i really cant solve this problem.

    So now to my Question: Please can you help me ?

    I hope you have time for it.

    Thank you

    yours faithfully

    Brian

    1. I’m glad to hear that you have tried several approaches on your own, but outside of actually doing it for you, the question is too vague for me to help. Where exactly are you getting stuck?

      1.) Can you create screen UI and connect event handlers to the buttons?
      2.) Do you understand how to post your own events?

      If you can do both of these steps, then you should in theory be able to complete your goal. Just make the events you post follow the same pattern as the one that my input events were using, make sure that all of the other classes that had been observing my input events now observe the new events instead, and voila you are done.

  16. Hey thejon2014

    I tryed out very much. But i have no idea about the eventHandler. Or how to simulate a Keypress. All the times when i tried to simulate a key, a bool or a int value. The InputController can not be overrieden. Any keypress with the arrows, moves the player 1 point. but i have no idea how this works. I also tryed to use a joystick. i could just move the tileselectionIndicator. but this doesnt really matter because the position from the tileselectionIndicator is not importand for the Gameplay. In playmode has been the indicater 5 points away from the player but the Inputcontroller dont moved the player because it wasnt a Keypress.

    I hope you understand my problem.

    I tryed out so many things… and you words sounds like its very easy 🙂

    Can you help me to change the Inputcontroller ?

    Thank you

    yours faithfully

    Brian

    1. To be honest, I am not sure whether or not I understand your problem. I thought you were trying to use UI instead of keyboard input (perhaps you are wanting to do a mobile touch screen or something). If that is the case, I don’t know why you are trying to simulate keypresses.

      So… if you are trying to use UI instead of keyboard input, then I can point you in the right direction. My newest project (the Pokemon board game) is designed for mobile and supports UI buttons. You can look over the demo project and code to get some ideas on how to create them and connect handlers to the click events. The general idea is pretty simple: Create a canvas, add a UI button, and then use the inspector to connect a handler to the onClick. You can also connect an event handler via code like the documentation shows here: https://docs.unity3d.com/ScriptReference/UI.Button-onClick.html

      Then, if you want to post custom events, I wrote a ton on those if you read my 3-part series – on my site, search for “Social Scripting Part 1” to begin.

  17. Hi John

    i have to apologize for the late replay.

    yes i want to creat a mobile Game with your Project. I tryed to simulate a key press with a Ui button because if i could do this i dont have to recode the scripts. I also work on another mobile game to learn C#. I work only with the onClick function and Virtualjoystics.

    I will be very glade if you could help me to switch the InputController from arrows to Ui Buttons.

    If i understood your right.. you really work for Niantic or Nintendo ?

    Because of the Pokemon Board Game ^^ im envy…

    Thank you very much for your time.

    🙂

    1. First, let me clear something up. I don’t work for Niantic or Nintendo. The project you are referring to I have labeled as “Unofficial” which should help indicate that. It is simply a fan game that my son designed and I programmed it for him as a fun way to do something together. Now that it’s “done” I’m sharing it with everyone else. The assets used in the project are copyrighted and is why I don’t include them, although I do show how easy they are to find all over the internet.

      Now for your other questions, I think I understand what you want, although the way it is phrased could be taken a few different ways. For example, to say you want to simulate keypresses could mean that you want to trick unity into thinking that the keyboard buttons had been pressed so that the Input Controller will just work without any additional changes. If that is what you are trying to do, then I think you are approaching the problem from the wrong direction.

      A similar but more appropriate approach would be to allow the Input Controller to observe the onClick events of your UI buttons. Within the handler methods you can then post the other events (the same ones posted by actual keyboard input) and the rest of the code in the project won’t have cared how the event was generated. But you should need to change code in the Input Controller to make this work. Since the Input Controller already inherits from MonoBehaviour this will be extra simple. You can simply provide a public method and connect the onClick right in the Unity inspector. Something like this should work (note * I don’t know why but the code snippets in the comments always strip out the generic part of the code… compare the lines below against the other code in the lesson or repository to fix it):


      public void UpButtonPressed () {
      if (moveEvent != null)
      moveEvent(this, new InfoEventArgs(new Point(0, 1)));
      }

      public void Fire1ButtonPressed () {
      if (fireEvent != null)
      fireEvent(this, new InfoEventArgs(0));
      }

  18. Hey John

    Thank you very much for the code. i changed it a Little bit but it works.

    public void UpButtonPressed()
    {
    if (moveEvent != null)
    moveEvent(this, new InfoEventArgs(new Point(0, 1)));
    }
    public void Fire1ButtonPressed()
    {
    if (fireEvent != null)
    fireEvent(this, new InfoEventArgs(0));
    }

    Now i can Play the whole Game with Ui Buttons 😉

    I cant belive how easy it was :-/ I tryed to solve the Problem from the wrong site.

    Thank you so much !!

    Now a dream come true, i can develope my own MMORPG ! 😉

  19. Hey, I just wanted to thank you so much on making and sharing this project with all of us. Im currently using this project as a template for a school project for my own JRPG style game.

    I’m fairly new to unity and was wondering if you had any tips on adding sprite animation to the project you posted, or at least where I should begin looking if I wanted to add my own animation to the game.

  20. Hi! Did you recently update these lessons? I don’t remember that the AI kept a score on which targets would be best (something I wanted to implement on my own and made a post on the forum about it) and it seems you basically made my work for me XD

    1. Nope – been this way since the beginning, but I’m glad its still working like everyone wants 🙂

Leave a Reply to Brennan Anderson Cancel reply

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