This document is about: PUN 2
SWITCH TO

PUN Classic (v1), PUN 2, Bolt는 휴업 모드입니다. Unity2022에 대해서는 PUN 2에서 서포트하지만, 신기능의 추가는 없습니다. 현재 이용중인 고객님의 PUN 및 Bolt 프로젝트는 중단되지 않고, 퍼포먼스나 성능이 떨어지는 일도 없습니다. 앞으로의 새로운 프로젝트에는 Photon Fusion 또는 Quantum을 사용해 주십시오.

9 - Player UI 프리팹

이 섹션에서는 플레이어 UI 시스템 생성을 보여 드릴 것 입니다.
플레이어의 이름과 현재 체력 값을 표시할 필요가 있을 것 입니다.
플레이어 주위를 따라가도록 UI 위치를 관리할 필요도 있습니다.

이 섹션은 네트워킹과는 관련은 없습니다.
하지만 굉장히 중요한 디자인 패턴에 대해서 다루며 고급 네트워킹 기능과 개발단계의 제약에 대해서도 소개 합니다.

자, UI는 필요가 없으므로 네트워크를 통해 동작하지는 않습니다.
이 사항에 대해서는 대역폭을 없애는 수 많은 방식이 존재 합니다.

이제 다음과 같은 질문이 나올 수 있습니다: 각 네트워크 플레이어의 UI 를 어떻게 해야 하나요?

전용 PlayerUI 스크립트를 가진 UI 프리팹을 작성할 것 입니다.
PlayerManager 스크립트는 이 UI 프리팹의 레퍼런스를 가지고 있을 것이며 PlayerManager 가 Start 할 때 이 UI 의 인스턴스를 생성하고 프리팹에게 그 플레이어를 따라 가라고 알려 줄 것 입니다.

UI 프리팹 생성하기

  1. UI Canvas 가 있는 아무 씬이나 오픈합니다.
  2. 캔버스에 UI 게임오브젝트 Slider 를 추가하여 Player UI로 이름을 부여 합니다.
  3. Rect Transform vertical anchor 를 Middle 로 설정하고 Horizontal anchor 를 center 로 설정 합니다.
  4. RectTransform width를 80 으로 설정하고 height 는 15로 설정 합니다.
  5. Background child 를 선택하고 이것의 Image component color를 빨강색으로 설정 합니다.
  6. Child "Fill Area/Fill" 을 선택하고 이미지 색상을 녹색으로 설정 합니다.
  7. Player UI의 차일드로 UI GameObject Text 를 추가하여 Player Name Text로 이름을 부여 합니다.
  8. Hierarchy 에서 Player UI 를 에셋의 Prefab Folder 로 드래그 합니다. 이제 프리팹이 만들어 졌습니다.
  9. 씬에서 인스턴스는 더 이상 필요가 없으므로 삭제 합니다.

PlayerUI 스크립트 기본

  1. 새로운 C# 스크립트를 생성하여 PlayerUI 로 이름을 부여 합니다.

  2. 아래는 기본 스크립트 구조로 편집하여 PlayerUI 스크립트에 저장 합니다:

    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. PlayerUI 스크립트를 저장합니다

이제 프리팹 자체를 생성 합니다.

  1. PlayerUI 스크립트를 PlayerUI 프리팹에 추가 합니다.
  2. "Player Name Text" 차일드 게임오브젝트를 public 필드 PlayerNameText 에드래그앤 드롭합니다.
  3. 슬라이더 컴포넌트를 public 필드 PlayerHealthSlider에 드래그앤 드롭 합니다.

인스턴스 생성 및 플레이어에게 바인딩 하기

플레이어에게 PlayerUI 바인딩 하기

PlayerUI 스크립트는 다른 플레이어들 사이에서 어떤 플레이어를 나타내는지 알아야 합니다:
체력과 이름을 표시해야 하기 때문에 이것의 바인딩을 위한 public 메소드를 생성 하도록 하겠습니다.

  1. PlayerUI 스크립트를 오픈 합니다.

  2. "Private Fields" 영역에서 private 프로퍼티를 추가 합니다.

    C#

    PlayerManager target;
    

    여기에서 고려해야 할 부분이 있는데 효율적으로 PlayerManager 레퍼런스 캐시를 위하여 주기적으로 체력을 봐야 할 것 입니다.

  3. "Public Methods" 영역에 아래의 public 메소드를 추가 합니다.

    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. MonoBehaviour 콜백 영역에 이 메소드를 추가합니다

    C#

    void Update()
    {
        // Reflect the Player Health
        if (playerHealthSlider != null) 
        {
            playerHealthSlider.value = target.Health;
        }
    }
    
  5. PlayerUI 스크립트를 저장합니다.

지금까지 한 것으로 타겟 플레이어의 이름과 체력을 표시하는 UI 를 이제 가지고 있습니다.

인스턴스생성

좋습니다. 이 프리팹의 인스턴스 생성을 어떻게 하는지 알았으므로, 매번 플레이어 프리팹의 인스턴스를 생성 합니다.
가장 좋은 방식은 PlayerManager 가 초기화 할 때 하는 것 입니다.

  1. PlayerManager 를 오픈 합니다.

  2. 다음과 같이 Player UI 레퍼런스에 참조를 가질 public 필드를 추가합니다:

    C#

    [Tooltip("The Player's UI GameObject Prefab")]
    [SerializeField]
    private GameObject playerUiPrefab;
    
  3. Start() 메소드내에 다음의 코드를 추가합니다

    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. PlayerManager 스크립트를 저장합니다

위의 모든 것은 표준 유니티 코딩 입니다. 우리가 방금 생성한 인스턴스에게 메시지를 전송하고 있다는 것을 주목 하세요.
수신자가 필요하며 이 의미는 SetTarget이 응답할 컴포넌트를 찾지 못했을 때 경고를 받게 된다는 것 입니다.
인스턴스로 부터 PlayerUI 컴포넌트를 받기 위한 방식중의 하나는 SetTarget 을 직접 호출 하는 것 입니다.
Component 들을 직접 사용하는 것이 일반적으로 권장 되지만 다양한 방식으로 동일한 사항을 할 수 있다는 것을 알아두는 것도 좋습니다.

하지만 아직 완성되지 않았기 때문에 플레이어 삭제를 처리하여 모든 신에서 고아가 되는 UI 인스턴스가 있어서는 안됩니다. 따라서 지정된 것이 없어진 타겟을 발견 했을 때 UI 인스턴스를 제거할 필요가 있습니다.

  1. PlayerUI 스크립트를 오픈합니다

  2. 다음 코드를 Update() 함수에 추가합니다.

    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. PlayerUI 스크립트를 저장합니다

    PlayerUI 스크립트를 저장 합니다. 이 코드는 쉽고 다루기 쉽습니다. Photon이 네트워크된 인스턴스들을 삭제하는 방식이 타겟 레퍼런스가 null 일 경우 UI 인스턴스 자체를 제거하는 것보다 쉽기 때문 입니다.
    이것은 왜 타겟이 없어졌는지에 상관없이 수 많은 잠재적인 문제점을 없애서 매우 안전합니다. 관련된 UI는 자체적으로 자동 제거 되어 매우 편리하고 빠릅니다.

    하지만 잠시만요... 새로운 레벨이 로드되었을 때, UI 는 파괴되며 우리의 플레이어는 남아있습니다... 따라서 레벨이 로드된 것을 알 때 이를 인스턴스화해야 합니다. 다음 작업을 수행합니다.

  4. PlayerManager 스크립트를 오픈합니다.

  5. CalledOnLevelWasLoaded() 메소드 내에서 다음의 코드를 추가합니다.

    C#

    GameObject _uiGo = Instantiate(this.playerUiPrefab);
    _uiGo.SendMessage("SetTarget", this, SendMessageOptions.RequireReceiver);
    
  6. PlayerManager 스크립트를 저장합니다.

이 문제를 처리하는 더 복잡하고 강력한 방법이 있으며, UI는 싱글톤으로 만들 수 있지만, 다른 플레이어가 룸에 가입하거나 나가는 경우에도 UI를 처리해야 하기 때문에 빠르게 복잡해집니다. 구현할 때 UI 프리팹을 인스턴스화하여 복제 비용을 부담합니다. 간단한 연습으로 "SetTarget" 메시지를 인스턴스화하고 전송하는 개인 방법을 만들 수 있으며, 다양한 위치에서 코드를 복제하는 대신 해당 메서드를 호출할 수 있습니다.

UI 캔버스로 페어런팅(Parenting)

유니티 UI 시스템의 중요한 제약 사항중의 하나는 모든 UI 요소들은 Canvas 게임 오브젝트내에 위치 해 있어야 한다는 것이기 때문에 우리는 PlayerUI 프리팹의 인스턴스가 생성되었을 때 제어해야하며 PlayerUI 의 인스턴스를 생성할때 처리 할 것 입니다.

  1. PlayerUI 스크립트를 오픈 합니다.

  2. "MonoBehaviour CallBacks" 영역에 다음 메소드를 추가 합니다.

    C#

    void Awake()
    {
        this.transform.SetParent(GameObject.Find("Canvas").GetComponent<Transform>(), false);
    }
    
  3. PlayerUI 스크립트를 저장합니다

    왜 강제로 설정하고 캔버스를 이 방식을 찾을 까요? 신이 로드와 언로드 될 때 우리 프리팹도 같이 로드와 언로드 되며 캔버스는 매번 달라지게 될 것 입니다.
    더 복잡한 코드 구조를 피하기 위해서 우리는 가장 빠른 방법을 선택 할 것 입니다. "Find" 의 오퍼레이션은 느리기 때문에 "Find" 를 사용하는 것은 권장사항이 아닙니다.
    더 복잡한 경우를 구현하는 것은 이 튜토리얼의 범위를 벗어나지만 유니티와 스크립팅에서 로딩과 언로딩을 고려한 캔버스 엘리먼트의 레퍼런스 관리는 경험이 좀 더 쌓이게 되면 익숙해질 것 입니다.

타겟 플레이어 따라가기

이제 흥미로운 부분입니다.
대상 플레이어를 따라다니는 Player UI 가 필요합니다. 여기에는 몇가지 해결해야 할 사항들이 있습니다:

  • UI 는 2d 요소이고 플레이어는 3d 요소 입니다. 이 경우에 어떻게 위치를 맞게 할 수 있을까요?
  • 플레이어의 약간 위쪽에 UI 가 위치해 있는 것을 원하지 않습니다. 플레이어 위치로부터 스크린 오프셋을 어떻게 얻을 수 있을까요?
  1. PlayerUI 스크립트를 오픈 합니다.

  2. "Public Fields" 안에 아래의 public 프로퍼티를 추가 합니다.

    C#

    [Tooltip("Pixel offset from the player target")]
    [SerializeField]
    private Vector3 screenOffset = new Vector3(0f,30f,0f);
    
  3. "Private Fields Messages" 영역에 아래의 2개 private 프로퍼티를 추가 합니다.

    C#

    float characterControllerHeight = 0f;
    Transform targetTransform;
    Vector3 targetPosition;
    
  4. SetTarget() 메소드의 _target이 설정되는 코드 밑에 다음 코드를 추가 합니다.

    C#

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

    우리의 플레이어가 Height 프로퍼티 기능을 가지고 있는 CharacterController 의 기반이라는 것을 알고 있으므로 이것을 이용하여 플레이어 위에 UI 요소의 적당한 오프셋을 결정 할 것 입니다.

  5. "Public Methods" 영역에 아래의 public 메소드를 추가 합니다.

    C#

    void LateUpdate() 
    {
        // #Critical
        // Follow the Target GameObject on screen.
        if (targetTransform!=null)
        {
            targetPosition = targetTransform.position;
            targetPosition.y += characterControllerHeight;
            this.transform.position = Camera.main.WorldToScreenPoint (targetPosition) + screenOffset;
        }
    }   
    
  6. PlayerUI 스크립트를 저장합니다

2d 위치와 3d 위치를 맞추기 위한 트릭으로 카메라의 WorldToScreenPoint 함수를 이용합니다. 그리고 게임에서 카메라 하나만을 가지고 있으므로 유니티 신의 디폴트 설정에 있는 메인 카메라를 사용할 수 있습니다.

우리가 몇 단계를 통해 이 오프셋을 어떻게 설정 했는지 주목 해 보세요: 먼저 타겟의 실제 위치를 얻은 후에 characterControllerHeight 을 더했고 마지막으로 플레이어의 스크린 상단을 감소 한 후 스크린 오프셋을 더했습니다.

이전 파트.

Back to top