This document is about: PUN 2
SWITCH TO

PUN Classic (v1)、PUN 2 和 Bolt 處於維護模式。 PUN 2 將支援 Unity 2019 至 2022,但不會添加新功能。 當然,您所有的 PUN & Bolt 專案可以用已知性能繼續運行使用。 對於任何即將開始或新的專案:請切換到 Photon Fusion 或 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 se.
However, it raises some very important design patterns, to provide some advanced features revolving around networking and the constraints it introduces in development.

So, the UI is not going to be networked, simply because we don't need to, plenty of other way 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 an UI for each networked player?

We'll have an UI Prefab with a 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 tells the prefab to follow that very player.

Creating the UI Prefab

  1. Open any Scene, where you have an 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 Rect Transform width to 80, and the height to 15
  5. Select the Background child, set it's Image component color to Red
  6. Select the child "Fill Area/Fill", set it's 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 Scripts 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 Private Fields
    
                [Tooltip("UI Text to display Player's Name")]
                [SerializeField]
                private Text playerNameText;
    
                [Tooltip("UI Slider to display Player's Health")]
                [SerializeField]
                private Slider playerHealthSlider;
    
                #endregion
    
                #region MonoBehaviour Callbacks
    
                #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 Fields" Region

    C#

    private PlayerManager target;
    

    We need to think ahead here, we'll be looking up for the health regularly, so it make 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.NickName;
        }
    }
    
  4. Add this method in the "MonoBehaviour Callbacks" Region

    C#

    void Update()
    {
        // Reflect the Player Health
        if (playerHealthSlider != null) 
        {
            playerHealthSlider.value = target.Health;
        }
    }
    
  5. Save the PlayerUI script

With this, we 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 a reference to the Player UI prefab as follows:

    C#

    [Tooltip("The Player's UI GameObject Prefab")]
    [SerializeField]
    public GameObject PlayerUiPrefab;
    
  3. Add this code inside the Start() method

    C#

    if (PlayerUiPrefab != null)
    {
        GameObject _uiGo =  Instantiate(PlayerUiPrefab);
        _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 easy, 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 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:

  4. Open the script PlayerManager

  5. Add this code inside the CalledOnLevelWasLoaded() method

    C#

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

  1. Open the script PlayerUI

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

    C#

    void Awake()
    {
        this.transform.SetParent(GameObject.Find("Canvas").GetComponent<Transform>(), false);
    }
    
  3. Save the PlayerUI Script

    Why going brute force and find the Canvas this way? Because when 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. However it's really not recommended to use "Find", because this is a slow operation.
    This is out of scope for this tutorial to implement a 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 loading and unloading into account.

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 object. How can we match positions in this case?
  • We don't want the UI to be slight above the player, how can we offset on screen from the Player position?
  1. Open PlayerUI script

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

    C#

    [Tooltip("Pixel offset from the player target")]
    [SerializeField]
    private Vector3 screenOffset = new Vector3(0f,30f,0f);
    
  3. Add these four fields to the "Private Fields" 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 = this.target.GetComponent<Transform>();
    targetRenderer = this.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 "MonoBehaviour Callbacks" 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 we only have one in our game, we can rely on accessing the Main Camera which is the default setup for a Unity Scene.

Notice how we setup 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