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:

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).

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;
		}
	}
}

Job Parser

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:

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.

31 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.

      1. Thanks for these tutorials, I’ve been learning a lot so far.

        However, I was doing some testing and it doesn’t seem that 0.45 is the correct value for this if you want to get the diagonals to match the picture you posted.

        The value you get from a dot product doesn’t really line up to degrees directly. Say we calculate a dot product between (0,1) (target facing north) and (1,1) (attacker SW of the target). Normalizing our vectors gives us (0,1) and (1/sqrt(2), 1/sqrt(2)). Doing the dot product:

        Dot = (0*1/sqrt(2)) + (1*1/sqrt(2)) = 1/sqrt(2) ~= .71

        So the threshold should actually be around .71. I personally used +-0.7f if I wanted the diagonal to be included with the back/front facing and used +-0.72f if I wanted the diagonal to be included in the side facing. If you want to match the picture provided above both thresholds would be set to +-0.7f. Since I’m emulating the original FFT, I ended up using:

        if (dot >= 0.72f) // backwards diagonals count as side facing
        return Facings.Back;
        if (dot <= -0.7f) // forwards diagonals count as front facing
        return Facings.Front;
        return Facings.Side;

  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.

  3. You may want to clarify on the “Jobs” part, that the script to modify is the JobParser, just in case someone thinks that goes into the Job class.

    1. No problem… I updated the heading to say “Job Parser” instead of “Jobs”, thanks for the feedback.

  4. For organization’s sake, what should I do with the Info classes script? For the time being, I placed it in the EventArgs folder, but since I can’t seem to find any reference as to where it should go, I figure I should ask, just to be sure.

      1. Ah, alright. I couldn’t find it in the Google Drive project repository, so that might be something to look into, unless it’s been renamed since, or something. Thanks again.

  5. Having a small difficulty, with displaying the correct damage dealt. I added a damage modifier based on the character type. For example if the unit type is Knight, the hp lost from a hit is reduced 25%. I simply listen for onhpdidchange, then if knight reduce25. If a mage add 25. The damage taken seems to actually subtract correctly, however the damage amount shown on the screen is the predict amount.

    Any idea how to update it to show correctly?

    1. Could be a few things. For example, if you wait to modify the damage dealt until after a HP change notification, then that is probably too late. I would try observing a different notification such as a when determining the amount of damage to inflict in the first place. I would expect to see those kinds of modifiers already factored in to the predicted amount. There are a variety of notifications for getting stats and determining attack power based on the attacker and target for these purposes.

      Even if you still want to wait and only modify based on damage dealt, then I would next suggest you try observing the “WillChangeNotification” of the HP instead of the “DidChangeNotification”. This is one last chance to modify the values before they are truly applied. Then when you go to display the final value, it should already have been correctly determined.

      Also, make sure the label, or whatever it is, which displays the damage dealt is listening to the correct notification. I would want to listen to something which tells me the amount of damage that was applied, not the actual amount of HP lost. For example, say you do a really strong attack that has a damage force of 9999, but the target only had 1 hitpoint. I would care more about seeing the strength of my attack than the mere amount of HP I could reduce.

      1. Thank you for the feedback! I checked again and as you said, I actually had already set to listen to the OnHPWillChange, Stats.WillChangeNotification(StatTypes.hp)

        Was trying base it off of the haste effect mostly.
        In the baseabilityeffect when I set the new hit rate info, I think that I need to update here somehow. It seems that in the predict function the getstat for damage is not applying my modification.

        1. So after some back and forth I may be getting further from the goal.

          Just to walk through and get my thoughts clear,

          So the unit has a new script on it called element. Element gives the unit an element type, and a weakness automatically via dictionary Enums. (This works out well and seems to function as intended.)

          Right now it listens to OnHPWillChange and adjusts for elemental strength/weakness accordingly. (This works fine, damage is dealt as expected.)

          The predict part of an ability however does not take the element into effect.

          Since the hit rate floating message is based off of the predict, not the actual amount dealt, the numbers do not match up.

          I feel that it should go here in predict somehow, then not register the OnHPWillChange listener. Basically it will just be an added 10% bonus or penalty for various elements.

          Is there an example of something like this? I am just not very sure I understand the exceptions system that is in place for calculating the attack damage. Perhaps my logic is missing something somewhere?

          1. Ok, so I made a call in the predict function to the element on the unit, which calculates and returns the bonus modification. All things works and run smoothly, but I definitely need to recheck notifications for the damage stacking. Feels like I will definitely need to refactor it later once I understand it better.

  6. Works brilliantly except for one thing:
    For some reason when targeting a unit with 0hp, it does not appear, and causes an exception when attempting to back out of the selection.

    Any idea what may cause this?

    1. Also of note that the hp of an attacked unit can fall negative, I’m not sure If I’ve missed something there? But the line that fails appears to be in panel: Probably because when looking over a unit with < 0 hp, the hit indicator does not appear, so backing out of the selection causes the null ref.

      Is it that I should have specified attacks cannot target 0HP targets at some point?

      1. A 0HP unit is knocked out, and would not be considered a valid attack target. See the `DefaultAbilityEffectTarget` component for an example – that should be attached to the Damage of an Attack ability.

        1. Mm, ok, I do have that attached.
          The scripts attached to the attack object include “Constant ability range” “Unit Ability area” “default Ability Effect Type” and “A Type Hit Rate”.

          Is there one of these that conflict?

          1. Ah, I’ve found out that I can actually target empty squares as well, and attacking these causes a similar crash. Not sure whats happening, but will keep posted if I find a solution.

        2. Wait, Just to clarify: What do you mean “attached to the damage of the attack ability”?
          Is that a separate game object to the attack one? If so, Where was setting that up covered?

          1. In the “Demo” section for the “Ability Range” lesson, you are instructed to create the “Attack” child game object on the “Hero” prefab. Then, in the “Demo” section for “Ability Area Of Effect”, you are instructed to try adding variations of ranges, areas and effect targets (one of each) to that same object.

            What I was referring to (and sorry for any confusion here because it may have been too early) was based on the completed demo project. You can see a prefab at “Assets/Resources/Abilities/Common/Attack”.

            This base of this GameObject has the following:
            * Ability
            * Physical Ability Power
            * Line Ability Range
            * Full Ability Area

            Then it has a child GameObject named Damage with the following:
            * A Type Hit Rate
            * Default Ability Effect Target
            * Damage Ability Effect

            Hope that helps!

  7. Ah, I think I may know where I’ve gone wrong. No computer in front of me at the moment, but thank you very much for your super helpful Tutorial all the same. I come from web dev, so the switch to games was a bit daunting, but feeling much more that I would be able to be creative after learning some key basics through your guides.

    Cheers for being awesome for the programming community.

  8. If I wanted to implement a system where if a unit was attacked once from the left side once from the right side and once from the back it would stagger them into and automatically hit state would I want to use cross product in my angle of attack process since the cross products would end up unique?

    1. I’m not sure a cross product would give you what you are looking for. You might try something like a Transform component’s InverseTransformDirection, where you convert an attacker’s forward vector to the defender’s local space. Then It would be much simpler to determine the direction including differentiating from left and right.

Leave a Reply

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