일전에 간략히 짚고 넘어갔던 디자인 패턴이다. UI에서 자주 쓰이는 디자인 패턴
유니티 러닝 - 모듈러식 코드베이스
유니티 - MVP
ASP.NET - MVC
유니티 - UI 최적화 팁MVP, MVC, MVVM
- 셋 다 아키텍처의 한 종류지만, 굳이 나눠서 생각할 필요없이 개념만 챙겨가서 설계에 쓰면 된다
Model, View, Controller논리 부분, 데이터, 출력을 분리하는 디자인 패턴MVC 패턴은 프로그램을 다음 세 개의 레이어로 분할한다두뇌. 컨트롤러는 게임 데이터를
Model의 정보를 토대로 연산 수행한다모델, 조작이 제대로 안되면 컨트롤러, 출력된 정보가 잘못되었다면 뷰 를 확인하면 된다MVP 모델을 더 많이 쓴다public class PlayerModel
{
public class PlayerModel : MonoBehaviour
{
[SerializeField] int hp;
public int HP { set { hp = value; OnHpChanged?.Invoke(hp); } get { return hp; } }
public event Action<int> OnHpChanged;
[SerializeField] int maxHP;
public int MaxHP { set { maxHP = value; OnMaxHPChanged?.Invoke(maxHP); } get { return maxHP; } }
public event Action<int> OnMaxHPChanged;
[SerializeField] Rigidbody rigid;
public Vector3 Velocity { set { rigid.velocity = value; OnVelocityChanged?.Invoke(rigid.velocity); } get { return rigid.velocity; } }
public Action<Vector3> OnVelocityChanged;
}
}public class PlayerController : MonoBehaviour
{
[SerializeField] PlayerModel model;
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
model.HP -= 1;
}
Move();
}
private void Move()
{
float xInput = Input.GetAxis("Horizontal");
float zInput = Input.GetAxis("Vertical");
Vector3 dir = new Vector3(xInput, 0, zInput);
if (dir.sqrMagnitude > 1)
{
dir = dir.normalized;
}
model.Velocity = Vector3.MoveTowards(model.Velocity, dir * 5, 10 * Time.deltaTime);
}
}
public class MVCPlayerHPView : MonoBehaviour
{
[SerializeField] TMP_Text textUI;
public void Settext(int curHP, int maxHP)
{
textUI.text = $"HP : {curHP} / {maxHP}";
}
}
public class MVCPlayerHPBarView : MonoBehaviour
{
[SerializeField] Slider slider;
public void SetHP(int curHP)
{
slider.value = curHP;
}
public void SetMaxHP(int maxHP)
{
slider.maxValue = maxHP;
}
}

컨트롤러public class PlayerController : MonoBehaviour
{
[SerializeField] PlayerModel model;
[SerializeField] int damage;
void Update()
{
if (Input.GetKeyDown(KeyCode.A))
{
TakeHit(damage);
}
}
void TakeHit(int damage)
{
model.HP -= damage;
}
}
모델public class PlayerModel : MonoBehaviour
{
[SerializeField] PlayerHPView hpView;
[SerializeField] PlayerHPBarView hpBarView;
[SerializeField] int hp;
public int HP
{
set
{
hp = value;
hpView.Settext(hp, maxHP);
hpBarView.SetHP(hp);
}
get
{
return hp;
}
}
[SerializeField] int maxHP;
public int MaxHP { set { maxHP = value; } get { return maxHP; } }
private void Start()
{
hpView.Settext(hp, maxHP);
hpBarView.SetHP(hp);
hpBarView.SetMaxHP(maxHP);
}
}
뷰public class PlayerHPView : MonoBehaviour
{
[SerializeField] TMP_Text textUI;
public void Settext(int curHP, int maxHP)
{
textUI.text = $"HP : {curHP} / {maxHP}";
}
}
뷰public class PlayerHPBarView : MonoBehaviour
{
[SerializeField] Slider slider;
public void SetHP(int curHP)
{
slider.value = curHP;
}
public void SetMaxHP(int maxHP)
{
slider.maxValue = maxHP;
}
}
Model, View, Presenter
중개자 역할을 하는 MVC의 배리에이션을 사용한다. MVP는 뷰가 직접 모델을 관찰하지 않는다이벤트와 관찰자 패턴이 사용된다Unity UI 의 Button, Toggle, Slider 컴포넌트와 상호작용할 수 있다뷰에서 UI 이벤트를 통해 받은 입력을 프레젠터에 되돌려 보내고, 프레젠터는 모델을 조작한다모델의 상태 변경 이벤트는 데이터가 업데이트 되었음을 프레젠터에 알린다public class MVPPlayerPresenter : MonoBehaviour
{
[Header("Model")]
[SerializeField] MVPPlayerModel model;
[Header("View")]
[SerializeField] TMP_Text playerHPTextUI;
[SerializeField] TMP_Text playerMaxHPTextUI;
[SerializeField] Slider playerHPBar;
// 이벤트 구독
private void OnEnable()
{
model.OnHPChanged += SetHP;
model.OnMaxHPChanged += SetMaxHP;
SetMaxHP(model.MaxHP);
SetHP(model.HP);
}
// 이벤트 구독해지
private void OnDisable()
{
model.OnHPChanged -= SetHP;
model.OnMaxHPChanged -= SetMaxHP;
}
public void SetHP(int hp)
{
playerHPTextUI.text = $"HP : {hp}";
playerHPBar.value = hp;
}
public void SetMaxHP(int maxHP)
{
playerMaxHPTextUI.text = $"{maxHP}";
playerHPBar.maxValue = maxHP;
}
}
public class MVPPlayerModel : MonoBehaviour
{
[SerializeField] TMP_Text hpView;
[SerializeField] Slider hpBarView;
[SerializeField] int hp;
public int HP { set { hp = value; OnHPChanged?.Invoke(hp); } get { return hp; } }
public event Action<int> OnHPChanged;
[SerializeField] int maxHP;
public int MaxHP { set { maxHP = value; OnMaxHPChanged?.Invoke(maxHP); } get { return maxHP; } }
public event Action<int> OnMaxHPChanged;
}
public class PlayerController : MonoBehaviour
{
[SerializeField] PlayerModel model;
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
model.HP -= 1;
}
Move();
}
private void Move()
{
float xInput = Input.GetAxis("Horizontal");
float zInput = Input.GetAxis("Vertical");
Vector3 dir = new Vector3(xInput, 0, zInput);
if (dir.sqrMagnitude > 1)
{
dir = dir.normalized;
}
model.Velocity = Vector3.MoveTowards(model.Velocity, dir * 5, 10 * Time.deltaTime);
}
}
public class PlayerPresenter : MonoBehaviour
{
[Header("Model")]
[SerializeField] PlayerModel model;
[Header("View")]
[SerializeField] TMP_Text playerHPTextUI;
[SerializeField] TMP_Text playerMaxTextUI;
[SerializeField] Slider playerHPSliderUI;
[SerializeField] TMP_Text playerSpeedTextUI;
private void OnEnable()
{
model.OnHpChanged += SetHP;
model.OnMaxHPChanged += SetMaxHP;
SetMaxHP(model.MaxHP);
SetHP(model.HP);
}
private void OnDisable()
{
model.OnHpChanged -= SetHP;
model.OnMaxHPChanged -= SetMaxHP;
}
private void Update()
{
SetSpeed(model.Velocity);
}
public void SetHP(int hp)
{
playerHPTextUI.text = $"{hp}";
playerHPSliderUI.value = hp;
}
public void SetMaxHP(int maxHP)
{
playerMaxTextUI.text = $"{maxHP}";
playerHPSliderUI.maxValue = maxHP;
}
public void SetSpeed(Vector3 velocity)
{
playerSpeedTextUI.text = $"{velocity.magnitude}";
}
}
public class PlayerModel : MonoBehaviour
{
[SerializeField] int hp;
public int HP { set { hp = value; OnHpChanged?.Invoke(hp); } get { return hp; } }
public event Action<int> OnHpChanged;
[SerializeField] int maxHP;
public int MaxHP { set { maxHP = value; OnMaxHPChanged?.Invoke(maxHP); } get { return maxHP; } }
public event Action<int> OnMaxHPChanged;
[SerializeField] Rigidbody rigid;
public Vector3 Velocity { set { rigid.velocity = value; OnVelocityChanged?.Invoke(rigid.velocity); } get { return rigid.velocity; } }
public Action<Vector3> OnVelocityChanged;
}
원활한 업무 분배: 뷰를 프리젠터에서 분리했으므로, 사용자 인터페이스 개발과MVP 및 MVC로 간소화된 유닛 테스팅: 게임플레이 로직과 사용자 인터페이스를 분리한다. 그렇기 때문에 에디터에서 플레이 모드를 사용하지 않아도 오브젝트를 코드와 연동해 시뮬레이션할 수 있으며, 시간이 상당히 단축됨유지 가능하며 가독성이 높은 코드: 비교적 작은 클래스를 만들게 되므로 코드의 가독성이 높아진다. 종속 관계가 적을수록 오류, 버그 가능성이 줄어든다.Input Field와 Scroll View로 채팅을 치면, 한 줄씩 아래에서부터 업데이트 되고 오래된 채팅은 위로 올라가는 기능을 구현한다public class PracticeChatLog : MonoBehaviour
{
[SerializeField] RectTransform parent;
[SerializeField] TMP_Text chatPrefab;
private TMP_Text chatText;
public void MakeChatLog(string chat)
{
chatText = Instantiate(chatPrefab);
chatText.text = $"<color=red>Player</color> : {chat}";
chatText.transform.SetParent(parent,false);
}
}
Content의 자식으로 들어갈 수 있게 SetParent()로 설정한다. 매개변수로 False가 들어가는 것은 World 기준 transform을 가지게 하는 설정이다. 이걸 안해주면 Scale이 0.25로 줄어들어서 글씨가 안보인다

Input Field의 Edit이 끝날 경우에 수행되는 이벤트로 함수를 추가한다. 이러면 입력창에서 엔터키나 다른곳으로 마우스 클릭을 하면 텍스트가 추가된다public class PracticePlayerController : MonoBehaviour
{
[SerializeField] private PracticePlayerModel model;
private void Start()
{
model = GetComponent<PracticePlayerModel>();
}
void Update()
{
if (Input.GetKeyDown(KeyCode.S))
{
if (model.playerPosition == PracticePlayerModel.PlayerPosition.Ground)
{
model.JumpCount++;
}
}
if (Input.GetKeyDown(KeyCode.A))
{
model.CurHP++;
}
if (Input.GetKeyDown(KeyCode.D))
{
model.CurHP--;
}
}
}
UI이외의 입력을 받기위해 어쩔 수 없이 추가한다상태 패턴을 추가. 조건으로 사용한다
Rigidbody 컴포넌트를 추가한다public class PracticePlayerModel : MonoBehaviour
{
[Header("Set Model Value")]
[SerializeField] int curHP = 50;
[SerializeField] int maxHP = 100;
[SerializeField] Rigidbody rigid;
private int jumpCount = 0;
public enum PlayerPosition { Ground, Air}
public PlayerPosition playerPosition = PlayerPosition.Ground;
private void Start()
{
rigid = GetComponent<Rigidbody>();
}
private void OnCollisionEnter(Collision collision)
{
if(1<<collision.gameObject.layer == 1<<6)
{
playerPosition = PlayerPosition.Ground;
}
}
private void OnCollisionExit(Collision collision)
{
if (1 << collision.gameObject.layer == 1 << 6)
{
playerPosition = PlayerPosition.Air;
}
}
public event Action<int> OnHPChanged;
public int CurHP { get { return curHP; } set { curHP = value; OnHPChanged?.Invoke(curHP); } }
public event Action<int> OnMaxHPChanged;
public int MaxHP { get { return maxHP; } set { maxHP = value;OnMaxHPChanged?.Invoke(MaxHP); } }
public event Action OnJumpCountChanged;
public int JumpCount { get { return jumpCount; } set { jumpCount = value; OnJumpCountChanged?.Invoke(); } }
public Rigidbody Rigid { get { return rigid; } set { rigid = value; } }
}

public class PracticePlayerPresenter : MonoBehaviour
{
[Header("Set Model")]
[SerializeField] private PracticePlayerModel model;
[Header("Set View")]
[SerializeField] private TMP_Text hpTextView;
[SerializeField] private Slider hpBarView;
[SerializeField] private TMP_Text jumpCountView;
[SerializeField] private Button healBtn;
[SerializeField] private Button hitBtn;
[SerializeField] private Button jumpBtn;
[Header("Set Value")]
[SerializeField] private int healAmount = 1;
[SerializeField] private int hitDamage = 1;
[SerializeField] private float jumpPower = 10;
void OnEnable()
{
model.OnHPChanged += SetHP;
model.OnMaxHPChanged += SetMaxHP;
model.OnJumpCountChanged += Jump;
SetHP(model.CurHP);
SetMaxHP(model.MaxHP);
healBtn.onClick.AddListener(HealHP);
hitBtn.onClick.AddListener(LoseHP);
jumpBtn.onClick.AddListener(CountJump);
}
private void OnDisable()
{
model.OnHPChanged -= SetHP;
model.OnMaxHPChanged -= SetMaxHP;
model.OnJumpCountChanged -= Jump;
}
public void SetHP(int curHP)
{
hpTextView.text = $"HP : {curHP} / {model.MaxHP}";
hpBarView.value = curHP;
}
public void SetMaxHP(int maxHP)
{
hpTextView.text = $"HP : {model.CurHP} {maxHP}";
hpBarView.maxValue = maxHP;
}
public void HealHP()
{
model.CurHP += healAmount;
}
public void LoseHP()
{
model.CurHP -= hitDamage;
}
public void Jump()
{
model.Rigid.AddForce(Vector3.up * jumpPower, ForceMode.Impulse);
jumpCountView.text = $"Jump : {model.JumpCount}";
}
public void CountJump()
{
if (model.playerPosition == PracticePlayerModel.PlayerPosition.Ground)
{
model.JumpCount++;
}
}
}
