9 - Player UI 프리팹
이 섹션에서는 플레이어 UI 시스템 생성을 보여 드릴 것 입니다.
플레이어의 이름과 현재 체력 값을 표시할 필요가 있을 것 입니다.
플레이어 주위를 따라가도록 UI 위치를 관리할 필요도 있습니다.
이 섹션은 네트워킹과는 관련은 없습니다.
하지만 굉장히 중요한 디자인 패턴에 대해서 다루며 고급 네트워킹 기능과 개발단계의 제약에 대해서도 소개 합니다.
자, UI는 필요가 없으므로 네트워크를 통해 동작하지는 않습니다.
이 사항에 대해서는 대역폭을 없애는 수 많은 방식이 존재 합니다.
이제 다음과 같은 질문이 나올 수 있습니다: 각 네트워크 플레이어의 UI 를 어떻게 해야 하나요?
전용 PlayerUI 스크립트를 가진 UI 프리팹을 작성할 것 입니다.
PlayerManager 스크립트는 이 UI 프리팹의 레퍼런스를 가지고 있을 것이며 PlayerManager 가 Start 할 때 이 UI 의 인스턴스를 생성하고 프리팹에게 그 플레이어를 따라 가라고 알려 줄 것 입니다.
UI 프리팹 생성하기
- UI Canvas 가 있는 아무 씬이나 오픈합니다.
- 캔버스에 UI 게임오브젝트 Slider 를 추가하여
Player UI
로 이름을 부여 합니다. - Rect Transform vertical anchor 를 Middle 로 설정하고 Horizontal anchor 를 center 로 설정 합니다.
- RectTransform width를 80 으로 설정하고 height 는 15로 설정 합니다.
Background
child 를 선택하고 이것의 Image component color를 빨강색으로 설정 합니다.- Child "Fill Area/Fill" 을 선택하고 이미지 색상을 녹색으로 설정 합니다.
Player UI
의 차일드로 UI GameObject Text 를 추가하여 Player Name Text로 이름을 부여 합니다.- Hierarchy 에서
Player UI
를 에셋의 Prefab Folder 로 드래그 합니다. 이제 프리팹이 만들어 졌습니다. - 씬에서 인스턴스는 더 이상 필요가 없으므로 삭제 합니다.
PlayerUI 스크립트 기본
새로운 C# 스크립트를 생성하여
PlayerUI
로 이름을 부여 합니다.아래는 기본 스크립트 구조로 편집하여
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 } }
PlayerUI
스크립트를 저장합니다
이제 프리팹 자체를 생성 합니다.
PlayerUI
스크립트를 PlayerUI 프리팹에 추가 합니다.- "Player Name Text" 차일드 게임오브젝트를 public 필드 PlayerNameText 에드래그앤 드롭합니다.
- 슬라이더 컴포넌트를 public 필드 PlayerHealthSlider에 드래그앤 드롭 합니다.
인스턴스 생성 및 플레이어에게 바인딩 하기
플레이어에게 PlayerUI 바인딩 하기
PlayerUI
스크립트는 다른 플레이어들 사이에서 어떤 플레이어를 나타내는지 알아야 합니다:
체력과 이름을 표시해야 하기 때문에 이것의 바인딩을 위한 public 메소드를 생성 하도록 하겠습니다.
PlayerUI
스크립트를 오픈 합니다."Private Fields" 영역에서 private 프로퍼티를 추가 합니다.
C#
PlayerManager target;
여기에서 고려해야 할 부분이 있는데 효율적으로 PlayerManager 레퍼런스 캐시를 위하여 주기적으로 체력을 봐야 할 것 입니다.
"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; } }
MonoBehaviour 콜백 영역에 이 메소드를 추가합니다
C#
void Update() { // Reflect the Player Health if (playerHealthSlider != null) { playerHealthSlider.value = target.Health; } }
PlayerUI
스크립트를 저장합니다.
지금까지 한 것으로 타겟 플레이어의 이름과 체력을 표시하는 UI 를 이제 가지고 있습니다.
인스턴스생성
좋습니다. 이 프리팹의 인스턴스 생성을 어떻게 하는지 알았으므로, 매번 플레이어 프리팹의 인스턴스를 생성 합니다.
가장 좋은 방식은 PlayerManager 가 초기화 할 때 하는 것 입니다.
PlayerManager
를 오픈 합니다.다음과 같이 Player UI 레퍼런스에 참조를 가질 public 필드를 추가합니다:
C#
[Tooltip("The Player's UI GameObject Prefab")] [SerializeField] private GameObject playerUiPrefab;
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); }
PlayerManager
스크립트를 저장합니다
위의 모든 것은 표준 유니티 코딩 입니다. 우리가 방금 생성한 인스턴스에게 메시지를 전송하고 있다는 것을 주목 하세요.
수신자가 필요하며 이 의미는 SetTarget
이 응답할 컴포넌트를 찾지 못했을 때 경고를 받게 된다는 것 입니다.
인스턴스로 부터 PlayerUI 컴포넌트를 받기 위한 방식중의 하나는 SetTarget
을 직접 호출 하는 것 입니다.
Component 들을 직접 사용하는 것이 일반적으로 권장 되지만 다양한 방식으로 동일한 사항을 할 수 있다는 것을 알아두는 것도 좋습니다.
하지만 아직 완성되지 않았기 때문에 플레이어 삭제를 처리하여 모든 신에서 고아가 되는 UI 인스턴스가 있어서는 안됩니다. 따라서 지정된 것이 없어진 타겟을 발견 했을 때 UI 인스턴스를 제거할 필요가 있습니다.
PlayerUI
스크립트를 오픈합니다다음 코드를
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; }
PlayerUI
스크립트를 저장합니다PlayerUI 스크립트를 저장 합니다. 이 코드는 쉽고 다루기 쉽습니다. Photon이 네트워크된 인스턴스들을 삭제하는 방식이 타겟 레퍼런스가 null 일 경우 UI 인스턴스 자체를 제거하는 것보다 쉽기 때문 입니다.
이것은 왜 타겟이 없어졌는지에 상관없이 수 많은 잠재적인 문제점을 없애서 매우 안전합니다. 관련된 UI는 자체적으로 자동 제거 되어 매우 편리하고 빠릅니다.하지만 잠시만요... 새로운 레벨이 로드되었을 때, UI 는 파괴되며 우리의 플레이어는 남아있습니다... 따라서 레벨이 로드된 것을 알 때 이를 인스턴스화해야 합니다. 다음 작업을 수행합니다.
PlayerManager
스크립트를 오픈합니다.CalledOnLevelWasLoaded()
메소드 내에서 다음의 코드를 추가합니다.C#
GameObject _uiGo = Instantiate(this.playerUiPrefab); _uiGo.SendMessage("SetTarget", this, SendMessageOptions.RequireReceiver);
PlayerManager
스크립트를 저장합니다.
이 문제를 처리하는 더 복잡하고 강력한 방법이 있으며, UI는 싱글톤으로 만들 수 있지만, 다른 플레이어가 룸에 가입하거나 나가는 경우에도 UI를 처리해야 하기 때문에 빠르게 복잡해집니다. 구현할 때 UI 프리팹을 인스턴스화하여 복제 비용을 부담합니다. 간단한 연습으로 "SetTarget" 메시지를 인스턴스화하고 전송하는 개인 방법을 만들 수 있으며, 다양한 위치에서 코드를 복제하는 대신 해당 메서드를 호출할 수 있습니다.
UI 캔버스로 페어런팅(Parenting)
유니티 UI 시스템의 중요한 제약 사항중의 하나는 모든 UI 요소들은 Canvas 게임 오브젝트내에 위치 해 있어야 한다는 것이기 때문에 우리는 PlayerUI 프리팹의 인스턴스가 생성되었을 때 제어해야하며 PlayerUI 의 인스턴스를 생성할때 처리 할 것 입니다.
PlayerUI
스크립트를 오픈 합니다."MonoBehaviour CallBacks" 영역에 다음 메소드를 추가 합니다.
C#
void Awake() { this.transform.SetParent(GameObject.Find("Canvas").GetComponent<Transform>(), false); }
PlayerUI
스크립트를 저장합니다왜 강제로 설정하고 캔버스를 이 방식을 찾을 까요? 신이 로드와 언로드 될 때 우리 프리팹도 같이 로드와 언로드 되며 캔버스는 매번 달라지게 될 것 입니다.
더 복잡한 코드 구조를 피하기 위해서 우리는 가장 빠른 방법을 선택 할 것 입니다. "Find" 의 오퍼레이션은 느리기 때문에 "Find" 를 사용하는 것은 권장사항이 아닙니다.
더 복잡한 경우를 구현하는 것은 이 튜토리얼의 범위를 벗어나지만 유니티와 스크립팅에서 로딩과 언로딩을 고려한 캔버스 엘리먼트의 레퍼런스 관리는 경험이 좀 더 쌓이게 되면 익숙해질 것 입니다.
타겟 플레이어 따라가기
이제 흥미로운 부분입니다.
대상 플레이어를 따라다니는 Player UI 가 필요합니다. 여기에는 몇가지 해결해야 할 사항들이 있습니다:
- UI 는 2d 요소이고 플레이어는 3d 요소 입니다. 이 경우에 어떻게 위치를 맞게 할 수 있을까요?
- 플레이어의 약간 위쪽에 UI 가 위치해 있는 것을 원하지 않습니다. 플레이어 위치로부터 스크린 오프셋을 어떻게 얻을 수 있을까요?
PlayerUI
스크립트를 오픈 합니다."Public Fields" 안에 아래의 public 프로퍼티를 추가 합니다.
C#
[Tooltip("Pixel offset from the player target")] [SerializeField] private Vector3 screenOffset = new Vector3(0f,30f,0f);
"Private Fields Messages" 영역에 아래의 2개 private 프로퍼티를 추가 합니다.
C#
float characterControllerHeight = 0f; Transform targetTransform; Vector3 targetPosition;
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 요소의 적당한 오프셋을 결정 할 것 입니다."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; } }
PlayerUI
스크립트를 저장합니다
2d 위치와 3d 위치를 맞추기 위한 트릭으로 카메라의 WorldToScreenPoint
함수를 이용합니다. 그리고 게임에서 카메라 하나만을 가지고 있으므로 유니티 신의 디폴트 설정에 있는 메인 카메라를 사용할 수 있습니다.
우리가 몇 단계를 통해 이 오프셋을 어떻게 설정 했는지 주목 해 보세요: 먼저 타겟의 실제 위치를 얻은 후에 characterControllerHeight
을 더했고 마지막으로 플레이어의 스크린 상단을 감소 한 후 스크린 오프셋을 더했습니다.