Merge pull request 'DEG-100 원거리 마법사 보스 개발' (!16) from DEG-100-원거리-마법사-보스-몬스터-개발-단계-3 into main

Reviewed-on: #16
Reviewed-by: jay <ayjindev@gmail.com>
Reviewed-by: Sehyeon <sehyeon1837@gmail.com>
This commit is contained in:
fiore 2025-05-07 08:07:18 +00:00
commit d5df426b8f
14 changed files with 300 additions and 100 deletions

Binary file not shown.

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

Binary file not shown.

View File

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

View File

@ -0,0 +1,43 @@
using System.Collections;
using UnityEngine;
public class KnockbackEffect : StatusEffect
{
private Vector3 _sourcePosition;
private float _knockbackForce;
private float _elapsed = 0f;
public KnockbackEffect(Vector3 sourcePosition, float knockbackForce,float duration)
{
effectName = DebuffType.Knockback.ToString();
this.duration = duration;
_sourcePosition = sourcePosition;
_knockbackForce = knockbackForce;
}
public override void ApplyEffect(CharacterBase target)
{
Vector3 direction = (target.transform.position - _sourcePosition).normalized;
direction.y = 0f; // 수직 방향 제거
target.StartCoroutine(KnockbackCoroutine(target, direction));
}
private IEnumerator KnockbackCoroutine(CharacterBase pc, Vector3 direction)
{
CharacterController controller = pc.GetComponent<CharacterController>();
if (controller == null) yield break;
_elapsed = 0f;
while (_elapsed < duration)
{
controller.Move(direction * (_knockbackForce * Time.deltaTime));
_elapsed += Time.deltaTime;
yield return null;
}
}
public override void RemoveEffect(CharacterBase target)
{
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 97861883fd2941e0a40071b12ca13de4
timeCreated: 1746082842

View File

@ -6,26 +6,21 @@ public class SlowDebuff : StatusEffect
public SlowDebuff(float duration, float slowMultiplier) public SlowDebuff(float duration, float slowMultiplier)
{ {
this.effectName = "Slow"; this.effectName = DebuffType.Slow.ToString();
this.duration = duration; this.duration = duration;
_slowMultiplier = slowMultiplier; _slowMultiplier = slowMultiplier;
} }
public override void ApplyEffect(CharacterBase target) public override void ApplyEffect(CharacterBase target)
{ {
if (target is PlayerController pc) target.moveSpeed *= _slowMultiplier;
{
pc.moveSpeed *= _slowMultiplier;
Debug.Log($"{target.characterName}에게 이동 속도 감소 적용됨"); Debug.Log($"{target.characterName}에게 이동 속도 감소 적용됨");
}
} }
public override void RemoveEffect(CharacterBase target) public override void RemoveEffect(CharacterBase target)
{ {
if (target is PlayerController pc) target.moveSpeed /= _slowMultiplier;
{
pc.moveSpeed /= _slowMultiplier;
Debug.Log($"{target.characterName}의 이동 속도 회복됨"); Debug.Log($"{target.characterName}의 이동 속도 회복됨");
} }
} }
}

View File

@ -24,6 +24,7 @@ public abstract class AoeControllerBase : MonoBehaviour
protected DamageEffectData _data; protected DamageEffectData _data;
private Action _slashAction; private Action _slashAction;
private Action _destroyAction; private Action _destroyAction;
protected string EffectName;
/// <summary> /// <summary>
/// 범위 공격 이펙트를 설정하고, 딜레이 후 폭발을 실행합니다. /// 범위 공격 이펙트를 설정하고, 딜레이 후 폭발을 실행합니다.
@ -38,6 +39,17 @@ public abstract class AoeControllerBase : MonoBehaviour
StartCoroutine(ExplodeAfterDelay()); StartCoroutine(ExplodeAfterDelay());
} }
public void SetEffect(DamageEffectData data, Action slashAction, Action destroyAction, string effectName)
{
_data = data;
_slashAction = slashAction;
_destroyAction = destroyAction;
EffectName = effectName;
ShowWarningEffect();
StartCoroutine(ExplodeAfterDelay());
}
protected virtual void ShowWarningEffect() protected virtual void ShowWarningEffect()
{ {
if (warningEffectInstance != null) if (warningEffectInstance != null)

View File

@ -2,8 +2,6 @@
using System.Collections; using System.Collections;
using UnityEngine; using UnityEngine;
public class ChariotAoeController : AoeControllerBase public class ChariotAoeController : AoeControllerBase
{ {
protected override void ShowDamageEffect() protected override void ShowDamageEffect()

View File

@ -1,5 +1,11 @@
using UnityEngine; using UnityEngine;
public enum DebuffType
{
Slow,
Knockback,
}
public class MagicAoEField : AoeControllerBase public class MagicAoEField : AoeControllerBase
{ {
protected override void HitCheck() protected override void HitCheck()
@ -13,14 +19,33 @@ public class MagicAoEField : AoeControllerBase
Debug.Log($"{hit.name}에게 {_data.damage} 데미지 적용"); Debug.Log($"{hit.name}에게 {_data.damage} 데미지 적용");
// TODO: 실제 데미지 처리 로직 호출 // TODO: 실제 데미지 처리 로직 호출
// 임시 데이미 처리 로직 // 임시 데이미 처리 로직
ApplyEffect(hit);
}
}
}
private void ApplyEffect(Collider hit)
{
PlayerController playerController = hit.transform.GetComponent<PlayerController>(); PlayerController playerController = hit.transform.GetComponent<PlayerController>();
switch (EffectName)
{
case "Slow":
if (playerController != null) if (playerController != null)
{ {
// playerController.AddStatusEffect(_slowDebuff);
var slow = new SlowDebuff(10f, 0.5f); // 10초간 50% 속도 var slow = new SlowDebuff(10f, 0.5f); // 10초간 50% 속도
playerController.AddStatusEffect(slow); playerController.AddStatusEffect(slow);
} }
} break;
case "Knockback":
if (playerController != null)
{
var knPos = transform.position;
knPos.y += 0.5f;
var knockback = new KnockbackEffect(knPos,10f, 0.5f); // 장판 중심에서 10f만큼
playerController.AddStatusEffect(knockback);
}
break;
} }
} }
} }

View File

@ -1,9 +1,6 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine; using UnityEngine;
using UnityEngine.AI;
using Random = UnityEngine.Random; using Random = UnityEngine.Random;
public class CasterDemonController : EnemyController public class CasterDemonController : EnemyController
@ -11,16 +8,15 @@ public class CasterDemonController : EnemyController
// Animation // Animation
public static readonly int Cast = Animator.StringToHash("Cast"); public static readonly int Cast = Animator.StringToHash("Cast");
public static readonly int Flee = Animator.StringToHash("Flee"); public static readonly int Flee = Animator.StringToHash("Flee");
public static readonly int MagicMissile = Animator.StringToHash("MagicMissile");
private bool _doneBattleSequence = true; public static readonly int Telepo = Animator.StringToHash("Telepo");
private bool _isFirstNoPath = true; public static readonly int Spin = Animator.StringToHash("Spin");
private Coroutine _currentSequence;
[SerializeField] private Transform teleportTransform; [SerializeField] private Transform teleportTransform;
[SerializeField] private Transform bulletShotPosition; [SerializeField] private Transform bulletShotPosition;
[SerializeField] private GameObject magicMissilePrefab; [SerializeField] private GameObject magicMissilePrefab;
[SerializeField] private GameObject teleportEffectPrefab; [SerializeField] private GameObject teleportEffectPrefab;
[SerializeField] private Vector3 teleportTargetPosition = Vector3.zero;
[Header("각종 데미지 이펙트 세트")] [Header("각종 데미지 이펙트 세트")]
[SerializeField] private GameObject chariotWarning; [SerializeField] private GameObject chariotWarning;
@ -29,40 +25,128 @@ public class CasterDemonController : EnemyController
[Space(10)] [Space(10)]
[SerializeField] private GameObject slowFieldWarning; [SerializeField] private GameObject slowFieldWarning;
[SerializeField] private GameObject slowFieldEffect; [SerializeField] private GameObject slowFieldEffect;
[SerializeField] private GameObject knockbackEffect;
private float _knockbackTimer = 10f;
private const float KnockBackThresholdTime = 10f;
private float _teleportDistance = 4f; // 플레이어 뒤로 떨어질 거리 // 텔레포트 쿨타임 처음엔 빨리 사용 가능함
private float _teleportTimer = 10f;
// 텔레포트 쿨타임
private float _teleportTimer = 0;
private const float TeleportThresholdTime = 20f; private const float TeleportThresholdTime = 20f;
// 다음 행동 생각 처음엔 즉시 실행
private float _tinkingTimer = 3f;
private float _thinkingThresholdTime = 3f;
private bool _doneBattleSequence = true;
private Coroutine _currentSequence;
private DamageEffectData? _knockbackData;
private DamageEffectData KnockbackData
{
get
{
if (_knockbackData == null)
{
_knockbackData = new DamageEffectData
{
damage = 0,
radius = 7.5f,
delay = 0.5f,
targetLayer = TargetLayerMask,
explosionEffectPrefab = knockbackEffect
};
}
return _knockbackData.Value;
}
}
private DamageEffectData? _slowFieldEffectData;
private DamageEffectData SlowFieldEffectData
{
get
{
if (_slowFieldEffectData == null)
{
_slowFieldEffectData = new DamageEffectData
{
damage = 0,
radius = 7.5f,
delay = 2.5f,
targetLayer = TargetLayerMask,
explosionEffectPrefab = slowFieldEffect
};
}
return _slowFieldEffectData.Value;
}
}
private DamageEffectData? _teleportEffectData;
private DamageEffectData TeleportEffectData
{
get
{
if (_teleportEffectData == null)
{
_teleportEffectData = new DamageEffectData
{
damage = (int)attackPower,
radius = 10,
delay = 1.5f,
targetLayer = TargetLayerMask,
explosionEffectPrefab = chariotEffect
};
}
return _teleportEffectData.Value;
}
}
// 특수 스킬 사용 가능 여부
private bool CanKnockback
{
get
{
if (!(_knockbackTimer >= KnockBackThresholdTime)) return false;
_knockbackTimer = 0f;
return true;
}
}
private bool CanTeleport { private bool CanTeleport {
get get
{ {
if (_teleportTimer >= TeleportThresholdTime ) if (!(_teleportTimer >= TeleportThresholdTime)) return false;
{
_teleportTimer = 0; _teleportTimer = 0;
return true; return true;
} }
}
private bool CanBattleSequence
{
get
{
if (_tinkingTimer >= _thinkingThresholdTime)
{
_tinkingTimer = 0;
return true;
}
return false; return false;
} }
} }
private void LateUpdate() private void LateUpdate()
{ {
_teleportTimer += Time.deltaTime; _teleportTimer += Time.deltaTime;
_tinkingTimer += Time.deltaTime;
_knockbackTimer += Time.deltaTime;
} }
public override void BattleSequence() public override void BattleSequence()
{ {
// 전투 행동이 이미 진행 중일 경우 실행 막기 // 전투 행동이 이미 진행 중일 경우 실행 막기
if (_doneBattleSequence) if (_doneBattleSequence && CanBattleSequence)
{ {
// 전투 행동 시작 // 전투 행동 시작
_doneBattleSequence = false; _doneBattleSequence = false;
// TODO : 배틀 중일 때 루프 // 사용할 공격 생각하기
// Debug.Log("## 몬스터의 교전 행동 루프");
Thinking(); Thinking();
} }
} }
@ -74,26 +158,18 @@ public class CasterDemonController : EnemyController
switch (selectedPattern) switch (selectedPattern)
{ {
case 0: case 0:
case 1: case 1:
case 2: case 2:
case 3: case 3:
case 4: case 4:
SetSequence(ShotMagicMissile);
break;
case 5: case 5:
case 6: case 6:
case 7: case 7:
case 8: case 8:
case 9: case 9:
// SetSequence(ShotMagicMissile()); SetSequence(SlowFieldSpell);
SetSequence(SlowFieldSpell());
break; break;
} }
@ -103,8 +179,13 @@ public class CasterDemonController : EnemyController
{ {
if (CanTeleport) if (CanTeleport)
{ {
action(); action.Invoke();
Teleport(); Teleport();
return;
}
if (CanKnockback)
{
SetSequence(KnockbackSpell);
} }
} }
@ -116,6 +197,7 @@ public class CasterDemonController : EnemyController
// 플레이어 위치를 바라보고 // 플레이어 위치를 바라보고
transform.LookAt(aimPosition); transform.LookAt(aimPosition);
SetAnimation(MagicMissile);
// 미사일 생성 및 초기화 // 미사일 생성 및 초기화
var missile = Instantiate( var missile = Instantiate(
@ -126,12 +208,11 @@ public class CasterDemonController : EnemyController
missile.GetComponent<MagicMissile>() missile.GetComponent<MagicMissile>()
.Initialize(new BulletData(aimPosition, 5f, 10f, 5f)); .Initialize(new BulletData(aimPosition, 5f, 10f, 5f));
yield return new WaitForSeconds(0.4f); yield return Wait.For(0.4f);
} }
// 짧은 텀 후 끝내기 // 짧은 텀 후 끝내기
yield return new WaitForSeconds(1f); yield return Wait.For(1f);
_doneBattleSequence = true;
} }
private Vector3 TargetPosOracle(out Vector3 basePos, out Rigidbody rb) private Vector3 TargetPosOracle(out Vector3 basePos, out Rigidbody rb)
@ -164,22 +245,16 @@ public class CasterDemonController : EnemyController
// 텔레포트와 함께 시전하는 범위 공격 // 텔레포트와 함께 시전하는 범위 공격
var aoe = Instantiate(chariotWarning, startPos, Quaternion.identity).GetComponent<ChariotAoeController>(); var aoe = Instantiate(chariotWarning, startPos, Quaternion.identity).GetComponent<ChariotAoeController>();
var effectData = new DamageEffectData
{
damage = (int)attackPower,
radius = 10,
delay = 1.5f,
targetLayer = TargetLayerMask,
explosionEffectPrefab = chariotEffect
};
aoe.SetEffect(effectData, null, null);
// 중앙으로 이동 aoe.SetEffect(TeleportEffectData, null, null);
Agent.Warp(Vector3.zero);
// 텔레포트 타겟 위치로 이동
Agent.Warp(teleportTargetPosition);
SetAnimation(Telepo);
if (teleportEffectPrefab != null) if (teleportEffectPrefab != null)
Instantiate(teleportEffectPrefab, Vector3.zero, Quaternion.identity); Instantiate(teleportEffectPrefab, teleportTargetPosition, Quaternion.identity);
} }
private IEnumerator SlowFieldSpell() private IEnumerator SlowFieldSpell()
@ -188,35 +263,42 @@ public class CasterDemonController : EnemyController
// 1. 시전 애니메이션 // 1. 시전 애니메이션
transform.LookAt(aimPosition); transform.LookAt(aimPosition);
SetAnimation(Cast); SetAnimation(Cast);
// 2. 장판 생성과 세팅 // 2. 장판 생성과 세팅
var effectData = new DamageEffectData
{
damage = 0,
radius = 7.5f,
delay = 2.5f,
targetLayer = TargetLayerMask,
explosionEffectPrefab = slowFieldEffect
};
var fixedPos = new Vector3(aimPosition.x, 0, aimPosition.z); var fixedPos = new Vector3(aimPosition.x, 0, aimPosition.z);
var warning = Instantiate(chariotWarning, fixedPos, Quaternion.identity).GetComponent<MagicAoEField>(); var warning = Instantiate(chariotWarning, fixedPos, Quaternion.identity).GetComponent<MagicAoEField>();
warning.SetEffect(effectData, null, null); warning.SetEffect(SlowFieldEffectData, null, null);
// TODO : 효과 적용
// 3. 짧은 텀 후 끝내기 // 3. 짧은 텀 후 끝내기
yield return new WaitForSeconds(1f); yield return Wait.For(1f);
_doneBattleSequence = true;
} }
private void SetSequence(IEnumerator newSequence) private IEnumerator KnockbackSpell()
{
// 시전 애니메이션
SetAnimation(Spin);
// 넉백 발생
var knockback = Instantiate(chariotWarning, transform).GetComponent<MagicAoEField>();
knockback.SetEffect(KnockbackData, null, null, DebuffType.Knockback.ToString());
yield return Wait.For(1f);
}
#region
private void SetSequence(Func<IEnumerator> newSequence)
{ {
if (_currentSequence != null) if (_currentSequence != null)
{
StopCoroutine(_currentSequence); StopCoroutine(_currentSequence);
}
_currentSequence = StartCoroutine(newSequence); _currentSequence = StartCoroutine(RunPattern(newSequence));
} }
private IEnumerator RunPattern(Func<IEnumerator> pattern)
{
yield return StartCoroutine(pattern());
_doneBattleSequence = true;
}
#endregion
} }

View File

@ -7,8 +7,11 @@ public class EnemyStateFlee :IEnemyState
{ {
private EnemyController _enemyController; private EnemyController _enemyController;
private Transform _playerTransform; private Transform _playerTransform;
private float _fleeDistance = 5f; // 도망치는 거리 private float _attackRange = 7f; // 공격 범위
private float _safeRange = 7f; // 공격 범위 private float _fleeDistance = 15f; // 도망치는 거리
private float _attackRangeSqr;
private float _fleeDistanceSqr;
// 경로 탐색 주기 조절용 // 경로 탐색 주기 조절용
private float _fleeSearchTimer = 0; private float _fleeSearchTimer = 0;
@ -33,6 +36,9 @@ public class EnemyStateFlee :IEnemyState
_stuckTimer = 0f; _stuckTimer = 0f;
_fleeSearchTimer = 0; _fleeSearchTimer = 0;
_attackRangeSqr = _attackRange * _attackRange;
_fleeDistanceSqr = _fleeDistance * _fleeDistance;
_enemyController.SetAnimation(CasterDemonController.Flee, true); _enemyController.SetAnimation(CasterDemonController.Flee, true);
} }
@ -44,11 +50,17 @@ public class EnemyStateFlee :IEnemyState
return; return;
} }
float currentDist = Vector3.Distance( float currentDist = (_enemyController.transform.position - _playerTransform.position).sqrMagnitude;
_enemyController.transform.position,
_playerTransform.position if (currentDist >= _fleeDistanceSqr)
); {
if (currentDist >= _safeRange) _enemyController.Agent.isStopped = true;
_enemyController.Agent.ResetPath();
_enemyController.SetState(EnemyState.Idle);
return;
}
if (currentDist >= _attackRangeSqr)
{ {
// 목적지 리셋 후 전투 시작 // 목적지 리셋 후 전투 시작
_enemyController.Agent.isStopped = true; _enemyController.Agent.isStopped = true;
@ -74,8 +86,8 @@ public class EnemyStateFlee :IEnemyState
private void CheckPath() private void CheckPath()
{ {
float moved = Vector3.Distance(_enemyController.transform.position, _lastPosition); float moved = (_enemyController.transform.position - _lastPosition).sqrMagnitude;
if (moved < StuckMoveThreshold) if (moved < StuckMoveThreshold * StuckMoveThreshold)
{ {
_stuckTimer += Time.deltaTime; _stuckTimer += Time.deltaTime;
if (_stuckTimer >= StuckThresholdTime) if (_stuckTimer >= StuckThresholdTime)
@ -95,14 +107,12 @@ public class EnemyStateFlee :IEnemyState
_fleeSearchTimer += Time.deltaTime; _fleeSearchTimer += Time.deltaTime;
if (_fleeSearchTimer <= FleeThresholdTime) return; if (_fleeSearchTimer <= FleeThresholdTime) return;
// 1) 목표 도망 위치 계산 // 플레이어 반대방향으로 도망
Vector3 fleeDirection = (_enemyController.transform.position - _playerTransform.position).normalized; Vector3 fleeDirection = (_enemyController.transform.position - _playerTransform.position).normalized;
Vector3 fleeTarget = _enemyController.transform.position + fleeDirection * _fleeDistance; Vector3 fleeTarget = _enemyController.transform.position + fleeDirection * _fleeDistance;
// 2) 경로 계산해 보기 // 경로 설정
_enemyController.Agent.SetDestination(fleeTarget); _enemyController.Agent.SetDestination(fleeTarget);
// 3) 이동
_enemyController.Agent.isStopped = false; _enemyController.Agent.isStopped = false;
_fleeSearchTimer = 0; _fleeSearchTimer = 0;
} }

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6757ce2a26e44e9c931666533e7e8036
timeCreated: 1746594438

View File

@ -0,0 +1,18 @@
using System.Collections.Generic;
using UnityEngine;
// 코루틴의 WaitForSeconds를 캐싱하는 유틸
public static class Wait
{
private static readonly Dictionary<float, WaitForSeconds> _waits = new();
public static WaitForSeconds For(float seconds)
{
if (!_waits.TryGetValue(seconds, out var wait))
{
wait = new WaitForSeconds(seconds);
_waits[seconds] = wait;
}
return wait;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e6fe7172532f42069e3fc7088ebd0718
timeCreated: 1746594464