Tactics RPG Hit Rate

There is still plenty to do before we have actually implemented a true “attack” ability. In this lesson we will determine what sort of chance there is that the unit will actually hit the target. Sometimes special status effects or abilities might alter the chances of hitting, but at a minimum we will take the angle of attack (front, side or back) into account. It is easier to hit an opponent if they don’t see the attack coming. In addition we will add another UI panel to indicate this new hit rate.

Facings

We will use a new enum to hold the three basic facing angles that I care about for this game. Create a new script named Facings located in the Scripts/Enums folder.

using UnityEngine;
using System.Collections;

public enum Facings
{
	Front,
	Side,
	Back
}

In order to determine an angle of attack (our Facing angle) we will need to determine the angle from the attacker to the target, and the angle from the target to its current direction. In order to help illustrate the idea, consider the following image:

Facings_zpsxs6xxqp4

In this image, the center square has an arrow pointing to the right. Imagine that this arrow is a target unit which we wish to attack and that it is facing toward the right. Our attacker could be located at any other square, and if it was, we need a good way to know what angle would the attack be from – the back, side, or front?

According to the image, if we place the unit on any yellow tile which is marked with an “F” then we would be attacking from the Front. I have similarly marked the Side (“S”) and Back (“B”) tile locations.

If you aren’t familiar with “advanced” math, your algorithm for implementing this might be a bit long and hard to read, because each tile could be any of the three Facings – it depends on the direction the target is facing as well as the direction of the attacker to the target.

If you are familiar with “advanced” math, then this problem is surprisingly easy. The solution is called a “dot product”. I thought about including the mathematical definition, but somehow I don’t find it very helpful at all, so as a non-mathematician have mercy on me making up my own. You can use a dot product (a single float value) to determine the relationship between two vectors – such as whether they face in the same direction (values greater than zero), a perpindicular direction (zero), or opposite directions (values less than zero), etc.

It might sound pretty difficult and advanced but it actually boils down to some very simple concepts that anyone with an elementary math education (maybe slightly more) should be able to understand. Take two Vectors (I assume you are familiar with Vector2 in Unity by now) and then multiply the x’s together, multiply the y’s together and add the result. That final number is the dot product. Of course Unity handles this “scary” math stuff automatically:

float d1 = Vector2.Dot( new Vector2(1, 0), new Vector2(0, 1) ); // 0 is Perpindicular
float d2 = Vector2.Dot( new Vector2(1, 0), new Vector2(1, 0) ); // 1 is Same Direction
float d3 = Vector2.Dot( new Vector2(1, 0), new Vector2(-1, 0) ); // -1 is Opposite Direction

Before I lose everyone (hopefully I haven’t already), I’ll just get to the point. If you were to take the dot product of the normalized vector from our attacker to our defender, and the normalized vector representing the angle the defender was facing, then you would get numbers in the range of -1 (attacking from the front) to +1 (attacking from the back). These relationships are shown in the image below as the dog (attacker) approaches the cat (defender).

DotProducts_zpsl3miv0hd

Enough talk, let’s see what this looks like as code. Create a new script called FacingsExtensions located in the Scripts/Extensions folder:

using UnityEngine;
using System.Collections;

public static class FacingsExtensions
{
	public static Facings GetFacing (this Unit attacker, Unit target)
	{
		Vector2 targetDirection = target.dir.GetNormal();
		Vector2 approachDirection = ((Vector2)(target.tile.pos - attacker.tile.pos)).normalized;
		float dot = Vector2.Dot( approachDirection, targetDirection );
		if (dot >= 0.45f)
			return Facings.Back;
		if (dot <= -0.45f)
			return Facings.Front;
		return Facings.Side;
	}
}

Note that in order for this code to compile I also added the following snippet to DirectionsExtensions

public static Point GetNormal (this Directions dir)
{
	switch (dir)
	{
	case Directions.North:
		return new Point(0, 1);
	case Directions.East:
		return new Point(1, 0);
	case Directions.South:
		return new Point(0, -1);
	default: // Directions.West:
		return new Point(-1, 0);
	}
}

I also added an implicit conversion from Point to Vector2 in the Point struct:

public static implicit operator Vector2(Point p)
{
	return new Vector2(p.x, p.y);
}

Hit Rate

Create a new script called HitRate in the Scripts/View Model Component/Ability/Hit Rate folder. This will be our abstract base class for another type of component which we will add to each type of ability. There will be three concrete implementations based on the design of Final Fantasy Tactics, where we have one kind that is used for a standard attack, one that is used for applying status ailments, and one that is used for abilities which should always hit.

using UnityEngine;
using System.Collections;

public abstract class HitRate : MonoBehaviour 
{
	#region Notifications
	/// <summary>
	/// Includes a toggleable MatchException argument which defaults to false.
	/// </summary>
	public const string AutomaticHitCheckNotification = "HitRate.AutomaticHitCheckNotification";

	/// <summary>
	/// Includes a toggleable MatchException argument which defaults to false.
	/// </summary>
	public const string AutomaticMissCheckNotification = "HitRate.AutomaticMissCheckNotification";

	/// <summary>
	/// Includes an Info argument with three parameters: Attacker (Unit), Defender (Unit), 
	/// and Defender's calculated Evade / Resistance (int).  Status effects which modify Hit Rate
	/// should modify the arg2 parameter.
	/// </summary>
	public const string StatusCheckNotification = "HitRate.StatusCheckNotification";
	#endregion

	#region Public
	/// <summary>
	/// Returns a value in the range of 0 t0 100 as a percent chance of
	/// an ability succeeding to hit
	/// </summary>
	public abstract int Calculate (Unit attacker, Unit target);
	#endregion

	#region Protected
	protected virtual bool AutomaticHit (Unit attacker, Unit target)
	{
		MatchException exc = new MatchException(attacker, target);
		this.PostNotification(AutomaticHitCheckNotification, exc);
		return exc.toggle;
	}

	protected virtual bool AutomaticMiss (Unit attacker, Unit target)
	{
		MatchException exc = new MatchException(attacker, target);
		this.PostNotification(AutomaticMissCheckNotification, exc);
		return exc.toggle;
	}

	protected virtual int AdjustForStatusEffects (Unit attacker, Unit target, int rate)
	{
		Info<Unit, Unit, int> args = new Info<Unit, Unit, int>(attacker, target, rate);
		this.PostNotification(StatusCheckNotification, args);
		return args.arg2;
	}

	protected virtual int Final (int evade)
	{
		return 100 - evade;
	}
	#endregion
}

The concrete subclasses of this component include several of the same “checks” but not in the same order. The base class provides the implementations of the shared checks, which are called in whatever order is important in the subclass in the Calculate method.

One such “check” to be performed is whether “something” will cause an ability to certainly succeed. For example, our ability might be a regular “Attack” and on a normal occassion the chance to hit needs to consider the target’s chance to evade. However, if the target were under the effects of a “Stop” or “Sleep” status effect, then the chance to evade would be “zero” and we would consider it an automatic hit type of event.

A similar but oppositie check is whether “something” can cause an ability to certainly fail. For example, if the ability is supposed to apply a status effect, but the target has “Immune” attributes for that status type, then the ability would need to fail no matter what. This would be particularly important on some boss fights to keep them from being too easy.

A final check modifies the chances of a hit, without forcing it to be “certain”. For example, if the Attacker is “Blind” then he can still hit an opponent, but his chances of hitting are worse.

A-Type Hit Rate

The first concrete subclass of our HitRate is the default type which will be used with most abilities such as a standard attack. Its chances of success are partially determined by the defender’s EVD (evade) stat. Create another script named ATypeHitRate in the same folder as the base class:

using UnityEngine;
using System.Collections;

public class ATypeHitRate : HitRate 
{
	public override int Calculate (Unit attacker, Unit target)
	{
		if (AutomaticHit(attacker, target))
		    return Final(0);

		if (AutomaticMiss(attacker, target))
			return Final(100);

		int evade = GetEvade(target);
		evade = AdjustForRelativeFacing(attacker, target, evade);
		evade = AdjustForStatusEffects(attacker, target, evade);
		evade = Mathf.Clamp(evade, 5, 95);
		return Final(evade);
	}

	int GetEvade (Unit target)
	{
		Stats s = target.GetComponentInParent<Stats>();
		return Mathf.Clamp(s[StatTypes.EVD], 0, 100);
	}

	int AdjustForRelativeFacing (Unit attacker, Unit target, int rate)
	{
		switch (attacker.GetFacing(target))
		{
		case Facings.Front:
			return rate;
		case Facings.Side:
			return rate / 2;
		default:
			return rate / 4;
		}
	}
}

S-Type Hit Rate

The second type of hit rate component is used for special abilities which focus on applying status effects. Its chances of success are partially determined by the defender’s RES (resistance) stat. Create another script named STypeHitRate in the same folder:

using UnityEngine;
using System.Collections;

public class STypeHitRate : HitRate 
{
	public override int Calculate (Unit attacker, Unit target)
	{
		if (AutomaticMiss(attacker, target))
			return Final(100);

		if (AutomaticHit(attacker, target))
			return Final(0);

		int res = GetResistance(target);
		res = AdjustForStatusEffects(attacker, target, res);
		res = AdjustForRelativeFacing(attacker, target, res);
		res = Mathf.Clamp(res, 0, 100);
		return Final(res);
	}

	int GetResistance (Unit target)
	{
		Stats s = target.GetComponentInParent<Stats>();
		return s[StatTypes.RES];
	}

	int AdjustForRelativeFacing (Unit attacker, Unit target, int rate)
	{
		switch (attacker.GetFacing(target))
		{
		case Facings.Front:
			return rate;
		case Facings.Side:
			return rate - 10;
		default:
			return rate - 20;
		}
	}
}

Full Type Hit Rate

Our final hit rate type is for special abilities which should normally hit without fail. There still may be exceptions to this rule, so I left a notification to allow a chance for misses. Add another script named FullTypeHitRate to the same folder:

using UnityEngine;
using System.Collections;

public class FullTypeHitRate : HitRate 
{
	public override int Calculate (Unit attacker, Unit target)
	{
		if (AutomaticMiss(attacker, target))
			return Final(100);

		return Final (0);
	}
}

Match Exception

When determining when an ability would have either an automatic hit or automatic miss exception, I posted a notification along with an instance of our next class, the MatchException. Create and add this class to the Scripts/Exceptions folder.

In order to determine these cases, it can be helpful to know who is attacking and who is being attacked This would provide access to any number of components you may want to check. Note that the sender would be a HitRate, which should be tied to the same game object as the ability being performed, and is also information you may need to know when determining whether or not to allow this particular exception.

I wanted to use a subclass of a BaseException because I like the way that a condition is normally a certain way and can only be toggled to the opposite way. This “safety net” will help to avoid situations where one condition flips a toggle one way and a different condition flips it an opposite way. The final result would be dependent upon the order which the scripts listened to and handled the notification and therefore could lead to some difficult to track down “bugs” in your code.

using UnityEngine;
using System.Collections;

public class MatchException : BaseException 
{
	public readonly Unit attacker;
	public readonly Unit target;

	public MatchException (Unit attacker, Unit target) : base (false)
	{
		this.attacker = attacker;
		this.target = target;
	}
}

Info

The hit rate also posted a status check notification to allow various status effects a chance to modify the evasion rates of an ability. This notification passes along an instance of an Info object which is just a generic class made up of one or more generic fields. I decided to use this class instead of needing to make a specific implementation every time I need to pass a few bits of information along with a notification.

Although it is nice not to have to make a ton of little info classes, note that this method does not provide any protections that I would have been able to specify in a manually created class. For example, in the implementation I pass along both the attacker and dender as the first two fields. Had I created this class manually, I would make those two fields readonly so that no “listener” would be able to modify them. The use of good code comments can alleviate this problem but it is still something to keep in mind.

using UnityEngine;
using System.Collections;

public class Info<T0>
{
	public T0 arg0;

	public Info (T0 arg0)
	{
		this.arg0 = arg0;
	}
}

public class Info<T0, T1> : Info<T0>
{
	public T1 arg1;

	public Info (T0 arg0, T1 arg1) : base (arg0)
	{
		this.arg1 = arg1;
	}
}

public class Info<T0, T1, T2> : Info<T0, T1>
{
	public T2 arg2;

	public Info (T0 arg0, T1 arg1, T2 arg2) : base (arg0, arg1)
	{
		this.arg2 = arg2;
	}
}

Stop Status Effect

Since we exposed a bit of functionality which could cause an ability to have an automatic hit case, let’s add it to our “Stop” status effect. Final Fantasy Tactics would do the same for a variety of other status effects like “Petrify”, “Hibernate”, and “Sleep”.

All we need to do is register for the notification in the OnEnable method, cleanup by un-registering in the OnDisable method, and then provide our notification handler:

// Add inside of OnEnable
this.AddObserver( OnAutomaticHitCheck, HitRate.AutomaticHitCheckNotification );

// Add inside of OnDisable
this.RemoveObserver( OnAutomaticHitCheck, HitRate.AutomaticHitCheckNotification );

// Add handler method
void OnAutomaticHitCheck (object sender, object args)
{
	Unit owner = GetComponentInParent<Unit>();
	MatchException exc = args as MatchException;
	if (owner == exc.target)
		exc.FlipToggle();
}

You could provide functionality for automatic misses in very much the same way, you would simply be observing a different notification under different circumstances.

Blind Status Effect

Let’s add a new status effect to help demonstrate how the “StatusCheck” portion of our Hit Rate will work. Create a new script named BlindStatusEffect in the Scripts/View Model Component/Status/Effects folder.

using UnityEngine;
using System.Collections;

public class BlindStatusEffect : MonoBehaviour 
{
	void OnEnable ()
	{
		this.AddObserver( OnHitRateStatusCheck, HitRate.StatusCheckNotification );
	}
	
	void OnDisable ()
	{
		this.RemoveObserver( OnHitRateStatusCheck, HitRate.StatusCheckNotification );
	}

	void OnHitRateStatusCheck (object sender, object args)
	{
		Info<Unit, Unit, int> info = args as Info<Unit, Unit, int>;
		Unit owner = GetComponentInParent<Unit>();
		if (owner == info.arg0)
		{
			// The attacker is blind
			info.arg2 += 50;
		}
		else if (owner == info.arg1)
		{
			// The defender is blind
			info.arg2 -= 20;
		}
	}
}

Jobs

When I originally created jobs, I didn’t include any default stats for EVD (evade) or RES (status resistance). In order to see the results of our hard work in this lesson we will need to give our units some ability to dodge an attack. I decided to go ahead and add a base amount of evasion and resistance as a stat modifier, just like the movement and jump range stats:

static void PartsStartingStats (string line)
{
	string[] elements = line.Split(',');
	GameObject obj = GetOrCreate(elements[0]);
	Job job = obj.GetComponent<Job>();
	for (int i = 1; i < Job.statOrder.Length + 1; ++i)
		job.baseStats[i-1] = Convert.ToInt32(elements[i]);

	StatModifierFeature evade = GetFeature (obj, StatTypes.EVD);
	evade.amount = Convert.ToInt32(elements[8]);

	StatModifierFeature res = GetFeature (obj, StatTypes.RES);
	res.amount = Convert.ToInt32(elements[9]);

	StatModifierFeature move = GetFeature (obj, StatTypes.MOV);
	move.amount = Convert.ToInt32(elements[10]);

	StatModifierFeature jump = GetFeature (obj, StatTypes.JMP);
	jump.amount = Convert.ToInt32(elements[11]);
}

I also needed to update my spreadsheet, JobStartingStats.csv as follows:

Name,MHP,MMP,ATK,DEF,MAT,MDF,SPD,EVD,RES,MOV,JMP
Warrior,43,5,61,89,11,58,100,50,50,4,1
Wizard,30,25,11,58,61,89,98,50,50,3,2
Rogue,32,13,51,67,51,67,110,50,50,5,3

Don’t forget to recreate the Job project assets using our Pre-Production tool. From the menu bar choose Pre Production->Parse Jobs.

Hit Success Indicator

In order to help the user make better informed decisions, let’s add a new UI element that will show the HitRate for the selected ability. Add a new script named HitSuccessIndicator to the Scripts/View Model Component folder.

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

public class HitSuccessIndicator : MonoBehaviour 
{
	const string ShowKey = "Show";
	const string HideKey = "Hide";

	[SerializeField] Canvas canvas;
	[SerializeField] Panel panel;
	[SerializeField] Image arrow;
	[SerializeField] Text label;
	Tweener transition;

	void Start ()
	{
		panel.SetPosition(HideKey, false);
		canvas.gameObject.SetActive(false);
	}

	public void SetStats (int chance, int amount)
	{
		arrow.fillAmount = (chance / 100f);
		label.text = string.Format("{0}% {1}pt(s)", chance, amount);
	}

	public void Show ()
	{
		canvas.gameObject.SetActive(true);
		SetPanelPos(ShowKey);
	}

	public void Hide ()
	{
		SetPanelPos(HideKey);
		transition.easingControl.completedEvent += delegate(object sender, System.EventArgs e) {
			canvas.gameObject.SetActive(false);
		};
	}

	void SetPanelPos (string pos)
	{
		if (transition != null && transition.easingControl.IsPlaying)
			transition.easingControl.Stop();

		transition = panel.SetPosition(pos, true);
		transition.easingControl.duration = 0.5f;
		transition.easingControl.equation = EasingEquations.EaseInOutQuad;
	}
}

Our UI element will be pretty simple – we will use the AttackArrowBacker and AttackArrowFill sprites to visually indicate the hit rate chance. In addition we will have a Text label beneath the arrow to more specifically show the chance of a hit as well as how much damage might be done (although we are not implementing the damage algorithm yet).

Use the following screen grabs to help recreate the prefab:

HitSuccessIndicator_zpsr0e0yc3h

HSI_Hierarchy_zpsdjdsseje

HSI_Root_zpsnd4kockz

HSI_Canvas_zpsyz03de9f

HSI_Panel_zps0nutsijy

HSI_ArrowBackground_zpsbnxefxmv

HSI_ArrowForeground_zpsdw0fl9xc

HSI_Label_zps6nvvkniy

Don’t forget to add a reference to this UI piece in our BattleController:

public HitSuccessIndicator hitSuccessIndicator;

Also add a wrapper in our BattleState:

public HitSuccessIndicator hitSuccessIndicator { get { return owner.hitSuccessIndicator; }}

Confirm Ability Target State

We will be displaying the HitSuccessIndicator from the ConfirmAbilityTargetState. We will need to show the panel at the end of the Enter method as long as we have at least one target:

if (turn.targets.Count > 0)
{
	hitSuccessIndicator.Show();
	SetTarget(0);
}

Since this state can show the panel, it is also responsible for hiding it before leaving. Make sure to hide the panel in the Exit method:

hitSuccessIndicator.Hide();

If the user switches the selected target (SetTarget method) we will also need to update the hit rate for the new target:

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);
		UpdateHitSuccessIndicator ();
	}
}

void UpdateHitSuccessIndicator ()
{
	int chance = CalculateHitRate();
	int amount = EstimateDamage();
	hitSuccessIndicator.SetStats(chance, amount);
}

int CalculateHitRate ()
{
	Unit target = turn.targets[index].content.GetComponent<Unit>();
	HitRate hr = turn.ability.GetComponentInChildren<HitRate>();
	return hr.Calculate(turn.actor, target);
}

int EstimateDamage ()
{
	return 50;
}

Hopefully it is obvious to you that EstimateDamage has a placeholder implementation for now. We wont be using fixed values in a more complete implementation.

Demo

There is one last step to take before we can test everything out – add the ATypeHitRate component to the “Attack” game object in the project assets “Hero” prefab. Assuming you have created the Hit Success Indicator and linked it up to the Battle Controller you should now see a hit rate appear in the confirm portion of your ability action.

Try targeting a unit from behind, from the side, and from the front to verify that the chance of hitting from each angle is different. If you use the demo from last week you can also test that targeting a unit with the “Stop” status effect is guaranteed to hit.

Summary

In this lesson we spent some time illustrating how a little bit of math can help simplify our code. By using a Dot Product we were able to determine the angle of an attack. We created a few different types of hit rate components which include the angle of attack to determine their hit rate chances. Since our hit rate components were flexible enough to use exceptions, we implemented some in the “Stop” status effect and even added a new “Blind” status effect to show how values could be modified. Finally, we added another simple UI element which displays the hit rate to the user.

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.

7 thoughts on “Tactics RPG Hit Rate

  1. In FacingsExtensions:
    Why do you use 0.45f as the condition threshold? I think that makes the angle to be about 63 degree(instead of 45).

    1. Great question, I wondered if anyone would notice that πŸ™‚

      When I originally implemented it as 0.45 it was because I wasn’t paying too close of attention and thought of it as degrees as you point out. Even after I thought about it I decided it was okay to leave it as it was because I wasn’t sure I could trust 0.5 to produce correct results due to floating point inaccuracy – it seems that Mathf.Approximately is really wonky particularly when using IL2CPP. I imagine that Unity will fix that in the future, but in my tests, 0.45 was working satisfactorily so I decided to leave it as it was.

  2. I hope things are going great for you! In case you were wondering if anyone missed having an update on Monday, rest assured at least one person did. Thanks again for the great series.

    1. Haha glad to hear that Jordan, don’t worry, I haven’t quit the blog – I just had overtime at work followed by a family vacation πŸ™‚ I’ll try my best to keep things going.

      1. I was pretty sure you hadn’t quite- just wanted you to feel appreciated! πŸ˜‰ Once a week is a fairly intense schedule for a tutorial series, so I am fairly sure people will forgive you for only keeping it most of the time, haha.

Leave a Reply

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