Tactics RPG Turn Order

As it is now, turns are completely linear as if our units were all standing in line. In a tactics battle, things are a lot more complex. Units with a higher speed stat should be able to act more quickly in general, but other exceptions to the rule might exist. Final Fantasy Tactics for example would include status effects such as Haste, Slow, or even Stop. In this lesson we will create a new class to determine when each unit on the board gets to take a turn while keeping things flexible enough to account for all of these types of scenarios.

StatTypes

I decided to add another entry to our StatTypes enum called CTR which stands for “counter” – it is a stat that is used to determine when a unit gets to act. Except in special conditions, it will increment according to the SPD stat and when it passes a certain threshold value, the Unit will be eligible for a turn. Simply taking a turn will knock this number back down, but choosing to take an action and or move will knock it down further still.

Open the StatTypes script and add the following entry inside the enum (just before Count):

CTR, // Counter - for turn order

Turn Order Controller

Create a new script named TurnOrderController in the Scripts/Controller folder.

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

public class TurnOrderController : MonoBehaviour 
{
	#region Constants
	const int turnActivation = 1000;
	const int turnCost = 500;
	const int moveCost = 300;
	const int actionCost = 200;
	#endregion

	#region Notifications
	public const string RoundBeganNotification = "TurnOrderController.roundBegan";
	public const string TurnCheckNotification = "TurnOrderController.turnCheck";
	public const string TurnCompletedNotification = "TurnOrderController.turnCompleted";
	public const string RoundEndedNotification = "TurnOrderController.roundEnded";
	#endregion

	#region Public
	public IEnumerator Round ()
	{
		BattleController bc = GetComponent<BattleController>();;
		while (true)
		{
			this.PostNotification(RoundBeganNotification);

			List<Unit> units = new List<Unit>( bc.units );
			for (int i = 0; i < units.Count; ++i)
			{
				Stats s = units[i].GetComponent<Stats>();
				s[StatTypes.CTR] += s[StatTypes.SPD];
			}

			units.Sort( (a,b) => GetCounter(a).CompareTo(GetCounter(b)) );

			for (int i = units.Count - 1; i >= 0; --i)
			{
				if (CanTakeTurn(units[i]))
				{
					bc.turn.Change(units[i]);
					yield return units[i];

					int cost = turnCost;
					if (bc.turn.hasUnitMoved)
						cost += moveCost;
					if (bc.turn.hasUnitActed)
						cost += actionCost;

					Stats s = units[i].GetComponent<Stats>();
					s.SetValue(StatTypes.CTR, s[StatTypes.CTR] - cost, false);

					units[i].PostNotification(TurnCompletedNotification);
				}
			}
			
			this.PostNotification(RoundEndedNotification);
		}
	}
	#endregion

	#region Private
	bool CanTakeTurn (Unit target)
	{
		BaseException exc = new BaseException( GetCounter(target) >= turnActivation );
		target.PostNotification( TurnCheckNotification, exc );
		return exc.toggle;
	}

	int GetCounter (Unit target)
	{
		return target.GetComponent<Stats>()[StatTypes.CTR];
	}
	#endregion
}

First, I added a few constants. It’s never a good idea to hard-code values in your code. These are sometimes referred to as “mystery numbers” because you don’t always understand the intent behind the number. When you name the value with a constant, the intent is much more obvious. In addition, should you ever decide to alter the values, you can do it in one place, instead of scattered throughout your script.

  • turnActivation – this is the minimum threshold value that the CTR stat must reach before a unit is eligible for a turn (except under special conditions).
  • turnCost – this is the minimum debit to the CTR stat when a unit takes its turn.
  • moveCost – this is an optional debit to the CTR stat, if the unit chooses not to move, it might get another turn more quickly.
  • actionCost – this is an optional debit to the CTR stat, if the unit chooses not to take an action (like attack), it might get another turn more quickly.

Next we have a few notifications. These are also defined as constants. Note that I used the class as a prefix within the string itself. This practice will help to avoid “collisions” in your notification names. It is entirely possible for example that I would have another “roundBegan” notification in some other class, but with the prefix of the class in the notification itself, I will be able to avoid accidental notification handling.

The real meat of this class is the Round method. Note that it is an IEnumerator but we will not be using Unity’s StartCoroutine to use it. Instead, we will use it natively and chose to Step through the code based on actually completing turns through our Battle States. We have done this before, such as with the ConversationController, but if you haven’t quite mastered the idea yet, hopefully seeing it again will help.

I am using an “infinite loop” here by using while (true). Be careful with these because if there is no “pause” point your game will “freeze”. In our case, there is always a “pause” with each unit’s turn, and if we code correctly, a game over state will be recognized before we ever run into a situation where there are no units which can take a turn.

Each cycle of the while loop is considered a complete “Round” – where a round allows all units which are allowed to take a turn to actually take their turn. At the very beginning of this round I post a notification letting our game know that a new round has begun. You could use this notification for any number of things (or nothing at all) but I will probably use it as an opportunity to decrement timers on status effects. For example “Poison” could last for “X” number of rounds – each round it would decrement a “duration” variable and if the value reaches zero could remove itself.

Next, I create a copy of the BattleController’s units list. I do this because it is generally a bad idea to iterate over a list which has the chance of being modified while you are iterating over it. Plus I want to be able to sort the list, but I don’t want the original list to be modified (the order there isn’t important now, but in the future who knows?).

Using our copied list of units, we iterate over each unit and increment the CTR stat by the SPD stat. Note that because we are using the indexer the stat component will allow the value to have exceptions applied. Any class can listen to the corresponding WillChange notification, grab the ValueChangeException argument and make changes. For example I could append a MultValueModifier – a multiplier of “2” could implement a Haste status effect or a multiplier of “0.5” could implement a Slow status effect. You can also simply FlipToggle on the exception and have the implementation for Stop.

Now that the final CTR values for all of the units are known, we sort the list of units according to that stat. Looping through the list again from highest to lowest, we allow any unit which can take a turn, to actually take a turn. When we check whether or not a unit can take a turn, we post another notification which allows our game to make some exceptions. By default, the exception is based on whether or not the unit has a high enough CTR stat to overcome the threshold. However, there are reasons we might not want to allow the unit to take a turn – such as if the unit has been defeated. Alternatively, you might come up with reasons that a unit which normally wouldn’t be able to take a turn, now can take a turn. This notification is your opportunity to make those sorts of changes.

When we find a unit which can act, we assign it to the BattleController’s Turn object. Then we “pause” execution of this loop using the yield statement which will allow control to be passed back to our BattleStates (and other code). Whenever we “continue” execution (through another BattleState) it will pick up exactly where it left off. We can check what has been done on that turn and reduce the CTR stat accordingly. I also fire a notification once the final costs have been applied and the turn is truly at its most “complete” point, though at the moment I don’t know what action I will take here – if any.

After giving all of the units a chanace to take a turn, we post a round complete notification and the whole process will then repeat itself.

Battle Controller

Because the BattleController maintains references to pretty much anything I could want, I will add a reference there for our TurnOrderController‘s round enumerator.

public IEnumerator round;

Init Battle State

To initialize the round enumerator we will go ahead and use our InitBattleState. Add the following line to the Init method:

owner.round = owner.gameObject.AddComponent<TurnOrderController>().Round();

Select Unit State

We will use the SelectUnitState to handle advancement of the round enumerator. Remove the old logic for stepping through each unit and use the following instead:

using UnityEngine;
using System.Collections;

public class SelectUnitState : BattleState 
{
	public override void Enter ()
	{
		base.Enter ();
		StartCoroutine("ChangeCurrentUnit");
	}

	public override void Exit ()
	{
		base.Exit ();
		statPanelController.HidePrimary();
	}

	IEnumerator ChangeCurrentUnit ()
	{
		owner.round.MoveNext();
		SelectTile(turn.actor.tile.pos);
		RefreshPrimaryStatPanel(pos);
		yield return null;
		owner.ChangeState<CommandSelectionState>();
	}
}

Demo

Open our battle scene – press “Play” and experiement to see who moves first – who had the highest speed stat? Make one unit move and act, another only move or act, and one simply wait. Who moves first on the next round? If everything worked correctly you should see the turn order changing based on your actions.

Summary

This week we replaced our placeholder linear turn order system with a notably more complex and dynamic system. The order a unit moves can be determined by a large number of factors including their stats and any number of other scripts such as those which give status effects. In addition, turns can be bypassed completely such as by a unit being KO’d.

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.

22 thoughts on “Tactics RPG Turn Order

  1. I have made a terrible mistake somewhere. I can’t seem to figure out how to get rid of this error.

    “NullReferenceException: Object reference not set to an instance of an object
    InitBattleState.SpawnTestUnits () (at Assets/Scripts/Controller/Battle States/InitBattleState.cs:44)
    InitBattleState+c__Iterator1.MoveNext () (at Assets/Scripts/Controller/Battle States/InitBattleState.cs:17)
    UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
    InitBattleState:Enter() (at Assets/Scripts/Controller/Battle States/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:22)”

    1. Just in case you are unfamiliar, a NullReferenceException indicates that you are trying to “work” with an object that isn’t there. For example, you can’t call a method on a script that you haven’t assigned, etc. Line 44 only has a few possible things which could be null:

      1.) Either the Hero Prefab didn’t have a “Unit” component so line 43 returned null (fix by adding the component to the prefab in the project pane)
      2.) The board reference is null (fix by making sure the BattleController has a reference to the board in the scene)

      I usually try troubleshooting this problem by adding some Debug.Logs just before the line that causes the error, such as
      Debug.Log(“Do I have a board? ” + (board != null));

  2. Ya, it looks like the Unit component got deleted off it or not saved lol. Thanks! Also, random question with your usage of Time.deltaTime. I don’t know much about networking but it seems like you might run into problems if you are playing a multiplayer game and each device has its own time. Do you know the way around this off hand or is it not actually the problem that I appears to be?

    1. Great questions. Unfortunately I haven’t done any multiplayer programming where timing and position is important (such as a first person shooter). If you were playing a turn based rpg like this it shouldn’t matter the speed at which turns complete, but the Time.deltaTime would still be useful to make it visually consistent across devices and platforms. I would think that even with the internet multiplayer stuff you would do general placement via Time.deltaTime and wait for server feedback to make small corrections as necessary, but you are better off asking someone who has actually done it 🙂

  3. With the data that the TurnOrderController cultivates (basically the sorted final CTR list) is it possible to display that data onscreen aside from a debug. Ultimately I would like to a have a list of the current order that the player can see. I’ve a feeling it’s similar to the AbilityMenuPanelController script without input control so not as complex but I’m scratching my head on how to “marry” the two because I don’t need the CTR value to display, just the unit’s name.

    1. I dont think the structure here is ideal for what you want, because the units are only sorted at the beginning of a round, but you would probably want them to remain sorted after every unit’s turn. However, the feature you want to add is not difficult and could easily exist in its own controller script. You could try something like listen for the “TurnCompletedNotification”, and then sort the entire list of units by the CTR stat (just like we do in the TurnOrderController script. You can get the name of the unit because it is the name of the GameObject that the Unit component is attached to. In other words you could use something like:

      “units[i].name”

  4. Hey, one minor question that has been bugging me. Can’t quite find a generic answer for it somewhere, and it seems to mess with my understanding of IEnumerators.

    In your Rounds() method here, you have a “yield return units[i]”. Obviously a Unit does not exactly make intuitive sense as to when/how it would be ‘finished’, and thus trigger the continued calculation. How should we think of this as working, intuitively?

    I kind of reasoned it out to mean “it never yields because there’s no functionality there, so it just waits until we call MoveNext() from elsewhere”, but then why “units[i]” in particular, and not anything else?

    I’ve since modified my turn / round code, so that I now have that statement as “yield return currentPlayer”, which is a Player type that I created when branching out the project. The result is the same, which again I can only justify as “because ‘Player’ doesn’t actually ever yield anything meaningful, it just acts as a ‘wait forever’ feature”.

    How accurate/inaccurate is this way of thinking about that yield statement? Is there anything you can point me towards to make that more clear?

    1. Check out the C# tutorial blog post I wrote on Coroutines, in particular under the heading “The Native Coroutine” as it might help a little. Here is how I think of it… A coroutine is a method which seems to be a mixture of sync and async. This is because each time the method runs, its code runs synchronously up until it encounters a yield statement, at which point all execution is paused until you tell it to continue again. It’s like a magical bookmark right into the middle of your running code. A yield return value is not the same as the return value of a normal method, but is just a current value that you might wish to work with while the method is paused – the returned value has no implied relationship to when the coroutine should resume. Does that help?

      1. Hello,

        Does that mean that we could have put any other value there, but we chose unit[i] just in case we want to access the unit through a round.Current call?

        What could we have used for another default value (that doesn’t really point to anything)? null doesn’t work since it just waits a frame, right? or would it work because we never call a StartCoroutine?

        Cheers

  5. Hi,

    First of all thank you so much for this amazing tutorial, it’s very interesting to read your posts.

    I noticed that in your TurnOrderController script, the RoundBeganNotification gets triggered multiple times. I might be wrong but to me it’s not the expected behavior so I wanted to let you know in case it’s a bug.

    1. Glad you enjoy it. Thanks for the heads up on the repeat notification. I was able to duplicate what you are saying, so I think there might be a bug in there. I’ll try to look into it more when I can, thanks.

      1. Hey.

        I added a few debugs to isolate the problem. It appears that the CanTakeTurn boolean was turned on after about nine rounds through the while loop, triggering nine times the RoundBeganNotification at once. This is because the StatTypes.CTR value required to actually take turn is 1000 (defined in the turnActivation variable). But StatTypes.CTR is incremented based on the StatTypes.SPD (about 100) and takes approximately 9 turns to reach the turnActivation value.

        So I’m pretty certain this is the problem but I’m not in the best position to submit a “safe” fix that would not impact an other script you made.

        Cheers !

        1. Ah, that sounds about right. It has been a while since I really looked at the game’s design and specifics like this are easy to forget. In order to make sure there is only one round notification at a time, you could always restructure it to look at the unit with the max CTR and then give all units the delta necessary to bring the max unit to full turn activation level. In other words, say you have units with CTR of: [0, 250, 700], then you would give all units a boost of 300 so they would now be: [300, 550, 1000] and at least one unit would be able to take a turn that round.

  6. Probably a question for the forums, but. Do you have any ideas/suggestions for stealth mechanics like the ones on Invisible Inc.?
    here’s a review in case you dont know the game.

    1. It wouldn’t be a simple task from where the project is currently, but I would probably try to insert logic where the AI is trying to determine its targets, that while trying to select them, it would then do additional checks (line of sight, invisibility checks etc) which could be accomplished either by ray-casts or some sort of status on the unit. As long as they are not considered a target, the AI wont move toward them or intentionally attack them (though they could still be hit by area attacks aimed at other targets).

  7. I might be missing something here, but:
    what happens if a unit dies on their own turn? wouldn’t that get the game stuck??

    1. It sounds familiar, but I don’t remember if I checked for and handled this use-case in this project or not. Should be easy enough to test using an area of effect attack that hits the caster. Potentially you will need to add something to the state machine that checks after each action whether or not the active unit is still alive and move on to the next turn/check victory conditions if needed.

  8. Hi,

    thank you for this tutorial series!
    Im wondering why you went with the turn start/end notifications within the turn order class and not as battle states itself? Is a particular reason for it or just to mix things up/dont bloat the battle states?

    1. My opinion is that it isn’t really a “right” or “wrong” architecturally speaking – it is more of a personal preference. I like for things to be able to just come together in a loosely coupled way – especially while prototyping. I can add a feature that responds to a notification, and if I decide later I don’t like it or want to change it, I only have to modify code in that one place. If I put everything in a battle state and invoked it directly, then I would have more explicit control and it would be easier to reason about what was happening, but If I decided I wanted to remove something I would have to modify code in both places. There are always pros and cons to every approach you will take, so you should experiment and see what you like working with the best.

  9. Hi, I first wanted to say thanks, I made this tutorial and managed to play this game in Unity 2020, and now I’m creating some new things like hp bar that I’ve already finished and I was wondering if there is a simple way to add animation to the atacks, spells, items, damage and etc… because I want to make the game even more interesting. Would you help me?

    1. Glad you’ve enjoyed the project! I am always happy to try to help, though I am not as experienced with the art side of things because I don’t have an artist to rely on. It would probably be worth posting in the forum for each thing as you go so it is easier to follow the thread.

      As a general note, I think Unity has some very good tutorials that teach how to do animation such as for characters and particles etc, as well as provides assets to learn with.
      https://learn.unity.com/

Leave a Reply to superschure Cancel reply

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