MVC 패턴 개념 정리
역할 | 책임 |
Model | • 데이터(상태) 보관 및 갱신 로직• 상태 변경 시 이벤트(옵저버) 발행 |
View | • 화면 렌더링·UI 표시 담당• Model 이벤트 구독 혹은 Controller 명령에 반응 |
Controller | • 사용자 입력·AI·타이머 등 이벤트 수신• Model 업데이트 및 View 제어 명령 |
- 관심사 분리 : 데이터 <-> 로직 <-> 표현을 분리해 각 클래스가 단일 책임(SRP)을 갖도록함
- 유닛테스트 가능 : Model과 Controller는 MonoBehaviour에 의존하지 않고 순수 클래스여서 테스트 작성 쉬움
- 유연한 확장 : View를 바꿔도 Model-Controller 변경 없이 재사용 가능
간단 예시 (버튼 누르면 점수 +1)
- ScoreModel 생성 -> ScoreView 바인딩 -> ScoreController가 Model, View 연결
- 버튼 클릭 시 Controller -> Model -> 이벤트 발생 -> View 갱신
Model
/// <summary>
/// 게임 점수(Score)를 관리하는 순수 데이터 모델
/// </summary>
public class ScoreModel {
public int Current { get; private set; } = 0;
public event Action<int> OnScoreChanged; // Score 변경 시 구독자에게 통보
/// <summary>
/// 점수 증가 처리
/// </summary>
public void AddPoint() {
Current += 1;
OnScoreChanged?.Invoke(Current);
}
}
Controller
/// <summary>
/// ScoreModel과 ScoreView를 연결하고, 버튼 클릭 처리를 담당하는 컨트롤러
/// </summary>
public class ScoreController {
private ScoreModel _model;
private ScoreView _view;
public ScoreController(ScoreModel model, ScoreView view) {
_model = model;
_view = view;
_model.OnScoreChanged += _view.UpdateScoreText;
_view.OnButtonClicked += _model.AddPoint;
}
}
View
/// <summary>
/// 씬 상의 버튼과 텍스트를 제어하는 뷰 컴포넌트
/// </summary>
public class ScoreView : MonoBehaviour {
[SerializeField] private TextMeshProUGUI _scoreText;
[SerializeField] private Button _addButton;
public event Action OnButtonClicked;
private void Awake() {
_addButton.onClick.AddListener(() => OnButtonClicked?.Invoke());
}
/// <summary>
/// 점수 변경 시 텍스트 갱신
/// </summary>
public void UpdateScoreText(int newScore) {
_scoreText.text = $"Score: {newScore}";
}
}
데이터 정의는 ScriptableObject랑 비슷한거 같은데 무슨 차이??
구분 | ScriptableObject(SO) | Model(MVC) |
생명 주기 | 에디터에서 에셋으로 생성·저장 → 런타임에 로드 | 런타임에 인스턴스화되고, 게임 오브젝트별로 고유한 상태 유지 |
용도 | 게임 설정·상수·레벨 디자인 등 공유 가능한 고정 데이터 보관 | 개별 객체의 동적 상태(체력, 레벨, 골드 등) 관리 및 변경 |
이벤트/로직 | 보통 데이터 정의만, 이벤트 콜백·비즈니스 로직 최소화 | 상태 변경 시 옵저버 패턴·비즈니스 로직 포함 가능 |
재사용성 | 프로젝트 전반에서 재사용 가능한 에셋 | 각 컨트롤러·뷰에 바인딩된 런타임 객체 |
편집과 직관성 | 인스펙터로 바로 편집 → 디자이너·기획자 관리 용이 | 코드로 제어 → 테스트·유닛테스트 작성에 최적화 |
그럼 언제 ScriptableObject를 쓰고 언제 Model을 쓰나?
ScriptableObject
- 변하지 않는 정적인 값 설정
- 에셋 단위로 관리
- 사운드, 이펙트 프리팹 참조, 테마 컬러 등 프로젝트 전역에서 재사용
Model
- 동적으로 변하는 값
- 개별 인스턴스의 실시간 상태
- 이벤트 발행 같은 옵저버 패턴으로 View, Controller에 알림
ScriptableObject → Model → Controller → View 구조의 스크립트 예시
- ScriptableObject로 기본 스탯만 관리 -> 디자이너가 인스펙터에서 쉽게 수정
- Model은 (현재 상태 + 이벤트) -> 동적 변화와 알림을 담당
- Controller가 Model <-> View 연결 및 중계 -> 관심사의 분리
- View는 오직 화면 처리 로직만 담당
MonsterDataSO.cs
// 1) 정적 데이터 에셋
[CreateAssetMenu(menuName="TD/MonsterDataSO")]
public class MonsterDataSO : ScriptableObject {
public string monsterName;
public float baseHealth;
public float baseSpeed;
public float baseDamage;
}
MonsterModel.cs
// 2) 동적 상태 모델
public class MonsterModel {
public float CurrentHealth { get; private set; }
public float CurrentSpeed { get; private set; }
public float CurrentDamage { get; private set; }
public event Action<float> OnHealthChanged;
public event Action OnDead;
public MonsterModel(MonsterDataSO data) {
CurrentHealth = data.baseHealth;
CurrentSpeed = data.baseSpeed;
CurrentDamage = data.baseDamage;
OnHealthChanged?.Invoke(CurrentHealth);
}
public void TakeDamage(float amount) {
if (CurrentHealth <= 0) return;
CurrentHealth = Mathf.Max(CurrentHealth - amount, 0);
OnHealthChanged?.Invoke(CurrentHealth);
if (CurrentHealth == 0) OnDead?.Invoke();
}
}
MonsterController.cs
// 3) 로직 중계 컨트롤러
public class MonsterController {
public MonsterController(MonsterModel model, MonsterView view) {
// 체력 변화 → View에 체력바 갱신 지시
model.OnHealthChanged += view.UpdateHealthBar;
// 사망 → View에 사망 애니메이션 재생 지시
model.OnDead += view.PlayDeathAnimation;
// (추가) AI나 플레이어 충돌 시
view.OnHitByPlayer += model.TakeDamage;
}
}
MonsterView.cs
// 4) 화면 처리 뷰
public class MonsterView : MonoBehaviour {
[SerializeField] private Slider healthBar;
public event Action<float> OnHitByPlayer;
public void UpdateHealthBar(float currentHp) {
healthBar.value = currentHp / healthBar.maxValue;
}
public void PlayDeathAnimation() {
// 애니메이터 트리거, 파티클 플레이 등
}
private void OnTriggerEnter(Collider other) {
if (other.CompareTag("PlayerAttack")) {
float damage = other.GetComponent<Attack>().Damage;
OnHitByPlayer?.Invoke(damage);
}
}
}
반응형
'개발 > Unity 3D' 카테고리의 다른 글
[Unity] ScriptableObject과 GC(Garbage Collection) (0) | 2025.06.24 |
---|---|
[Unity] 씬 전환 'SceneManagement vs Addressable' (0) | 2025.06.17 |
[Unity] Addressable 'Release vs ReleaseInstance' (1) | 2025.06.17 |
[Unity] Addressable 개요 (0) | 2025.06.17 |
[Unity] Unity 듀얼 모니터 확장 출력 (1) | 2025.06.13 |