9 - Player UI Prefab
This section will guide you to create the Player UI system.
We'll need to show the name of the player and its current health.
We'll also need to manage the UI position to follow the players around.
This section has nothing to do with networking per say, however, it raises some very important design patterns, to provide some advanced features revolving around networking and the constraints introduced in development.
So, the UI is not going to be networked, simply because we don't need to, plenty of other ways to go about this and avoid taking up traffic.
It's always something to strive for if you can get away with a feature not to be networked, it's good.
The legitimate question now would be: how can we have a UI for each networked player?
We'll have a UI Prefab with dedicated PlayerUI Script.
Our PlayerManager script will hold a reference of this UI Prefab and will simply instantiate this UI Prefab when the PlayerManager Starts and tell the prefab to follow that player.
Creating the UI Prefab
- Open any Scene, where you have a UI Canvas
- Add a Slider UI GameObject to the canvas, name it
Player UI
- Set the Rect Transform vertical anchor to Middle and the Horizontal anchor to center
- Set the RectTransform width to 80 and the height to 15
- Select the
Background
child, set its Image component color to Red - Select the Child "Fill Area/Fill", set its Image color to green
- Add a Text UI GameObject as a child of
Player UI
, name itPlayer Name Text
- Add a
CanvasGroup
Component toPlayer UI
- Set the
Interactable
andBlocks Raycast
property tofalse
on thatCanvasGroup
Component - Drag
Player UI
from the hierarchy into your Prefab Folder in your Assets, you know have a prefab - Delete the instance in the scene, we don't need it anymore.
The PlayerUI Script Basics
Create a new C# script and call it
PlayerUI
Here's the basic script structure, edit and save
PlayerUI
script accordingly:C#
using UnityEngine; using UnityEngine.UI; using System.Collections; namespace Com.MyCompany.MyGame { public class PlayerUI: MonoBehaviour { #region Public Properties [Tooltip("UI Text to display Player's Name")] public Text PlayerNameText; [Tooltip("UI Slider to display Player's Health")] public Slider PlayerHealthSlider; #endregion #region Private Properties #endregion #region MonoBehaviour Messages #endregion #region Public Methods #endregion } }
Save the
PlayerUI
Script
Now let's create the Prefab itself.
- Add
PlayerUI
Script to the PrefabPlayerUI
- Drag and drop the child GameObject "Player Name Text" into the public field PlayerNameText
- Drag and drop the Slider Component into the public field PlayerHealthSlider
Instantiation and Binding With The Player
Binding PlayerUI with Player
The PlayerUI
script will need to know which player it represents for one reason amongst others: being able to show its health and name, let's create a public method for this binding to be possible.
Open the script
PlayerUI
Add a private property in the "Private Properties" Region
C#
PlayerManager _target;
We need to think ahead here, we'll be looking up for the health regularly, so it makes sense to cache a reference of the PlayerManager for efficiency.
Add this public method in the "Public Methods" region
C#
public void SetTarget(PlayerManager target) { if (target == null) { Debug.LogError("<Color=Red><a>Missing</a></Color> PlayMakerManager target for PlayerUI.SetTarget.",this); return; } // Cache references for efficiency _target = target; if (PlayerNameText != null) { PlayerNameText.text = _target.photonView.owner.name; } }
Add this method in the MonoBehaviour Messages Region
C#
void Update() { // Reflect the Player Health if (PlayerHealthSlider != null) { PlayerHealthSlider.value = _target.Health; } }
Save the
PlayerUI
Script
With this, so far we'll now have the UI to show the targeted Player's name and health.
Instantiation
OK, so we know already how we want to instantiate this Prefab, every time we instantiate a player Prefab.
The best way to do this is inside the PlayerManager during its initialization.
Open the script
PlayerManager
Add a public field to hold reference to the Player UI reference as follows:
C#
[Tooltip("The Player's UI GameObject Prefab")] public GameObject PlayerUiPrefab;
Add this code inside the
Start()
MethodC#
if (PlayerUiPrefab!=null) { GameObject _uiGo = Instantiate(PlayerUiPrefab) as GameObject; _uiGo.SendMessage ("SetTarget", this, SendMessageOptions.RequireReceiver); } else { Debug.LogWarning("<Color=Red><a>Missing</a></Color> PlayerUiPrefab reference on player Prefab.", this); }
Save the
PlayerManager
Script
All of this is standard Unity coding. However, notice that we are sending a message to the instance we've just created.
We require a receiver, which means we will be alerted if the SetTarget
did not find a component to respond to it.
Another way would have been to get the PlayerUI Component from the instance and then call SetTarget
directly.
It's generally recommended to use Components directly, but it's also good to know you can achieve the same thing in various ways.
However, this is far from being sufficient, we need to deal with the deletion of the player, we certainly don't want to have orphan UI instances all over the Scene, so we need to destroy the UI instance when it finds out that the target it's been assigned is gone.
Open
PlayerUI
ScriptAdd this to the
Update()
functionC#
// Destroy itself if the target is null, It's a fail-safe when Photon is destroying Instances of a Player over the network if (_target == null) { Destroy(this.gameObject); return; }
Save
PlayerUI
Script
This code, while simple, is actually quite handy.
Because of the way Photon deletes Instances that are networked, it's easier for the UI Instance to simply destroy itself if the target reference is found null.
This avoids a lot of potential problems and is very secure, no matter the reason why a target is missing, the related UI will automatically destroy itself too, very handy and quick.
But wait... when a new level is loaded, the UI is being destroyed yet our player remains... so we need to instantiate it as well when we know a level was loaded, let's do this:
Open the script
PlayerManager
Add this code inside the CalledOnLevelWasLoaded() Method
C#
GameObject _uiGo = Instantiate(this.PlayerUiPrefab) as GameObject; _uiGo.SendMessage("SetTarget", this, SendMessageOptions.RequireReceiver);
Save the
PlayerManager
Script
Note that there are more complex/powerful ways to deal with this and the UI could be made out with a singleton, but it would quickly become complex because other players joining and leaving the room would need to deal with their UI as well. In our implementation, this is straightforward, at the cost of a duplication of where we instantiate our UI prefab. As a simple exercise, you can create a private method that would instantiate and send the "SetTarget" message and from the various places, call that method instead of duplicating the code.
Parenting to UI Canvas
One very important constraint with the Unity UI system is that any UI element must be placed within a Canvas GameObject and so we need to handle this when this PlayerUI Prefab will be instantiated, we'll do this during the initialization of the PlayerUI.
Open the script
PlayerUI
Add this method inside the "MonoBehaviour Messages" region
C#
void Awake() { this.GetComponent<Transform>().SetParent (GameObject.Find("Canvas").GetComponent<Transform>()); }
Save the
PlayerUI
Script
Why going brute force and find the Canvas this way? because when the scenes are going to be loaded and unloaded, so is our Prefab and the Canvas will be everytime different.
To avoid more complex code structure, we'll go for the quickest way.
It's not really recommended to use "Find" however, because this is a slow operation.
This is out of scope for this tutorial to implement more complex handling of such case, but a good exercise when you'll feel comfortable with Unity and scripting to find ways into coding a better management of the reference of the Canvas Element that takes into account loading and unloading.
Following The Target Player
That's an interesting part, we need to have the Player UI following on screen the Player target.
This means several small issues to solve:
- The UI is a 2D element and the player is a 3D element. How can we match positions in this case?
- We don't want the UI to be slightly above the player, how can we offset on screen from the Player position?
Let's start solving those:
Open
PlayerUI
ScriptAdd this public property inside the "Public Properties" region
C#
[Tooltip("Pixel offset from the player target")] public Vector3 ScreenOffset = new Vector3(0f, 30f, 0f);
Add these four private properties inside the "Private Properties" region
C#
float _characterControllerHeight = 0f; Transform _targetTransform; Renderer _targetRenderer; CanvasGroup _canvasGroup; Vector3 _targetPosition;
Add this inside the
Awake
Method regionC#
_canvasGroup = this.GetComponent<CanvasGroup>();
Append the following code to the
SetTarget()
method after_target
was set:C#
_targetTransform = _target.GetComponent<Transform>(); _targetRenderer = _target.GetComponent<Renderer>(); CharacterController _characterController = _target.GetComponent<CharacterController>(); // Get data from the Player that won't change during the lifetime of this Component if (_characterController != null) { _characterControllerHeight = _characterController.height; }
We know our player to be based off a CharacterController, which features a
Height
property, we'll need this to do a proper offset of the UI element above the Player.Add this public method in the "Public Methods" region:
C#
void LateUpdate() { // Do not show the UI if we are not visible to the camera, thus avoid potential bugs with seeing the UI, but not the player itself. if (targetRenderer!=null) { this._canvasGroup.alpha = targetRenderer.isVisible ? 1f : 0f; } // #Critical // Follow the Target GameObject on screen. if (_targetTransform != null) { _targetPosition = _targetTransform.position; _targetPosition.y += _characterControllerHeight; this.transform.position = Camera.main.WorldToScreenPoint (_targetPosition) + ScreenOffset; } }
Save the
PlayerUI
script
So, the trick to match a 2D position with a 3D position is to use the WorldToScreenPoint
function of a Camera and since in our game we only have one, we can rely on accessing the main camera which is the default setup for a Unity Scene.
Notice how we set up the offset in several steps: first we get the actual position of the target, then we add the _characterControllerHeight
and finally, after we've deduced the screen position of the top of the Player, we add the screen offset.