2026-02-02 08:30:23 +00:00
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.AI;
|
|
|
|
|
|
|
2026-02-04 14:06:25 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 근접 공격 몬스터 (칼, 도끼 등)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class MeleeMonster : MonsterClass
|
2026-02-02 08:30:23 +00:00
|
|
|
|
{
|
2026-02-04 14:06:25 +00:00
|
|
|
|
[Header("=== 근접 공격 설정 ===")]
|
|
|
|
|
|
[SerializeField] private MonsterWeapon myWeapon;
|
2026-02-02 08:30:23 +00:00
|
|
|
|
[SerializeField] private float attackRange = 2f;
|
|
|
|
|
|
[SerializeField] private float attackDelay = 1.5f;
|
|
|
|
|
|
|
|
|
|
|
|
private float lastAttackTime;
|
|
|
|
|
|
|
|
|
|
|
|
[Header("공격 / 이동 애니메이션")]
|
|
|
|
|
|
[SerializeField] private string[] attackAnimations = { "Monster_Attack_1" };
|
|
|
|
|
|
[SerializeField] private string Monster_Walk = "Monster_Walk";
|
|
|
|
|
|
|
2026-02-02 15:02:12 +00:00
|
|
|
|
[Header("AI 상세 설정")]
|
2026-02-02 08:30:23 +00:00
|
|
|
|
[SerializeField] private float stopBuffer = 0.3f;
|
|
|
|
|
|
[SerializeField] private float patrolRadius = 5f;
|
|
|
|
|
|
[SerializeField] private float patrolInterval = 2f;
|
|
|
|
|
|
|
|
|
|
|
|
private float nextPatrolTime;
|
|
|
|
|
|
private float repathInterval = 0.3f;
|
|
|
|
|
|
private float nextRepathTime;
|
|
|
|
|
|
private int attackIndex;
|
|
|
|
|
|
private bool isPlayerInZone;
|
|
|
|
|
|
|
2026-02-04 14:06:25 +00:00
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
|
|
// 초기화
|
|
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
|
|
|
2026-02-02 08:30:23 +00:00
|
|
|
|
protected override void Init()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (agent != null) agent.stoppingDistance = attackRange - 0.4f;
|
|
|
|
|
|
if (animator != null) animator.applyRootMotion = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 14:06:25 +00:00
|
|
|
|
protected override void OnResetStats()
|
|
|
|
|
|
{
|
|
|
|
|
|
// 무기에 데미지 설정
|
|
|
|
|
|
if (myWeapon != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
myWeapon.SetDamage(attackDamage);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
|
|
// AI 로직
|
|
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
|
|
|
2026-02-02 08:30:23 +00:00
|
|
|
|
protected override void ExecuteAILogic()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (isHit || isAttacking || isResting) return;
|
|
|
|
|
|
|
|
|
|
|
|
float distance = Vector3.Distance(transform.position, playerTransform.position);
|
2026-02-04 14:06:25 +00:00
|
|
|
|
|
|
|
|
|
|
if (isPlayerInZone || distance <= attackRange * 2f)
|
|
|
|
|
|
HandlePlayerTarget();
|
|
|
|
|
|
else
|
|
|
|
|
|
Patrol();
|
2026-02-02 08:30:23 +00:00
|
|
|
|
|
|
|
|
|
|
UpdateMovementAnimation();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void HandlePlayerTarget()
|
|
|
|
|
|
{
|
|
|
|
|
|
float distance = Vector3.Distance(transform.position, playerTransform.position);
|
2026-02-04 14:06:25 +00:00
|
|
|
|
|
|
|
|
|
|
if (distance <= attackRange - stopBuffer)
|
|
|
|
|
|
TryAttack();
|
2026-02-02 08:30:23 +00:00
|
|
|
|
else if (Time.time >= nextRepathTime)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (agent.isOnNavMesh) agent.SetDestination(playerTransform.position);
|
|
|
|
|
|
nextRepathTime = Time.time + repathInterval;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void TryAttack()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Time.time < lastAttackTime + attackDelay) return;
|
|
|
|
|
|
lastAttackTime = Time.time;
|
2026-02-03 14:41:49 +00:00
|
|
|
|
|
2026-02-02 08:30:23 +00:00
|
|
|
|
string attackName = attackAnimations[attackIndex];
|
|
|
|
|
|
attackIndex = (attackIndex + 1) % attackAnimations.Length;
|
|
|
|
|
|
|
2026-02-03 14:41:49 +00:00
|
|
|
|
isAttacking = true;
|
|
|
|
|
|
|
|
|
|
|
|
if (agent.isOnNavMesh)
|
2026-02-02 08:30:23 +00:00
|
|
|
|
{
|
2026-02-03 14:41:49 +00:00
|
|
|
|
agent.isStopped = true;
|
|
|
|
|
|
agent.velocity = Vector3.zero;
|
2026-02-02 08:30:23 +00:00
|
|
|
|
}
|
2026-02-03 14:41:49 +00:00
|
|
|
|
|
|
|
|
|
|
animator.Play(attackName, 0, 0f);
|
2026-02-02 08:30:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void UpdateMovementAnimation()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (isAttacking || isHit || isResting) return;
|
2026-02-04 14:06:25 +00:00
|
|
|
|
|
|
|
|
|
|
if (agent.velocity.magnitude < 0.1f)
|
|
|
|
|
|
animator.Play(Monster_Idle);
|
|
|
|
|
|
else
|
|
|
|
|
|
animator.Play(Monster_Walk);
|
2026-02-02 08:30:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void Patrol()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Time.time < nextPatrolTime) return;
|
2026-02-04 14:06:25 +00:00
|
|
|
|
|
|
|
|
|
|
Vector3 randomPoint = transform.position + new Vector3(
|
|
|
|
|
|
Random.Range(-patrolRadius, patrolRadius),
|
|
|
|
|
|
0,
|
|
|
|
|
|
Random.Range(-patrolRadius, patrolRadius)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-02-02 08:30:23 +00:00
|
|
|
|
if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas))
|
|
|
|
|
|
if (agent.isOnNavMesh) agent.SetDestination(hit.position);
|
2026-02-04 14:06:25 +00:00
|
|
|
|
|
2026-02-02 08:30:23 +00:00
|
|
|
|
nextPatrolTime = Time.time + patrolInterval;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 14:06:25 +00:00
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
|
|
// 공격 이벤트 (애니메이션에서 호출)
|
|
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
|
|
|
|
|
|
|
|
public void OnAttackStart()
|
|
|
|
|
|
{
|
|
|
|
|
|
isAttacking = true;
|
|
|
|
|
|
isResting = false;
|
|
|
|
|
|
if (myWeapon != null) myWeapon.EnableHitBox();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void OnAttackEnd()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (myWeapon != null) myWeapon.DisableHitBox();
|
|
|
|
|
|
isAttacking = false;
|
|
|
|
|
|
if (!isDead && !isHit) StartCoroutine(RestAfterAttack());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
System.Collections.IEnumerator RestAfterAttack()
|
|
|
|
|
|
{
|
|
|
|
|
|
isResting = true;
|
|
|
|
|
|
yield return new WaitForSeconds(attackRestDuration);
|
|
|
|
|
|
isResting = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
|
|
// 피격 / 사망 시 무기 끄기
|
|
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
|
|
|
|
|
|
|
|
protected override void OnStartHit()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (myWeapon != null) myWeapon.DisableHitBox();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected override void OnDie()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (myWeapon != null) myWeapon.DisableHitBox();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
|
|
// Trigger 감지 (플레이어 인식 영역)
|
|
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
|
|
|
|
|
|
|
|
private void OnTriggerEnter(Collider other)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (other.CompareTag("Player")) isPlayerInZone = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnTriggerExit(Collider other)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (other.CompareTag("Player")) isPlayerInZone = false;
|
|
|
|
|
|
}
|
2026-02-02 08:30:23 +00:00
|
|
|
|
}
|