[20260210] 실습 미션

SmartBear·2026년 2월 10일

[기본 실습] UI-Button 클릭 MVP

동작 결과 (결과물 모습)

  1. 화면 중앙의 버튼을 누를 때마다 숫자가 올라갑니다(+1).
  2. 첫 번째 실험: 숫자가 텍스트(0, 1, 2, 3...)로 표시되는 화면을 연결해 봅니다.
  3. 두 번째 실험: 텍스트 화면을 빼고, 게이지(Slider)가 차오르는 화면으로 갈아 끼웁니다.
  4. 핵심: 화면을 교체할 때, 클릭 횟수를 계산하는 중심 코드(Presenter/Model)는 단 한 줄도 수정하지 않고 그대로 작동해야 합니다.

2. 필수 스크립트 & 이벤트

2-1. Model (ClickModel.cs)

  • 데이터: int count (현재 클릭 수)
  • 핵심 이벤트: Action<int> OnCountChanged
  • 역할: 숫자가 바뀔 때마다 "변경된 숫자" 를 담아 외침

2-2. View (ClickView_Text.cs)

  • 핵심 이벤트: Action OnButtonClicked
  • 역할: 버튼 클릭 시 "버튼이 눌렸어요!" 출력
  • 핵심 함수: void SetDisplay(int value)
  • 역할: 전달받은 숫자로 텍스트(TMP) 를 갱신

2-3. Presenter (ClickPresenter.cs)

  • 연결: ClickView_Text를 직접 참조.
  • 로직: View의 보고를 받아 Model을 깨우고, Model의 외침을 들어 View를 갱신.

Scripts

ClickModel.cs

using System;
using UnityEngine;

public class ClickModel
{
    public int Count;
    
    public event Action<int> OnCountChanged;

    public void Clicked(int count)
    {
        Debug.Log($"Changed the count!! {Count} -> {count}");
        Count += count;
        OnCountChanged?.Invoke(Count);
    }
}

ClickView_Text.cs

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

public class ClickView_Text : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI _tmp;
    [SerializeField] private Slider _slider;
    [SerializeField] private Button _btn;
    
    public event Action OnButtonClicked;

    public void OnEnable()
    {
        _btn.onClick.AddListener(OnClick);
    }
    public void OnDisable()
    {
        _btn.onClick.RemoveListener(OnClick);
    }
    
    public void OnClick()
    {
        Debug.Log("Pressed the button!!");
        OnButtonClicked?.Invoke();
    }
    
    public void SetDisplay(int count)
    {
        if (_tmp != null) _tmp.text = count.ToString();
        if (_slider != null) _slider.value = count / 100f;
    }
}

ClickPresenter.cs

using UnityEngine;

public class ClickPresenter : MonoBehaviour
{
    private ClickModel _clickModel = new();
    public ClickView_Text _clickViewText;

    private void OnEnable()
    {
        _clickViewText.OnButtonClicked += Count;
        _clickModel.OnCountChanged += _clickViewText.SetDisplay;
    }

    private void OnDisable()
    {
        _clickViewText.OnButtonClicked -= Count;
        _clickModel.OnCountChanged -= _clickViewText.SetDisplay;
    }

    public void Count()
    {
        _clickModel.Clicked(1);
    }
}

결과


[응용 실습] "보스 레이드" - 다중 연동 체력 시스템

1. 동작 결과 (결과물 모습)

  • 입력: '공격' 버튼을 누르면 보스의 체력이 일정량 깎입니다.
  • 출력 (3가지 동시 반응):
  1. 메인 체력바: 상단의 커다란 슬라이더(Slider)가 줄어듭니다.
  2. HP 수치: 텍스트(TMP)로 현재 체력을 "80 / 100" 형태로 표시합니다.
  3. 위험 감지: 체력이 30% 이하가 되면 "위험!" 팝업이나 빨간 경고창을 띄웁니다.

2. 필수 스크립트 & 이벤트

2-1. Model (BossStatModel.cs)

  • 데이터: int currentHp, int maxHp
  • 이벤트: Action<float> OnHpRatioChanged (0.0~1.0 비율 전달)
  • 역할: 체력을 계산하고, 값이 변할 때마다 "현재 체력 비율"을 외칩니다.

2-2. View (독립적인 3종 요정)

  • BossHPBarView: SetGauge(float ratio) 함수로 슬라이더 갱신.
  • BossHPTextView: SetText(int current, int max) 함수로 숫자 표시.
  • BossWarningView: SetWarning(bool active) 함수로 경고창 On/Off.

※ 주의: 각 View는 서로의 존재를 절대 몰라야 하며, 본인한테 할당된 UI 컴포넌트 역할만 수행합니다.

2-3. Presenter (BossHPPresenter.cs)

  • 핵심 로직 (1:N 연결):

    1. 공격 버튼 클릭 보고 수신 ➔ Model의 체력을 깎음.
    2. Model의 이벤트 하나에 3개의 View 업데이트 함수를 모두 연결(+=)함.

3. 체크리스트

  • 독립성: HPBarView를 삭제해도 HPTextView는 에러 없이 정상 작동하는가?
  • 효율성: Presenter가 Update()에서 매 프레임 체크하지 않고, 오직 이벤트가 터질 때만 UI를 갱신하는가?
  • 확장성: 새로운 UI(예: 보스 표정 변화)를 추가할 때 Presenter의 이벤트 연결 코드 외에 Model을 수정하지 않는가?

Scripts

구찮아서 Button Handling 은 그냥 Presenter 에서 해버림 ㅋㅋ

BossStatModels.cs

using System;
using UnityEngine;

public class BossStatModel
{
    public int CurrentHp = 100;
    public int MaxHp = 100;
    public event Action OnCurrentHpChanged;

    public void TakeDamage(int damage)
    {
        CurrentHp -= damage;
        OnCurrentHpChanged?.Invoke();
    }
}

BossHpBarView.cs

using UnityEngine;
using UnityEngine.UI;

public class BossHpBarView : MonoBehaviour
{
    private Slider _slider;

    public void Awake()
    {
        _slider = GetComponent<Slider>();
    }
    
    public void OnSetValue(float value)
    {
        _slider.value = value;
    }
}

BossHpTextView.cs

using TMPro;
using UnityEngine;

public class BossHpTextView : MonoBehaviour
{
    private TextMeshProUGUI _text;

    private void Awake()
    {
        _text = GetComponent<TextMeshProUGUI>();
    }

    public void OnSetText(string text)
    {
        _text.text = text;
    }
}

BossWarning.cs

using TMPro;
using UnityEngine;

public class BossWarningView : MonoBehaviour
{
    private TextMeshProUGUI _text;

    private void Awake()
    {
        _text = GetComponent<TextMeshProUGUI>();
    }

    public void OnSetText(string text)
    {
        _text.text = text;
    }
}

BossPresenter.cs

using UnityEngine;

public class BossPresenter : MonoBehaviour
{
    BossStatModel _bossStat = new();
    
    [SerializeField] BossHpBarView _hpBarView;
    [SerializeField] BossHpTextView _hpTextView;
    [SerializeField] BossWarningView _bossWarningView;

    public void Start()
    {
        HpBarView();
        HpTextView();
    }
    
    public void OnEnable()
    {
        _bossStat.OnCurrentHpChanged += HpBarView;
        _bossStat.OnCurrentHpChanged += HpTextView;
        _bossStat.OnCurrentHpChanged += WarningView;
    }

    public void OnDisable()
    {
        _bossStat.OnCurrentHpChanged -= HpBarView;
        _bossStat.OnCurrentHpChanged -= HpTextView;
        _bossStat.OnCurrentHpChanged -= WarningView;
    }
    
    public void OnAttack()
    {
        int damage = 5;
        _bossStat.TakeDamage(damage);
    }

    private void HpBarView() => _hpBarView.OnSetValue(_bossStat.CurrentHp / (float)_bossStat.MaxHp);
    private void HpTextView() => _hpTextView.OnSetText($"{_bossStat.CurrentHp}/{_bossStat.MaxHp}");
    private void WarningView()
    {
        float ratio = _bossStat.CurrentHp / (float)_bossStat.MaxHp;
        if (ratio < 0.3f)
        {
            _bossWarningView.OnSetText($"위험!!! ");
        }
    }
}

결과

profile
Python Dev with Infra -> Game Programmer

0개의 댓글