유니티
옵저버 패턴 - 플레이어 상태 UI
미역제자
2024. 9. 22. 14:50
옵저버 패턴이란?
옵저버 패턴은 객체의 상태 변화를 감지하고, 그 변화를 다른 객체들에게 자동으로 통보하는 디자인 패턴입니다. 이를 통해 주체(Subject)와 옵저버(Observer) 간의 관계를 정의할 수 있습니다.
옵저버 패턴의 기본 요소:
- Subject(주체): 상태 변화를 알리고 관리하는 객체입니다. 여러 옵저버를 등록하거나 해제할 수 있으며, 상태가 변화하면 옵저버들에게 이를 통지합니다.
- Observer(옵저버): 주체의 상태 변화를 감지하는 객체입니다. 주체로부터 통지를 받아 상태를 갱신하거나 특정 작업을 수행합니다.
사용한 옵저버 패턴 내용 정리
사용하고자 한 내용
플레이어의 체력, 목마름, 배고픔, 스테미나와 같은 여러 수치들이 변하는 것을 UI에서 관찰하고, 값이 변하면 스스로 UI가 변하도록 만들고 싶어서 사용.
주체와 옵저버
- Observer = UI_PlayerCondition (또 다른 옵저버 추가 가능)
- Subject = PlayerCondition
스크립트 역할
- PlayerCondition
- 옵저버 관리 : Attach(), Detach()
- 옵저버에 변경정보 알림: Notify()
- PlayerStatusUpdater
- 시간당 감소하는 배고픔 목마름 :TimeToDecrease()
- 배고픔, 목마름 0일때 체력 감소: HealthDecrease()
- 스테미나 회복, 감소: StaminaUpdate()
- UI_PlayerCondition
- 변하는 플레이어의 State를 반영하는 상태 UI.
- PlayerStatus
- 플레이어의 여러 정보, 수치
옵저버 패턴의 흐름
- PlayerCondition(주체)는 플레이어 상태(배고픔, 목마름, 스테미나 등)가 변경될 때, 옵저버로 등록된 객체들( UI_PlayerCondition 등)에 상태 변화를 통지.
- UI_PlayerCondition (옵저버)는 이 변화를 감지하고, 상태를 반영하여 UI를 업데이트.
- 이를 통해 플레이어의 상태가 변동될 때마다 자동으로 UI가 갱신되고, 게임 흐름을 끊김 없이 유지할 수 있다
상세 코드
더보기
PlayerCondition
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
#region Condition 옵저버
// 상태 변화를 관찰하는 옵저버가 구현해야 하는 메서드를 선언.
public interface IObserver
{
void OnPlayerStateChanged(PlayerState state);
}
#endregion
#region Condition 주체(Subject)
// 옵저버를 관리하고 상태 변화를 알리는 역할.
public interface ISubject
{
public void Attach(IObserver observer);
public void Detach(IObserver observer);
public void Notify();
}
#endregion
// "ISubject" 인터페이스를 구현하고, 상태 변화를 알리는 역할.
public class PlayerCondition : MonoBehaviour, ISubject
{
private List<IObserver> _observers = new List<IObserver>();
private PlayerState _state;
private float _maxValue = 100f;
private void Start()
{
// TODO: 저장 시점에 어떤 값을 가지고 있는지, 게임 시작시 초기화.
_state = new PlayerState(_maxValue, _maxValue, _maxValue, _maxValue);
UI_PlayerCondition uI_PlayerCondition = FindObjectOfType<UI_PlayerCondition>();
if (uI_PlayerCondition != null)
{
Attach(uI_PlayerCondition);
}
// PlayerStateUpdater를 추가하여 시간에 따른 상태 감소를 처리.
PlayerStateUpdater updater = gameObject.AddComponent<PlayerStateUpdater>();
updater.Initialize(this, _state);
}
#region 옵저버 관리
public void Attach(IObserver observer)
{
_observers.Add(observer);
}
public void Detach(IObserver observer)
{
_observers.Remove(observer);
}
#endregion
public void Notify()
{
// 모든 옵저버들에게 공지.
foreach (var observer in _observers)
{
observer.OnPlayerStateChanged(_state);
}
}
#region 플레이어 State 수치 Update
public void UpdateHunger(float amount)
{
_state.Hunger += amount;
Notify();
}
public void UpdateHealth(float amount)
{
_state.Health += amount;
Notify();
}
public void UpdateThirst(float amount)
{
_state.Thirst += amount;
Notify();
}
public void UpdateStamina(float amount)
{
_state.Stamina += amount;
Notify();
}
#endregion
}
UI_PlayerCondition
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UI_PlayerCondition : UI_Scene, IObserver
{
[HideInInspector] public Image health;
[HideInInspector] public Image hunger;
[HideInInspector] public Image thirsty;
[HideInInspector] public Image stamina;
public Transform ConditionParent;
private void Start()
{
//ConditionParent = GameManager.Instance._UI.transform.Find("HUD_Canvas/PlayerCondition");
//GameObject condition = Instantiate(ConditionPrefab, ConditionParent); //시작시 condition추가
//PlayerController.instance.heartAnim = condition.transform.Find("Health/Image").GetComponent<Animator>();
SetImage();
}
private void SetImage()
{
GameObject[] go = GameObject.FindGameObjectsWithTag("ConditionUI");
for (int i = 0; i < go.Length; i++)
{
switch (go[i].name)
{
case "HungerImage":
hunger = go[i].GetComponent<Image>();
break;
case "ThirstyImage":
thirsty = go[i].GetComponent<Image>();
break;
case "StaminaImage":
stamina = go[i].GetComponent<Image>();
break;
case "HealthImage":
health = go[i].GetComponent<Image>();
break;
}
}
}
public void OnPlayerStateChanged(PlayerState state)
{
hunger.fillAmount = state.Hunger / 100f;
thirsty.fillAmount = state.Thirst / 100f;
stamina.fillAmount = state.Stamina / 100f;
health.fillAmount = state.Health / 100f;
}
}
PlayerStatus
public class PlayerStatus
{
public float Hunger { get; set; }
public float Health { get; set; }
public float Thirst { get; set; }
public float Stamina { get; set; }
public PlayerStatus(float hunger, float health, float thirst, float stamina)
{
Hunger = hunger;
Health = health;
Thirst = thirst;
Stamina = stamina;
}
}
PlayerStatusUpdater
public class PlayerStatusUpdater : MonoBehaviour
{
#region Field
private PlayerCondition _playerCondition;
private PlayerStatus _status;
[HideInInspector] public float hungerDecreaseRate = 0.5f; // 배고픔 감소 속도
[HideInInspector] public float thirstDecreaseRate = 0.5f; // 목마름 감소 속도
[HideInInspector] public float healthDecreaseRate = 1f; // 체력 감소 속도
[HideInInspector] public float staminaDecreaseRate = 1f; // 체력 감소 속도
[HideInInspector] public bool isRun = false; //TODO 달리기 상태일때 is Run값 바꿔주기.
[HideInInspector] public bool canRun = false; //TODO 달리기 상태일때 is Run값 바꿔주기.
private bool _isStaminaLock = false;
#endregion
private void Start()
{
GetInputController();
}
private void GetInputController()
{
// PlayerInputController의 OnRunStateChanged 이벤트 구독.
PlayerInputController inputController = GetComponent<PlayerInputController>();
if (inputController != null)
{
inputController.OnRunStateChanged += HandleRunStateChanged;
}
}
private void HandleRunStateChanged(bool isRunning)
{
isRun = isRunning; // 이벤트를 통해 isRun 업데이트
}
public void Initialize(PlayerCondition playerCondition, PlayerStatus state)
{
this._playerCondition = playerCondition;
this._status = state;
}
private void Update()
{
TimeToDecrease();
HealthDecrease();
StaminaUpdate(isRun);
IsPlayerDead();
}
private void TimeToDecrease()
{
// 시간에 따라 배고픔과 목마름이 감소.
_playerCondition.UpdateHunger(-hungerDecreaseRate * Time.deltaTime);
_playerCondition.UpdateThirst(-thirstDecreaseRate * Time.deltaTime);
}
private void HealthDecrease()
{
// 배고픔이 0이면 체력 감소.
if (_status.Hunger <= 0)
{
_playerCondition.UpdateHealth(-healthDecreaseRate * Time.deltaTime);
}
// 목마름이 0이면 체력 감소.
if (_status.Thirst <= 0)
{
_playerCondition.UpdateHealth(-healthDecreaseRate * 2f * Time.deltaTime);
}
}
private void StaminaUpdate(bool isRun = false)
{
// 스태미나 회복 시도 (달리기 키를 누르고 있지 않을 때, 달리키를 누르더라도 달릴 수 없는 상황일 때.)
if (!isRun || _isStaminaLock)
{
// 스태미나 회복.
if (_status.Stamina <= 100)
{
// 배고픔에 따라 다른 스태미나 회복량.
if (_status.Hunger >= 50)
_playerCondition.UpdateStamina(staminaDecreaseRate * Time.deltaTime);
else
_playerCondition.UpdateStamina(staminaDecreaseRate * 0.5f * Time.deltaTime);
}
}
else
{
// 달리기 중이고 스태미나가 0 이상일 때만 감소
if (CheckStamina())
{
// 스태미나 감소.
_playerCondition.UpdateStamina(-staminaDecreaseRate * Time.deltaTime);
}
}
}
private bool CheckStamina()
{
if (_status.Stamina <= 0) // 스태미나가 0 이하일 때
{
_isStaminaLock = true;
return false;
}
if (_status.Stamina >= 20) // 스태미나가 20 이상일 때
{
_isStaminaLock = false;
return true;
}
// 스태미나가 0과 20 사이일 때
return !_isStaminaLock;
}
public bool IsPlayerDead()
{
// TODO 죽을 때 화면 등등..
if(_status.Health <= 0)
{
return true;
}
else
{
return false;
}
}
}