This document is about: PUN 1
SWITCH TO

PUN Classic (v1), PUN 2 and Bolt are in maintenance mode. PUN 2 will support Unity 2019 to 2022, but no new features will be added. Of course all your PUN & Bolt projects will continue to work and run with the known performance in the future. For any upcoming or new projects: please switch to Photon Fusion or Quantum.

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

  1. Open any Scene, where you have a UI Canvas
  2. Add a Slider UI GameObject to the canvas, name it Player UI
  3. Set the Rect Transform vertical anchor to Middle and the Horizontal anchor to center
  4. Set the RectTransform width to 80 and the height to 15
  5. Select the Background child, set its Image component color to Red
  6. Select the Child "Fill Area/Fill", set its Image color to green
  7. Add a Text UI GameObject as a child of Player UI, name it Player Name Text
  8. Add a CanvasGroup Component to Player UI
  9. Set the Interactable and Blocks Raycast property to false on that CanvasGroup Component
  10. Drag Player UI from the hierarchy into your Prefab Folder in your Assets, you know have a prefab
  11. Delete the instance in the scene, we don't need it anymore.

The PlayerUI Script Basics

  1. Create a new C# script and call it PlayerUI

  2. 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
    
        }
    }
    
  3. Save the PlayerUI Script

Now let's create the Prefab itself.

  1. Add PlayerUI Script to the Prefab PlayerUI
  2. Drag and drop the child GameObject "Player Name Text" into the public field PlayerNameText
  3. 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.

  1. Open the script PlayerUI

  2. 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.

  3. 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;
        }
    }
    
  4. Add this method in the MonoBehaviour Messages Region

    C#

    void Update()
    {
        // Reflect the Player Health
        if (PlayerHealthSlider != null) 
        {
            PlayerHealthSlider.value = _target.Health;
        }
    }
    
  5. 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.

  1. Open the script PlayerManager

  2. 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;
    
  3. Add this code inside the Start() Method

    C#

    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);
    }
    
  4. 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.

  1. Open PlayerUI Script

  2. Add this to the Update() function

    C#

    // 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;
    }
    
  3. 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:

  1. Open the script PlayerManager

  2. Add this code inside the CalledOnLevelWasLoaded() Method

    C#

    GameObject _uiGo = Instantiate(this.PlayerUiPrefab) as GameObject;
    _uiGo.SendMessage("SetTarget", this, SendMessageOptions.RequireReceiver);
    
  3. 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.

  1. Open the script PlayerUI

  2. Add this method inside the "MonoBehaviour Messages" region

    C#

    void Awake()
    {
        this.GetComponent<Transform>().SetParent (GameObject.Find("Canvas").GetComponent<Transform>());
    }
    
  3. 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:

  1. Open PlayerUI Script

  2. Add this public property inside the "Public Properties" region

    C#

    [Tooltip("Pixel offset from the player target")]
    public Vector3 ScreenOffset = new Vector3(0f, 30f, 0f);
    
  3. Add these four private properties inside the "Private Properties" region

    C#

    float _characterControllerHeight = 0f;
    Transform _targetTransform;
    Renderer _targetRenderer;
    CanvasGroup _canvasGroup;
    Vector3 _targetPosition;
    
  4. Add this inside the Awake Method region

    C#

            _canvasGroup = this.GetComponent<CanvasGroup>();
    
  5. 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.

  6. 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;
        }
    }
    
  7. 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.

Previous Part.

Back to top