240 lines
7.6 KiB
C#
240 lines
7.6 KiB
C#
|
|
using UnityEngine;
|
||
|
|
using UnityEngine.AI;
|
||
|
|
using System.Collections;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 돌진 공격 몬스터
|
||
|
|
/// - 플레이어를 향해 빠르게 돌진
|
||
|
|
/// - 부딪히면 데미지
|
||
|
|
/// </summary>
|
||
|
|
public class ChargeMonster : MonsterClass
|
||
|
|
{
|
||
|
|
[Header("=== 돌진 공격 설정 ===")]
|
||
|
|
[SerializeField] private float chargeSpeed = 15f; // 돌진 속도
|
||
|
|
[SerializeField] private float chargeRange = 10f; // 돌진 시작 거리
|
||
|
|
[SerializeField] private float chargeDuration = 2f; // 돌진 지속 시간
|
||
|
|
[SerializeField] private float chargeDelay = 3f; // 돌진 쿨타임
|
||
|
|
[SerializeField] private float prepareTime = 0.5f; // 돌진 준비 시간
|
||
|
|
|
||
|
|
private float lastChargeTime;
|
||
|
|
private bool isCharging = false;
|
||
|
|
private bool isPreparing = false;
|
||
|
|
private Vector3 chargeDirection;
|
||
|
|
|
||
|
|
[Header("공격 / 이동 애니메이션")]
|
||
|
|
[SerializeField] private string chargeAnimation = "Monster_Charge";
|
||
|
|
[SerializeField] private string prepareAnimation = "Monster_ChargePrepare";
|
||
|
|
[SerializeField] private string Monster_Walk = "Monster_Walk";
|
||
|
|
|
||
|
|
[Header("AI 상세 설정")]
|
||
|
|
[SerializeField] private float patrolRadius = 5f;
|
||
|
|
[SerializeField] private float patrolInterval = 2f;
|
||
|
|
|
||
|
|
private float nextPatrolTime;
|
||
|
|
private bool isPlayerInZone;
|
||
|
|
private Rigidbody _rigidbody;
|
||
|
|
|
||
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
|
// 초기화
|
||
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
|
|
||
|
|
protected override void Awake()
|
||
|
|
{
|
||
|
|
base.Awake();
|
||
|
|
_rigidbody = GetComponent<Rigidbody>();
|
||
|
|
|
||
|
|
if (_rigidbody == null)
|
||
|
|
{
|
||
|
|
_rigidbody = gameObject.AddComponent<Rigidbody>();
|
||
|
|
}
|
||
|
|
|
||
|
|
_rigidbody.isKinematic = true; // NavMeshAgent 사용 시 Kinematic
|
||
|
|
}
|
||
|
|
|
||
|
|
protected override void Init()
|
||
|
|
{
|
||
|
|
if (agent != null)
|
||
|
|
{
|
||
|
|
agent.stoppingDistance = 1f;
|
||
|
|
}
|
||
|
|
if (animator != null) animator.applyRootMotion = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
|
// AI 로직
|
||
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
|
|
||
|
|
protected override void ExecuteAILogic()
|
||
|
|
{
|
||
|
|
if (isHit || isCharging || isPreparing) return;
|
||
|
|
|
||
|
|
float distance = Vector3.Distance(transform.position, playerTransform.position);
|
||
|
|
|
||
|
|
if (isPlayerInZone || distance <= chargeRange * 1.5f)
|
||
|
|
{
|
||
|
|
HandleChargeCombat(distance);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
Patrol();
|
||
|
|
}
|
||
|
|
|
||
|
|
UpdateMovementAnimation();
|
||
|
|
}
|
||
|
|
|
||
|
|
void HandleChargeCombat(float distance)
|
||
|
|
{
|
||
|
|
// 돌진 범위 안이면 돌진 준비
|
||
|
|
if (distance <= chargeRange && Time.time >= lastChargeTime + chargeDelay)
|
||
|
|
{
|
||
|
|
StartCoroutine(PrepareCharge());
|
||
|
|
}
|
||
|
|
// 아니면 추적
|
||
|
|
else if (!isCharging)
|
||
|
|
{
|
||
|
|
if (agent.isOnNavMesh)
|
||
|
|
agent.SetDestination(playerTransform.position);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
IEnumerator PrepareCharge()
|
||
|
|
{
|
||
|
|
isPreparing = true;
|
||
|
|
|
||
|
|
// 이동 멈춤
|
||
|
|
if (agent.isOnNavMesh)
|
||
|
|
{
|
||
|
|
agent.isStopped = true;
|
||
|
|
agent.velocity = Vector3.zero;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 플레이어 방향 저장
|
||
|
|
chargeDirection = (playerTransform.position - transform.position).normalized;
|
||
|
|
chargeDirection.y = 0;
|
||
|
|
|
||
|
|
// 플레이어 바라보기
|
||
|
|
transform.rotation = Quaternion.LookRotation(chargeDirection);
|
||
|
|
|
||
|
|
// 준비 애니메이션
|
||
|
|
if (!string.IsNullOrEmpty(prepareAnimation))
|
||
|
|
{
|
||
|
|
animator.Play(prepareAnimation, 0, 0f);
|
||
|
|
}
|
||
|
|
|
||
|
|
Debug.Log("[ChargeMonster] 돌진 준비 중...");
|
||
|
|
yield return new WaitForSeconds(prepareTime);
|
||
|
|
|
||
|
|
// 돌진 시작!
|
||
|
|
StartCoroutine(Charge());
|
||
|
|
}
|
||
|
|
|
||
|
|
IEnumerator Charge()
|
||
|
|
{
|
||
|
|
isPreparing = false;
|
||
|
|
isCharging = true;
|
||
|
|
lastChargeTime = Time.time;
|
||
|
|
|
||
|
|
// NavMeshAgent 끄기
|
||
|
|
if (agent.isOnNavMesh) agent.enabled = false;
|
||
|
|
|
||
|
|
// Rigidbody Kinematic 끄기
|
||
|
|
if (_rigidbody != null) _rigidbody.isKinematic = false;
|
||
|
|
|
||
|
|
// 돌진 애니메이션
|
||
|
|
animator.Play(chargeAnimation, 0, 0f);
|
||
|
|
|
||
|
|
Debug.Log("[ChargeMonster] 돌진 시작!");
|
||
|
|
|
||
|
|
float chargeStartTime = Time.time;
|
||
|
|
|
||
|
|
while (Time.time < chargeStartTime + chargeDuration)
|
||
|
|
{
|
||
|
|
// 빠르게 앞으로 이동
|
||
|
|
if (_rigidbody != null)
|
||
|
|
{
|
||
|
|
_rigidbody.velocity = chargeDirection * chargeSpeed;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
transform.position += chargeDirection * chargeSpeed * Time.deltaTime;
|
||
|
|
}
|
||
|
|
|
||
|
|
yield return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 돌진 종료
|
||
|
|
if (_rigidbody != null)
|
||
|
|
{
|
||
|
|
_rigidbody.velocity = Vector3.zero;
|
||
|
|
_rigidbody.isKinematic = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// NavMeshAgent 다시 켜기
|
||
|
|
if (agent != null) agent.enabled = true;
|
||
|
|
|
||
|
|
isCharging = false;
|
||
|
|
Debug.Log("[ChargeMonster] 돌진 종료!");
|
||
|
|
}
|
||
|
|
|
||
|
|
void UpdateMovementAnimation()
|
||
|
|
{
|
||
|
|
if (isCharging || isPreparing || isHit || isResting) return;
|
||
|
|
|
||
|
|
if (agent.velocity.magnitude < 0.1f)
|
||
|
|
animator.Play(Monster_Idle);
|
||
|
|
else
|
||
|
|
animator.Play(Monster_Walk);
|
||
|
|
}
|
||
|
|
|
||
|
|
void Patrol()
|
||
|
|
{
|
||
|
|
if (Time.time < nextPatrolTime) return;
|
||
|
|
|
||
|
|
Vector3 randomPoint = transform.position + new Vector3(
|
||
|
|
Random.Range(-patrolRadius, patrolRadius),
|
||
|
|
0,
|
||
|
|
Random.Range(-patrolRadius, patrolRadius)
|
||
|
|
);
|
||
|
|
|
||
|
|
if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas))
|
||
|
|
if (agent.isOnNavMesh) agent.SetDestination(hit.position);
|
||
|
|
|
||
|
|
nextPatrolTime = Time.time + patrolInterval;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
|
// 충돌 감지 (돌진 중 플레이어와 충돌)
|
||
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
|
|
||
|
|
private void OnCollisionEnter(Collision collision)
|
||
|
|
{
|
||
|
|
if (!isCharging) return;
|
||
|
|
|
||
|
|
if (collision.gameObject.CompareTag("Player"))
|
||
|
|
{
|
||
|
|
if (collision.gameObject.TryGetComponent<PlayerHealth>(out var playerHealth))
|
||
|
|
{
|
||
|
|
if (!playerHealth.isInvincible)
|
||
|
|
{
|
||
|
|
playerHealth.TakeDamage(attackDamage);
|
||
|
|
Debug.Log($"[ChargeMonster] 돌진 적중! 데미지: {attackDamage}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
|
// Trigger 감지
|
||
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
|
|
||
|
|
private void OnTriggerEnter(Collider other)
|
||
|
|
{
|
||
|
|
if (other.CompareTag("Player")) isPlayerInZone = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnTriggerExit(Collider other)
|
||
|
|
{
|
||
|
|
if (other.CompareTag("Player")) isPlayerInZone = false;
|
||
|
|
}
|
||
|
|
}
|