Ability Menu

In this post we will continue to flesh out the UI by adding the Ability Menu. This menu will allow the user to determine what phase of a turn is active- such as moving, attacking, etc. as well as what to do during a turn- such as what kind of skill to use during an attack. We will actually implement the menu where possible (Move), and anything we haven’t gotten to yet will use placeholder content (Attack, Magic etc). We will also see how to support canceling a move and be able to restore an earlier state.

Object Pooling

You can pool just about anything – including menu elements. Our Ability Menu will have the ability to grow or shrink based on the list of entries you tell it to display. Because of that, it makes sense to be able to pool those entries. So before we get started, check out my post on Object Pooling and import the final implementation scripts into this project.

Ability Menu Entry

This component handles the display of a single entry in the menu. Each entry can have various flags applied and based on the flags which are set, the entry will assume one of three visible states: Default, Selected, or Locked. Each of the different states will have visible changes such as different colors for the text, stroke, bullet, etc.

Add a script named AbilityMenuEntry to Scripts/View Model Component. Add a using statement for UI because we will be editing some UI components.

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

public class AbilityMenuEntry : MonoBehaviour 
{
	// Add Code Here
}

We will need a reference to an Image which we can set to use 1 of 3 different sprites (set depending on state flags). We will also need a reference to a Text component to show what the menu entry is for, and finally we will get a reference to the Outline component on the Text component object so we can also change its color. We will manually link up all but the Outline, which will be connected during Awake:

[SerializeField] Image bullet;
[SerializeField] Sprite normalSprite;
[SerializeField] Sprite selectedSprite;
[SerializeField] Sprite disabledSprite;
[SerializeField] Text label;
Outline outline;

void Awake ()
{
	outline = label.GetComponent<Outline>();
}

Lets add a property which wraps the Text component’s string:

public string Title
{
	get { return label.text; }
	set { label.text = value; }
}

The flags I mentioned before will be implemented as an enum:

[System.Flags]
enum States
{
	None = 0,
	Selected = 1 << 0,
	Locked = 1 << 1
}

Let’s add a Property and backing field to hold the current State, and then add a few convenience properties which allow me to get and set whether or not the flags are set – without other classes ever needing to know about the States enum in the first place.

public bool IsLocked
{
	get { return (State & States.Locked) != States.None; }
	set
	{
		if (value)
			State |= States.Locked;
		else
			State &= ~States.Locked;
	}
}

public bool IsSelected
{
	get { return (State & States.Selected) != States.None; }
	set
	{
		if (value)
			State |= States.Selected;
		else
			State &= ~States.Selected;
	}
}

States State
{ 
	get { return state; }
	set
	{
		if (state == value)
			return;
		state = value;
		
		if (IsLocked)
		{
			bullet.sprite = disabledSprite;
			label.color = Color.gray;
			outline.effectColor = new Color32(20, 36, 44, 255);
		}
		else if (IsSelected)
		{
			bullet.sprite = selectedSprite;
			label.color = new Color32(249, 210, 118, 255);
			outline.effectColor = new Color32(255, 160, 72, 255);
		}
		else
		{
			bullet.sprite = normalSprite;
			label.color = Color.white;
			outline.effectColor = new Color32(20, 36, 44, 255);
		}
	}
}
States state;

Finally, let’s add a public method to Reset the flags all at once:

public void Reset ()
{
	State = States.None;
}

Ability Menu Panel Controller

Next, let’s add the script which will handle displaying the menu and the menu entries, manage skipping locked entries, etc. It should be able to show and hide itself, and when not used, disable its canvas for efficiency. Add a new script named AbilityMenuPanelController to Scripts/Controller. Like before, we will need to add a using Statement for UI. We will also use Generics.

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

public class AbilityMenuPanelController : MonoBehaviour 
{
	// Add Code Here
}

As we have done before, we will use const string declarations for safety sake when working with the Panel and Pool Manager. Add the following consts:

const string ShowKey = "Show";
const string HideKey = "Hide";
const string EntryPoolKey = "AbilityMenuPanel.Entry";
const int MenuCount = 4;

We will need to expose a few fields such as: a reference to the menu entry prefab (for instantiating our pooled objects), a reference to the Heading’s title label (to show context), a reference to the Panel (for Toggling visibility and for a container for menu entries), a reference to the canvas (so we can disable it when not used), a list holding all of the active menu entries, and a value representing the currently selected index in the menu.

[SerializeField] GameObject entryPrefab;
[SerializeField] Text titleLabel;
[SerializeField] Panel panel;
[SerializeField] GameObject canvas;
List<AbilityMenuEntry> menuEntries = new List<AbilityMenuEntry>(MenuCount);
public int selection { get; private set; }

During MonoBehaviour’s Awake, we will configure the Pool Manager so that it can generate the menu entries for us and have them ready.

void Awake ()
{
	GameObjectPoolController.AddEntry(EntryPoolKey, entryPrefab, MenuCount, int.MaxValue);
}

To get menu entries and return them to the pool manager, I added some convenient methods:

AbilityMenuEntry Dequeue ()
{
	Poolable p = GameObjectPoolController.Dequeue(EntryPoolKey);
	AbilityMenuEntry entry = p.GetComponent<AbilityMenuEntry>();
	entry.transform.SetParent(panel.transform, false);
	entry.transform.localScale = Vector3.one;
	entry.gameObject.SetActive(true);
	entry.Reset();
	return entry;
}

void Enqueue (AbilityMenuEntry entry)
{
	Poolable p = entry.GetComponent<Poolable>();
	GameObjectPoolController.Enqueue(p);
}

Anytime I want to clear the menu, I will want to make sure and loop through each entry and Enqueue it.

void Clear ()
{
	for (int i = menuEntries.Count - 1; i >= 0; --i)
		Enqueue(menuEntries[i]);
	menuEntries.Clear();
}

In MonoBehaviour’s Start, we will make sure the Panel has hidden itself and then disable the canvas until we need it.

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

When we are ready to show and hide the Menu through game events, we will want to animate it into position. I added a method to catch the Tweener and specify a consistent duration and easing equation.

Tweener TogglePos (string pos)
{
	Tweener t = panel.SetPosition(pos, true);
	t.easingControl.duration = 0.5f;
	t.easingControl.equation = EasingEquations.EaseOutQuad;
	return t;
}

The menu itself will always highlight a single entry as Selected. Since entries can be locked, we will need to know whether or not we are allowed to select any given entry. If we can’t select an entry, then we will need to try to select something else instead.

bool SetSelection (int value)
{
	if (menuEntries[value].IsLocked)
		return false;
	
	// Deselect the previously selected entry
	if (selection >= 0 && selection < menuEntries.Count)
		menuEntries[selection].IsSelected = false;
	
	selection = value;
	
	// Select the new entry
	if (selection >= 0 && selection < menuEntries.Count)
		menuEntries[selection].IsSelected = true;
	
	return true;
}

We will expose two public methods which will allow the menu controller to try to select the next or previous entry in its list. Since the adjacent entry or entries could in theory be locked, I need to try setting the selection in a loop. I use a for loop which, at most, will loop enough to test every entry in the menu list, potentially coming back to where it started. It may not be intuitive how to achieve this using a for loop, since to check each entry you will need the index value to wrap. The solution for wrapping is the modulus operator.

public void Next ()
{
	for (int i = selection + 1; i < selection + menuEntries.Count; ++i)
	{
		int index = i % menuEntries.Count;
		if (SetSelection(index))
			break;
	}
}

public void Previous ()
{
	for (int i = selection - 1 + menuEntries.Count; i > selection; --i)
	{
		int index = i % menuEntries.Count;
		if (SetSelection(index))
			break;
	}
}

To initially load and display the menu, I’ve exposed a method called Show where you pass along the title to display in the header, as well as a list of string which are the text to show for each entry in the menu.

public void Show (string title, List<string> options)
{
	canvas.SetActive(true);
	Clear ();
	titleLabel.text = title;
	for (int i = 0; i < options.Count; ++i)
	{
		AbilityMenuEntry entry = Dequeue();
		entry.Title = options[i];
		menuEntries.Add(entry);
	}
	SetSelection(0);
	TogglePos(ShowKey);
}

After loading the menu, you may wish to specify that some of the menu entries are locked. In this game, you are allowed to Move and or take an Action once in each turn. If you move, then the next time you see the command menu on the same turn, the move option will be locked – this way you aren’t allowed to move again until you get another turn.

public void SetLocked (int index, bool value)
{
	if (index < 0 || index >= menuEntries.Count)
		return;

	menuEntries[index].IsLocked = value;
	if (value && selection == index)
		Next();
}

When the user confirms a menu selection (using the Fire1 input) then we can dismiss the panel.

public void Hide ()
{
	Tweener t = TogglePos(HideKey);
	t.easingControl.completedEvent += delegate(object sender, System.EventArgs e) 
	{
		if (panel.CurrentPosition == panel[HideKey])
		{
			Clear();
			canvas.SetActive(false);
		}
	};
}

Turn

To help display the funcionality of our new menu, I feel that it would be a good time to go ahead and model a Turn. Each turn I want a unit to be able to Move and or Attack, but to do so in any order. Moving should be undo-able, but not attacking, otherwise you could undo and repeat an attack until whatever kind of random modifiers in play would occur the way you desired. In addition, if you move and then attack, you should not be able to undo your move because you could cheat the system by moving in close, attacking, undoing your move, and then moving out of reach of a counter attack.

The process of updating the turn data, handing undo, etc will all come through Game States, but the relevant data itself will be held in a model. Add a script named Turn to the Scripts/Model folder.

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

public class Turn 
{
	public Unit actor;
	public bool hasUnitMoved;
	public bool hasUnitActed;
	public bool lockMove;
	Tile startTile;
	Directions startDir;

	public void Change (Unit current)
	{
		actor = current;
		hasUnitMoved = false;
		hasUnitActed = false;
		lockMove = false;
		startTile = actor.tile;
		startDir = actor.dir;
	}

	public void UndoMove ()
	{
		hasUnitMoved = false;
		actor.Place(startTile);
		actor.dir = startDir;
		actor.Match();
	}
}

Battle Controller

We will need to add a few more properties to the Battle Controller, so go ahead and open it up for editing. We will add an instance of Turn, a list of all the units in battle (dont forget to add a using System.Collections.Generic), and of course the AbilityMenuPanelController.

public AbilityMenuPanelController abilityMenuPanelController;
public Turn turn = new Turn();
public List<Unit> units = new List<Unit>();

Since we have now modeled a Turn, and the turn knows what unit is currently selected, we dont need the reference to currentUnit in the BattleController. Go ahead and remove it, and fix the references which will now be broken (see MoveSequenceState and MoveTargetState).

Battle State

We are likely to want to use the properties we just added to the Battle Controller in our Battle States, so lets wrap them for convenience.

public AbilityMenuPanelController abilityMenuPanelController { get { return owner.abilityMenuPanelController; }}
public Turn turn { get { return owner.turn; }}
public List<Unit> units { get { return owner.units; }}

Init Battle State

Since we added a List of all the Units to the battle controller, we need to populate it with something. The units are currently instantiated in the Init state, so open that script and add the following line at the end of the for loop in the SpawnTestUnits method.

units.Add(unit);

Select Unit State

The SelectUnitState was a temporary implementation we added before which allowed you to select what unit to move. In the real game, the units will be selected from a turn order based on their speed. We are not going to add that feature completely yet, but I will simulate something which is closer – we will have the units get turns in a simple linear fashion, one after another. I want to do this because it helps show the turn sequence more clearly since you have no control over whose turn it is.

Replace the contents of the script with the following:

using UnityEngine;
using System.Collections;

public class SelectUnitState : BattleState 
{
	int index = -1;

	public override void Enter ()
	{
		base.Enter ();
		StartCoroutine("ChangeCurrentUnit");
	}

	IEnumerator ChangeCurrentUnit ()
	{
		index = (index + 1) % units.Count;
		turn.Change(units[index]);
		yield return null;
		owner.ChangeState<CommandSelectionState>();
	}
}

Explore State

Now that we have removed the ability to move the cursor freely around the board from the SelectUnitState, we need to add a way to return that ability to the user. Sometimes you want to get a good idea of the layout of the board to plan your movement route, or to see what enemies are on the board and where they are located. This state will be accessable by canceling from the command menu.

Add a script named ExploreState to the Scripts/Controller/Battle States folder.

using UnityEngine;
using System.Collections;

public class ExploreState : BattleState 
{
	protected override void OnMove (object sender, InfoEventArgs<Point> e)
	{
		SelectTile(e.info + pos);
	}
	
	protected override void OnFire (object sender, InfoEventArgs<int> e)
	{
		if (e.info == 0)
			owner.ChangeState<CommandSelectionState>();
	}
}

Base Ability Menu State

There will be several game states which are very similar – basically I will model a state for each page and sub-page of the menu. Their shared functionality will be to load and display the menu on Enter, dismiss the menu on Exit, use button presses to confirm or cancel, and use keyboard / joystick input for changing the menu selection. Because of the shared functionality, I created an abstract base class so that I only need to implement the parts of the code which are different.

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

public abstract class BaseAbilityMenuState : BattleState
{
	protected string menuTitle;
	protected List<string> menuOptions;

	public override void Enter ()
	{
		base.Enter ();
		SelectTile(turn.actor.tile.pos);
		LoadMenu();
	}

	public override void Exit ()
	{
		base.Exit ();
		abilityMenuPanelController.Hide();
	}

	protected override void OnFire (object sender, InfoEventArgs<int> e)
	{
		if (e.info == 0)
			Confirm();
		else
			Cancel();
	}

	protected override void OnMove (object sender, InfoEventArgs<Point> e)
	{
		if (e.info.x > 0 || e.info.y < 0)
			abilityMenuPanelController.Next();
		else
			abilityMenuPanelController.Previous();
	}

	protected abstract void LoadMenu ();
	protected abstract void Confirm ();
	protected abstract void Cancel ();
}

Command Selection State

The first concrete subclass of BaseAbilityMenuState will be CommandSelectionState. Add the script to the Scripts/Controller/Battle States folder. This state will display the menu you see at the beginning of a turn, which allows you to either Move, make an Action, or Wait. Wait would end your turn immediately, but the other two options have additional states and or sub menus. If you Move or make an Action, then you will also return to this state, but with the relevant option now locked.

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

public class CommandSelectionState : BaseAbilityMenuState 
{
	// Add Code Here
}

The only code we need to add is the implementation of the abstract methods listed in the base class. For loading the menu, we will always have the same three options, so it is fine just to hard-code them. However, the options can be locked based on when in the turn you see this menu.

protected override void LoadMenu ()
{
	if (menuOptions == null)
	{
		menuTitle = "Commands";
		menuOptions = new List<string>(3);
		menuOptions.Add("Move");
		menuOptions.Add("Action");
		menuOptions.Add("Wait");
	}

	abilityMenuPanelController.Show(menuTitle, menuOptions);
	abilityMenuPanelController.SetLocked(0, turn.hasUnitMoved);
	abilityMenuPanelController.SetLocked(1, turn.hasUnitActed);
}

Since the menu options wont change, we can also hard-code the actions to take on the menu selection.

protected override void Confirm ()
{
	switch (abilityMenuPanelController.selection)
	{
	case 0: // Move
		owner.ChangeState<MoveTargetState>();
		break;
	case 1: // Action
		owner.ChangeState<CategorySelectionState>();
		break;
	case 2: // Wait
		owner.ChangeState<SelectUnitState>();
		break;
	}
}

If you try to cancel from this state, one of two things will occur – it will either undo your Move (if possible) or switch to the ExploreState so you can search the board.

protected override void Cancel ()
{
	if (turn.hasUnitMoved && !turn.lockMove)
	{
		turn.UndoMove();
		abilityMenuPanelController.SetLocked(0, false);
		SelectTile(turn.actor.tile.pos);
	}
	else
	{
		owner.ChangeState<ExploreState>();
	}
}

Category Selection State

When the user chooses Action from the Command Selection State, this state will become active. It allows you to determine what kind of action to take. You can simply Attack, or you can activate a special skill from categories of skills like White or Black Magic.

Although Attack will be a fixed entry in this sub-menu, the other categories themselves should be dynamic based on the Unit selected and whatever determines what skills they actually have (like their Job, etc). Because I haven’t added this portion of the system, we will just hard-code a few options to serve as place-holder content.

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

public class CategorySelectionState : BaseAbilityMenuState 
{
	protected override void LoadMenu ()
	{
		if (menuOptions == null)
		{
			menuTitle = "Action";
			menuOptions = new List<string>(3);
			menuOptions.Add("Attack");
			menuOptions.Add("White Magic");
			menuOptions.Add("Black Magic");
		}
		
		abilityMenuPanelController.Show(menuTitle, menuOptions);
	}

	protected override void Confirm ()
	{
		switch (abilityMenuPanelController.selection)
		{
		case 0:
			Attack();
			break;
		case 1:
			SetCategory(0);
			break;
		case 2:
			SetCategory(1);
			break;
		}
	}
	
	protected override void Cancel ()
	{
		owner.ChangeState<CommandSelectionState>();
	}

	void Attack ()
	{
		turn.hasUnitActed = true;
		if (turn.hasUnitMoved)
			turn.lockMove = true;
		owner.ChangeState<CommandSelectionState>();
	}

	void SetCategory (int index)
	{
		ActionSelectionState.category = index;
		owner.ChangeState<ActionSelectionState>();
	}
}

Note that this implementation doesn’t check whether or not to lock any menu entries, but in a fully implemented game, there might be conditions which could. For example, some sort of status ailment might prevent you from Attacking. Those sorts of scenarios could be addressed in the LoadMenu method.

When the user Confirms an Attack, I mark the Turn model to show that an action has taken place. This way the next time the CommandSelectionState is entered, we wont be able to take another action. If the Unit has also moved, I also tell the turn to lock movement so that it can’t be undone either.

Whenever the user selects a skill category, we will enter another sub-menu and state, but first, we let the state know which category we picked.

Finally, if the user should cancel from this state, we simply drop back to the previous menu.

Action Selection State

The final menu state we will add is the one which appears when picking a skill category from the CategorySelectionState. The implementation shown below is all hard coded, but only because we have not designed this portion of the game yet. In the future, the choices should be dynamically driven based on the unlocked skill-set of the current unit.

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

public class ActionSelectionState : BaseAbilityMenuState 
{
	public static int category;
	string[] whiteMagicOptions = new string[] { "Cure", "Raise", "Holy" };
	string[] blackMagicOptions = new string[] { "Fire", "Ice", "Lightning" };

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

		if (category == 0)
		{
			menuTitle = "White Magic";
			SetOptions(whiteMagicOptions);
		}
		else
		{
			menuTitle = "Black Magic";
			SetOptions(blackMagicOptions);
		}

		abilityMenuPanelController.Show(menuTitle, menuOptions);
	}

	protected override void Confirm ()
	{
		turn.hasUnitActed = true;
		if (turn.hasUnitMoved)
			turn.lockMove = true;
		owner.ChangeState<CommandSelectionState>();
	}

	protected override void Cancel ()
	{
		owner.ChangeState<CategorySelectionState>();
	}

	void SetOptions (string[] options)
	{
		menuOptions.Clear();
		for (int i = 0; i < options.Length; ++i)
			menuOptions.Add(options[i]);
	}
}

Move Target State

Now that we have menu states, it would be nice to be able to cancel out of the move target state. Modify the OnFire method to the following:

protected override void OnFire (object sender, InfoEventArgs<int> e)
{
	if (e.info == 0)
	{
		if (tiles.Contains(owner.currentTile))
			owner.ChangeState<MoveSequenceState>();
	}
	else
	{
		owner.ChangeState<CommandSelectionState>();
	}
}

Move Sequence State

We will also need to change the exit target of MoveSequenceState. Modify the Sequence method as follows:

IEnumerator Sequence ()
{
	Movement m = turn.actor.GetComponent<Movement>();
	yield return StartCoroutine(m.Traverse(owner.currentTile));
	turn.hasUnitMoved = true;
	owner.ChangeState<CommandSelectionState>();
}

Scene Setup

The menu we will be creating looks something like this:

00_AMC_Scene_zpsdiuz4ubu

This screen grab shows the menu with a single entry called “Move” but the menu itself is dynamic and able to hold as many entries as we ask it to. We will be creating two prefabs from this: the entry itself (so we can clone it and add entries) and the menu panel (without the entry attached to the hierarchy).

Use the following hierarchy screen grab and inspector screen grabs (there is one for each object – in order – from top to bottom) to help you recreate the asset. When you have finished creating all the pieces, attaching scripts and assigning references, then drag from the Ability Menu Entry in the Hierarchy pane to the Project pane to create a prefab of just that bit of content. Afterward, delete the Ability Menu Entry from the hierarchy and then create a prefab by dragging the Ability Menu Controller to the Project pane. It is important that you don’t include the entry in this prefab, or you will always have one more entry in your menu than you expected.

Note that the reference to Entry Prefab on the Ability Menu Controller should point to the prefab in the Project, not the instance that had been in the scene. If you accidentally assigned the local reference, then when the entry is deleted, your assignment will be lost.

01_AMC_Hierarchy_zpsy45l1rkl

02_AMC_Inspector_zpsist3vnab

03_AMC_Canvas_zpssecumi8f

04_AMC_Panel_zpsnpxaqknv

05_AMC_Header_zpswt6jtayc

06_AMC_HeaderLabel_zpsmbytm5q5

07_AMC_Entry_zpsttwuquqh

08_AMC_EntryBullet_zpsr1fettzf

09_AMC_EntryLabel_zpspmodgbaz

Make sure to save the Project as well as the Scene so that the Ability Menu Entry is included. Also, don’t forget to assign the reference in the Battle Controller for this menu.

Go ahead and play the scene. Make sure to try out various combinations of making progress on your turn and then cancelling it to verify that our ability to undo is correctly implemented.

In The Future…

Although moving a unit on the board feels complete, there is a lot left to do regarding unit Actions. When you choose to Move you enter a state which allows you to pick your move target. When you take an Action, whether attacking or using magic, we will need to provide a similar state which allows us to pick a target(s). Then, after picking a target(s), we should have another state which gives us an estimate of the effect of applying the action to the target(s).

We will also add a state at the end of a turn which allows you to control the end facing direction of your unit, instead of coming back to the Command Selection State and making you choose Wait.

Those few additions will help the turn flow feel much better, but what we have is good enough to get a general idea, and to see how to work with the Menu itself, which was the primary goal of this lesson.

Summary

This was another huge lesson – I hope I didn’t lose anyone! We created a reusable menu which allowed the user to control the flow of a turn. We modeled the turn data itself, and hooked up the menu to a new sequence of Game States which made it easy to apply commands and even undo actions. There is still a lot left to do in order to make it a fully playable game, but we are looking more and more complete all the time!

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.

48 thoughts on “Ability Menu

  1. Hi! Thank you very much for this tutorial! I have a small problem, hidden whenever the menu and then display it again appears with the font size smaller. Whenever hides and redisplays the letter is shrinking. I can not find where is the error

    1. Hey Gustavo, the first thing I would recommend is to download the source code from my repository and verify that the problem doesn’t also exist there. Assuming everything there works on your system, you can start looking for areas where your code might be different to help locate the source of the issue. It sounds like a programming problem – I would start by looking for anywhere in your scripts where you are assigning font size.

      1. Hey, download files from your repository and the problem also arises there. Curiously, this problem is not in the project that you share in the first part of Introduction. I will review the scripts to see whether the problem detected. If I find the solution I share here.

        1. I must admit I wasn’t expecting to hear you say you see the problem in the repository also. I had thought that maybe you made some extra modifications on your own that might explain it, because I am not assigning or modifying font size anywhere in my code, and that is the only thing I would expect could be changing the font size, unless you modified the text component itself and enabled best fit or something (different string lengths could make the text appear bigger or smaller to fit).

          The original project was made with an older version of Unity, and the code, prefabs, etc have all changed in the newer version as well. I suppose that could be note-worthy. Are you on Mac or PC? What version of Unity are you on? I am using Unity 5.1.1 f1 on a mac.

        2. Hey Gustavo, I found the problem. It was my fault – I didn’t bother to check the project on different screen sizes and the canvas changing size allowed the problem to appear. The issue is that I didn’t update the local scale of the menu entry after adding it to the menu panel. Use the following line in the AbilityMenuPanelController’s Dequeue method (after setting the panel as the parent):
          entry.transform.localScale = Vector3.one;

  2. When I view my BattleController in the inspector, I see the MoveTargetState is still enabled even after moving to other states. Is this okay?

    1. That is fine, Enabled vs Disabled (a monobehaviour field) is different than the State’s Enter vs Exit (what state is the state machine’s active state). Just keep in mind the difference when registering and unregistering for events.

      1. This sparks my curiosity – why is it that InitBattleState becomes disabled when it is no longer the active state, while other states remain enabled? I can’t actually see any section in the code that would cause it to become disabled.

        1. To be honest I am not sure what you are seeing. I checked out the project at this commit just to be sure, and I don’t see the toggle which marks a state as enabled or disabled on any of the state scripts.

          In my experience you will only see that toggle when the script has an OnEnable (or maybe OnDisable) method declared.

          If your state is indeed becoming disabled then you can put a Debug.Log statement in the OnDisable method and see what is triggering it in the stack trace.

  3. Hello again,

    Another great installment! Really loving this, learning a ton about UI (and so much more)!

    However, I ran into my first real roadblock! I don’t think I am setting up my Ability Menu Controller properly, and I can’t seem to figure out why I’m getting a NullReference.

    NullReferenceException: Object reference not set to an instance of an object
    CommandSelectionState.LoadMenu () (at Assets/_scripts/Controller/Battle States/CommandSelectionState.cs:18)

    // it goes back all the way to SelectUnitState, the error is thrown as soon as it attempts to begin a turn

    Now, I figured I’d misunderstood your setup of the prefabs of the Controller and Entry, but I have tried just about every reference and re-reference I can, to no avail. I’ve scoured your code a few times now, to compare to mine, and it’s absolutely due to some strange referencing I did wrong.

    Any ideas?? Thanks so much!

    1. I also get this error
      NullReferenceException: Object reference not set to an instance of an object
      MoveTargetState.Enter () (at Assets/Scripts/Controller/BattleState/MoveTargetState.cs:13)
      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[MoveTargetState] () (at Assets/Scripts/Common/State Machine/State Machine.cs:24)
      CommandSelectionState.Confirm () (at Assets/Scripts/Controller/BattleState/CommandSelectionState.cs:28)
      BaseAbilityMenuState.OnFire (System.Object sender, .InfoEventArgs`1 e) (at Assets/Scripts/Controller/BattleState/BaseAbilityMenuState.cs:26)
      InputController.Update () (at Assets/Scripts/Controller/InputController.cs:27)

      1. No, you shouldn’t get any exceptions when playing. Just be extra sure you got everything hooked up properly in the inspector. You can try debugging to figure out what is null – it tells you the problem class and line to give you an idea of what you may have missed: MoveTargetState line 13.

  4. Hey, can i send you my project so you can see what’s wrong? Because i can literally not find what the problem is, i’ve tried pretty much everything, sorry for the inconvenience :/

  5. Can someone help me understand this method? I need to understand the |= and &= operators. I looked up their operator definitions, but it’s very unclear to me what it does exactly. Thanks!

    public bool IsLocked
    {
    get { return (State & States.Locked) != States.None; }
    set
    {
    if (value)
    State |= States.Locked;
    else
    State &= ~States.Locked;
    }
    }

      1. By the way, and it can’t be said enough, excellent tutorial series here. It’s not only helping me with unity, but also with the subject of object orientation.

  6. Hi again,

    I have another issue at this part. I followed all the scripts written (and the one from the object pooling post) and assigned all prefabs correctly, but when I started the scene i get a stack overflow error:

    StackOverflowException: The requested operation caused a stack overflow.
    GameObjectPoolController.get_Instance () (at Assets/Scripts/Controller/GameObjectPoolController.cs:22)

    which in the code refers to this section:

    public class GameObjectPoolController : MonoBehaviour {

    #region Fields / Properties
    static GameObjectPoolController Instance
    {
    get
    {
    if (instance == null)
    CreateSharedInstance();
    return Instance;
    }
    }

    the odd part is that even with this error the scene plays properly until i move one unit to another tile and afterwards the scene just freezes. I checked the source code from the repository and i cant find the object pooling script (I don’t know if you saved it on a different name/location or didnt use it at all) and I honestly don’t know what went wrong in my code.

    I’ll try rechecking the scripts again, and maybe you have an idea about this.

    Thanks in advance!

    1. God, I feel like a total doofus. I wrote Instance with a capital I instead of instance with lowercase i. Careless mistakes like these are the bane of me, I swear.

      Sorry for the trouble, it works now!

  7. I’m having the same problem as Tyler, the only difference is that I referenced the Ability Menu Controller object in the Battle Controller and I’m still getting the NullReferenceException.

    1. To be a little more clear. The nullReferenceException happens at abilityMenuPanelController.Show(menuTitle, menuOptions);

      I’ve debugged what I could and both menuTitle and menuOptions exist when the function is called- the problem is definitely with abilityMenuPanelController.

      1. Lawl so I was also a total dingus and forgot to put the getters and setters in my BattleState code.

        Anyways thanks for doing these tutorials. I greatly appreciate them!

  8. I’m getting “The name ‘turn’ does not exist in the current context”, “The name ‘units’ does not exist in the current context” and “The name ‘abilityMenuPanelController’ does not exist in the current context” errors for every instance they are mentioned.

    Unsure where to even start, I’ve even copied the scripts directly from the commit in the repository, and I’m still getting the same error.
    I’d assume the problem lies within BattleController, but nothing I’ve tried has solved it.

    1. Can you give me a bit more context? Did you follow along from the beginning until here and are now seeing problems, or did you read to here, decide to start following along, and copied some scripts to a new project? Also “how” did you copy scripts from the repository? Are you actually using something like SourceTree to checkout the files or did you copy and paste or what?

  9. I’ve been following from the start up to this point.
    You can view the old versions on files through the commits section in that repository, so after getting these errors and being unable to see what was causing them, I was pasting from there, but it seemed to be identical anyway. You can also see exactly what files were modified or created, and I made sure I modified and created everything that was on that list, including all the object pooling scripts.

    1. The following snippet can produce an error like the one you mentioned:


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

      public class Test : MonoBehaviour {

      // Use this for initialization
      void Start () {
      Turn turn = new Turn (); // Warning -> "The variable 'turn' is assigned but its value is never used"
      }

      // Update is called once per frame
      void Update () {
      turn = null; // Error -> "The name 'turn' does not exist in the current context"
      }
      }

      In this example, the variable “turn” was actually declared, but the context of the “Update” method is separate from the “Start” method and doesn’t know about the variable declared there. It is possible you have an issue like this. It might be helpful to begin by sharing a copy of the first file which is causing a problem and post the code in question. Unity is pretty good about posting the most important warnings and errors first if you look in the Console output after trying to build your code. For example I might see:

      “Assets/Scripts/Test.cs(14,3): error CS0103: The name `turn’ does not exist in the current context”

      Which would tell me that the offending script was “Test.cs” and even tells me the line number that caused the problem.

  10. Hey Jon,

    As I’ve been going through this series, I’ve been trying to replace basically every use of native events with your Notification Center system, mostly as a fun exercise, but also because it seems more efficient. Most of it has been pretty straightforward (everything technically works, after a little debugging), but I’m struggling to “get” some of these cases (I believe these are anonymous delegates that you’re adding as listeners? I don’t really understand these). They look something like this:

    tweener.easingControl.completedEvent += delegate ( … ) { … }

    You’ve used this in the ConversationController and in the AbilityMenuPanelController so far, and I’ve been thinking of ways to use the Notification Center that do effectively the same thing, but I’m kind of concerned about how to go about that. When you use native events and you add listeners this way, is there any concern about garbage collection? I ask because I’ve noticed that these delegates are never unsubscribed from anywhere. Does destroying the tweener object (and the easing control attached to it) also get rid of these delegate listeners, or are they still lingering somewhere?

    Instead, I’ve been using tweener.AddObserver() from your system to subscribe “normal” methods to the tweener, but it seems like the only place I could reasonably place a RemoveObserver() is when the handler is actually being invoked, otherwise the tweener will likely be destroyed before I can remove any listeners. Isn’t it true that you don’t want to remove observers while they are being invoked, since that’s what causes big spikes in memory? I’m not sure what else I could do.

    I’m sorry if I’m asking a lot in this comment, but I’m really trying to understand these things, and I would think that you’re the best person to ask about this, since you wrote the Notification Center after all.

    1. Sounds like a good exercise, and these are great questions. Let’s see what I can do to clear things up to the best of my knowledge. First of all, the anonymous delegate is not really any different than creating a standalone method except it isn’t named, and you don’t have as much flexibility to unsubscribe it as you noticed.

      So for example while the anonymous delegate looks like this:
      delegate (parameters) { body }

      The equivalent method signature would be:
      void MethodName (parameters) { body }

      So as you noticed, there are times where I don’t unsubscribe from events and it is important to know why it is OK sometimes and not others. The way the garbage collector works is to see what has references to what going all the way up to the root, let’s call them “anchor” objects. Objects which are still in-scope won’t ever be released. Objects that are referenced by some root object of the app or which are static are all considered in-scope. Anything that such an object holds a reference to will also never be released. So if you make my notification center (which is static) hold a reference to an notification handler, then the object which holds that method will not be able to be released unless you unsubscribe first. Similarly if any events you subscribe to are static then you will have the same problem.

      The events in my EasingControl are not “static” so they are subject to be collected as long as the instance they are attached to goes out of scope. In this case the Tweener gets added to a game object and as long as it is there, the event reference will be tied to an “anchor” hierarchy. When the component destroys itself that “anchor” hierarchy will be broken and it will become subject to garbage collection. Therefore if you try to swap out the events in the EasingControl for Notifications, then you will have the extra responsibility of making sure these events are properly unsubscribed from.

      Also, you are correct that modifying the event list during its invocation loop causes memory spikes, but this would occur inside of the handler method itself (or as a chain reference of calls from that method), so you wouldn’t want to unsubscribe in the method that was invoked by the event or notification.

      One way to make sure you understand when it is safe “not” to unsubscribe would be to create a normal C# class. Log a message to the console from the destructor to verify that the object can go out of scope even if it is has an event handler that was not unsubscribed.

      This is a pretty advanced topic, so I hope I was able to help you understand it a little better. Feel free to ask any more questions where I was unclear.

  11. Hey Just a little problem i got. The “move” place Holder for the ability menu won’t disappear.
    like my menu looks like this :
    -Move
    -Move
    -Action
    -Wait
    I tried everything but nothing works, what’s the problem?

    1. Just a guess, but make sure that the “Ability Menu Controller” prefab wasn’t saved with an “Ability Menu Entry” as part of the hierarchy. I show them together so you can see where it goes and what it will look like on the Canvas, but they should be treated as separate prefabs.

  12. Hey,

    All the screenshots are returning to a broken link of that same “please update” photo. Any chance you’ve got the screenshots to share again?

    Cheers,

    Alex

  13. Hey another question. Is there a simple way to add a skill description to these menu entries? So when hovering over a specific job skill, it also gives a description of what it does?

    Thanks,

    Felkin

    P.S I’ve finished the tutorial fully and am going back revisiting things I’d like to change but I’m having trouble figuring out where to implement a description.

  14. OK, I’ve tried following this tutorial, but it seems I’m having bugs again.
    It is very similar to davooslice’s bug in the comments section of Path Finding (which is weird since before this unit movement worked fine for me).
    To reiterate, every time I click the ‘move’ option in the ability menu, it shows this error:

    NullReferenceException: Object reference not set to an instance of an object
    MoveTargetState.Enter () (at Assets/Scripts/Controller/BattleStates/MoveTargetState.cs:25)
    StateMachine.Transition (.State value) (at Assets/Scripts/Common/State Machine/StateMachine.cs:52)
    StateMachine.set_CurrentState (.State value) (at Assets/Scripts/Common/State Machine/StateMachine.cs:15)
    StateMachine.ChangeState[MoveTargetState] () (at Assets/Scripts/Common/State Machine/StateMachine.cs:33)
    CommandSelectionState.Confirm () (at Assets/Scripts/Controller/BattleStates/CommandSelectionState.cs:29)
    BaseAbilityMenuState.OnFire (System.Object sender, .InfoEventArgs`1 e) (at Assets/Scripts/Controller/BattleStates/BaseAbilityMenuState.cs:27)
    InputController.Update () (at Assets/Scripts/Controller/InputController.cs:31)

    but I added the debug measures you suggested to davooslice, it seems like the ‘mover’ is null. This is weird since, again, movement works before I entered the ability menu codes in. And this tutorial didn’t touch movements or MoveTargetState.

    Any ideas why?

    1. It sounds like you’ve already found the root of the problem. If the ‘mover’ is null and the script expects it not to be, then you have to figure out why the reference is missing. Sometimes this happens if you have created a prefab and forget to save the project (so then the prefab saves incorrectly). Or perhaps your prefab is correct but you have modified the version in the scene and saved the scene. Sometimes people assign a reference to the project asset itself, rather than an instance of the asset in the scene. Another possibility is that you have mistakenly added a duplicate of the script somewhere and the duplicate script doesn’t have the reference. Hopefully that gives you some good ideas of stuff to look for. Good luck!

      1. OK, already checked and I still can’t find what’s wrong with it. I checked the prefabs, redefined all of it; checked if the script gets duplicated/attached to another object (it didn’t); traced the supposed missing mover (the Movement classes appended on the Units) – they get attached fine. I also tried downloading the BitBucket repository, but when I play the Battle scene I get an error about Jobs/Warrior not existing. I’m sending you my side of the tutorial, in case I missed something. It’s driving me nuts for the whole yesterday.
        https://www.dropbox.com/s/v6cjip584ci2rn0/tactics%20RPG%20tutorial%202.zip?dl=0

        1. In your project, while the scene is running, if you select the “Battle Controller” object in the scene hierarchy pane, then left click on the field’s value for “Current Unit” you will see that the battle controller selects the “Hero” prefab in the project pane, rather than one of the instantiated hero’s in the scene. Furthermore, the “Hero” prefab does not have any of the “Movement” components attached and is why the “GetComponent” call returns null.

          If you want to use the demo project on my repository, then from the file menu choose “Pre Production->Parse Jobs”. This will create the jobs necessary to allow you to play the scene. Note that you can see the project in the state it was in for this lesson by using some sort of version control that allows you to do a “checkout” of the relevant “commit”.

          1. Ok, found out what’s wrong: I didn’t remove the references to currentUnit. That said, I was kinda confused a bit when removing it, since the tutorial only said “and fix the references which will now be broken”, and not specifying what we should replace it with (in this case, ‘owner.currentUnit’ gets replaced with ‘turn.actor’). A bit of clearing it up would help future learners.
            Thanks!

Leave a Reply

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