[유저 데이터 시스템] 스테이터스 UI

Jongmin Kim·2025년 8월 12일

Lapi

목록 보기
5/14

시작

앞서 구현한 로컬 경험치 서비스와 로컬 유저 서비스를 토대로 스테이터스 UI를 제작할 차례다.
만약 UI로 사용하기에 적절한 이미지가 없다면 아래의 이미지를 사용하도록 하자.


여기에서 말했듯이 UI는 MVP 아키텍처 패턴을 이용해서 제작할 것이다. 그래서 Model, View, Presenter의 순서대로 살펴보도록 하겠다.



Model

MVP 패턴에서 Model이라는 요소 자체의 의미가 실제 데이터가 존재하는 곳이기 때문에 Model을 새롭게 정의할 필요가 없다. 하지만 나는 만들었다.

using System;

public class StatusModel
{
    private PlayerStatus m_player_status;

	// 체력에 변동이 있으면 실행될 이벤트
    public Action<float, float> OnUpdatedHP
    {
        get => m_player_status.OnUpdatedHP;
        set => m_player_status.OnUpdatedHP = value;
    }

	// 마나에 변동이 있으면 실행될 이벤트
    public Action<float, float> OnUpdateMP
    {
        get => m_player_status.OnUpdatedMP;
        set => m_player_status.OnUpdatedMP = value;
    }

	// 생성자 주입을 통해 PlayerStatus를 주입받는다.
    public StatusModel(PlayerStatus player_status)
    {
        m_player_status = player_status;
    }

	// PlayerStatus를 초기화한다.
    public void Initialize()
    {
        m_player_status.Initialize();
    }
}

여기서 PlayerStatus플레이어의 스테이터스 상태에 관한 정보를 실시간으로 조정하는 클래스다. 이 글의 취지와 맞지 않아 모든 내용을 실을 수는 없지만 요약하면 다음과 같다.

public class PlayerStatus : MonoBehaviour, IStatus
{
    private IUserService m_user_service;
    private PlayerCtrl m_controller;

    public Action<float, float> OnUpdatedHP;
    public Action<float, float> OnUpdatedMP;

    public float HP => m_user_service.Status.HP;
    public float MP => m_user_service.Status.MP;
    public float MaxHP => m_controller.DefaultStatus.HP
                                + (m_user_service.Status.Level - 1) * m_controller.GrowthStatus.HP
                                + m_controller.EquipmentEffect.HP;

    public float MaxMP => m_controller.DefaultStatus.MP
                                + (m_user_service.Status.Level - 1) * m_controller.GrowthStatus.MP
                                + m_controller.EquipmentEffect.MP;
                                
// 이하 생략

즉, 어떤 형태냐고 설명하자면 다음과 같은 순서대로 진행될 것이다.

  1. 플레이어가 몬스터에게 피격 당한다.
  2. PlayerStatusUpdateHP()가 호출된다.
  3. UpdateHP() 내부에서 OnUpdatedHP 델리게이트를 실행한다.
  4. 델리게이트에 등록된 모든 이벤트들이 실행된다.

이 모든 이벤트들 중 스테이터스 UI에서의 체력과 마나 갱신이 포함된다.



View

View는 다양한 형태로 다양한 곳에 사용될 수 있다. 따라서 View 자체를 하나의 구체적인 스크립트로 구현하기보다 View를 인터페이스로 정의하는 것이 더 바람직하다.

public interface IStatusView
{
    void Inject(StatusPresenter presenter);
    void UpdateLV(int level, float exp_rate);  	// 경험치 갱신에 사용된다.
    void UpdateHP(float hp_rate);				// 체력 갱신에 사용된다.
    void UpdateMP(float mp_rate);				// 마나 갱신에 사용된다.
}

다양한 형태로 사용될 수 있지만 우리는 대표적인 스테이터스 UI로 활용한다. 우선 다음과 같이 UI를 구성한다.

그리고 스테이터스 UI에 부착할 IStatusView를 구현하는 클래스를 작성한다.

using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class StatusView : MonoBehaviour, IStatusView
{
    [Header("UI 관련 컴포넌트")]
    [Header("레벨")]
    [SerializeField] private TMP_Text m_level_label;

    [Header("경험치")]
    [SerializeField] private Slider m_exp_slider;

    [Header("체력")]
    [SerializeField] private Slider m_hp_slider;

    [Header("마나")]
    [SerializeField] private Slider m_mp_slider;

	// 코루틴을 경험치, 체력, 마나 별로 두어 코루틴이 동시 실행되는 일이 없도록 한다.
    private Coroutine m_exp_coroutine;
    private Coroutine m_hp_coroutine;
    private Coroutine m_mp_coroutine;

    private StatusPresenter m_presenter;

	// 객체가 파괴되는 시점에서 델리게이트에 등록된 이벤트를 해제한다.
    private void OnDestroy()
    {
        m_presenter.Dispose();
    }
	
    // Inject()를 통해서 외부에서 프레젠터를 주입받는다.
    public void Inject(StatusPresenter presenter)
    {
        m_presenter = presenter;
    }

	// 레벨과 경험치를 갱신한다.
    public void UpdateLV(int level, float exp_rate)
    {
        m_level_label.text = $"LV.{level}";

        if (m_exp_coroutine != null)
        {
            StopCoroutine(m_exp_coroutine);
            m_exp_coroutine = null;
        }

        m_exp_coroutine = StartCoroutine(UpdateSlider(m_exp_slider, exp_rate));
    }

	// 체력을 갱신한다.
    public void UpdateHP(float hp_rate)
    {
        if (m_hp_coroutine != null)
        {
            StopCoroutine(m_hp_coroutine);
            m_hp_coroutine = null;
        }

        m_hp_coroutine = StartCoroutine(UpdateSlider(m_hp_slider, hp_rate));
    }

	// 마나를 갱신한다.
    public void UpdateMP(float mp_rate)
    {
        if (m_mp_coroutine != null)
        {
            StopCoroutine(m_mp_coroutine);
            m_mp_coroutine = null;
        }

        m_mp_coroutine = StartCoroutine(UpdateSlider(m_mp_slider, mp_rate));
    }

	// 슬라이더의 값을 부드럽게 보간하여 변경시키는 코루틴이다.
    private IEnumerator UpdateSlider(Slider slider, float rate)
    {
        var elapsed_time = 0f;
        var target_time = 1f;

        while (elapsed_time <= target_time)
        {
            // 없어도 무방하여 생략한다.
            
            elapsed_time += Time.deltaTime;

            var delta = elapsed_time / target_time;
            slider.value = Mathf.Lerp(slider.value, rate, delta);

            yield return null;
        }

        slider.value = rate;
    }
}



Presenter

마지막으로 Presenter를 작성할 차례다. Presenter는 Model과 View 사이에서 중개하는 역할로 Model과 View에 대한 참조가 모두 필요하다.

using System;
using EXPService;
using SkillService;
using UserService;

public class StatusPresenter : IDisposable
{
    private readonly IStatusView m_view;
    private readonly StatusModel m_model;
    private readonly IEXPService m_exp_service;
    private readonly IUserService m_user_service;
    private readonly ISkillService m_skill_service;

	// 생성자를 통해 필요한 서비스와 model, view를 주입받는다.
    public StatusPresenter(IStatusView view,
                           StatusModel model,
                           IEXPService exp_service,
                           IUserService user_service,
                           ISkillService skill_service)
    {
        m_view = view;
        m_model = model;
        m_exp_service = exp_service;
        m_user_service = user_service;
        m_skill_service = skill_service;

		// 각 서비스와 model에 이벤트를 등록한다.
        m_user_service.OnUpdatedLevel += UpdateLV;
        m_model.OnUpdatedHP += UpdateHP;
        m_model.OnUpdateMP += UpdateMP;

        m_user_service.InitializeLevel();

        m_view.Inject(this);
    }

	// 이벤트를 통해서 받은 정보로 레벨과 경험치 갱신을 한다.
    public void UpdateLV(int level, int current_exp)
    {
        var max_exp = m_exp_service.GetEXP(level);
        while (current_exp >= max_exp)
        {
            current_exp -= max_exp;

            m_user_service.UpdateLevel(-max_exp);
            m_user_service.Status.Level++;
            m_skill_service.UpdatePoint(3);	// 지금은 없어도 무방하다.
            m_model.Initialize();
        }

        m_view.UpdateLV(m_user_service.Status.Level, (float)current_exp / (float)max_exp);
    }

	// 이벤트를 통해서 받은 정보로 체력 갱신을 한다.
    public void UpdateHP(float current_hp, float max_hp)
    {
        m_view.UpdateHP(current_hp / max_hp);
    }

	// 이벤트를 통해서 받은 정보로 마나 갱신을 한다.
    public void UpdateMP(float current_mp, float max_mp)
    {
        m_view.UpdateMP(current_mp / max_mp);
    }

	// 객체가 파괴될 때 이벤트 등록을 해제한다.
    public void Dispose()
    {
        m_user_service.OnUpdatedLevel -= UpdateLV;
        m_model.OnUpdatedHP -= UpdateHP;
        m_model.OnUpdateMP -= UpdateMP;
    }
}



인스톨러 등록

스테이터스 UI에 의존성을 주입할 방법을 생성자와 메서드를 통해서 만들긴 했지만 아직 의존성 주입을 하는 곳을 찾지 못했다.

이 의존성을 주입하는 곳이 바로 인스톨러다. 다음과 같이 StatusUIInstaller를 정의하고 하이어라키 뷰에서 Bootstrapper 하위에 배치시킨다.

using EXPService;
using SkillService;
using UnityEngine;
using UserService;

public class StatusUIInstaller : MonoBehaviour, IInstaller
{
    [Header("플레이어 상태 컴포넌트")]
    [SerializeField] private PlayerStatus m_player_status;

    [Header("스테이터스 UI 뷰")]
    [SerializeField] private StatusView m_status_view;

    public void Install()
    {
        DIContainer.Register<PlayerStatus>(m_player_status);
        DIContainer.Register<IStatusView>(m_status_view);

        var status_model = new StatusModel(m_player_status);
        DIContainer.Register<StatusModel>(status_model);

        var status_presenter = new StatusPresenter(m_status_view,
                                                   status_model,
                                                   ServiceLocator.Get<IEXPService>(),
                                                   ServiceLocator.Get<IUserService>(),
                                                   ServiceLocator.Get<ISkillService>());
        DIContainer.Register<StatusPresenter>(status_presenter);
    }
}

그리고 인스펙터로 주입 가능한 컴포넌트들을 인스펙터에서 연결 후 의존성을 주입한다.



마무리

다음과 같이 정상적으로 UI가 작동하는 것을 확인할 수 있다.

profile
Game Client Programmer

0개의 댓글