Merge pull request #19 from Degulleo/DEG-41-Battle-Pattern2

DEG-41 전투 패턴 구현 리워크
This commit is contained in:
Fiore 2025-04-24 14:02:16 +09:00 committed by GitHub
commit 71a566262c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 728 additions and 247 deletions

View File

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

Binary file not shown.

8
Assets/JYY/Models.meta Normal file
View File

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

BIN
Assets/JYY/Models/Y Bot.fbx (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,109 @@
fileFormatVersion: 2
guid: decb96f75d6a2344baa18f902f21a131
ModelImporter:
serializedVersion: 22200
internalIDToNameTable: []
externalObjects: {}
materials:
materialImportMode: 2
materialName: 0
materialSearch: 1
materialLocation: 1
animations:
legacyGenerateAnimations: 4
bakeSimulation: 0
resampleCurves: 1
optimizeGameObjects: 0
removeConstantScaleCurves: 0
motionNodeName:
rigImportErrors:
rigImportWarnings:
animationImportErrors:
animationImportWarnings:
animationRetargetingWarnings:
animationDoRetargetingWarnings: 0
importAnimatedCustomProperties: 0
importConstraints: 0
animationCompression: 3
animationRotationError: 0.5
animationPositionError: 0.5
animationScaleError: 0.5
animationWrapMode: 0
extraExposedTransformPaths: []
extraUserProperties: []
clipAnimations: []
isReadable: 0
meshes:
lODScreenPercentages: []
globalScale: 1
meshCompression: 0
addColliders: 0
useSRGBMaterialColor: 1
sortHierarchyByName: 1
importPhysicalCameras: 1
importVisibility: 1
importBlendShapes: 1
importCameras: 1
importLights: 1
nodeNameCollisionStrategy: 1
fileIdsGeneration: 2
swapUVChannels: 0
generateSecondaryUV: 0
useFileUnits: 1
keepQuads: 0
weldVertices: 1
bakeAxisConversion: 0
preserveHierarchy: 0
skinWeightsMode: 0
maxBonesPerVertex: 4
minBoneWeight: 0.001
optimizeBones: 1
meshOptimizationFlags: -1
indexFormat: 0
secondaryUVAngleDistortion: 8
secondaryUVAreaDistortion: 15.000001
secondaryUVHardAngle: 88
secondaryUVMarginMethod: 1
secondaryUVMinLightmapResolution: 40
secondaryUVMinObjectScale: 1
secondaryUVPackMargin: 4
useFileScale: 1
strictVertexDataChecks: 0
tangentSpace:
normalSmoothAngle: 60
normalImportMode: 0
tangentImportMode: 3
normalCalculationMode: 4
legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
blendShapeNormalImportMode: 1
normalSmoothingSource: 0
referencedClips: []
importAnimation: 1
humanDescription:
serializedVersion: 3
human: []
skeleton: []
armTwist: 0.5
foreArmTwist: 0.5
upperLegTwist: 0.5
legTwist: 0.5
armStretch: 0.05
legStretch: 0.05
feetSpacing: 0
globalScale: 1
rootMotionBoneName:
hasTranslationDoF: 0
hasExtraRoot: 1
skeletonHasParents: 1
lastHumanDescriptionAvatarSource: {instanceID: 0}
autoGenerateAvatarMappingIfUnspecified: 1
animationType: 3
humanoidOversampling: 1
avatarSetup: 1
addHumanoidExtraRootOnlyWhenUsingAvatar: 1
importBlendShapeDeformPercent: 1
remapMaterialsIfMaterialImportModeIsNone: 0
additionalBone: 0
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/JYY/Prefabs/AOEIndicator.prefab (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 44d67bf59c049fb46876a549120a16d7
guid: 0e60f1766f73dbc439ff69a154cc9a99
PrefabImporter:
externalObjects: {}
userData:

Binary file not shown.

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 9adff7c5e2e43974fa9f2d241ef2e433
guid: dc0537feab3e2944aa554e23fb3923a3
PrefabImporter:
externalObjects: {}
userData:

BIN
Assets/JYY/Prefabs/AoE Indicator Chariot.prefab (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Assets/JYY/Prefabs/AoE Indicator Horizontal.prefab (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: e9e020ef2784edf4ca2a83ae9e1edefd
guid: 44d67bf59c049fb46876a549120a16d7
PrefabImporter:
externalObjects: {}
userData:

BIN
Assets/JYY/Prefabs/AoE Indicator Vertical.prefab (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9adff7c5e2e43974fa9f2d241ef2e433
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/JYY/Prefabs/AoE Slash Blue Chariot.prefab (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3f431e991bd65014c833e89305ddd5e3
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/JYY/Prefabs/Charge slash red.prefab (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 8a0ddc8dd6d760e4e93999c4d49dc16c
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/JYY/Prefabs/Explosion.prefab (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 2ccd5acc2f6d74d4bb687fd2ee94b9de
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/JYY/Prefabs/Red energy explosion 1.prefab (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 49ada3fea2be23f4a988d4bc21dcaa32
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/JYY/Prefabs/Snow slash 1.prefab (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 03a874e2d684ee04a9729b8363fdbb9c
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

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,3 @@
fileFormatVersion: 2
guid: d3b800a343fe42afa7494329ef235461
timeCreated: 1745300808

View File

@ -0,0 +1,109 @@
using System;
using System.Collections;
using UnityEngine;
[Serializable]
public struct DamageEffectData
{
public int damage;
public float radius;
public float delay;
public LayerMask targetLayer;
public GameObject explosionEffectPrefab;
}
/// <summary>
/// AOE 범위 공격의 공통 로직을 처리하는 추상 베이스 클래스입니다.
/// </summary>
public abstract class AoeControllerBase : MonoBehaviour
{
[Header("경고 이펙트")]
[SerializeField] protected GameObject warningEffectInstance;
protected DamageEffectData _data;
private Action _slashAction;
private Action _destroyAction;
/// <summary>
/// 범위 공격 이펙트를 설정하고, 딜레이 후 폭발을 실행합니다.
/// </summary>
public void SetEffect(DamageEffectData data, Action slashAction, Action destroyAction)
{
_data = data;
_slashAction = slashAction;
_destroyAction = destroyAction;
ShowWarningEffect();
StartCoroutine(ExplodeAfterDelay());
}
protected virtual void ShowWarningEffect()
{
if (warningEffectInstance != null)
warningEffectInstance.SetActive(true);
float diameter = _data.radius * 2f;
transform.localScale = new Vector3(diameter, 1f, diameter);
}
private IEnumerator ExplodeAfterDelay()
{
yield return new WaitForSeconds(_data.delay);
Explode();
}
/// <summary>
/// 폭발 이펙트 생성, 데미지 처리, 콜백 호출 순서로 실행합니다.
/// </summary>
protected virtual void Explode()
{
// 경고 이펙트 숨기기
if (warningEffectInstance != null)
warningEffectInstance.SetActive(false);
ShowDamageEffect();
// 슬래시 액션(애니메이션 트리거) 호출
_slashAction?.Invoke();
// 범위 내 데미지 처리
HitCheck();
// 패턴 클리어 콜백
_destroyAction?.Invoke();
// 자기 자신 제거
Destroy(gameObject, 2f);
}
protected virtual void HitCheck()
{
var hits = Physics.OverlapSphere(transform.position, _data.radius, _data.targetLayer);
foreach (var hit in hits)
{
if (hit.CompareTag("Player"))
{
Debug.Log($"{hit.name}에게 {_data.damage} 데미지 적용");
// TODO: 실제 데미지 처리 로직 호출
}
}
}
protected virtual void ShowDamageEffect()
{
// 폭발 이펙트 생성
if (_data.explosionEffectPrefab != null)
{
var effect = Instantiate(_data.explosionEffectPrefab, transform.position, transform.rotation);
effect.transform.localScale = new Vector3(_data.radius, _data.radius, _data.radius);
Destroy(effect, 2f);
}
}
protected virtual void OnDrawGizmosSelected()
{
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, _data.radius);
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1dd40dca214a4038be21f64ce645e5d9
timeCreated: 1745393630

View File

@ -0,0 +1,4 @@
public class BoomAoeController : AoeControllerBase
{
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e32c2002701d49df83faa36b3da55436
timeCreated: 1745395178

View File

@ -0,0 +1,19 @@
using System;
using System.Collections;
using UnityEngine;
public class ChariotAoeController : AoeControllerBase
{
protected override void ShowDamageEffect()
{
// 폭발 이펙트 생성
if (_data.explosionEffectPrefab != null)
{
var effect = Instantiate(_data.explosionEffectPrefab, transform.position, transform.rotation);
effect.transform.localScale = new Vector3(2f, 2f, 2f);
Destroy(effect, 2f);
}
}
}

View File

@ -0,0 +1,52 @@
using UnityEngine;
public class HorizontalAoeController : AoeControllerBase
{
private float slashAngle = 270f;
private int gizmoSegments = 20;
protected override void HitCheck()
{
var hits = Physics.OverlapSphere(transform.position, _data.radius, _data.targetLayer);
foreach (var hit in hits)
{
if (!hit.CompareTag("Player")) continue;
Vector3 dir = hit.transform.position - transform.position;
dir.y = 0;
float angleToForward = Vector3.Angle(transform.forward, dir);
if (angleToForward <= slashAngle * 0.5f)
{
Debug.Log($"{hit.name}이(가) 횡적 슬래시 데미지 범위에 있습니다.");
Debug.Log($"{hit.name}에게 {_data.damage} 데미지 적용");
// TODO: 실제 데미지 처리 로직 호출
}
}
}
private void OnDrawGizmosSelected()
{
// 부채꼴 형태 기즈모 드로잉
Gizmos.color = Color.red;
Vector3 origin = transform.position;
float halfAngle = slashAngle * 0.5f;
float step = slashAngle / gizmoSegments;
// 시작 포인트
Vector3 prevDir = Quaternion.AngleAxis(-halfAngle, Vector3.up) * transform.forward;
Vector3 prevPoint = origin + prevDir.normalized * _data.radius;
Gizmos.DrawLine(origin, prevPoint);
for (int i = 1; i <= gizmoSegments; i++)
{
float currentAngle = -halfAngle + step * i;
Vector3 currDir = Quaternion.AngleAxis(currentAngle, Vector3.up) * transform.forward;
Vector3 currPoint = origin + currDir.normalized * _data.radius;
Gizmos.DrawLine(prevPoint, currPoint);
prevPoint = currPoint;
}
// 마지막 라인
Gizmos.DrawLine(origin, prevPoint);
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e77da766410d47f9a7effebd30fd33f6
timeCreated: 1745394588

View File

@ -0,0 +1,51 @@
using UnityEngine;
public class VerticalAoeController : AoeControllerBase
{
protected override void ShowWarningEffect()
{
if (warningEffectInstance != null)
warningEffectInstance.SetActive(true);
var centerCap = Vector3.forward * _data.radius;
float diameter = _data.radius * 2f;
transform.localScale = new Vector3(_data.radius, 1f, diameter);
transform.Translate(centerCap, Space.Self);
}
protected override void ShowDamageEffect()
{
// 폭발 이펙트 생성
if (_data.explosionEffectPrefab != null)
{
var effect = Instantiate(_data.explosionEffectPrefab, transform.position, transform.rotation);
effect.transform.localScale = new Vector3(_data.radius, _data.radius, _data.radius);
Destroy(effect, 2f);
}
}
protected override void HitCheck()
{
// 박스 판정 (사각형 직선)
Vector3 halfExtents = new Vector3(_data.radius, 1f, _data.radius * 2f);
Collider[] hits = Physics.OverlapBox(transform.position, halfExtents, transform.rotation, _data.targetLayer);
foreach (var hit in hits)
{
if (!hit.CompareTag("Player")) continue;
Debug.Log($"{hit.name} 사각형 범위에 있어 데미지 적용");
// TODO: 데미지 로직
}
}
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.red;
Vector3 center = transform.position;
Vector3 size = new Vector3(_data.radius, 1f, _data.radius * 2f);
Gizmos.matrix = Matrix4x4.TRS(center, transform.rotation, Vector3.one);
Gizmos.DrawWireCube(Vector3.zero, size);
Gizmos.matrix = Matrix4x4.identity;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8eb40ee5f0d943469026d2b6522dbe46
timeCreated: 1745393484

View File

@ -1,14 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyAnimatorStateAttack : StateMachineBehaviour
{
// OnStateExit is called when a transition ends and the state machine finishes evaluating this state
// override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
// {
// animator.gameObject.GetComponent<EnemyController>().SetState(EnemyState.Trace);
// }
}

View File

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

View File

@ -1,42 +0,0 @@
using System.Collections.Generic;
using UnityEngine;
public class EnemyAttackController : MonoBehaviour
{
[Header("각종 전조 오브젝트")]
[SerializeField] private GameObject verticalWarningArea;
[SerializeField] private GameObject horizontalWarningArea;
[SerializeField] private GameObject chariotWarningArea;
[SerializeField] private GameObject dynamoWarningArea;
// 배열에 담아서 관리
private List<GameObject> warningAreas;
private GameObject _activeArea;
private void Awake()
{
warningAreas = new List<GameObject>()
{
verticalWarningArea,
horizontalWarningArea,
chariotWarningArea,
dynamoWarningArea
};
}
// 랜덤 전조 호출
public void TriggerRandomWarning(Vector3 spawnPosition, Quaternion spawnRotation)
{
// 0 ~ Count-1 사이 랜덤 인덱스
int idx = Random.Range(0, warningAreas.Count);
GameObject selected = warningAreas[idx];
// 예시: Instantiate 방식으로 화면에 띄우기
_activeArea = Instantiate(selected, spawnPosition, spawnRotation);
}
public void DestroyWarningArea()
{
Destroy(_activeArea);
}
}

View File

@ -2,49 +2,46 @@
using UnityEngine;
using UnityEngine.AI;
public enum EnemyState { None, Idle, Trace, Attack, GetHit, Move, Dead }
public enum EnemyState { None, Idle, Trace, Attack, Dead }
[RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(Animator))]
[RequireComponent(typeof(EnemyAttackController))]
public abstract class EnemyController : CharacterBase
{
[Header("AI")]
[SerializeField] private float detectCircleRadius = 10f; // 플레이어 탐지 범위
[SerializeField] private LayerMask targetLayerMask; // 플레이어 레이어 마스크
public Transform TraceTargetTransform { get; private set; }
public NavMeshAgent Agent { get; private set; }
public Animator EnemyAnimator { get; private set; }
public EnemyState CurrentState {get; private set;}
public LayerMask TargetLayerMask => targetLayerMask;
public float MoveSpeed => moveSpeed;
public bool IsMeleeCombat { get; protected set; }
public EnemyAttackController EnemyAttackController { get; private set; }
public float WalkSpeed => walkSpeed;
public float RunSpeed => runSpeed;
public Transform TraceTargetTransform { get; private set; }
[SerializeField] private float walkSpeed = 5;
[SerializeField] private float runSpeed = 8;
// -----
// 애니메이션 관련
private int _currentAnimationTrigger = -1;
// 애니메이션 파라미터 해시값
public static readonly int Idle = Animator.StringToHash("Idle");
public static readonly int Dead = Animator.StringToHash("Dead");
public static readonly int Trace = Animator.StringToHash("Trace");
// -----
// 상태 변수
private EnemyStateIdle _enemyStateIdle;
private EnemyStateTrace _enemyStateTrace;
private EnemyStateAttack _enemyStateAttack;
private EnemyStateGetHit _enemyStateGetHit;
private EnemyStateDead _enemyStateDead;
private EnemyStateMove _enemyStateMove;
private Dictionary<EnemyState, IEnemyState> _enemyStates;
private void Awake()
protected virtual void Awake()
{
EnemyAnimator = GetComponent<Animator>();
Agent = GetComponent<NavMeshAgent>();
EnemyAttackController = GetComponent<EnemyAttackController>();
}
protected override void Start()
@ -55,24 +52,20 @@ public abstract class EnemyController : CharacterBase
_enemyStateIdle = new EnemyStateIdle();
_enemyStateTrace = new EnemyStateTrace();
_enemyStateAttack = new EnemyStateAttack();
_enemyStateGetHit = new EnemyStateGetHit();
_enemyStateDead = new EnemyStateDead();
_enemyStateMove = new EnemyStateMove();
_enemyStates = new Dictionary<EnemyState, IEnemyState>
{
{ EnemyState.Idle, _enemyStateIdle },
{ EnemyState.Trace, _enemyStateTrace },
{ EnemyState.Attack, _enemyStateAttack },
{ EnemyState.GetHit, _enemyStateGetHit },
{ EnemyState.Dead, _enemyStateDead },
{ EnemyState.Move, _enemyStateMove}
};
SetState(EnemyState.Idle);
}
private void Update()
protected virtual void Update()
{
if (CurrentState != EnemyState.None)
{
@ -90,6 +83,14 @@ public abstract class EnemyController : CharacterBase
_enemyStates[CurrentState].Enter(this);
}
public override void Die()
{
base.Die();
// TODO : 사망 후 동작
}
#region
// 일정 반경에 플레이어가 진입하면 플레이어 소리를 감지했다고 판단
@ -107,5 +108,38 @@ public abstract class EnemyController : CharacterBase
#endregion
#region
// Trigger
public void SetAnimation(int hashName)
{
if (_currentAnimationTrigger != -1)
{
EnemyAnimator.ResetTrigger(_currentAnimationTrigger);
}
EnemyAnimator.SetTrigger(hashName);
_currentAnimationTrigger = hashName;
}
// Bool
public void SetAnimation(int hashName, bool value)
{
EnemyAnimator.SetBool(hashName, value);
}
// Float
public void SetAnimation(int hashName, float value)
{
EnemyAnimator.SetFloat(hashName, value);
}
// Integer
public void SetAnimation(int hashName, int value)
{
EnemyAnimator.SetInteger(hashName, value);
}
#endregion
}

View File

@ -4,65 +4,21 @@ using UnityEngine;
public class EnemyStateAttack : IEnemyState
{
private static readonly int VertiSlash = Animator.StringToHash("VertiSlash");
private static readonly int VertiAttack = Animator.StringToHash("VertiAttack");
private EnemyController _enemyController;
private Animator _animator;
private Coroutine _attackRoutine;
private EnemyAttackController _enemyAttackController;
private enum AttackType
{
VerticalAttack, // 위에서 아래로 베는 것
HorizontalAttack, // 옆으로 베는 것
ChariotAttack, // 원형
DynamoAttack, // 도넛
};
private AttackType _currentAttackType;
public void Enter(EnemyController enemyController)
{
_enemyController = enemyController;
_animator = _enemyController.EnemyAnimator;
_enemyAttackController = _enemyController.EnemyAttackController;
_animator.SetBool(VertiAttack, true);
_attackRoutine = _enemyController.StartCoroutine(VerticalAttackSequence());
}
public void Update()
{
}
private IEnumerator VerticalAttackSequence()
{
// 1. 전조 이펙트 생성
_enemyAttackController.TriggerRandomWarning(_enemyController.transform.position, _enemyController.transform.rotation);
// 2. 검을 들어올림
yield return new WaitForSeconds(3f);
// 3. 대기(전조와 검 들어올리는 애니메이션을 위함)
// 4. 검 휘두르기
_animator.SetTrigger(VertiSlash);
_enemyAttackController.DestroyWarningArea();
// TODO : 5. 공격 판정 발생
yield return new WaitForSeconds(1f);
// 6. 애니메이션 트리거 종료 -> 애니메이터 상태 머신으로 처리
_enemyController.SetState(EnemyState.Trace);
}
public void Exit()
{
if (_attackRoutine != null)
{
_enemyController.StopCoroutine(_attackRoutine);
_attackRoutine = null;
}
_animator.SetBool(VertiAttack, false);
_animator = null;
_enemyAttackController = null;
_enemyController = null;
}
}

View File

@ -5,7 +5,7 @@
public void Enter(EnemyController enemyController)
{
_enemyController = enemyController;
_enemyController.EnemyAnimator.SetTrigger("Dead");
_enemyController.SetAnimation(EnemyController.Dead);
}
public void Update()

View File

@ -1,19 +0,0 @@
public class EnemyStateGetHit: 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: 7067b1b0eacf490c863a4cb68c290d3a
timeCreated: 1744799049

View File

@ -2,13 +2,14 @@
public class EnemyStateIdle: IEnemyState
{
private static readonly int Idle = Animator.StringToHash("Idle");
private EnemyController _enemyController;
public void Enter(EnemyController enemyController)
{
_enemyController = enemyController;
_enemyController.EnemyAnimator.SetBool(Idle, true);
Debug.Log("## Idle 상태 진입");
_enemyController.SetAnimation(EnemyController.Idle, true);
}
public void Update()
@ -22,7 +23,7 @@ public class EnemyStateIdle: IEnemyState
public void Exit()
{
_enemyController.EnemyAnimator.SetBool(Idle, false);
_enemyController.SetAnimation(EnemyController.Idle, false);
_enemyController = null;
}
}

View File

@ -1,19 +0,0 @@
public class EnemyStateMove : 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: 5abb29c9137942fe95bc66670d8ef521
timeCreated: 1744799142

View File

@ -2,9 +2,6 @@
public class EnemyStateTrace : IEnemyState
{
private static readonly int MoveSpeed = Animator.StringToHash("MoveSpeed");
private static readonly int Trace = Animator.StringToHash("Trace");
private EnemyController _enemyController;
private Transform _detectPlayerTransform;
@ -14,7 +11,7 @@ public class EnemyStateTrace : IEnemyState
public void Enter(EnemyController enemyController)
{
_enemyController = enemyController;
Debug.Log("## Trace 상태 진입");
_detectPlayerTransform = _enemyController.TraceTargetTransform;
if (!_detectPlayerTransform)
{
@ -28,24 +25,31 @@ public class EnemyStateTrace : IEnemyState
_enemyController.Agent.SetDestination(_detectPlayerTransform.position);
}
_enemyController.EnemyAnimator.SetBool(Trace, true);
_enemyController.SetAnimation(EnemyController.Trace, true);
}
public void Update()
{
// 일정 주기로 찾은 플레이어의 위치를 갱신해서 갱신된 위치로 이동
FindTargetPosition();
if(_enemyController.IsMeleeCombat) return;
if (_enemyController.Agent.enabled != true) return;
PlayerTracking();
if (_enemyController.Agent.remainingDistance <= _enemyController.Agent.stoppingDistance)
{
// TODO: 타겟에 도착함 -> 공격 준비
_enemyController.SetState(EnemyState.Attack);
// _enemyController.SetState(EnemyState.Attack);
}
}
public void Exit()
{
_detectPlayerTransform = null;
_enemyController.SetAnimation(EnemyController.Trace, false);
_enemyController = null;
}
// 일정 주기로 찾은 플레이어의 위치를 갱신해서 갱신된 위치로 이동
private void FindTargetPosition()
{
if (_detectPlayerInCircleWaitTime > MaxDetectPlayerInCircleWaitTime)
@ -67,26 +71,16 @@ public class EnemyStateTrace : IEnemyState
// 플레이어를 추적하는 속도를 제어하는 함수
private void PlayerTracking()
{
FindTargetPosition();
float distance = (_detectPlayerTransform.position - _enemyController.transform.position).magnitude;
if (distance > 5f)
{
// 먼 거리: 뛰기
_enemyController.Agent.speed = _enemyController.RunSpeed;
_enemyController.Agent.acceleration = 20f;
_enemyController.Agent.angularSpeed = 270f;
// _enemyController.EnemyAnimator.SetFloat("MoveSpeed", 1f); // 애니메이션도 Run으로
// NavMeshAgent 회전에 맡기기
_enemyController.Agent.updateRotation = true;
}
else if (distance > 2f)
if (distance > 2f)
{
// 가까운 거리: 걷기
_enemyController.Agent.speed = _enemyController.WalkSpeed;
_enemyController.Agent.speed = _enemyController.MoveSpeed;
_enemyController.Agent.acceleration = 8f;
_enemyController.Agent.angularSpeed = 720f;
// _enemyController.EnemyAnimator.SetFloat("MoveSpeed", 0.4f); // Walk 애니메이션
_enemyController.Agent.updateRotation = true;
}
@ -107,23 +101,7 @@ public class EnemyStateTrace : IEnemyState
Time.deltaTime * 10f // 회전 속도
);
}
// _enemyController.Agent.angularSpeed = 1080f;
// _enemyController.Agent.acceleration = 999f;
// _enemyController.EnemyAnimator.SetFloat("MoveSpeed", 0f);
}
// 실제 속도 기반으로 애니메이션 제어
float currentSpeed = _enemyController.Agent.velocity.magnitude;
_enemyController.EnemyAnimator.SetFloat(MoveSpeed, currentSpeed);
}
public void Exit()
{
_detectPlayerTransform = null;
_enemyController.EnemyAnimator.SetBool(Trace, false);
_enemyController = null;
}
}

View File

@ -1,7 +1,208 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Serialization;
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");
[Header("공격 패턴 관련")]
[SerializeField] private float patternInterval = 3f;
[SerializeField] private float meleeRange = 2f;
[SerializeField] private float bombTriggerDelay = 1.5f;
[Header("폭탄 패턴 설정")]
[SerializeField] private int bombCount = 1;
[SerializeField] private Vector3 bombScale = new Vector3(5f, 5f, 5f);
[Header("각종 데미지 이펙트 세트")]
[SerializeField] private GameObject chariotSlashWarning;
[SerializeField] private GameObject chariotSlash;
[SerializeField] private GameObject boomExplosion;
[Space(10)]
[SerializeField] private GameObject verticalWarning;
[SerializeField] private GameObject verticalSlash;
[Space(10)]
[SerializeField] private GameObject horizontalWarning;
[SerializeField] private GameObject horizontalSlash;
private float _patternTimer = 0f;
private int _currentPatternIndex = 0;
private bool _isPatternRunning = false;
private bool _isFirstAttack = true;
private List<Action> _patternActions;
protected override void Awake()
{
base.Awake();
_patternActions = new List<Action>
{
ChariotSlashPattern,
VerticalSlashPattern,
HorizontalSlashPattern
};
}
protected override void Update()
{
base.Update();
if (CurrentState != EnemyState.Trace || _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);
_isFirstAttack = false;
}
}
else
{
if (Agent.isStopped) Agent.isStopped = false;
Agent.SetDestination(TraceTargetTransform.position);
_patternTimer += Time.deltaTime;
if (!_isPatternRunning && _patternTimer >= patternInterval)
{
BombThrowPattern();
}
}
}
private void ExecutePattern(int patternIndex)
{
_isPatternRunning = true;
Agent.isStopped = true;
IsMeleeCombat = true;
_patternActions[_currentPatternIndex]?.Invoke();
_currentPatternIndex = (_currentPatternIndex + 1) % _patternActions.Count; // 패턴 순환
}
// 순환 패턴과 별개로 동작하는 특수 패턴
private void BombThrowPattern()
{
Debug.Log("BombThrowPattern: 보스가 폭탄을 던집니다.");
SetAnimation(BoomShot);
_isPatternRunning = true;
Agent.isStopped = true;
for (int i = 0; i < bombCount; i++)
{
Vector3 targetPos = TraceTargetTransform.position;
targetPos.y = 0.1f; // 지면에 맞춤
var warning = Instantiate(chariotSlashWarning, targetPos, Quaternion.identity);
warning.transform.localScale = bombScale;
var aoe = warning.GetComponent<BoomAoeController>();
var effectData = new DamageEffectData
{
damage = (int)attackPower,
radius = bombScale.x,
delay = bombTriggerDelay,
targetLayer = TargetLayerMask,
explosionEffectPrefab = boomExplosion
};
aoe.SetEffect(effectData, null, PatternClear);
}
}
private void ChariotSlashPattern()
{
Debug.Log("ChariotSlashPattern: 보스가 차지 슬래시를 사용합니다.");
WindUpAnimation();
var warning = Instantiate(chariotSlashWarning, transform.position, Quaternion.identity)
.GetComponent<ChariotAoeController>();
var effectData = new DamageEffectData
{
damage = (int)attackPower,
radius = 7.5f,
delay = 2.5f,
targetLayer = TargetLayerMask,
explosionEffectPrefab = chariotSlash
};
warning.SetEffect(effectData, SlashAnimationPlay, PatternClear);
}
private void VerticalSlashPattern()
{
Debug.Log("VerticalSlashPattern: 보스가 수직 슬래시를 사용합니다.");
WindUpAnimation();
var warning = Instantiate(verticalWarning, transform.position, transform.rotation)
.GetComponent<VerticalAoeController>();
var effectData = new DamageEffectData
{
damage = (int)attackPower,
radius = 5f,
delay = 2f,
targetLayer = TargetLayerMask,
explosionEffectPrefab = verticalSlash
};
warning.SetEffect(effectData, SlashAnimationPlay, PatternClear);
}
private void HorizontalSlashPattern()
{
Debug.Log("HorizontalSlashPattern: 보스가 횡적 슬래시를 사용합니다.");
WindUpAnimation();
var warning = Instantiate(horizontalWarning, transform.position, transform.rotation)
.GetComponent<HorizontalAoeController>();
var effectData = new DamageEffectData
{
damage = (int)attackPower,
radius = 15f,
delay = 2f,
targetLayer = TargetLayerMask,
explosionEffectPrefab = horizontalSlash
};
warning.SetEffect(effectData, SlashAnimationPlay, PatternClear);
}
private void WindUpAnimation()
{
SetAnimation(WindUp);
}
private void SlashAnimationPlay()
{
SetAnimation(Slash);
}
private void PatternClear()
{
_isPatternRunning = false;
IsMeleeCombat = false;
_patternTimer = 0f;
Agent.isStopped = false;
}
}