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:
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.
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.
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
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.
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.
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.
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;
Hey. Thanks!! Now it functions correctly.
When I view my BattleController in the inspector, I see the MoveTargetState is still enabled even after moving to other states. Is this okay?
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.
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.
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.
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!
Gah!!! Doy, please disregard my last comment. I forgot to reference the Ability Menu in the Battle Controller!
And here I was, thinking I had tried everything…
I’m glad you got it working π
Hi, is it normal that everything works fine except when i press the move button?
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)
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.
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 :/
if not that’s okay but how do i debug it
Never mind, everything’s great it works, it was line 13, thanks π
Here is an article which introduces you to debugging, debugging in unity
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;
}
}
I’d recommend you take a quick detour and read my C# tutorials. In particular, this lesson discusses the code you are trying to understand:
https://theliquidfire.wordpress.com/2015/03/18/enums-and-flags/
Wow, thank you very much! I got it now.
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.
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!
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!
No worries, I’ve made that same mistake before. Glad you were able to work it out!
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.
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.
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!
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.
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?
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.
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.
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.
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.
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?
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.
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
I have updated the screen shots for this post and am working on updating them everywhere I find them broken. Thanks for the heads up.
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.
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?
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!
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
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”.
Ah, I see! Alright, let me try it. Thanks for the heads-up!
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!
Good tip thanks for clarifying that!
I’m having a bit of an issue with the final result.
I went over all the settings of the prefabs twice to make sure everything was correct, but some things seem off:
– The header only says “Com” (not sure if this is intentional)
– On the first unit, the options appear greyed out until you select them
– For some reason, the word “Action” is not completely drawn.
Some of these may be code-related, but seeing as I only copy-paste, seems unlikely.
Here is a reference image on the issues:
https://i.imgur.com/Q25XvJT.png
Thanks in advance.
Changing the Horizontal Overflow to Overflow apparently solved the incomplete words issue, but I find it strange since on the Conversations lesson everything worked OK. The only things I couldn’t change here are related to some locked parameters on the Rect Transform on the Canvas (can’t change anything there) and on the Ability Menu Panel (locked to 74 width instead of the 85 on your images).
The issue of the greyed out options keeps happening, though.
Sorry for the late response, I just got back from a vacation. Are you still struggling with this? Have you tried downloading the project from the respository and see if you encounter the same problems? I imagine that the UI scripts may have changed a bit since I created this tutorial so creating and following along from scratch may or may not work the same. If you are seeing locked parameters (such as on a rect transform), it is likely because another component has taken responsibility for it – such as a stacking or layout component. This could be on the same object or a parent object, so you will need to take time and get familiar with everything. Good luck!
Kind of. As I said before, the words are no longer cut on the menu due to Horizontal Overflow, but the issue of the options being greyed until selected (this is a visual only issue, the options still work as intended) on the first unit still persists. I’ve downloaded the project on the intro of the tutorial and it does not happen there.
Take your time, as I said, the issue is only visual and does not affect anything important.
I have experienced this as well (the words appearing grey until highlighted, but then returning to white).
I thought this was linked to the IsLocked state in AbilityMenuState, but this isn’t triggered until it’s expected to, and the colours are actually different. Examining the “Text” children of the “Ability Menu Entry(Clone)” created at the start of the game, the Text color is 50, 50, 50, 255 (RGB, A) and the outline is 0, 42, 79, 128. The IsLocked colour is 128, 128, 128, 255 for the Text color and 20, 36, 44, 255 for the outline (as expected).
The only way to keep replicating it is to restart the game, as it only appears at the beginning, and once any of these affected options are highlighted, the bug will never appear again. It is possible it will appear on the second unit if instead of scrolling down from Move to Wait, you go up instead, skipping Action and selecting Wait. What this will do is cause only Wait to be greyed out on the second unit, but Action will be white, as expected.
The version of Unity I’m using is 2022.3.33f1 (DX11) and I’m using the Legacy Text but I’m not sure this is relevant.
I’ll keep debugging, but as Oscar has said it’s a minor visual bug so not a problem.
I had to create an empty game object and attach turn script to it and add it to the battle controller in the inspector for my project to load the ability select menu. How bad is this going to mess me up?
In the project, the “TurnOrderController” is added to the “Battle Controller” root game object by the “InitBattleState” script. I don’t know how a different configuration might mess you up, because some scripts may expect a certain hierarchy in order to obtain their references to each other. As long as you know how to find it you should be ok.
Hi! can you please explain to me what is this check doing?
e.info == 0
I see you use it on the onfire input, but im not sure against what value it’s being checked?
You might want to take a quick review of the “User Input Controller” lesson. There are three “Fire” button inputs that the controller recognizes, and the ‘e.info’ value is the index of the fire button that was activated. This way we can use one button index for confirm type actions, and another button index for cancel type actions, etc.
yeah, dumb me. thanks.
Hi!
I’m trying to follow this tutorial, and I’m sure I’ll get it to work if I follow the steps carefully. However I have a question about the why rather than the how.
I learned in a previous tutorial how to set up a state machine to control the various states of the game. Why did you opt to go for a flags structure here (for none, selected and locked) rather than another state machine? I understand that flags can be useful when we want multiple values to be true at the same time, but it’s not the case here, is it? Can you enlighten me?
Thanks,
Great tutorial, I’m learning a ton and am hoping to get to the end of it during this social distancing period π
Hey,
I’ve now finished the tutorial, with everything working. My questions about the flags remain, but I think I’m starting to get a hold of the bigger architecture of what we’re trying to do.
I’d love to make sure I’m on the right track. Can you tell me if my conceptual understanding is correct? Here is how I interpret our implementation:
– The menu entries themselves wonβt have statemachine states, instead they will be flagged as having a certain state (none, selected, locked) which wonβt directly affect their functionality, just the way the entry looks (the M of the MVC architecture)
– An ability panel controller, which role is to place the menu panels on the screen using the anchoring system created earlier, and show the entries (the V of the MVC architecture)
– The battlecontroller is also the menu controller, so moving inside of the menus is all controlled by different states of the battlecontroller (the C of the MVC architecture). We create a bunch of these states for the different depths of the menu: CommandSelectionState (level 0), CategorySelectionState (lvl 1), ActionSelectionState (lvl 2). All of these states have stuff in common since they are all menu states, so they inherit from a general BaseAbilityMenu state.
Thanks again for a great tutorial, I’m learning way faster than I would on my own.
Keep in mind of course that this isn’t strictly MVC architecture and some of the semantics could be argued, but in a general sense I think you’ve got the right idea. Component based architecture, especially as seen in Unity with Monobehaviours can tend to blur the lines between all three (model view and controller).
Hey, I only started coding seriously six months ago, good enough for me π
Good question. I have a couple of thoughts. First, a state machine is designed to always have at most one state active at a time. In the current implementation, that is what you see in my code, and so it could be a fit. However, imagine a menu that might have additional State options such as “Highlighted”, then you could highlight entries that weren’t the selected entry, or could highlight the selected entry. There could be multiple flags per entry active at the same time.
The other thought is on complexity. When things are very simple (and this menu is), it might actually feel like more work to create a whole state machine to do the same job. If the pattern isn’t saving you work, or leading to greater clarity, etc, then you can be guilty of over-architecting your project. I’m not saying a state machine is wrong here, just that there is a balance to keep in mind.
Overall, I would recommend that you feel free to try your own suggestion. If you think it looks or feels better, then great. If it doesn’t feel right, you’ll have greater intuition about when to use it or not use it in the future. Good luck!
This makes a lot of sense, thanks a lot!
Haven’t seen anyone else with this problem so I suppose I’m just a fool, but I have no idea how to fix the “current unit” references once we edit the Battle Controller script to include our Turn information. Downloaded you project files and even when I follow their solution, it still tells me that ‘current’ does not exist in the current context within MoveSequenceState and MoveTargetState.
(For example, when I change
Movement m = owner.currentUnit.GetComponent();
to
Movement m = current.GetComponent();
One of the easiest ways to determine exactly what changes I made between lessons is to look at the history of the commits of my project in Source Control.
If you aren’t familiar with that process, this might help. Look for portions of lines that say: “owner.currentUnit” and replace it with “turn.actor”
Hey Jonathan im a beginner in c# and i love tactics game.So im trying to do a system where i store the “Jobs” as scriptable object one for warrior,rogue and wizard with all the stat and i have some difficulties to integrate it into the project would you know how to do that or have a useful ressource about it that i don’t know? thanks a lot m8 π
Just a couple of lessons further and I do exactly that. Hope it helps!
http://theliquidfire.com/2015/08/10/tactics-rpg-jobs/
Hi, awesome tutorial, thank you very much for remaining active in these comments for so many years. I would like some help making some modifications to these scripts if that’s ok. I would like it if the movement option was a button instead of one of the options in the list. This way the player could enter the movement state from any of the menu states instead of backing out of the current menu state to the command selection state to do so. However, I am having trouble implementing this the way it is currently structured.
My first iteration was to simply add a button, but because it is not an abilityMenuEntry object it is not apart of the menuEntries list and so you cannot scroll to it using keyboard functionality.
My second implementation was to try and create a second variant of the abilityMenuEntry object but the GameObjectPoolController only takes one prefab. This also means that if I made the abilityMenuEntry class a base class for two varying ability menu entries then I would be able to add both different types of menu entries but I would only be able to pass the prefab for one.
Lastly, I would be ok with making it a button but it then become the only action that requires a mouse click as opposed to both a mouse click or keyboard input.
Any help would be appreciated.
Hey, glad you are enjoying the project and are trying to make it your own. Making changes is a great exercise! My Forum is the ideal place to get help with something like this. You never know how much back and forth will be needed, and I think its much easier to follow along the way things are structured there, compared with the comments here. Plus you can attach screen grabs, etc that really help show what you are trying to accomplish.