Tactics RPG Victory Conditions

Now that we have enemies, we can also provide an actual “goal” for the battle. First we must be able to actually defeat the enemies, as well as risk defeat for our own units. There needs to be a consequence for a unit’s hit points dropping to zero, so we will add a “Knock Out” status effect which disables a unit from acting or taking additional turns. Likewise there should be an effect for defeating all enemy units, or allowing all hero units to perish. These are sample “victory conditions” which we will track, and which will allow the battle to end.

Knock-Out Status Effect

I’ve decided to implement “Knock-Out” as a Status Effect – for obvious reasons. It modifies a particular unit in a particular way, it has a condition of application (no hit points), and it can be removed, such as by a “Revive” ability. Add a new script named KnockOutStatusEffect to the Scripts/View Model Component/Status/Effects folder.

At the moment this status effect is responsible for three basic things:

  1. Visual indication of a KO – when the status is enabled the unit it applies to will appear squashed. In a complete game, you might use this opportunity to flip a toggle on an animation controller so that it plays a death state animation.
  2. Disabling the incrementation of the turn counter stat. This way a KO’d unit wont be able to build up a huge supply of counter points and be able to take multiple turns in a row if revived.
  3. Disabling the ability to take turns. This way if a unit already had a high enough counter to act, it still wont be given a turn.

The visual aid of squashing a unit is applied in the OnEnable method and removed in the OnDisable method by modifying the scale of the transform. Both of the other tasks require listening to notifications – note that we must specify a sender so we don’t block ALL units from taking turns. In the notification handlers, we simply flip the exception’s toggle in order to disable an action.

using UnityEngine;
using System.Collections;

public class KnockOutStatusEffect : StatusEffect
{
	Unit owner;
	Stats stats;

	void Awake ()
	{
		owner = GetComponentInParent<Unit>();
		stats = owner.GetComponent<Stats>();
	}

	void OnEnable ()
	{
		owner.transform.localScale = new Vector3(0.75f, 0.1f, 0.75f);
		this.AddObserver(OnTurnCheck, TurnOrderController.TurnCheckNotification, owner);
		this.AddObserver(OnStatCounterWillChange, Stats.WillChangeNotification(StatTypes.CTR), stats); 
	}

	void OnDisable ()
	{
		owner.transform.localScale = Vector3.one;
		this.RemoveObserver(OnTurnCheck, TurnOrderController.TurnCheckNotification, owner);
		this.RemoveObserver(OnStatCounterWillChange, Stats.WillChangeNotification(StatTypes.CTR), stats);
	}

	void OnTurnCheck (object sender, object args)
	{
		// Dont allow a KO'd unit to take turns
		BaseException exc = args as BaseException;
		if (exc.defaultToggle == true)
			exc.FlipToggle();
	}

	void OnStatCounterWillChange (object sender, object args)
	{
		// Dont allow a KO'd unit to increment the turn order counter
		ValueChangeException exc = args as ValueChangeException;
		if (exc.toValue > exc.fromValue)
			exc.FlipToggle();
	}
}

Note that I exposed the BaseException‘s “defaultToggle” field, although I still block changes to it.

public readonly bool defaultToggle;

This change was necessary because I couldn’t just blindly change the exception toggle, or our KO status could end up allowing units which normally couldn’t take turns (because they didn’t have a high enough move counter), to now take turns. It is only when a KO’d unit has a high enough move counter, and so normally could take a turn, that we need to make a flip. An alternate fix could have been to have the TurnOrderController post different notifications, one for when a unit can take a turn and one for when it cant. Then we could have listened to a more specific event and flipped the exception toggle without any extra checks.

Stat Comparison Condition

The KO status effect can be removed, but the question is who should be responsible for removing it? There are two main options. The first option is that we could have the same class which inflicts the KO status effect be responsible for removing it. This isn’t always a great solution because the “inflictor” may need to go out of scope before the status would need to be removed. For example, if an enemy unit uses an ability to inflict a status ailment, and then you defeat the enemy, we could choose to remove that enemy from battle. Of course if we did, then it wouldn’t be there to remove the status it had inflicted.

This dependency chain could cause problems in the other direction as well. We could change the previous example so that the enemy unit didn’t die, but perhaps the status which was inflicted was “cured” by a different ability at a different time. When the “duration” condition of the enemy ability would be completed and the enemy ability would then try to remove the status – it wouldn’t find it (or if it had maintained a reference, then there might be some other kind of problem if the Status Effect GameObject had been destroyed or unparented from the expected hierarchy, etc.).

Both of the previous problem cases are not an issue when using self-maintaining status condition components. For example when an enemy ability inflicts a status ailment it normally attaches it with a DurationStatusCondition and then it does not need to be concerned with keeping any references to remove the status later, nor does it care if a different ability is able to cure the status effect.

This sort of custom component is our second option, and is the option I have chosen for this use-case. Create a new script named StatComparisonCondition and place it in the same folder as the other StatusCondition scripts.

using UnityEngine;
using System;
using System.Collections;

public class StatComparisonCondition : StatusCondition 
{
	#region Fields
	public StatTypes type { get; private set; }
	public int value { get; private set; }
	public Func<bool> condition { get; private set; }
	Stats stats;
	#endregion

	#region MonoBehaviour
	void Awake ()
	{
		stats = GetComponentInParent<Stats>();
	}

	void OnDisable ()
	{
		this.RemoveObserver(OnStatChanged, Stats.DidChangeNotification(type), stats);
	}
	#endregion

	#region Public
	public void Init (StatTypes type, int value, Func<bool> condition)
	{
		this.type = type;
		this.value = value;
		this.condition = condition;
		this.AddObserver(OnStatChanged, Stats.DidChangeNotification(type), stats);
	}

	public bool EqualTo ()
	{
		return stats[type] == value;
	}
	
	public bool LessThan ()
	{
		return stats[type] < value;
	}
	
	public bool LessThanOrEqualTo ()
	{
		return stats[type] <= value;
	}
	
	public bool GreaterThan ()
	{
		return stats[type] > value;
	}
	
	public bool GreaterThanOrEqualTo ()
	{
		return stats[type] >= value;
	}
	#endregion

	#region Notification Handlers
	void OnStatChanged (object sender, object args)
	{
		if (condition != null && !condition())
			Remove();
	}
	#endregion
}

It’s a semi-long script but functionality-wise is very simple. It has a few fields, one for a type of stat to observe, one for a value to compare against, and one for a special delegate which returns a bool data type. The script provides several convenience methods which you can use for that delegate (EqualTo, LessThan, etc) but you are also able to provide your own if necessary. You configure the component by calling the Init method and passing along parameters for each of those fields.

We will be able to use this script for our KO status condition by passing along a stat type of “HP”, a value of zero, and we can use “EqualTo” for our condition delegate. What this means is that every time the “HP” stat changes, the script will check whether or not “HP” equals zero, and if not, then the condtion will be removed. Assuming that this is the only condition causing the “KO” status, then the status will also be removed.

Auto Status Controller

Next we need something to actually apply the KO status whenever a unit’s HP are reduced to zero. That could be done in the Stats or Health components, but I really like all of my components to be as simple as possible and to really just have a single “job”. For example, I could say that the “job” of the Health component is to “maintain a relationship between two stats- to make sure that HP never holds a value less than the min or greater than the max HP”. I would need to use “and” in my job description to say that the component also applies KO status whenever one of the stats reaches zero, which really means that it is another job, and is suitable to another component.

So I decided to create a new script whose job is to “apply status effects which occur as a result of events rather than direct actions”. If the script were to get very large I might break it down into one script per status effect. For now, just create a new script called AutoStatusController and place it in the Scripts/Controller directory.

using UnityEngine;
using System.Collections;

public class AutoStatusController : MonoBehaviour 
{
	void OnEnable ()
	{
		this.AddObserver(OnHPDidChangeNotification, Stats.DidChangeNotification(StatTypes.HP));
	}

	void OnDisable ()
	{
		this.RemoveObserver(OnHPDidChangeNotification, Stats.DidChangeNotification(StatTypes.HP));
	}

	void OnHPDidChangeNotification (object sender, object args)
	{
		Stats stats = sender as Stats;
		if (stats[StatTypes.HP] == 0)
		{
			Status status = stats.GetComponentInChildren<Status>();
			StatComparisonCondition c = status.Add<KnockOutStatusEffect, StatComparisonCondition>();
			c.Init(StatTypes.HP, 0, c.EqualTo);
		}
	}
}

This script will need to be attached to an object in the battle scene. I have attached it to the “Battle Controller” gameobject. Once this is done, you could play the scene and see how “KO” works. Try attacking a single unit until its HP are reduced to zero. It should squash down. Wait a few turns to see that the defeated unit is not allowed to take a turn. Now use another unit to revive the KO’d unit – it should scale back to its normal size and be allowed to take turns again.

Health

I mentioned in the previous lesson that you could modify the Health component to have a “Minimum Hit Point” field so you could specify values other than zero and have some enemies not actually be KO’d. I went ahead and added that feature. Check out the repository if you need help, but basically I reference the minimum field in all the places I had been referencing zero.

I added this so that I could have special victory conditions where you are targeting a single unit, and that single unit needs to be able to live another day for a typical RPG sort of story – it escapes!

Victory Conditions

Now that we have enemies, we can have a real game. By this I mean that we can have some sort of conditions upon which to base whether we win or lose. Create a new script named BaseVictoryCondition and add it to the Scripts/Controller/Victory Conditions directory. I will break this script down into several bits:

using UnityEngine;
using System.Collections;

public abstract class BaseVictoryCondition : MonoBehaviour
{
	public Alliances Victor
	{
		get { return victor; } 
		protected set { victor = value; }
	}
	Alliances victor = Alliances.None;
	
	// ... Add next code samples here
}

Note that this class is abstract – it provides some common functionality that will be reused, but it only forms the base for other subclasses. In a fully implemented game like Final Fantasy Tactics there are a lot of different missions with a lot of different objectives, for example:

  • You might need to find an item(s) on the game board and enemies might keep spawning until you either find them or are defeated.
  • You might need to escort a unit and either keep it from being defeated for a certain number of turns, or just defeat all of the enemy units before they can get to it.
  • You might need to simply defeat all enemy units.
  • You might need to defeat a particular enemy unit.

To external classes, the only thing that appears in this class is the Victor property. When a battle has just started, or is in progress, the property will return “None”, meaning that there is no winner. The script will be self managed and listen to events posted by other classes to determine when a winning or losing condtion has occured and then it will update the property accordingly.

Sometimes I would post an event for something as “important” as deciding that a battle has been decided, but in this case, I will simply check for a Victory whenever changing states.

protected BattleController bc;

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

We will need a reference to the BattleController (so we can reference other game content like the list of units) so I added a field and assigned it in the Awake method. Note that the methods are marked as virtual in case subclasses need to extend or change any of the logic.

protected virtual void OnEnable ()
{
	this.AddObserver(OnHPDidChangeNotification, Stats.DidChangeNotification(StatTypes.HP));
}

protected virtual void OnDisable ()
{
	this.RemoveObserver(OnHPDidChangeNotification, Stats.DidChangeNotification(StatTypes.HP));
}

protected virtual void OnHPDidChangeNotification (object sender, object args)
{
	CheckForGameOver();
}

Next, I register for notifications that a Unit’s hit points have changed. Since I don’t specify a “sender” of the notification in the parameters, it will listen to ANY sender. Note that this will check for a GameOver every time a unit’s HP actually changes – even if it is getting healed. Actions in a turn based game like this don’t occur very rapidly so we wont notice a perfomance issue, but in another game, I might want to listen to more specific events, such as that the HitPoints were fully depleted.

protected virtual bool IsDefeated (Unit unit)
{
	Health health = unit.GetComponent<Health>();
	if (health)
		return health.MinHP == health.HP;
	
	Stats stats = unit.GetComponent<Stats>();
	return stats[StatTypes.HP] == 0;
}

With this method I can determine if any particular unit is considered “defeated” or not. Since we added the “Minimum Hit Point” feature to the Health component, I need to check whether or not that value has been reached. I suppose that it is possible that there may be scenarios where some unit’s may not have a Health component (although I dont know what that reason would be) so as a fallback, I simply check the stats component.

protected virtual bool PartyDefeated (Alliances type)
{
	for (int i = 0; i < bc.units.Count; ++i)
	{
		Alliance a = bc.units[i].GetComponent<Alliance>();
		if (a == null)
			continue;

		if (a.type == type && !IsDefeated(bc.units[i]))
		    return false;
	}
	return true;
}

Next I have a method which can determine if any “Party” of units has been defeated. This method loops through all the units which the Battle Controller has a reference to. If a single unit of the party alliance is “not” defeated, then I can immediately return that the party is not defeated. If I have looped through all the units and not found an exception, then the party “is” defeated.

protected virtual void CheckForGameOver ()
{
	if (PartyDefeated(Alliances.Hero))
		Victor = Alliances.Enemy;
}

Finally, here is the notification handler which checks whether or not the “Hero” party has been defeated any time any unit’s hit points have been changed. I only check for this because all mission types can be “lost” by letting the entire hero party be defeated, on the other hand, defeating all of an enemy party may not necessarily constitue a “win”. I will let the subclasses extend from here and determine that.

Defeat All Enemies Victory Condition

Create a new subclass named DefeatAllEnemiesVictoryCondition. All we need to do is override the base class check for game over. Be sure to maintain the functionality by calling the “base” implementation – we still want to “lose” if all of the hero party is defeated. In the event that the Hero party has not been defeated (so that the Victor would still say “None”) then I proceed to check whether or not the player has won. In this case, the player must defeat the entire enemy party to win.

using UnityEngine;
using System.Collections;

public class DefeatAllEnemiesVictoryCondition : BaseVictoryCondition 
{
	protected override void CheckForGameOver ()
	{
		base.CheckForGameOver();
		if (Victor == Alliances.None && PartyDefeated(Alliances.Enemy))
			Victor = Alliances.Hero;
	}
}

Defeat Target Victory Condition

Add another subclass named DefeatTargetVictoryCondition. In this variation, we only care about a single target. If you can KO the single target you can win even with other enemy Units still active on the board. Ideally when this victory condition is in play the user should be notified both in the mission objective (before beginning the battle) as well as in the scene with some sort of marker indicating who the target actually is.

using UnityEngine;
using System.Collections;

public class DefeatTargetVictoryCondition : BaseVictoryCondition 
{
	public Unit target;

	protected override void CheckForGameOver ()
	{
		base.CheckForGameOver ();
		if (Victor == Alliances.None && IsDefeated(target))
			Victor = Alliances.Hero;
	}
}

Init Battle State

Because maps might be reusable with different enemy sets or story elements, some portions of the battle configuration should be dynamic. In other words, we don’t want to attach a Victory Condition to anything in the scene, but instead would load one during the init step of the battle, based on some sort of external setting.

Despite my good advice, I don’t have any such external setting yet. I simply specified a particular entry and manually configured it. I chose the Victory condition where you don’t actually have to defeat the entire enemy party, you merely need to defeat the enemy leader. In addition I modified the “Minimum Hit Point” field of the target enemy so that he isn’t actually defeated. He will live to fight another battle!

void AddVictoryCondition ()
{
	DefeatTargetVictoryCondition vc = owner.gameObject.AddComponent<DefeatTargetVictoryCondition>();
	Unit enemy = units[ units.Count - 1 ];
	vc.target = enemy;
	Health health = enemy.GetComponent<Health>();
	health.MinHP = 10;
}

Note that you will need to invoke this AddVictoryCondition method from somewhere. I added a call just after the SpawnTestUnits call in the Init method.

Cut Scene State

Just like there is an “intro” cut-scene for our battle, there will often need to be an “outro” cut-scene. Whenever this state enters, it should load the story based on whether battle has ended or not. In some cases you may also wish to have different scenes play based on whether you won or lost. Of course if you lose, you could just go to a Game Over screen. The example below does not reflect final code. A better example would not hard code references to resources because they will need to change based on the level you are playing. Note that I moved the data management from the Awake and OnDestroy methods to the Enter and Exit methods so that it could change.

The content of the conversation data doesn’t really matter. Feel free to create your own or grab the ones from my repository. Just make sure that you either update the resource strings or use the same names and paths.

public override void Enter ()
{
	base.Enter ();
	if (IsBattleOver())
	{
		if (DidPlayerWin())
			data = Resources.Load<ConversationData>("Conversations/OutroSceneWin");
		else
			data = Resources.Load<ConversationData>("Conversations/OutroSceneLose");
	}
	else
	{
		data = Resources.Load<ConversationData>("Conversations/IntroScene");
	}
	conversationController.Show(data);
}

Likewise, when the state is exiting, it needs to go to an appropriate state – if the battle has ended it will go to a new EndBattleState

void OnCompleteConversation (object sender, System.EventArgs e)
{
	if (IsBattleOver())
		owner.ChangeState<EndBattleState>();
	else
		owner.ChangeState<SelectUnitState>();
}

I declared the IsBattleOver and DidPlayerWin methods in the base class BattleState as follows:

protected virtual bool DidPlayerWin ()
{
	return owner.GetComponent<BaseVictoryCondition>().Victor == Alliances.Hero;
}

protected virtual bool IsBattleOver ()
{
	return owner.GetComponent<BaseVictoryCondition>().Victor != Alliances.None;
}

End Battle State

The end battle state is really just a placeholder for now which causes the scene to reload. It could be replaced by a sequence of several other states, such as showing what items you had earned and how much experience you had gained. Eventually it would be responsible for returning to some other part of the game where you can manage your team, choose other missions, etc.

using UnityEngine;
using System.Collections;

public class EndBattleState : BattleState 
{
	public override void Enter ()
	{
		base.Enter ();
		Application.LoadLevel(0);
	}
}

Perform Ability State

It is after performing an ability that you are most likely to have triggered a victory condition. We will need to pay close attention to what has happened – you might even have KO’d yourself, and we need to act in a way which makes sense under various conditions.

IEnumerator Animate ()
{
	// TODO play animations, etc
	yield return null;
	ApplyAbility();

	if (IsBattleOver())
		owner.ChangeState<CutSceneState>();
	else if (!UnitHasControl())
		owner.ChangeState<SelectUnitState>();
	else if (turn.hasUnitMoved)
		owner.ChangeState<EndFacingState>();
	else
		owner.ChangeState<CommandSelectionState>();
}

I modified the Animate method so that it has a few more conditions which determine which state will follow. If a victor has been declared, and therefore the battle is over, then we change to the Cut Scene State where we can play our outro scene. If the unit has killed itself (whether by accident or not who can tell) then we don’t bother letting it continue its turn (such as moving and or deciding on an end facing direction.

bool UnitHasControl ()
{
	return turn.actor.GetComponentInChildren<KnockOutStatusEffect>() == null;
}

For now, KO is the only status which would disable a turn, so I simply checked for it directly. If there were more ways to stop you in the future I might post an event instead and allow status effects the ability to respond through exception toggles.

Stat Panel

There is one bit of polish we ought to make now that we can determine the difference between hero and enemy units. Open up the StatPanel script and make a small change to the Display method by swapping this:

// Temp until I add a component to determine unit alliances
background.sprite = UnityEngine.Random.value > 0.5f? enemyBackground : allyBackground;

with this:

Alliance alliance = obj.GetComponent<Alliance>();
background.sprite = alliance.type == Alliances.Enemy ? enemyBackground : allyBackground;

Facing Indicator

Since I am doing a little extra polish, I also brough in the Facing Indicator from the prototype project. This simple script displays a highlighted sphere in the facing direction of a unit. It is pretty obvious what direction a unit is facing anyway, but without the indicator it isn’t always obvious what the purpose of the active state is.

using UnityEngine;
using System.Collections;

public class FacingIndicator : MonoBehaviour 
{
	[SerializeField] Renderer[] directions;
	[SerializeField] Material normal;
	[SerializeField] Material selected;
	
	public void SetDirection (Directions dir)
	{
		int index = (int)dir;
		for (int i = 0; i < 4; ++i)
			directions[i].material = (i == index) ? selected : normal;
	}
}

It is constructed with four simple sphere objects parented to a central empty game object. Each sphere is offset in the direction that the unit would be facing and they are scaled smaller so they look nice. Parent the indicator to the Tile Selection Indicator. Feel free to grab it from the repository if you struggle making your own.

Add a field for it to the BattleController and hook it up in the scene. Have the Game Object begin as disabled. Then open up the EndFacingState. Enable the Game Object as well as call SetDirection on Enter, and keep it updated inside OnMove.

owner.facingIndicator.gameObject.SetActive(true);
owner.facingIndicator.SetDirection(turn.actor.dir);

Demo

Go ahead and play the scene. You are still controlling both the hero and enemy units, so play once where the heroes win, and once where they lose. Don’t forget to try reviving a unit – enjoy!

Summary

In this lesson we actually started turning this project into a real game. It could potentially be fun if played by two local players, one controlling the hero party and one controlling the enemy party. Now, when units are KO’d, they are not able to take new turns, and when a whole party is KO’d the battle is over. Just for fun, we added some outro conversation data. We also added some polish here and there.

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.

10 thoughts on “Tactics RPG Victory Conditions

  1. Hey, great guide.

    I’ve been following it to design my own project for awhile now and I was wondering, how would you go about adding a conversation choice that changes the victory condition of the upcoming battle?

    For example, there’s a story character in a battle but they betrayed you at some point so you can choose to save them or leave them, which would change the victory condition to “protect NPC” or “Kill all the enemies”

    1. This is a somewhat complicated modification. You would need to fully grasp all of the content in the Tactics RPG project and then you should be able to handle it. For example, a sub ‘State Machine’ could be responsible for determining when you are just reading a conversation vs needing to respond to a question or make some sort of decision. The menu for determining what decision to take could work in much the same way as the ability menu does. Then you would need some sort of manager script which could save your response and then either in response to the menu entries or at another time when it is appropriate, you can load the Victory Condition before the battle has truly started.

    2. An “easy” way, would be to develop a previous scene using some kind of conversation managing plugin like Fungus or something similar and then load a different scene depending on the choices made.

      1. It’s a start, but I wouldn’t personally want to have duplicate battle scenes where the only difference is a victory condition setting – for something like this I think it would be much easier to have a single scene with different assets that load into it based on current game state.

  2. I’ve encountered a bit of an issue. When killing an AI unit with a status effect on the start of their turn (such as poison), the KO Status Effect will be applied to the unit, however it’s still able to take a turn. Any idea why it’s still able to take an extra turn even though it’s considered dead by having the KO effect? The unit will move around, and cast spells at 0HP while flattened, and will continue to do so until hit with a normal attack or spell during another turn.

    I changed the AutoStatusController to check if the HP is <= 0 and if so placing it at 0, instead of just ==0 to fix the units running around with negative HP.

    void OnHPDidChangeNotification (object sender, object args)
    {
    Stats stats = sender as Stats;
    if (stats[StatTypes.HP] <= 0)
    {
    stats[StatTypes.HP] = 0;
    Status status = stats.GetComponentInChildren();
    StatComparisonCondition c = status.Add();
    c.Init(StatTypes.HP, 0, c.EqualTo);
    }
    }

    However I’m a little stumped on why these “zombie” units are still acting after death.

    Thanks,

    Felkin

    1. If a turn has already begun and the unit dies, then there needs to be some code somewhere that can cancel a turn. I’m not sure I added that, though it seems like an important step, it would also be important for any move that applies damage to the user. One quick solution might be to apply the effect of poison at a different point, such as the “TurnComplete” or “RoundEnded” phase rather than a “TurnBegin” phase. This would allow you to prevent a unit from moving in the “TurnOrderController”‘s “CanTakeTurn” method.

  3. Any idea how difficult would be to implement location-based (you lose when all or a certain unit reaches a certain tile or you win when that happens) or time-based (resist X turns or win within X turns) victory conditions with your system?

    1. Shouldn’t be too hard. Just create your own subclass of the BaseVictoryCondition, and observe the relevant notifications. Whenever a turn ends you can check position of a unit. Whenever a round ends you can increment a counter, etc.

  4. I am having trouble with a survive x turns conditional.

    I created a new condition.

    I use the round ended notification to check for game over.
    The check uses the round counter (changed to public for testing, will make a property to get later.) and compares it to a value of the battle state owner.

    Added a debug log to check the method is called.

    In game I play through rounds, and I even get the debug log in the method, but it seems that the victor just doesn’t get set and the game continues, any ideas?

Leave a Reply

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