Tactics RPG Stat Panel

We’ve been modifying stats for a while now, but it’s not very desirable to have to switch the inspector to debug mode just to see their values. In this lesson, we will go ahead and create a Stat Panel view, which shows an avatar along with his or her name and a few important stats. There will actually be two of these panels, one for the currently active (or selected) unit and a second for the target of an action.

Stat Panel

Create a new script named StatPanel in the Scripts/View Model Component folder. The purpose of this script is to present a few relevant bits of information about any given unit including a portrait, name, hit points, magic points, and level.

To function, all you must do is call the Display method and pass along a GameObject (of a Unit) so that the panel can get whatever components it needs to determine how to update its various pieces.

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

public class StatPanel : MonoBehaviour 
{
	public Panel panel;
	public Sprite allyBackground;
	public Sprite enemyBackground;
	public Image background;
	public Image avatar;
	public Text nameLabel;
	public Text hpLabel;
	public Text mpLabel;
	public Text lvLabel;

	public void Display (GameObject obj)
	{
		// Temp until I add a component to determine unit alliances
		background.sprite = UnityEngine.Random.value > 0.5f? enemyBackground : allyBackground;
		// avatar.sprite = null; Need a component which provides this data
		nameLabel.text = obj.name;
		Stats stats = obj.GetComponent<Stats>();
		if (stats)
		{
			hpLabel.text = string.Format( "HP {0} / {1}", stats[StatTypes.HP], stats[StatTypes.MHP] );
			mpLabel.text = string.Format( "MP {0} / {1}", stats[StatTypes.MP], stats[StatTypes.MMP] );
			lvLabel.text = string.Format( "LV. {0}", stats[StatTypes.LVL]);
		}
	}
}

Stat Panel Controller

Next create a new script called StatPanelController in the Scripts/Controller folder. This script manages both the primary and secondary stat panels and controls when they show or hide themselves. The code here is not very different from what we have already seen, and shares a lot in common with the ConversationController.

using UnityEngine;
using System.Collections;

public class StatPanelController : MonoBehaviour 
{
	#region Const
	const string ShowKey = "Show";
	const string HideKey = "Hide";
	#endregion

	#region Fields
	[SerializeField] StatPanel primaryPanel;
	[SerializeField] StatPanel secondaryPanel;
	
	Tweener primaryTransition;
	Tweener secondaryTransition;
	#endregion

	#region MonoBehaviour
	void Start ()
	{
		if (primaryPanel.panel.CurrentPosition == null)
			primaryPanel.panel.SetPosition(HideKey, false);
		if (secondaryPanel.panel.CurrentPosition == null)
			secondaryPanel.panel.SetPosition(HideKey, false);
	}
	#endregion

	#region Public
	public void ShowPrimary (GameObject obj)
	{
		primaryPanel.Display(obj);
		MovePanel(primaryPanel, ShowKey, ref primaryTransition);
	}

	public void HidePrimary ()
	{
		MovePanel(primaryPanel, HideKey, ref primaryTransition);
	}

	public void ShowSecondary (GameObject obj)
	{
		secondaryPanel.Display(obj);
		MovePanel(secondaryPanel, ShowKey, ref secondaryTransition);
	}

	public void HideSecondary ()
	{
		MovePanel(secondaryPanel, HideKey, ref secondaryTransition);
	}
	#endregion

	#region Private
	void MovePanel (StatPanel obj, string pos, ref Tweener t)
	{
		Panel.Position target = obj.panel[pos];
		if (obj.panel.CurrentPosition != target)
		{
			if (t != null && t.easingControl != null)
				t.easingControl.Stop();
			t = obj.panel.SetPosition(pos, true);
			t.easingControl.duration = 0.5f;
			t.easingControl.equation = EasingEquations.EaseOutQuad;
		}
	}
	#endregion
}

Create the Prefab

You can begin by opening the Battle scene. We will be creating a new prefab which can manage the display of a primary and secondary stat panel. You could think of it as an attacker and defender panel, although that fails to encompass the full range of scenarios, such as when one ally takes an action to help another ally. The final result should look like the following picture:

The relevant hierarchy setup (names, parent chain etc) follows:

I will show the inspector settings for each relevant object below, from which you should be able to recreate this asset. When there are two side-by-side inspectors, the left one is for the primary stat panel and the right one is for the secondary stat panel. I would recommend fully creating the primary stat panel first, and then duplicate it and modify only those stats which need to change. Also, note that the labels are all basically the same except for their position and size, so you can create one and duplicate the others.

Implementation

There are many different scripts which will be slightly modified in order to implement our new system. Each entry follows:

BattleController

We will add a reference to our StatPanelController in the BattleController. Make sure to update the reference in the scene after modifying the script. Add the following line:

public StatPanelController statPanelController;

Battle State

Let’s do a convenience property wrapper in the base BattleState class so that all subclasses can directly reference it:

public StatPanelController statPanelController { get { return owner.statPanelController; }}

I will also add another method which makes it easy to get a Unit from a board position:

protected virtual Unit GetUnit (Point p)
{
	Tile t = board.GetTile(p);
	GameObject content = t != null ? t.content : null;
	return content != null ? content.GetComponent<Unit>() : null;
}

Then, I will add some additional methods which can tell the StatPanelController to refresh an appropriate panel (show or hide it) based on whether or not the indicated location has a unit or not.

protected virtual void RefreshPrimaryStatPanel (Point p)
{
	Unit target = GetUnit(p);
	if (target != null)
		statPanelController.ShowPrimary(target.gameObject);
	else
		statPanelController.HidePrimary();
}

protected virtual void RefreshSecondaryStatPanel (Point p)
{
	Unit target = GetUnit(p);
	if (target != null)
		statPanelController.ShowSecondary(target.gameObject);
	else
		statPanelController.HideSecondary();
}

There are a lot of possibile use cases for a turn, such as entering from one state to the next linearly, or entering and then canceling back out before trying something different. Trying to only show a panel or hide a panel when necessary can get kind of confusing given all the use cases, and can be even more confusing if you insert additional states or modify them in the future – one seemingly innocent modifcation could lead to a weird “bug” where the panel stayed visible when it shouldn’t or hid when it should have stayed visible.

In order to keep things as simple as possible, I handle this issue by making every state completely responsible for itself. If a state shows a panel, it needs to hide the panel before exiting. If the next state also needs the panel, it is responsible for making sure it appears. It may seem like extra work, but it should help your sanity level in the end, and could actually help reduce a bunch of extra code that checks previous states, etc. in order to manage weird use cases.

CommandSelectionState, CategorySelectionState, and ActionSelectionState

These states will merely need to keep the stat panel visible, with the currently active unit displayed:

public override void Enter ()
{
	base.Enter ();
	statPanelController.ShowPrimary(turn.actor.gameObject);
}

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

ExploreState and MoveTargetState

These states need to show whatever unit is highlighted by the cursor, regardless of whose turn it currently is. Add a call to refresh the stat panel in Enter and OnMove (after selecting the new tile), and add a call to hide the stat panel in Exit.

// Add to end of Enter and OnMove methods
RefreshPrimaryStatPanel(pos);

// Add to end of Exit method
statPanelController.HidePrimary();

SelectUnitState

This state is similar to the last two. In the ChangeCurrentUnit method (which is called from Enter) add a call to refresh the stat panel just after the turn has changed to the next unit. Hide the stat panel in Exit as we have done before.

Demo

Run the Battle scene and then cancel out of the CommandSelectionState so you can explore the map. Move the cursor over the different units on the board and see that the stat panel appears when a unit is selected (and shows that unit’s stats) and hides when no unit is selected. Complete a turn and note that the stat panel shows the currently active unit in the other states.

Note that we didn’t get around to implementing the secondary stat panel because we havent added skills and actions yet. However, the implementation for displaying it will be almost identical to what we have done here. You simply tell the controller to show the secondary panel instead of the primary one.

Summary

In this lesson we implemented the UI for showing unit stats. Nothing here was terribly different from material we have already done in the past – it is just more complete now. Big projects like this take a lot of 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.

11 thoughts on “Tactics RPG Stat Panel

  1. Great !!! An observation, setting Level Label the secondary side is not. Instead, it is repeated Label MP configuration.

      1. It is expected behavior for now. If you look at the first line of the StatPanel’s Display method you will see a comment mentioning that I haven’t added any component which defines a unit’s alliance. In the future, blue backgrounds will mark the player units and red backgrounds will mark the enemy units. Until then, I added a random statement so that people would know that the background could change and would also know where and how to change it. If it bothers you, you can just comment out the line for now.

  2. Hi again, and another great tutorial!

    I’m just wondering, will the steps cover grabbing an avatar sprite element from prefab units? As I see in this stage we manually set the stat panel to have one avatar and I don’t see any methods written to change it dynamically yet (unless I have missed it somehow). I have a few ideas of my own, but I want to know if you already covered it in future steps of the tutorial.

    Thanks again!

    1. Nope I never changed the avatar on the stat panel. I had a quick demo of it on the conversation panel, but have left this task up to the reader. I’m glad you’ve already got some ideas for it! Let me know how it goes.

  3. Hello, first of all, thanks for the tutorial.
    Now my doubt, when run the game apear this error:

    NullReferenceException: Object reference not set to an instance of an object
    StatPanelController.MovePanel (StatPanel obj, System.String pos, Tweener& t) (at Assets/Scripts/Controller/StatPanelController.cs:65)
    StatPanelController.HidePrimary () (at Assets/Scripts/Controller/StatPanelController.cs:38)
    SelectUnitState.Exit () (at Assets/Scripts/Controller/Battle States/SelectUnitState.cs:19)
    StateMachine.Transition (State value) (at Assets/Scripts/Common/State Machine/StateMachine.cs:33)
    StateMachine.set_CurrentState (State value) (at Assets/Scripts/Common/State Machine/StateMachine.cs:10)
    StateMachine.ChangeState[T] () (at Assets/Scripts/Common/State Machine/StateMachine.cs:24)

    I tried to use your code and i comprove the hierarchy but i can’t solve it

    1. Hi David, I usually troubleshoot by looking at the first line of the console’s output. In this case, it says you have a NullReferenceException. Most likely you missed a configuration step on a prefab or scene element etc. where you should connect a reference for a script. Looking at the next line down, we can see that the error occurs while trying to move our StatPanelController – look in that script at line number 65. If you want to know exactly which object is null, you can rely on breakpoints for debugging. Or if that is too advanced, try adding `Debug.Log` statements to try and print whether any `object` instance that is referenced on that line is null. For example, if I want to check whether an object named “Foo” is null I might do something like: Debug.Log(“Is Foo null: ” + (Foo == null).ToString());

      Otherwise, take another pass at comparing the setup of my Stat Panel against yours and hopefully you’ll find the difference. Good luck!

Leave a Reply to David Cancel reply

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