This document is about: PUN 1
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" 을 선택하고 Image color 를 녹색으로 설정 합니다.
  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 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
            
        }
    }

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

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

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

Binding PlayerUI with Player

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

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

  2. Private Properties Region 에서 private 프로퍼티를 추가 합니다.

    C#

    PlayerManager _target;
    

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

  3. Public Methods region 에 아래의 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.name;
        }
    }
    
  4. MonoBehaviour Messages Region 에 아래의 메소드를 추가 합니다.

    C#

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

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

인스턴스생성

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

  1. PlayerUI 를 오픈 합니다.

  2. 이 코드를 Start() 메소드에 추가 합니다.

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

위의 모든 것은 표준 유니티 코딩 입니다. 우리가 방금 생성한 인스턴스에게 메시지를 전송하고 있다는 것을 주목 하세요. 수신자가 필요하며 이 의미는 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 스크립트를 저장 합니다.

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

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

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

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

  2. MonoBehaviour Messages region 에 다음 메소드를 추가 합니다.

    C#

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

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

타겟 플레이어 따라가기

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

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

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

    C#

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

    C#

    float _characterControllerHeight = 0f;
    Transform _targetTransform;
    
  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