DEG-95 원거리 마법사 보스 패턴 설계 시작

- 근접 몬스터 전투 패턴 리팩토링
- 전략 패턴과 템플릿 메서드 패턴을 조합
This commit is contained in:
fiore 2025-04-25 15:17:27 +09:00
parent 5e61ff03c9
commit 9b1709b92c
41 changed files with 207 additions and 74 deletions

View File

@ -27,9 +27,6 @@ public class EnemyControllerEditor : Editor
case EnemyState.Trace:
GUI.backgroundColor = new Color(1, 0, 1, 1f);
break;
case EnemyState.Attack:
GUI.backgroundColor = new Color(1, 1, 0, 1f);
break;
case EnemyState.Dead:
GUI.backgroundColor = new Color(1, 0, 0, 1f);
break;
@ -49,10 +46,6 @@ public class EnemyControllerEditor : Editor
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Idle")) enemyController.SetState(EnemyState.Idle);
if (GUILayout.Button("Trace")) enemyController.SetState(EnemyState.Trace);
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Attack")) enemyController.SetState(EnemyState.Attack);
if (GUILayout.Button("Dead")) enemyController.SetState(EnemyState.Dead);
EditorGUILayout.EndHorizontal();
}

BIN
Assets/JYY/Animator/Dummy Monster.controller (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: cb4d2ce283530414f92984caccad61a0
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 9100000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,6 +1,7 @@
fileFormatVersion: 2
guid: 3f431e991bd65014c833e89305ddd5e3
PrefabImporter:
guid: 81ad24bdbd6cad44990b4f077f0687e7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:

BIN
Assets/JYY/Materials/Dummy/Dummy Monster.mat (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9a8690b5dbb7c5643896a7d881a8fd6f
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/JYY/Materials/Dummy/Dummy Player.mat (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fc5cecc864a5b0c49b266cec5aaec666
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2dbfaa22c1aa28242a4e80ed518bfae2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

BIN
Assets/JYY/Prefabs/[Enemy] PldDog.prefab (Stored with Git LFS)

Binary file not shown.

BIN
Assets/JYY/Scenes/MonsterTest.unity (Stored with Git LFS)

Binary file not shown.

View File

@ -0,0 +1,8 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CasterDemonController : EnemyController
{
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 79bc1ad21773bcf4e982d5fc7a93887b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,8 +1,10 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public enum EnemyState { None, Idle, Trace, Attack, Dead }
public enum EnemyState { None, Idle, Trace, Dead, Flee}
public enum MonsterType { Melee, Caster, Ranged }
[RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(Animator))]
@ -11,10 +13,12 @@ public abstract class EnemyController : CharacterBase
[Header("AI")]
[SerializeField] private float detectCircleRadius = 10f; // 플레이어 탐지 범위
[SerializeField] private LayerMask targetLayerMask; // 플레이어 레이어 마스크
[SerializeField] private MonsterType monsterType;
public MonsterType MonsterType => monsterType;
public Transform TraceTargetTransform { get; private set; }
public NavMeshAgent Agent { get; private set; }
public Animator EnemyAnimator { get; private set; }
private Animator EnemyAnimator { get; set; }
public EnemyState CurrentState {get; private set;}
public LayerMask TargetLayerMask => targetLayerMask;
public float MoveSpeed => moveSpeed;
@ -31,10 +35,14 @@ public abstract class EnemyController : CharacterBase
// -----
// 상태 변수
// Commons
private EnemyStateIdle _enemyStateIdle;
private EnemyStateTrace _enemyStateTrace;
private EnemyStateAttack _enemyStateAttack;
private EnemyStateDead _enemyStateDead;
// Melee
private EnemyStateTrace _enemyStateTrace;
// Caster
private EnemyStateFlee _enemyStateFlee;
private Dictionary<EnemyState, IEnemyState> _enemyStates;
@ -49,18 +57,36 @@ public abstract class EnemyController : CharacterBase
base.Start();
// 상태 객체 생성
// Commons
_enemyStateIdle = new EnemyStateIdle();
_enemyStateTrace = new EnemyStateTrace();
_enemyStateAttack = new EnemyStateAttack();
_enemyStateDead = new EnemyStateDead();
// Melee
_enemyStateTrace = new EnemyStateTrace();
// Caster
_enemyStateFlee = new EnemyStateFlee();
switch (MonsterType)
{
case MonsterType.Melee:
_enemyStates = new Dictionary<EnemyState, IEnemyState>
{
{ EnemyState.Idle, _enemyStateIdle },
{ EnemyState.Trace, _enemyStateTrace },
{ EnemyState.Attack, _enemyStateAttack },
{ EnemyState.Dead, _enemyStateDead },
};
break;
case MonsterType.Caster:
_enemyStates = new Dictionary<EnemyState, IEnemyState>
{
{ EnemyState.Idle, _enemyStateIdle },
{ EnemyState.Flee, _enemyStateFlee },
{ EnemyState.Dead, _enemyStateDead },
};
break;
case MonsterType.Ranged:
break;
}
SetState(EnemyState.Idle);
}
@ -84,6 +110,13 @@ public abstract class EnemyController : CharacterBase
}
// 전략 패턴과 템플릿 메서드 패턴을 활용
public virtual void BattleSequence()
{
// 이 메서드는 자식 요소에서 오버라이드하여 구현합니다.
Debug.LogWarning("BattleSequence가 구현되지 않았습니다.");
}
public override void Die()
{
base.Die();
@ -106,6 +139,12 @@ public abstract class EnemyController : CharacterBase
return null;
}
private void OnDrawGizmos()
{
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, detectCircleRadius);
}
#endregion
#region

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 06fb9eac62a34336b6023cc088be367d
timeCreated: 1745554862

View File

@ -0,0 +1,52 @@
using UnityEngine;
public class EnemyStateFlee :IEnemyState
{
private EnemyController _enemyController;
private Transform _detectPlayerTransform;
private const float MaxDetectPlayerInCircleWaitTime = 0.2f;
private float _detectPlayerInCircleWaitTime = 0f;
private float _fleeDistance = 5f; // 도망치는 거리
private float _attackRange = 7f; // 공격 범위
public void Enter(EnemyController enemyController)
{
_enemyController = enemyController;
Debug.Log("## Flee 상태 진입");
_detectPlayerTransform = _enemyController.TraceTargetTransform;
if (!_detectPlayerTransform)
{
_enemyController.SetState(EnemyState.Idle);
return;
}
}
public void Update()
{
// 도망치는 방향 계산
Vector3 fleeDirection = (_enemyController.transform.position - _detectPlayerTransform.position).normalized;
Vector3 fleeTarget = _enemyController.transform.position + fleeDirection * _fleeDistance;
_enemyController.Agent.SetDestination(fleeTarget);
float distance = Vector3.Distance(_enemyController.transform.position, _detectPlayerTransform.position);
// 일정 범위 안으로 플레이어가 접근하면 공격 시퀸스
if (distance <= _attackRange)
{
return;
}
}
public void Exit()
{
_detectPlayerTransform = null;
_enemyController = null;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 52fff5e2310f42608e3e3c5b45dcae38
timeCreated: 1745554266

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 452917766030468e87209330ca1b410e
timeCreated: 1745554696

View File

@ -1,8 +1,8 @@
using UnityEngine;
using System;
using UnityEngine;
public class EnemyStateIdle: IEnemyState
{
private EnemyController _enemyController;
public void Enter(EnemyController enemyController)
@ -17,7 +17,17 @@ public class EnemyStateIdle: IEnemyState
var detectPlayerTransform = _enemyController.DetectPlayerInCircle();
if (detectPlayerTransform)
{
switch (_enemyController.MonsterType)
{
case MonsterType.Melee:
_enemyController.SetState(EnemyState.Trace);
break;
case MonsterType.Caster:
_enemyController.SetState(EnemyState.Flee);
break;
case MonsterType.Ranged:
break;
}
}
}

View File

@ -1,24 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyStateAttack : IEnemyState
{
private EnemyController _enemyController;
public void Enter(EnemyController enemyController)
{
_enemyController = enemyController;
}
public void Update()
{
}
public void Exit()
{
_enemyController = null;
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 29b87c92807e4f6b94c8a08ccc510321
timeCreated: 1744799701

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 64b8e3ece30b427eb71a9b6e33292292
timeCreated: 1745554748

View File

@ -35,11 +35,8 @@ public class EnemyStateTrace : IEnemyState
PlayerTracking();
if (_enemyController.Agent.remainingDistance <= _enemyController.Agent.stoppingDistance)
{
// TODO: 타겟에 도착함 -> 공격 준비
// _enemyController.SetState(EnemyState.Attack);
}
// 전투 패턴은 몬스터 객체에게 위임
_enemyController.BattleSequence();
}
public void Exit()

View File

@ -8,6 +8,8 @@ using Random = UnityEngine.Random;
public class PldDogController : EnemyController
{
// ----
// 팔라딘 독 고유 액션
private static readonly int WindUp = Animator.StringToHash("WindUp");
private static readonly int Slash = Animator.StringToHash("Slash");
private static readonly int BoomShot = Animator.StringToHash("BoomShot");
@ -34,7 +36,7 @@ public class PldDogController : EnemyController
[SerializeField] private GameObject horizontalWarning;
[SerializeField] private GameObject horizontalSlash;
private float _patternTimer = 0f;
[SerializeField] private float _patternTimer = 0f;
private int _currentPatternIndex = 0;
private bool _isPatternRunning = false;
private bool _isFirstAttack = true;
@ -53,23 +55,20 @@ public class PldDogController : EnemyController
};
}
protected override void Update()
public override void BattleSequence()
{
base.Update();
if (CurrentState != EnemyState.Trace || _isPatternRunning)
return;
_patternTimer += Time.deltaTime;
if (_isPatternRunning) return;
float distanceToPlayer = Vector3.Distance(transform.position, TraceTargetTransform.position);
if (distanceToPlayer <= meleeRange) // 근접 범위
{
if (!Agent.isStopped) Agent.isStopped = true;
_patternTimer += Time.deltaTime;
if (!_isPatternRunning && (_isFirstAttack || _patternTimer >= patternInterval))
{
ExecutePattern(_currentPatternIndex);
ExecutePattern();
_isFirstAttack = false;
}
@ -78,16 +77,16 @@ public class PldDogController : EnemyController
{
if (Agent.isStopped) Agent.isStopped = false;
Agent.SetDestination(TraceTargetTransform.position);
_patternTimer += Time.deltaTime;
if (!_isPatternRunning && _patternTimer >= patternInterval)
{
Debug.Log("## 폭탄 던질 조건 만족");
BombThrowPattern();
}
}
}
private void ExecutePattern(int patternIndex)
private void ExecutePattern()
{
_isPatternRunning = true;
Agent.isStopped = true;