Tactics RPG Magic

We have enough components to make several abilities, and in this lesson will make a few more. Magic based abilities are often the most powerful in an RPG, so they need a way to be regulated. If players could use them freely the game would end up both boring and monotonous. There should be a reason for players to use different abilities at different times. In order to foster this kind of strategy, this lesson covers the use of magic and magic points.

Bugs & Fixes

I noticed a couple of bugs in my Tweener / EasingControl code. First, if I tried to “attack” a tile that didn’t have any enemies and then exit out of the state, I would generate a NullReferenceException. This was occuring because the RectTransformAnimationExtensions was trying to configure the EasingControl for a Tweener on the HitSuccessIndicator, and because there were no targets, the script didn’t try to make the panel appear, and the GameObject was disabled. Because the GameObject was disabled, the Awake method of the Tweener never ran, and the EasingControl was never created, therefore you can’t configure it.

I fixed the first issue by making Tweener a subclass of EasingControl. I had considered a few different options like using a RequireComponent tag, but that would have caused more trouble rather than help because it doesn’t actually add a new component if one already existed, and it makes cleanup that much harder.

Second, even when the EasingControl did exist, there were problems with trying to start a Tick coroutine while the GameObject it was on was disabled. I fixed this issue inside SetPlayState – if the MonoBehaviour isn’t both Active and Enabled, then the state is actually set to Paused and the previousPlayState field is marked with the target state. That way when the OnEnable method is run, the script will be able to Resume correctly.

Make sure to check out the code from the repository in order to get these fixes. There is too much other stuff to write about to add it all here.

Refactoring

Much of the code I am writing now didn’t exist in my first prototype. As a side effect, I am slowing down a bit and there may be more gaps between posts on this project – weekly posts may be too hard for me to keep up, but I will try to at least post every other week. I just wanted to give you all a heads-up.

Furthermore, as I am covering fresh ground I am likely to want to repeatedly change things until it feels “right” – you should never be afraid to throw away your work. I often enough throw away entire projects (and no, I dont plan to throw this one away) and completely start over. You would be surprised how quick you can recreate it, and also how much better subsequent versions can be.

For example, when I created the DamageAbilityEffect component, I had several notifications for getting stats etc. In this lesson I want to add a Heal equivalent which also needs to be able to get stats. Therefore I refactored the shareable portions into the base class.

Likewise, I had code for performing an ability in the PerformAbilityState but decided to make an actual Ability component which handled all the performing logic. This way, if there are different states that might be able to apply an ability I wouldn’t need duplicate code. For example, in some RPG’s you can apply certain abilities through a menu even outside of battle, such as Curing the party or removing Status Ailments. At the moment I don’t plan on having a different state which can perform abilities, and I also dont plan to let you use abilities outside of a battle (I would rather start each battle “fresh”), but it still “felt” better to me, thus the change.

As with the bug fixes, I do not plan to show all of the Refactored code. I’ll try to point out when it is happening, but the changes themselves can be seen in the repository. I will only be showing the new classes and code that are relevant to the ideas of the lesson.

Stat Wrappers

I’ve been waiting for awhile to add some more stat wrappers. If you don’t know what I am talking about you might want to check out the lesson on Stats. In that lesson I showed a Rank script which managed the relationship between Experience and a unit’s Level.

Health

Now that we are able to Attack (and in a moment Cure) we need a way to make sure a unit’s Hit Points don’t go below zero, or above the Max Hit Points stats. Go ahead and add a new script named Health to the Scripts/View Model Component/Actor/ directory.

This script keeps the Hit Points stat in a legal range by using a ClampValueModifier whenever the Hit Points will change notification is fired. Note that you could expose another field for the minimum hit points which could be something other than zero. This way you could have “Immortal” enemies, or special story matches where the enemy doesn’t actually die, but escapes once his HP drops to a certain point.

This script also listens for changes to Max Hit Points which could come as a result of leveling-up, or equipping special gear, etc. In the cases where the max is raised, I decide to also raise the Hit Points by the same amount. In cases where it is reduced (perhaps you un-equipped that gear) then I simply make sure HP stays within bounds.

using UnityEngine;
using System.Collections;

public class Health : MonoBehaviour 
{
	#region Fields
	public int HP
	{
		get { return stats[StatTypes.HP]; }
		set { stats[StatTypes.HP] = value; }
	}

	public int MHP
	{
		get { return stats[StatTypes.MHP]; }
		set { stats[StatTypes.MHP] = value; }
	}

	Stats stats;
	#endregion
	
	#region MonoBehaviour
	void Awake ()
	{
		stats = GetComponent<Stats>();
	}
	
	void OnEnable ()
	{
		this.AddObserver(OnHPWillChange, Stats.WillChangeNotification(StatTypes.HP), stats);
		this.AddObserver(OnMHPDidChange, Stats.DidChangeNotification(StatTypes.MHP), stats);
	}
	
	void OnDisable ()
	{
		this.RemoveObserver(OnHPWillChange, Stats.WillChangeNotification(StatTypes.HP), stats);
		this.RemoveObserver(OnMHPDidChange, Stats.DidChangeNotification(StatTypes.MHP), stats);
	}
	#endregion

	#region Event Handlers
	void OnHPWillChange (object sender, object args)
	{
		ValueChangeException vce = args as ValueChangeException;
		vce.AddModifier(new ClampValueModifier(int.MaxValue, 0, stats[StatTypes.MHP]));
	}

	void OnMHPDidChange (object sender, object args)
	{
		int oldMHP = (int)args;
		if (MHP > oldMHP)
			HP += MHP - oldMHP;
		else
			HP = Mathf.Clamp(HP, 0, MHP);
	}
	#endregion
}

Mana

Because we will be adding a Magic Point “cost” to the use of some magical abilities, then our magic points stat will now start being modified as well. We will need a component to manage the relationship between Magic Points and Max Magic Points which is nearly identical to the Health component we just added. The only difference is that I also want Magic Points to regenerate over time. For this I added a listener for the TurnBeganNotification and give a unit back a percentage of its max stat on each new turn.

using UnityEngine;
using System.Collections;

public class Mana : MonoBehaviour 
{
	#region Fields
	public int MP
	{
		get { return stats[StatTypes.MP]; }
		set { stats[StatTypes.MP] = value; }
	}
	
	public int MMP
	{
		get { return stats[StatTypes.MMP]; }
		set { stats[StatTypes.MMP] = value; }
	}

	Unit unit;
	Stats stats;
	#endregion
	
	#region MonoBehaviour
	void Awake ()
	{
		stats = GetComponent<Stats>();
		unit = GetComponent<Unit>();
	}
	
	void OnEnable ()
	{
		this.AddObserver(OnMPWillChange, Stats.WillChangeNotification(StatTypes.MP), stats);
		this.AddObserver(OnMMPDidChange, Stats.DidChangeNotification(StatTypes.MMP), stats);
		this.AddObserver(OnTurnBegan, TurnOrderController.TurnBeganNotification, unit);
	}
	
	void OnDisable ()
	{
		this.RemoveObserver(OnMPWillChange, Stats.WillChangeNotification(StatTypes.MP), stats);
		this.RemoveObserver(OnMMPDidChange, Stats.DidChangeNotification(StatTypes.MMP), stats);
		this.RemoveObserver(OnTurnBegan, TurnOrderController.TurnBeganNotification, unit);
	}
	#endregion
	
	#region Event Handlers
	void OnMPWillChange (object sender, object args)
	{
		ValueChangeException vce = args as ValueChangeException;
		vce.AddModifier(new ClampValueModifier(int.MaxValue, 0, stats[StatTypes.MHP]));
	}
	
	void OnMMPDidChange (object sender, object args)
	{
		int oldMMP = (int)args;
		if (MMP > oldMMP)
			MP += MMP - oldMMP;
		else
			MP = Mathf.Clamp(MP, 0, MMP);
	}

	void OnTurnBegan (object sender, object args)
	{
		if (MP < MMP)
			MP += Mathf.Max(Mathf.FloorToInt(MMP * 0.1f), 1);
	}
	#endregion
}

Init Battle State

We will need to add the Health and Mana components to our heroes, and since those components require the Stats component, they must be added afterward. Add these two lines just after adding the rank component at the end of the SpawnTestUnits for loop:

instance.AddComponent<Health>();
instance.AddComponent<Mana>();

Heal Ability Effect

One of the first Magical abilities I wanted to add was a way to restore Hit Points. Therefore, I needed to add a “Heal” equivalent of the “Damage” ability effect. Make sure you check out the refactoring to that component and the BaseAbilityEffect parent class in the repository, or the code here wont compile. Then, add a new script named HealAbilityEffect to the same directory as DamageAbilityEffect.

The algorithm for applying the Healing effect should be different than for Damaging. This is because a unit doesn’t “want” the effect of damage, so it makes sense that a good “defense” stat would reduce the amount of damage done. However, under normal circumstances, a unit would “want” the effect of healing, and therefore you wouldn’t want his defense to reduce the amount of healing he could receive.

Unfortunately the guides I was referencing for the Damage algorithm didn’t list a Heal algorithm. I wasn’t sure what to use, so my prediction algorithm is pretty bare – I simply use the Power stat. Feel free to tweak this to something better.

using UnityEngine;
using System.Collections;

public class HealAbilityEffect : BaseAbilityEffect 
{
	public override int Predict (Tile target)
	{
		Unit attacker = GetComponentInParent<Unit>();
		Unit defender = target.content.GetComponent<Unit>();
		return GetStat(attacker, defender, GetPowerNotification, 0);
	}

	protected override int OnApply (Tile target)
	{
		Unit defender = target.content.GetComponent<Unit>();
		
		// Start with the predicted value
		int value = Predict(target);
		
		// Add some random variance
		value = Mathf.FloorToInt(value * UnityEngine.Random.Range(0.9f, 1.1f));
		
		// Clamp the amount to a range
		value = Mathf.Clamp(value, minDamage, maxDamage);
		
		// Apply the amount to the target
		Stats s = defender.GetComponent<Stats>();
		s[StatTypes.HP] += value;
		return value;
	}
}

Unique Ability Effects

I have shown abilities with multiple effects, like an attack that could both cause damage and inflict blind, but I haven’t shown any abilities with unique effects. By this I mean an ability would do different things to different kinds of units rather than multiple things to the same type of unit.

Cure will be our first ability to need unique effects, but I will create a few in this lesson. Cure has the unique effect of healing normal units but damaging the undead. To implement this we will add a few new components:

Undead

Add a new component named Undead to the Scripts/View Model Component/Actor/ directory. At the moment, this script will be an empty MonoBehaviour. It is simply used as a marker to identify if a Unit is undead or not based on whether or not the component is attached. Of course it may include additional logic later.

using UnityEngine;
using System.Collections;

public class Undead : MonoBehaviour 
{

}

Undead Ability Effect Target

Now that we have the Undead component we can add another AbilityEffectTarget type. This component makes sure a Unit either Does or Does NOT have the Undead component based on the state of a toggle. Create and add this script in the same directory as the other AbilityEffectTarget scripts.

public class UndeadAbilityEffectTarget : AbilityEffectTarget 
{
	/// <summary>
	/// Indicates whether the Undead component must be present (true)
	/// or must not be present (false) for the target to be valid.
	/// </summary>
	public bool toggle;

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

		bool hasComponent = tile.content.GetComponent<Undead>() != null;
		if (hasComponent != toggle)
			return false;
		
		Stats s = tile.content.GetComponent<Stats>();
		return s != null && s[StatTypes.HP] > 0;
	}
}

Revive Ability Effect

Another Ability I will add is “Raise” – which actually has three unique effects. Non-undead units are healed, Undead units are damaged, and KO’d units are revived. Note that you can’t “Cure” a KO’d unit because the effect target which I will combine with it has a requirement that the Hit Points are greater than zero, and that makes this effect extra valuable.

using UnityEngine;
using System.Collections;

public class ReviveAbilityEffect : BaseAbilityEffect 
{
	public float percent;

	public override int Predict (Tile target)
	{
		Stats s = target.content.GetComponent<Stats>();
		return Mathf.FloorToInt(s[StatTypes.MHP] * percent);
	}

	protected override int OnApply (Tile target)
	{
		Stats s = target.content.GetComponent<Stats>();
		int value = s[StatTypes.HP] = Predict(target);
		return value;
	}
}

Dependent Ability Effects

For more variety I wanted to show an example of making a dependent ability effect. I had mentioned the suggestion that some abilities might only work based on an earlier effect succeeding. For example, if you perform an attack and the attack doesn’t miss, then an “Inflict Blind” effect might also trigger.

The example I decided to implement is “Drain” – an effect which will “heal” the attacker by however much damage was inflicted. Some games may call it a “vampiric” effect.

using UnityEngine;
using System.Collections;

public class AbsorbDamageAbilityEffectTarget : BaseAbilityEffect 
{
	#region Fields
	public int trackedSiblingIndex;
	BaseAbilityEffect effect;
	int amount;
	#endregion

	#region MonoBehaviour
	void Awake ()
	{
		effect = GetTrackedEffect();
	}

	void OnEnable ()
	{
		this.AddObserver(OnEffectHit, BaseAbilityEffect.HitNotification, effect);
		this.AddObserver(OnEffectMiss, BaseAbilityEffect.MissedNotification, effect);
	}

	void OnDisable ()
	{
		this.RemoveObserver(OnEffectHit, BaseAbilityEffect.HitNotification, effect);
		this.RemoveObserver(OnEffectMiss, BaseAbilityEffect.MissedNotification, effect);
	}
	#endregion

	#region Base Ability Effect
	public override int Predict (Tile target)
	{
		return 0;
	}
	
	protected override int OnApply (Tile target)
	{
		Stats s = GetComponentInParent<Stats>();
		s[StatTypes.HP] += amount;
		return amount;
	}
	#endregion

	#region Event Handlers
	void OnEffectHit (object sender, object args)
	{
		amount = (int)args;
	}

	void OnEffectMiss (object sender, object args)
	{
		amount = 0;
	}
	#endregion

	#region Private
	BaseAbilityEffect GetTrackedEffect ()
	{
		Transform owner = GetComponentInParent<Ability>().transform;
		if (trackedSiblingIndex >= 0 && trackedSiblingIndex < owner.childCount)
		{
			Transform sibling = owner.GetChild(trackedSiblingIndex);
			return sibling.GetComponent<BaseAbilityEffect>();
		}
		return null;
	}
	#endregion
}

Ability

As I mentioned earlier, I refactored some code out of the Battle State and added an Ability component in its place. Among the jobs of this component will be to verify that an Ability can actually be performed. When presenting a turn to a user, we will disable abilities in the menu that can not be performed – which could happen as a result of lacking enough Magic points, or by a status ailment like Silence. However, when it comes to an AI unit, we will allow them to try to use abilities and fail due to the same types of exceptions. This way the user can enjoy a break or a special strategy from time to time.

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

public class Ability : MonoBehaviour 
{
	public const string CanPerformCheck = "Ability.CanPerformCheck";
	public const string FailedNotification = "Ability.FailedNotification";
	public const string DidPerformNotification = "Ability.DidPerformNotification";

	public bool CanPerform ()
	{
		BaseException exc = new BaseException(true);
		this.PostNotification(CanPerformCheck, exc);
		return exc.toggle;
	}

	public void Perform (List<Tile> targets)
	{
		if (!CanPerform())
		{
			this.PostNotification(FailedNotification);
			return;
		}

		for (int i = 0; i < targets.Count; ++i)
			Perform(targets[i]);

		this.PostNotification(DidPerformNotification);
	}

	void Perform (Tile target)
	{
		for (int i = 0; i < transform.childCount; ++i)
		{
			Transform child = transform.GetChild(i);
			BaseAbilityEffect effect = child.GetComponent<BaseAbilityEffect>();
			effect.Apply(target);
		}
	}
}

Ability Magic Cost

Now let’s add the component which regulates the use of our Magical abilities – a component which requires magic points in order to use an ability. We will listen to two of the notifications sent by the Ability component. We will listen to the CanPerform notification and “Flip” an exception toggle when the unit doesn’t have enough magic points to cover the cost of using the ability. We will also listen to the DidPerform notification to actually remove the specified number of Magic Points.

Create a script named AbilityMagicCost and place it in the Scripts/View Model Component/Ability directory.

using UnityEngine;
using System.Collections;

public class AbilityMagicCost : MonoBehaviour 
{
	#region Fields
	public int amount;
	Ability owner;
	#endregion

	#region MonoBehaviour
	void Awake ()
	{
		owner = GetComponent<Ability>();
	}

	void OnEnable ()
	{
		this.AddObserver(OnCanPerformCheck, Ability.CanPerformCheck, owner);
		this.AddObserver(OnDidPerformNotification, Ability.DidPerformNotification, owner);
	}

	void OnDisable ()
	{
		this.RemoveObserver(OnCanPerformCheck, Ability.CanPerformCheck, owner);
		this.RemoveObserver(OnDidPerformNotification, Ability.DidPerformNotification, owner);
	}
	#endregion

	#region Notification Handlers
	void OnCanPerformCheck (object sender, object args)
	{
		Stats s = GetComponentInParent<Stats>();
		if (s[StatTypes.MP] < amount)
		{
			BaseException exc = (BaseException)args;
			exc.FlipToggle();
		}
	}

	void OnDidPerformNotification (object sender, object args)
	{
		Stats s = GetComponentInParent<Stats>();
		s[StatTypes.MP] -= amount;
	}
	#endregion
}

Ability Menu

Next, will need a way to be able to select abilities in-game. At the moment, we have sort of tied the “Attack” menu selection to the Attack ability, but we dont want to manually tie each ability to an action, and we will only want to show the abilities a unit can actually use. We will accomplish this dynamic menu setup by GameObject hierarchy and another component…

Ability Catalog

Add another script named AbilityCatalog to the same Ability directory. This script makes the assumption that we will work with a particular hierarchy of GameObjects. There will be a GameObject with the Catalog script. Then, any children of this GameObject will be considered Categories of abilities. The names of the children will be treated as the names of the Categories and will be displayed in the menu. Next, any children of the Cateogory GameObjects (or grandchildren of the catalog object) will be assumed to be Abilities. The name of the Ability GameObject will likewise be displayed in the menu.

using UnityEngine;
using System.Collections;

/// <summary>
/// Assumes that all direct children are categories
/// and that the direct children of categories
/// are abilities
/// </summary>
public class AbilityCatalog : MonoBehaviour 
{
	public int CategoryCount ()
	{
		return transform.childCount;
	}

	public GameObject GetCategory (int index)
	{
		if (index < 0 || index >= transform.childCount)
			return null;
		return transform.GetChild(index).gameObject;
	}

	public int AbilityCount (GameObject category)
	{
		return category != null ? category.transform.childCount : 0;
	}

	public Ability GetAbility (int categoryIndex, int abilityIndex)
	{
		GameObject category = GetCategory(categoryIndex);
		if (category == null || abilityIndex < 0 || abilityIndex >= category.transform.childCount)
			return null;
		return category.transform.GetChild(abilityIndex).GetComponent<Ability>();
	}
}

Category Selection State

First we will need to modify the LoadMenu method. The only specific menu entry will be “Attack” because I want that to always be an entry. Beyond that, we will show whatever category entries exist in our ability catalog.

protected override void LoadMenu ()
{
	if (menuOptions == null)
		menuOptions = new List<string>();
	else
		menuOptions.Clear();

	menuTitle = "Action";
	menuOptions.Add("Attack");

	AbilityCatalog catalog = turn.actor.GetComponentInChildren<AbilityCatalog>();
	for (int i = 0; i < catalog.CategoryCount(); ++i)
		menuOptions.Add( catalog.GetCategory(i).name );
	
	abilityMenuPanelController.Show(menuTitle, menuOptions);
}

Next, we will need to modify the Confirm method. If attack is chosen, then we will call the Attack method as we did before (though note that I also changed the “Turn” script’s “Ability” reference from a GameObject to the new “Ability” script. If we select anything else, we will need to go into the next state using whatever category we selected.

protected override void Confirm ()
{
	if (abilityMenuPanelController.selection == 0)
		Attack();
	else
		SetCategory(abilityMenuPanelController.selection - 1);
}

Action Selection State

When a category has been selected, we need to dynamically load the Abilities contained within it. As we did before, we will need to update the LoadMenu and Confirm methods, but this time we will also be locking abilities which are currently un-usable. You can make that happen in this weeks demo by simply using up your magic points. After a few turns the unit will regenerate enough magic points to use the ability again.

protected override void LoadMenu ()
{
	catalog = turn.actor.GetComponentInChildren<AbilityCatalog>();
	GameObject container = catalog.GetCategory(category);
	menuTitle = container.name;

	int count = catalog.AbilityCount(container);
	if (menuOptions == null)
		menuOptions = new List<string>(count);
	else
		menuOptions.Clear();

	bool[] locks = new bool[count];
	for (int i = 0; i < count; ++i)
	{
		Ability ability = catalog.GetAbility(category, i);
		AbilityMagicCost cost = ability.GetComponent<AbilityMagicCost>();
		if (cost)
			menuOptions.Add(string.Format("{0}: {1}", ability.name, cost.amount));
		else
			menuOptions.Add(ability.name);
		locks[i] = !ability.CanPerform();
	}

	abilityMenuPanelController.Show(menuTitle, menuOptions);
	for (int i = 0; i < count; ++i)
		abilityMenuPanelController.SetLocked(i, locks[i]);
}

protected override void Confirm ()
{
	turn.ability = catalog.GetAbility(category, abilityMenuPanelController.selection);
	owner.ChangeState<AbilityTargetState>();
}

Demo

Let’s configure our Hero prefab to have a bunch of new abilities. We will leave the “Attack” ability in place, but then we will add a sibling GameObject called “Ability Catalog” (make sure to add the AbilityCatalog script) and add the following Categories, Abilities and Effects in a hierarchy like this:

Unit->Ability Catalog->Ability Category->Ability->Ability Effect

The following list shows the abilities I implemented in the repository. Note that I copied the names and values from Final Fantasy Tactics Advance so you will want to replace or modify them with something original. You can try to create them yourself using the various components to assemble them, or refer to the prefab in the repository.

  • White Magic
    • Cure
      • Heal
      • Damage
    • Raise
      • Heal
      • Damage
      • Revive
    • Holy
      • Damage
  • Sagacity Skil
    • Water
      • Damage
    • Blind
      • Inflict Blind
    • Drain
      • Damage
      • Absorb

After creating (or copying) the abilities I made this week, go ahead and try them out. There is a lot to play test and make sure everything works as expected. I added a non-exhaustive list of things to try out in this demo and it’s easy to tell that this was a huge lesson!

  1. When you use a magical ability are the Magic Points of the caster going down?
  2. Are abilities locked in the menu when you dont have enough magic points to use them?
  3. At the start of a new turn do some Hit Points get regenerated?
  4. Are health and magic stat points clamped between zero and the max values?
  5. Do the correct effects apply to the correct targets? (Try adding an Undead component to one of the Units while playing and verify that Cure becomes deadly)
  6. Can Cure apply to a KO’d unit or do you have to use Raise to revive a KO’d unit first?

Summary

In this lesson we created two new stat wrappers, one for Health and one for Mana. We also created additional components so that we could create a greater variety of Abilities. In addition to the effects we had before, we can now have unique effects which target the Undead, we can Revive units and we can Absorb damage. We have now seen abilities which are physically based and magically based, abilities which have multiple effects on a single target, abilities which have unique effects for unique targets, and abilities which have effects dependent upon the success of other effects. We updated the Ability Menu so that the Categories and Ability entries are dynamic. We also added the ability to make exceptions on when an Ability can be used – our first exception being due to a magic point cost.

There were several files I modified for refactoring and bug fixes here and there which I didn’t necessarily call out. If you haven’t been checking the repository, I would definitely suggest looking over the commits this time around.

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.

46 thoughts on “Tactics RPG Magic

  1. I have a question. How would you go about changing the 3D hero and enemy blobs into 2D static sprites? Rotation of the sprites could be a little tricky, and I wondered how you might approach it for it to work in conjunction with the other scripts.

    Also, are you going to be looking at automating the AI turns in any way?

    Thinking of a game like Fire Emblem, the player moves all his pieces in his turn, and then the enemy takes a turn and moves all it’s pieces. Is that kind of AI something you are interested in covering? Very curious.
    Thank you for the very advanced tutorials. It’s hard to find someone with high levels of programming knowledge willing to share.

    1. Its hard to say for sure how I would do a 2D version without actually trying it. I would want to make a prototype or two and spend some time with it to see what the pros and cons are of various approaches. As a simple step toward 2D, you could just use a GameObject with a SpriteRenderer on it. It would be an easy matter to swap sprites based on the Direction of the unit. You would get the nice 2D look but be able to take advantage of the easy 3D board setup and let the zDepth be sorted by actual 3D rather than complex tile engine stuff.

      As for AI, yep I am actually working on it right now, but its pretty complex so its not quite ready. Hopefully I will have a fully working setup by the end of the month that I can share – its my goal at least. I keep hearing people request Fire Emblem, but I haven’t been able to play it much. The turns in this project will be like Final Fantasy Tactics where each unit takes its turn based on its own speed and actions rather than moving an entire team all at once. It wouldn’t be hard to modify the turn system separate from the AI though so you could move all units in one turn.

      1. I am glad to hear you are working on an AI implementation- I am starting to think about that in my roguelike, and it will be interesting to see how you decide to add it to your framework. Thank you again for these tutorials!

  2. Such a great series!

    I’m just curious about where and how will you put the effects and animations for each of these abilities. Finished the tutorial and I am starting to put some other stuff on it. I have already done the question above, using 2D sprites instead of blobs. But not having those effects is noticeable.

    I learned a lot from your blog, well actually most of my programming knowledge. That’s thanks to all the hard work you’ve done. Looking forward to your next project!

    1. I’m glad you are enjoying it! Unfortunately I don’t intend to add any special fx to this project. Art is a really time intensive and expensive part of a project like this and goes well beyond my available time and resources. The only “art” I have really added was very simple programmer art that didn’t take much effort on my own part to create.

      On one of my side projects I am recreating a Turn Based RPG using real assets I find online such as sprites, sound fx and music, etc. which would be very beneficial for learning from. No word on when that will be in a sharable state but I am at least working on it πŸ™‚

      1. Well that’s too bad ’cause I was just wondering how can I add these art since I am actually an animator. As of the moment, I’ve implemented idle/walk/normal attack animation, and it changes on which direction the unit is facing. Thanks anyway. Excited to see your next project.

        1. BTW, Unity actually has a variety of good project demos and tutorials to learn from for animation and special fx etc. https://unity3d.com/learn/tutorials
          If you haven’t spent much time on their website it might be worth a quick detour. I would probably implement special fx similar to how I did the animate walk state from one square to another. Just make a new state for animating the attack and special fx etc. Unity’s animator states can have scripts attached, key frames, etc so it can be easy to know when things complete and then transition back to another state to continue gameplay. Good luck!

  3. Hey, i did what you said but now i get this error :
    NullReferenceException: Object reference not set to an instance of an object
    CutSceneState.Enter () (at Assets/Scripts/Controller/BattleState/CutSceneState.cs:27)
    StateMachine.Transition (.State value) (at Assets/Scripts/Common/State Machine/State Machine.cs:40)
    StateMachine.set_CurrentState (.State value) (at Assets/Scripts/Common/State Machine/State Machine.cs:9)
    StateMachine.ChangeState[CutSceneState] () (at Assets/Scripts/Common/State Machine/State Machine.cs:24)
    InitBattleState+c__Iterator2.MoveNext () (at Assets/Scripts/Controller/BattleState/InitBattleState.cs:20)
    UnityEngine.SetupCoroutine.InvokeMoveNext (IEnumerator enumerator, IntPtr returnValueAddress) (at /Users/builduser/buildslave/unity/build/Runtime/Export/Coroutines.cs:17)

    And i know what it means i just don’t get what the problem is.

    1. i also have this error now that i made the objects for the abilities :
      NullReferenceException: Object reference not set to an instance of an object
      Health.get_MHP () (at Assets/Scripts/View Model Component/Actor/Health.cs:15)
      Health.OnMHPDidChange (System.Object sender, System.Object args) (at Assets/Scripts/View Model Component/Actor/Health.cs:51)
      NotificationCenter.PostNotification (System.String notificationName, System.Object sender, System.Object e) (at Assets/Scripts/Common/NotificationCenter/NotificationCenter.cs:181)
      NotificationExtensions.PostNotification (System.Object obj, System.String notificationName, System.Object e) (at Assets/Scripts/Common/NotificationCenter/NotificationExtensions.cs:15)
      Stats.SetValue (StatTypes type, Int32 value, Boolean allowExceptions) (at Assets/Scripts/View Model Component/Actor/StatComponent.cs:54)
      Job.LoadDefaultStats () (at Assets/Scripts/View Model Component/Actor/Job.cs:56)
      InitBattleState.SpawnTestUnits () (at Assets/Scripts/Controller/BattleState/InitBattleState.cs:39)
      InitBattleState+c__Iterator2.MoveNext () (at Assets/Scripts/Controller/BattleState/InitBattleState.cs:17)
      UnityEngine.SetupCoroutine.InvokeMoveNext (IEnumerator enumerator, IntPtr returnValueAddress) (at /Users/builduser/buildslave/unity/build/Runtime/Export/Coroutines.cs:17)
      UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
      InitBattleState:Enter() (at Assets/Scripts/Controller/BattleState/InitBattleState.cs:9)
      StateMachine:Transition(State) (at Assets/Scripts/Common/State Machine/StateMachine.cs:40)
      StateMachine:set_CurrentState(State) (at Assets/Scripts/Common/State Machine/StateMachine.cs:9)
      StateMachine:ChangeState() (at Assets/Scripts/Common/State Machine/StateMachine.cs:24)
      BattleController:Start() (at Assets/Scripts/Controller/BattleController.cs:24)

      1. This error also tells you what and where to look for the problem. The issue is that it expected a reference to an object and had nothing instead (a null reference). Line 15 of the Health script tries to use an indexer on a reference to the stats component. If in the Awake method it didn’t find a reference to the stats method then it would be null here and could explain the problem.

    2. The error messages tell you what and where the problem is. Here it says there was a NullReferenceException which means it tried to do something with an object that wasn’t there. This could have happened because you moved or renamed a project asset (or haven’t created it yet), or perhaps the CutSceneState failed to load the conversation data because there was a typo or spelling mistake in the file path you provided. It tells you that the problem occurs in the CutSceneState script on line 27. So just after that line, do some debug logs and check to see what exactly is null and then see if you can figure out why it is null.

  4. Could you post screenshots of how an ability prefab is made (i.e. Cure)? Things aren’t working as expected for me and I want to make sure I have everything down pat.

    1. Sure thing. I added a composited screen grab in the demo section of this post. Also, don’t forget that you can “checkout” (version control term) a commit from my repository at this level and explore the project exactly as I had it configured.

      1. <3 Thank you so much! This sort of thing really helps me find out if it's my own code's failings or if it's a case of me failing to understand the tutorial.

  5. So I seem to be having a UI issue that may be of my own doing.

    What’s happening is that the scene is starting up (and instantiating the one unit I have as a test), but the UI Ability Menu and Stat Panel aren’t being created when I highlight the one particular unit.

    I know that I’m in an ability menu state because when I press alt (key2) I enter the explore state. When I press ctrl tiles get highlighted and my character is able to move (Supposedly. When I finalize the command the character does nothing and the game goes to a stand still). Pressing up/down when in this menu-less state and then pressing ctrl enters the different states available (confirmed by Debug.Log).

    I’m trying to figure out if this has more to do with the Bugfixing done at the beginning of the tutorial or the changes made to ActionSelectionState/CategorySelectionState (and their implementations into Unity) at the end of the tutorial. (I’m leaning ActionSelectionState, because container turns out to be null when we get to GameObject container = catalog.GetCategory(category), but that could be a separate issue on its own).

    Do you have any suggestions as to where I should start looking when it comes to a problem like this?

    1. It could be just about anything, so I would really recommend you use a source control tool like “SourceTree” to checkout the project at the relevant commit. This will allow you to compare everything including the code and the project assets as well. Furthermore, it will highlight the bits of code that actually changed so you know where to look. SourceTree is free so no worries there, and there are SOOO many benefits to learning a version control system.

      If you get stuck on that I can always zip up a copy of the project for you to compare against. Let me know.

  6. There’s a couple of things I noticed when doing this tutorial and its demo. For one, the OnMPWillChange function in the Mana script seems wrong. We create a value change exception called “vce” and call its AddModifier function to add a ClampValueModifier, but we clamp the mana point (MP) value between 0 and max hit points (MHP), when I think we want to clamp MP between 0 and max mana points (MMP). Secondly, the third check in the demo checklist asks if the HP is regenerating each turn, but we never coded that in the Health script. As it is coded right now, only mana is regenerating each turn. To add HP regeneration to the Health script, I mirrored the way the Mana script regenerated mana. I added a Unit “unit” in the Fields region to go along with Stats “stats.” I call GetComponent in the Awake function, getting the Unit component, and set “unit” equal to that. In OnEnable I added “this.AddObserver(OnTurnBegan, TurnOrderController.TurnBeganNotification, unit);” and thus in OnDisable I added “this.RemoveObserver(OnTurnBegan, TurnOrderController.TurnBeganNotification, unit);” I created an OnTurnBegan function just like in the Mana script, but change all references of HP and MHP to MP and MMP, respectively. This led to HP regeneration every turn, but it regenerated even when the Unit was dead, so I changed the if statement in the OnTurnBegan function to this “if ((HP < MHP) && (HP != 0))", basically adding a check to see if HP was equal to 0, i.e. that the Unit wasn't dead, before HP regeneration. Hopefully this helps some of the readers following that got stuck. Also, these tutorials are really helpful for somebody trying to learn code like me, even if there are some areas that could be improved, so thanks for doing all of this Jon!

  7. Good sir, I have really enjoyed these tutorials. Especially the introduction of some more advanced concepts I had been missing out on like your exceptions and also the bitmask operations. I am curious, however, as to how/why you decided to use tracked sibling index in the drain ability effect. I assume the number is populated in the editor at ability design time. I wonder why you chose to go with the index vs a reference to the sibling itself…? Im guessing its so that you are not wasting memory holding onto the abilityeffect when you can just store an int but doesnt it open the system to designer errors? If more ability effects where added to the ability prefab at a later point by another team member. is it possible that the indexes would be shifted and cause the desired behavior to break? What cons do you see to holding a reference to the actual ability effect being tracked instead of using the index? Thanks so much for your great blog.

    Justin

    1. Hey Justin,

      Great questions. It’s been a long time since I wrote this code, so I might not remember all of the reasons, but one of my patterns is to write pre-production scripts which can create assets based off of other data like spreadsheets. It is easier to store and apply things like an “int” in those cases and I probably just casually passed that data along without giving it much thought. It wouldn’t have been difficult to simply cache the reference based on that index (assuming it was added first), and yes – with this kind of setup I think it would be a better choice. Good catch.

      Practically speaking, the pattern here shouldn’t be too “fragile” as long as it was setup correctly as a project asset because the component it wants is cached on Awake anyway. Any “added” components would have indexes greater than the ones I would assume were setup beforehand and wouldn’t be an issue. Of course, if you really tried to break it, you could do something in another Awake method on a component on the same object that executed first and “removed” earlier components, but hopefully you wont do that. Even if you had cached a reference, people can find ways to break your setups, if they are determined or careless enough πŸ™‚

    2. I remembered another reason… If you have more than one of the same kind of component on an object and you drag the “object” to the field, it will just assign the first matching component it finds rather than one you specify. At the time, I didn’t know you could also drag a sibling “component” in the inspector to the field. I only tried that for the first time today and it works. Learn new things all the time. πŸ™‚

  8. This tutuorial is great and I really feel like I have learned so much from it. I am having an issue with this section. I followed the code in the repository to fix the bugs and create the ability catalog. However, I am getting two error messages whenever I select Attack or White Magic/Sagacity Skill. It is either failing to find a target or switch states. I’ve pored over all the code 3 times, so maybe something is wrong in the hiearchy. I`m not sure if there are any scripts I need to add to the scene. Unfortunate I can`t locate any prefabs in the repostiory from this section. They all seem to be from newer commitments.

    This is the error I get whenever I select Attack from the menu then try to select a target. Either a hero clone or a empty tile.

    NullReferenceException: Object reference not set to an instance of an object
    ConfirmAbilityTargetState.UpdateHitSuccessIndicator () (at Assets/Scripts/Controller/Battle States/ConfirmAbilityTargetState.cs:114)
    ConfirmAbilityTargetState.SetTarget (Int32 target) (at Assets/Scripts/Controller/Battle States/ConfirmAbilityTargetState.cs:90)
    ConfirmAbilityTargetState.Enter () (at Assets/Scripts/Controller/Battle States/ConfirmAbilityTargetState.cs:23)
    StateMachine.Transition (.State value) (at Assets/Scripts/Common/State Machine/StateMachine.cs:53)
    StateMachine.set_CurrentState (.State value) (at Assets/Scripts/Common/State Machine/StateMachine.cs:24)
    StateMachine.ChangeState[ConfirmAbilityTargetState] () (at Assets/Scripts/Common/State Machine/StateMachine.cs:37)
    AbilityTargetState.OnFire (System.Object sender, .InfoEventArgs`1 e) (at Assets/Scripts/Controller/Battle States/AbilityTargetState.cs:45)
    InputController.Update () (at Assets/Scripts/Controller/InputController.cs:29)

    This here is the error I get when I select White Mage or Sagacity Skill from the menu. The game won`t progress to the select target state.

    NullReferenceException: Object reference not set to an instance of an object
    ConfirmAbilityTargetState.UpdateHitSuccessIndicator () (at Assets/Scripts/Controller/Battle States/ConfirmAbilityTargetState.cs:114)
    ConfirmAbilityTargetState.SetTarget (Int32 target) (at Assets/Scripts/Controller/Battle States/ConfirmAbilityTargetState.cs:90)
    ConfirmAbilityTargetState.Enter () (at Assets/Scripts/Controller/Battle States/ConfirmAbilityTargetState.cs:23)
    StateMachine.Transition (.State value) (at Assets/Scripts/Common/State Machine/StateMachine.cs:53)
    StateMachine.set_CurrentState (.State value) (at Assets/Scripts/Common/State Machine/StateMachine.cs:24)
    StateMachine.ChangeState[ConfirmAbilityTargetState] () (at Assets/Scripts/Common/State Machine/StateMachine.cs:37)
    AbilityTargetState.OnFire (System.Object sender, .InfoEventArgs`1 e) (at Assets/Scripts/Controller/Battle States/AbilityTargetState.cs:45)
    InputController.Update () (at Assets/Scripts/Controller/InputController.cs:29)

    1. The important part of the error is always listed first. It says you have a NullReferenceException – “null” means “nothing” so a null reference means you have a reference to nothing. It was expecting a reference to an object and didn’t find one. The script “ConfirmAbilityTargetState” and more specifically its method “UpdateHitSuccessIndicator” at line 114 is the location where the failed expectation is occurring. As you continue reading down the output it will probably be less helpful, but gives you an indication of the code flow that was taken to get to this point.

      Anyway, the trick is to open that script, and see what objects are potentially referenced on that line. You can add Debug Logs just before it to check each and every reference for null. When you figure out what is null then you have a better idea what you might need to fix. Nothing exists at line 114 in the code on the repository so either you have added some extra white space or have modified something on your own. I’m guessing your problem is the reference to the “hitSuccessIndicator” since that is the nearest line. I would go to the “Battle Controller” root game object and verify that it has a reference to the “Hit Success Indicator” that is in the scene.

  9. Hi Jon

    I finally had the time to work on this again, though I’m meeting some problems.
    Skills don’t get locked when a unit doesn’t have enough MP, but I’m not entirely sure which script should I look to fix this since there’s a lot of things connected to the menu (ability, abilitymenupanelcontroller,abilitymenu entry etc)

    I compared the scripts in the repo and I seem to have everything identical. Could you give a hint for this?

    Thanks!

    1. The “AbilityMenuEntry” script is the one with the “IsLocked” parameter which would prevent you from activating a skill.

      The “AbilityMenuPanelController” holds the list of “AbilityMenuEntry” and sets the “IsLocked” property inside of a “SetLocked” method (line 66). Any entry which is locked would then be skipped over when attempting to “SetSelection” later (line 122).

      The “ActionSelectionState” knows which entry should be locked or not based on the magic cost and the MP available to the current unit. This occurs in the “LoadMenu” method. There are two loops, one which gets the labels for each menu option and caches whether or not the ability can be performed which it will then apply in a second loop after showing the menu.

      Check out what’s happening in the “Ability.CanPerform” method if your ability isn’t locked but all of the rest of the code until here is working as expected. Hope that helps.

  10. Firstly thank you for taking the time to create such a ridiculously well structured project tutorial. You’re making rocket science accessible here. So I’m testing abilities out and the funny thing is that the abilities are working so far but none are taking cost at all. HP damage is being dealt but MP cost is not being deducted. However I have no errors. Also, I changed an ability’s MP cost to an amount higher than any unit’s MMP and the menu item locked so I know that the MP/MMP is being read it’s just not being deducted.

    Wondering if anyone has come across this at all?

    1. I’m glad you are enjoying the project so much! The magic cost is definitely being applied on the repository project, so it may be a “logic” bug or a prefab misconfigured, etc.

      It is easy to accidentally reference a HP or MHP instead of a MP or MMP stat in your code, in fact there is a bug in the code in my repo where it clamps the magic points based on MHP instead of MMP, so I would double check that.

      Also, make sure that the AbilityMagicCost component is attached to the same object as the Ability component because that is where the magic points will actually be removed after an ability is used.

      1. I sat with it and went backwards while comparing to the repo and realized I didn’t update the PerformAbilityState script to work with the Ability script. User error strikes again. Thanks for your reply and for the heads-up on the MMP clamp as well. Once I get through this ability testing phase I’m going to start working on a party manager and job change scripts.

        1. Glad you got it working – user error gets all of us πŸ™‚

          Good luck on the party manager and job change scripts- its a great idea to test what you’ve learned so far. If you get stuck or need help feel free to post your work in the forums and I’ll be happy to take a look.

  11. How can I change the death to a different animation? I think it was a tweeter making it small, but can not find where the code for it is. I think I can just tell the animation to play at that spot instead, if I can find it.

    1. Look in the KnockOutStatusEffect script. When the component has OnEnable() or OnDisable() invoked it will set the localScale of the unit it is attached to. This could easily be changed to setting an Animator property.

  12. I could be missing something, but shouldn’t “AbsorbDamageAbilityEffectTarget” be classified and organized as an Effect instead of an Effect Target? Thanks ahead of time for any clarification.

    1. You’re absolutely right, good catch! The naming and project location of this file are misleading and should be fixed.

  13. Really wishing the refactor in this and last few lessons had their own commits. Took me a while to figure out what was wrong and which file I missed. In the beginning its difficult to judge which changes on the commit are something in the tutorial, and which ones are part of the refactor so I inevitably miss one or two. I missed BaseAbilityPower this time and it took a while to figure out why notifications were going haywire. Anyway, sorry about the crit. I really am enjoying your tutorials, even if I’m moving at the pace of a crippled snail. Really looking forward to finishing the series and continuing to build on it. Thanks.

    1. Constructive criticism is always appreciated. You are right that I could probably have made that more clear. Initially I kind of thought of it as a sort of exercise for the reader to try refactoring, and that if they got stuck they could always refer to the repository. Next time, I’ll try to make everything more clear.

  14. Hi Jon,
    Thank you for creating this amazing tutorial, but I have some issue about this program.
    First thing is when I use ability to make someone blind or some else status. The status will remove itself after I end the turn no matter what duration I’ve set.
    Second is when a unit have one and only one ability in ability catalog and the mana cost is higher than the mana that unit has, that ability can still be select and cast but because the mana cost is higher than it has the cast will fail (i.e if warrior learned a white magic called holy and this is the only white magic that he learned, the mana cast is far higher than he has, but he can still select that ability and cast it).

    1. Here are a few suggestions that may help:

      1. Use your debugger to step through the code (by using breakpoints and/or debug logs) so that you can check when and why things happen. This is a helpful way to learn and is a great way to figure out where things go wrong.

      2. Read through the relevant lesson(s) again and make sure nothing was missed. Perhaps you missed something even several posts back such as from the Status Effects lesson. You may want to copy and paste code examples instead of manually typing them, just in case.

      3. I would recommend checking out the code from the project repository, then checkout the commit for this lesson. Compare the code against what you have and you should hopefully find the problem.

      1. Thanks for the advice, and I found out why status removed itself so fast. It because I set all of the unit’s speed to a really low value and because of that it keep trigger the turn order controller to post round began notification…
        Thanks a lot

  15. Hello Again,

    I’ve refactored some of the ability selection and controller scripts into base classes to be reused all over the place which is great but I do have a question about scrolling the panel.

    If I constrict the size of the ability menu and I have 15 abilities that won’t all fit what is the best way to scroll the content since we’re not using Unity’s canvas system?

    Some kind of virtual scroll?

      1. I meant that the navigation of elements isn’t done using unitys canvas input but I actually did figure out how to do it by using a mask and shifting the position of the panel when I need to scroll

  16. Hi Jon,

    Thank you for the amazing tutorial. But I got a problem after I change Tweener / EasingControl code. The conversation panel’s arrow stops moving after that and I have no idea why that happens, I’m using Unity 2021.3.16f.

    Could you help me please?

    Thanks

  17. I’ve really enjoyed your tutorial up to this point.

    Your approach to the refactoring; mentioning you’ve done some refactoring before vaguely gesturing to some scripts in your repo: Honestly, it’s terrible, and nearly killed my motivation to pursue this project even though I’m so close to the finish line.

    From a teaching perspective, it’s like one of those painting videos where you see a rough outline, then the artist shows the piece they “worked on themselves”, resulting in some hyper-realistic picture you can barely tell was the target from the rough outline.

    And I understand it’s good to get students to try and think for themselves a bit, but I don’t think something like refactoring is the time/place for that, especially when the changes you made were quite big. I was ready to pull my hair out until I finally realised that the ApplyAbility() function had been RADICALLY refactored, and short of you mentioning the PerformAbilityState script at the very beginning of this post, you make no mention to highlight this change, or the impact it has.

    Again, I want to make clear that I’ve really enjoyed your projects, but your approach to refactoring has been an absolute killer for me, nearly driving me away from the project completely. Please review how you approach refactoring in your future projects – I haven’t looked at any others at this point so I don’t know if this is a reoccuring theme for you, or if this being your first project you didn’t take refactoring into consideration as much at the time.

    For what it’s worth too, this is coming from someone who’s been working with Unity/scripting in C# for nearly a decade now. If I’m tripping over things like refactoring I can only imagine what those refactoring segments have been like for novice’s coming into this project.

    1. Thanks for taking the time to give some constructive feedback. I had probably assumed that a lesson devoted to the refactor would have been as tedious to read as it would have been to write, but I am sure there was some way to improve. Out of curiosity, was the repository unhelpful because you are unfamiliar with source control? If I had a starter project/complete project at the start and end of each tutorial, would that have helped? (That is what I do for my current project)

Leave a Reply to thejon2014 Cancel reply

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