436 lines
12 KiB
C#
436 lines
12 KiB
C#
using System.Collections;
|
|
using UnityEngine;
|
|
using UnityEngine.AI;
|
|
|
|
[RequireComponent(typeof(NavMeshAgent))]
|
|
[RequireComponent(typeof(Animator))]
|
|
public class EnemyAI : MonoBehaviour, IPauseable
|
|
{
|
|
public enum AIState { Wander, Chase, Attack, Stun }
|
|
|
|
[Header("現在の状態")]
|
|
[SerializeField] private AIState currentState = AIState.Wander;
|
|
|
|
[Header("索敵設定")]
|
|
[SerializeField] private float detectionRange = 10f;
|
|
[SerializeField] private float loseRange = 15f;
|
|
[SerializeField] private string playerTag = "Player";
|
|
|
|
[Header("攻撃設定")]
|
|
[SerializeField] private float attackRange = 1.5f;
|
|
[SerializeField] private float attackDamage = 10f;
|
|
[SerializeField] private float attackCooldown = 2.0f;
|
|
[SerializeField] private float telegraphDuration = 0.6f;
|
|
[SerializeField] private Collider rightHandCollider;
|
|
[SerializeField] private float comboInterval = 0.2f;
|
|
|
|
[Header("徘徊設定")]
|
|
[SerializeField] private float wanderRadius = 8f;
|
|
[SerializeField] private float minWaitTime = 2f;
|
|
[SerializeField] private float maxWaitTime = 4f;
|
|
[SerializeField] private float wanderTimeout = 7f;
|
|
|
|
private NavMeshAgent agent;
|
|
private Animator anim;
|
|
private Transform playerTransform;
|
|
|
|
private bool isCooldown;
|
|
private bool isAttacking;
|
|
private bool isWandering;
|
|
private bool isKnockbacking;
|
|
private bool isDead;
|
|
private bool isPaused;
|
|
|
|
private static readonly int SpeedHash = Animator.StringToHash("Speed");
|
|
private static readonly int StunHash = Animator.StringToHash("Stun");
|
|
private static readonly int HitHash = Animator.StringToHash("Hit");
|
|
private static readonly int TelegraphHash = Animator.StringToHash("Telegraph");
|
|
private static readonly int AttackHash = Animator.StringToHash("Attack");
|
|
|
|
private void Awake()
|
|
{
|
|
agent = GetComponent<NavMeshAgent>();
|
|
anim = GetComponent<Animator>();
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
SetFistCollider(false);
|
|
if (WorldPauseManager.Instance != null)
|
|
{
|
|
WorldPauseManager.Instance.Register(this);
|
|
}
|
|
StartCoroutine(AILoop());
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
if (WorldPauseManager.Instance != null)
|
|
{
|
|
WorldPauseManager.Instance.Unregister(this);
|
|
}
|
|
}
|
|
|
|
public void Pause()
|
|
{
|
|
isPaused = true;
|
|
if (anim != null) anim.speed = 0f;
|
|
if (agent != null && agent.enabled && agent.isOnNavMesh)
|
|
{
|
|
agent.isStopped = true;
|
|
}
|
|
}
|
|
|
|
public void Resume()
|
|
{
|
|
isPaused = false;
|
|
if (anim != null) anim.speed = 1f;
|
|
if (agent != null && agent.enabled && agent.isOnNavMesh && !isDead && !isKnockbacking && currentState != AIState.Stun)
|
|
{
|
|
agent.isStopped = false;
|
|
}
|
|
}
|
|
|
|
private IEnumerator WaitForUnpausedSeconds(float duration)
|
|
{
|
|
float timer = 0f;
|
|
while (timer < duration)
|
|
{
|
|
if (!isPaused)
|
|
{
|
|
timer += Time.deltaTime;
|
|
}
|
|
yield return null;
|
|
}
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (isDead || isPaused) return;
|
|
|
|
UpdateAnimation();
|
|
UpdateRotationOnAttack();
|
|
}
|
|
|
|
private IEnumerator AILoop()
|
|
{
|
|
while (!isDead)
|
|
{
|
|
if (isPaused)
|
|
{
|
|
yield return null;
|
|
continue;
|
|
}
|
|
|
|
if (playerTransform == null) FindPlayer();
|
|
|
|
if (!isAttacking && !isKnockbacking && currentState != AIState.Stun)
|
|
{
|
|
switch (currentState)
|
|
{
|
|
case AIState.Wander: WanderBehavior(); break;
|
|
case AIState.Chase: ChaseBehavior(); break;
|
|
case AIState.Attack: AttackBehavior(); break;
|
|
}
|
|
}
|
|
|
|
yield return WaitForUnpausedSeconds(0.2f);
|
|
}
|
|
}
|
|
|
|
private void FindPlayer()
|
|
{
|
|
GameObject playerObj = GameObject.FindGameObjectWithTag(playerTag);
|
|
if (playerObj != null) playerTransform = playerObj.transform;
|
|
}
|
|
|
|
private void WanderBehavior()
|
|
{
|
|
if (playerTransform != null)
|
|
{
|
|
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
|
|
if (distanceToPlayer <= detectionRange)
|
|
{
|
|
currentState = AIState.Chase;
|
|
isWandering = false;
|
|
agent.ResetPath();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!isWandering)
|
|
{
|
|
StartCoroutine(WanderMoveRoutine());
|
|
}
|
|
}
|
|
|
|
private void ChaseBehavior()
|
|
{
|
|
if (playerTransform == null) { currentState = AIState.Wander; return; }
|
|
|
|
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
|
|
|
|
if (distanceToPlayer <= attackRange)
|
|
{
|
|
currentState = AIState.Attack;
|
|
agent.ResetPath();
|
|
return;
|
|
}
|
|
|
|
if (distanceToPlayer > loseRange)
|
|
{
|
|
currentState = AIState.Wander;
|
|
agent.ResetPath();
|
|
return;
|
|
}
|
|
|
|
if (!isKnockbacking)
|
|
{
|
|
agent.SetDestination(playerTransform.position);
|
|
}
|
|
}
|
|
|
|
private void AttackBehavior()
|
|
{
|
|
if (playerTransform == null) { currentState = AIState.Wander; return; }
|
|
|
|
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
|
|
|
|
if (distanceToPlayer > attackRange)
|
|
{
|
|
currentState = AIState.Chase;
|
|
return;
|
|
}
|
|
|
|
if (!isCooldown)
|
|
{
|
|
StartCoroutine(PerformPunchAttack());
|
|
}
|
|
}
|
|
|
|
private void UpdateAnimation()
|
|
{
|
|
float targetSpeed = 0f;
|
|
if (!isAttacking && !isKnockbacking && currentState != AIState.Stun && agent.hasPath && agent.velocity.magnitude > 0.1f)
|
|
{
|
|
targetSpeed = 1f;
|
|
}
|
|
anim.SetFloat(SpeedHash, targetSpeed, 0.1f, Time.deltaTime);
|
|
}
|
|
|
|
private void UpdateRotationOnAttack()
|
|
{
|
|
if (currentState == AIState.Attack && playerTransform != null && !isKnockbacking && currentState != AIState.Stun)
|
|
{
|
|
Vector3 direction = (playerTransform.position - transform.position).normalized;
|
|
direction.y = 0;
|
|
|
|
if (direction != Vector3.zero)
|
|
{
|
|
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), Time.deltaTime * 10f);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void TriggerKnockback(float duration)
|
|
{
|
|
if (isDead || currentState == AIState.Stun) return;
|
|
|
|
SetFistCollider(false);
|
|
StopAllCoroutines();
|
|
isAttacking = false;
|
|
isWandering = false;
|
|
|
|
if (agent.isOnNavMesh) agent.isStopped = false;
|
|
StartCoroutine(KnockbackRoutine(duration));
|
|
}
|
|
|
|
public void TriggerParryStun(float duration)
|
|
{
|
|
if (isDead) return;
|
|
|
|
SetFistCollider(false);
|
|
StopAllCoroutines();
|
|
isAttacking = false;
|
|
isWandering = false;
|
|
isKnockbacking = false;
|
|
|
|
StartCoroutine(ParryStunRoutine(duration));
|
|
}
|
|
|
|
public void DisableAIOnDeath()
|
|
{
|
|
isDead = true;
|
|
StopAllCoroutines();
|
|
if (agent != null && agent.isOnNavMesh)
|
|
{
|
|
agent.isStopped = true;
|
|
agent.ResetPath();
|
|
}
|
|
}
|
|
|
|
private IEnumerator ParryStunRoutine(float duration)
|
|
{
|
|
currentState = AIState.Stun;
|
|
|
|
if (agent.isOnNavMesh)
|
|
{
|
|
agent.isStopped = true;
|
|
agent.ResetPath();
|
|
}
|
|
|
|
anim.SetTrigger(StunHash);
|
|
yield return WaitForUnpausedSeconds(duration);
|
|
|
|
if (agent.isOnNavMesh) agent.isStopped = false;
|
|
|
|
currentState = AIState.Chase;
|
|
isCooldown = false;
|
|
StartCoroutine(AILoop());
|
|
}
|
|
|
|
private IEnumerator KnockbackRoutine(float duration)
|
|
{
|
|
isKnockbacking = true;
|
|
|
|
if (agent.isOnNavMesh)
|
|
{
|
|
agent.isStopped = true;
|
|
agent.ResetPath();
|
|
}
|
|
|
|
anim.SetTrigger(HitHash);
|
|
yield return WaitForUnpausedSeconds(duration);
|
|
|
|
if (agent.isOnNavMesh) agent.isStopped = false;
|
|
|
|
isKnockbacking = false;
|
|
isCooldown = false;
|
|
StartCoroutine(AILoop());
|
|
}
|
|
|
|
private IEnumerator WanderMoveRoutine()
|
|
{
|
|
isWandering = true;
|
|
Vector3 newPos = Vector3.zero;
|
|
bool isPathValid = false;
|
|
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
newPos = GetRandomNavMeshPoint(transform.position, wanderRadius);
|
|
NavMeshPath path = new NavMeshPath();
|
|
if (agent.CalculatePath(newPos, path) && path.status == NavMeshPathStatus.PathComplete)
|
|
{
|
|
isPathValid = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (isPathValid)
|
|
{
|
|
agent.SetDestination(newPos);
|
|
float elapsedTime = 0f;
|
|
|
|
while (agent.remainingDistance > agent.stoppingDistance)
|
|
{
|
|
yield return WaitForUnpausedSeconds(0.5f);
|
|
elapsedTime += 0.5f;
|
|
if (elapsedTime >= wanderTimeout)
|
|
{
|
|
agent.ResetPath();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
yield return WaitForUnpausedSeconds(Random.Range(minWaitTime, maxWaitTime));
|
|
isWandering = false;
|
|
}
|
|
|
|
private IEnumerator PerformPunchAttack()
|
|
{
|
|
isAttacking = true;
|
|
isCooldown = true;
|
|
|
|
anim.SetTrigger(TelegraphHash);
|
|
yield return WaitForUnpausedSeconds(telegraphDuration);
|
|
|
|
if (isKnockbacking || isDead)
|
|
{
|
|
isAttacking = false;
|
|
yield break;
|
|
}
|
|
|
|
anim.SetTrigger(AttackHash);
|
|
}
|
|
|
|
public void OnPunchHit()
|
|
{
|
|
if (isKnockbacking || currentState == AIState.Stun || isPaused) return;
|
|
SetFistCollider(true);
|
|
}
|
|
|
|
public void OnRecoveryEnd()
|
|
{
|
|
if (isKnockbacking || currentState == AIState.Stun) return;
|
|
|
|
SetFistCollider(false);
|
|
isAttacking = false;
|
|
|
|
if (playerTransform == null)
|
|
{
|
|
currentState = AIState.Wander;
|
|
isCooldown = false;
|
|
return;
|
|
}
|
|
|
|
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
|
|
|
|
if (distanceToPlayer <= attackRange)
|
|
{
|
|
StartCoroutine(ResetCooldownQuickly());
|
|
}
|
|
else
|
|
{
|
|
currentState = AIState.Chase;
|
|
StartCoroutine(ResetCooldown());
|
|
}
|
|
}
|
|
|
|
private IEnumerator ResetCooldownQuickly()
|
|
{
|
|
yield return WaitForUnpausedSeconds(comboInterval);
|
|
isCooldown = false;
|
|
}
|
|
|
|
private IEnumerator ResetCooldown()
|
|
{
|
|
yield return WaitForUnpausedSeconds(attackCooldown);
|
|
isCooldown = false;
|
|
}
|
|
|
|
private void SetFistCollider(bool enabledState)
|
|
{
|
|
if (rightHandCollider != null) rightHandCollider.enabled = enabledState;
|
|
}
|
|
|
|
private Vector3 GetRandomNavMeshPoint(Vector3 center, float radius)
|
|
{
|
|
Vector3 randomDirection = Random.insideUnitSphere * radius + center;
|
|
if (NavMesh.SamplePosition(randomDirection, out NavMeshHit hit, radius, NavMesh.AllAreas))
|
|
{
|
|
return hit.position;
|
|
}
|
|
return center;
|
|
}
|
|
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
Gizmos.color = Color.red;
|
|
Gizmos.DrawWireSphere(transform.position, detectionRange);
|
|
Gizmos.color = Color.yellow;
|
|
Gizmos.DrawWireSphere(transform.position, loseRange);
|
|
Gizmos.color = Color.blue;
|
|
Gizmos.DrawWireSphere(transform.position, attackRange);
|
|
}
|
|
} |