From 9efb08fdf3f7bafdb1572d1643afcfd61923a3db Mon Sep 17 00:00:00 2001 From: nakjun Date: Mon, 29 Jun 2026 19:26:37 +0900 Subject: [PATCH] First commit --- Battle/AttackType.cs | 4 +++ Battle/BattleManager.cs | 12 ++++++++ Core/ComponentBase.cs | 18 ++++++++++++ Core/Health.cs | 35 +++++++++++++++++++++++ Core/IHealthView.cs | 14 ++++++++++ README.md | 61 +++++++++++++++++++++++++++++++++++++++++ UI/HPBar.cs | 33 ++++++++++++++++++++++ Units/Champion.cs | 44 +++++++++++++++++++++++++++++ Units/Minion.cs | 34 +++++++++++++++++++++++ Units/Tower.cs | 44 +++++++++++++++++++++++++++++ Units/Unit.cs | 39 ++++++++++++++++++++++++++ 11 files changed, 338 insertions(+) create mode 100644 Battle/AttackType.cs create mode 100644 Battle/BattleManager.cs create mode 100644 Core/ComponentBase.cs create mode 100644 Core/Health.cs create mode 100644 Core/IHealthView.cs create mode 100644 README.md create mode 100644 UI/HPBar.cs create mode 100644 Units/Champion.cs create mode 100644 Units/Minion.cs create mode 100644 Units/Tower.cs create mode 100644 Units/Unit.cs diff --git a/Battle/AttackType.cs b/Battle/AttackType.cs new file mode 100644 index 0000000..7b8365a --- /dev/null +++ b/Battle/AttackType.cs @@ -0,0 +1,4 @@ +namespace Battle +{ + public enum AttackType { Physical, Magic } +} diff --git a/Battle/BattleManager.cs b/Battle/BattleManager.cs new file mode 100644 index 0000000..88733ce --- /dev/null +++ b/Battle/BattleManager.cs @@ -0,0 +1,12 @@ +namespace Battle +{ + public static class BattleManager + { + public static int CurrentGameSeconds => 0; + + public static void SpawnMinion() + { + //풀을 활용한 미니언 스폰 로직 + } + } +} \ No newline at end of file diff --git a/Core/ComponentBase.cs b/Core/ComponentBase.cs new file mode 100644 index 0000000..5ade206 --- /dev/null +++ b/Core/ComponentBase.cs @@ -0,0 +1,18 @@ +namespace Core +{ + public class ComponentBase + { + public virtual void Awake() { } + public virtual void Start() { } + public virtual void Update() { } + public virtual void OnDestroy() { } + + //소멸 + public void Destroy() + { + OnDestroy(); + + //객체 삭제 로직 + } + } +} diff --git a/Core/Health.cs b/Core/Health.cs new file mode 100644 index 0000000..c82b15a --- /dev/null +++ b/Core/Health.cs @@ -0,0 +1,35 @@ +using System; + +namespace Core +{ + public class Health : IHealthView + { + public int MaxHP { get; private set; } + public int CurrentHP { get; private set; } + public bool IsDead => CurrentHP <= 0; // 필드 대신 계산 (상태 중복 제거) + + public event Action HPChanged; + public event Action Died; + + public Health(int maxHp) + { + MaxHP = maxHp; + CurrentHP = maxHp; + } + + //데미지 받기 + public void TakeDamage(int amount) + { + if (IsDead) return; + CurrentHP = Math.Max(0, CurrentHP - amount); + HPChanged?.Invoke(MaxHP, CurrentHP); + if (IsDead) Died?.Invoke(); + } + + public void Revive() + { + CurrentHP = MaxHP; // CurrentHP가 MaxHP가 되면 IsDead도 자동 false + HPChanged?.Invoke(MaxHP, CurrentHP); + } + } +} diff --git a/Core/IHealthView.cs b/Core/IHealthView.cs new file mode 100644 index 0000000..c3b0970 --- /dev/null +++ b/Core/IHealthView.cs @@ -0,0 +1,14 @@ +using System; + +namespace Core +{ + public interface IHealthView + { + int CurrentHP { get; } + int MaxHP { get; } + bool IsDead { get; } + + event Action HPChanged; // + event Action Died; + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..7e5d774 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# MTM 인게임 유닛 설계 — 사전 과제 + +챔피언 / 미니언 / 타워 유닛과, **유닛과 분리된 HP 표시(HPBar)** 구조를 설계했습니다. +요구사항대로 **구조 설계 중심**이며, 세부 수치/로직은 의도가 드러나는 스텁(`=> 0`, 주석 등)으로 남겼습니다. + +## 파일 구성 + +| 파일 | 역할 | +| --- | --- | +| `Core/ComponentBase.cs` | Unity `MonoBehaviour`의 **엔진 비종속 대역**. 생명주기(`Awake/Start/Update/OnDestroy`) + `Destroy()` | +| `Core/IHealthView.cs` | HP를 **'보여주는' 쪽이 보는 읽기 전용 계약**. `CurrentHP/MaxHP/IsDead` + `HPChanged`/`Died` 이벤트 | +| `Core/Health.cs` | HP를 **'작동'시키는 코어**. `IHealthView` 구현, `TakeDamage`/`Revive`, 사망·변경 통지 | +| `Battle/BattleManager.cs` | 게임 진행 시간(`CurrentGameSeconds`), 미니언 스폰 (제공 원본 확장) | +| `Battle/AttackType.cs` | 공격 타입 enum (물리/마법) | +| `Units/Unit.cs` | 모든 유닛의 추상 베이스. `Health` 소유, `IHealthView` 노출, 데미지/사망 공통 흐름 | +| `Units/Champion.cs` | 죽으면 **일정 시간 후 부활** | +| `Units/Minion.cs` | 죽으면 **소멸**, 생성 시 게임 시간 비례 공격력 | +| `Units/Tower.cs` | 죽으면 **영구 파괴**(잔해는 남음) | +| `UI/HPBar.cs` | 임의 대상의 HP를 **'보여주는'** UI. `IHealthView`만 구독 (제공 원본 확장) | + +## 과제 (1) — 유닛 3종 + +공통점(HP 소유·데미지·사망 흐름)은 `Unit`에 모으고, **유닛마다 다른 "죽으면 무엇을 하는가"는 `OnDied()` 추상 메서드로 분리**했습니다(Template Method). 초기화는 `Awake → Init()(자식이 스탯 세팅) → Health 생성` 순서를 부모가 보장합니다. + +| 유닛 | 요구사항 | 구현 | +| --- | --- | --- | +| **Champion** | 죽으면 일정 시간 후 부활 | `OnDied`에서 `_reviveAt` 예약 → `Update`에서 `CurrentGameSeconds` 도달 시 `Revive()`(= `Health.Revive()` + 부활 지점 스폰 훅). 부활 후 `IsDead=false`라 다음 죽음에 재예약되어 사이클 반복 | +| **Minion** | 죽으면 소멸 / 생성 시 시간 비례 공격력 | `OnDied → Destroy()`(오브젝트 제거). `Init`에서 `GetAtkByGameTime(BattleManager.CurrentGameSeconds)`로 공격력 세팅 | +| **Tower** | 죽으면 영구 파괴 | `OnDied`에서 `IsDestroyed = true` + 기능 정지/잔해 전환. **`Destroy()`를 부르지 않아** 오브젝트(흔적)는 남고, 부활 로직이 없어 영구 파괴 | + +## 과제 (2) — '보여주는' 객체와 '작동하는' 코어의 분리 + +### 의존 방향을 한쪽으로만 (유닛 → HPBar 의존: 0) + +``` + Champion/Minion/Tower ──소유──▶ Health ──구현──▶ IHealthView ◀──구독──── HPBar + (Units) (Core) (Core) (UI) + + 의존 방향: UI → Core, Units → Core, Units → Battle +``` + +- **작동(코어) = `Health`** : 데미지/회복/사망을 실제로 처리하는 단일 진실 공급원. UI를 전혀 모르며, 상태 변화는 `HPChanged`/`Died` **이벤트로만** 알립니다(Observer). +- **표시 = `HPBar`** : `IHealthView`(읽기 전용)만 구독합니다. `IHealthView`엔 `TakeDamage`가 없어 **UI가 모델을 변경할 수 없습니다**(읽기/표시 책임만). +- **연결** : 유닛이 `HealthView` 프로퍼티로 `IHealthView`를 노출하고, 제3자(스포너/매니저 등)가 `hpBar.Bind(unit.HealthView)`로 연결합니다. 유닛은 HPBar를 전혀 참조하지 않습니다. + +### 이점 +- **양방향 무의존** — 유닛은 UI를, UI는 구체 유닛을 모릅니다. +- **임의 대상 반영** — `IHealthView`만 만족하면 무엇이든 표시 가능(유닛 외 오브젝트도 동일). +- **항상 최신 상태** — 폴링이 아닌 이벤트 푸시. `Bind` 시 1회 즉시 반영, 이후 변화 시점에만 갱신. `OnDestroy`에서 구독 해제로 누수 방지. + +## 설계 결정 / 트레이드오프 + +- **`ComponentBase`** : `UnityEngine` 의존 없이 어디서나 컴파일/리뷰되도록 둔 `MonoBehaviour` 대역입니다. 생명주기 이름을 동일하게 미러링해, 실제 Unity로 옮길 때는 `ComponentBase`를 `MonoBehaviour`로 교체하면 그대로 이어집니다. +- **`AttackType` 파라미터** : 현재 구현은 `Atk`를 그대로 반환하지만, **물리/마법 등 데미지 타입별 분기로 확장**할 것을 대비해 `GetAttackDamage(AttackType)` 시그니처에 포함했습니다. +- **조립(composition) 계층 생략** : 누가 `Awake/Update`를 구동하고 `HPBar.Bind`를 호출하는지(스포너/매니저)는 본 **설계 테스트의 범위를 벗어나 생략**했습니다. 위 구조상 외부에서 한 줄로 조립 가능합니다. +- **스탯은 외부 주입 전제** : `MaxHP`/`Atk` 등 실제 수치는 데이터 테이블 등 **외부에서 주입**하는 것을 전제로 하며, 본 코드에서는 스텁(0)으로 두었습니다. + +## 적용 패턴 +- **Template Method** — `Init` / `OnDied` / `GetAttackDamage`로 유닛별 차이만 분리 +- **Observer** — `HPChanged` / `Died` 이벤트로 코어 → 표시 단방향 통지 +- **Interface Segregation** — `IHealthView`로 UI에는 읽기/구독 책임만 노출 diff --git a/UI/HPBar.cs b/UI/HPBar.cs new file mode 100644 index 0000000..2b3ece8 --- /dev/null +++ b/UI/HPBar.cs @@ -0,0 +1,33 @@ +using Core; + +namespace UI +{ + public class HPBar : ComponentBase + { + private IHealthView _target; + + public void Bind(IHealthView target) + { + Unbind(); + + _target = target; + if (_target == null) return; + + _target.HPChanged += RefreshHP; + RefreshHP(target.MaxHP, target.CurrentHP); + } + + public void Unbind() + { + if (_target != null) _target.HPChanged -= RefreshHP; + _target = null; + } + + public void RefreshHP(int maxHP,int currentHP) + { + //UI 갱신 + } + + public override void OnDestroy() => Unbind(); + } +} diff --git a/Units/Champion.cs b/Units/Champion.cs new file mode 100644 index 0000000..f741dfa --- /dev/null +++ b/Units/Champion.cs @@ -0,0 +1,44 @@ +using Battle; // BattleManager.CurrentGameSeconds 참조 + +namespace Units +{ + public class Champion : Unit + { + private int _championAtk; + private int _championMaxHP; + + private int _reviveAt; + private int GetReviveDelay() => 5; + + + protected override void Init() + { + MaxHP = _championMaxHP; + Atk = _championAtk; + } + + public override void Update() + { + if (_health.IsDead && BattleManager.CurrentGameSeconds >= _reviveAt) + Revive(); // 시간 도달 시 부활 + } + + protected override int GetAttackDamage(AttackType attackType) + { + //데미지 계산 + return Atk; + } + + protected override void OnDied() + { + _reviveAt = BattleManager.CurrentGameSeconds + GetReviveDelay(); // 예약 + } + + private void Revive() + { + _health.Revive(); + + //부활 지점에서 스폰 로직 + } + } +} diff --git a/Units/Minion.cs b/Units/Minion.cs new file mode 100644 index 0000000..403d5fb --- /dev/null +++ b/Units/Minion.cs @@ -0,0 +1,34 @@ +using Battle; + +namespace Units +{ + public class Minion : Unit + { + private int _minionAtk; + private int _minionMaxHP; + + protected override void Init() + { + MaxHP = _minionMaxHP; + Atk = GetAtkByGameTime(); + } + + protected override int GetAttackDamage(AttackType attackType) + { + //데미지 계산 + + return Atk; + } + + protected override void OnDied() + { + //소멸 + Destroy(); + } + + private int GetAtkByGameTime() + { + return _minionAtk * BattleManager.CurrentGameSeconds; + } + } +} diff --git a/Units/Tower.cs b/Units/Tower.cs new file mode 100644 index 0000000..a67624d --- /dev/null +++ b/Units/Tower.cs @@ -0,0 +1,44 @@ +using Battle; + +namespace Units +{ + public class Tower : Unit + { + private int _towerAtk; + private int _towerMaxHP; + + public bool IsDestroyed { get; private set; } // 영구 파괴 여부 + + protected override void Init() + { + MaxHP = _towerMaxHP; + Atk = _towerAtk; + } + + protected override int GetAttackDamage(AttackType attackType) + { + //데미지 계산 + + return Atk; + } + + protected override void OnDied() + { + //영구 파괴 + IsDestroyed = true; + + StopFunction(); // 공격/타겟팅 등 기능 정지 + SwitchToWreckage(); // '파괴된 구조물' 잔해 비주얼로 전환 (파괴되었지만 구조물의 흔적은 남아있음) + } + + private void StopFunction() + { + // 공격·상호작용 비활성화 + } + + private void SwitchToWreckage() + { + // 부서진 잔해로 모델 교체 + } + } +} diff --git a/Units/Unit.cs b/Units/Unit.cs new file mode 100644 index 0000000..0ec3ff4 --- /dev/null +++ b/Units/Unit.cs @@ -0,0 +1,39 @@ +using Core; +using Battle; + +namespace Units +{ + public abstract class Unit : ComponentBase + { + //스탯 + protected int MaxHP; + protected int Atk; + + protected Health _health; + public IHealthView HealthView => _health; + + public override void Awake() + { + Init(); + _health = new Health(MaxHP); + _health.Died += OnDied; //죽었을때 이벤트 + } + + protected abstract void Init(); + + public void TakeDamage(int amount) + { + _health.TakeDamage(amount); + } + + public void AttackDamage(Unit target, AttackType attackType) + { + target.TakeDamage(GetAttackDamage(attackType)); + } + + //추상 메서드 + //상속받은 쪽에서 반드시 구현 + protected abstract int GetAttackDamage(AttackType attackType); //유닛별 데미지 계산식 적용 + protected abstract void OnDied(); //죽었을때 이벤트 + } +}