アニメーション追加&改善

This commit is contained in:
oogushiyuuga
2026-05-28 16:02:53 +09:00
parent b47e878d81
commit 1568a27840
1361 changed files with 708 additions and 971 deletions

View File

@@ -3,198 +3,386 @@ using UnityEngine.AI;
using System.Collections;
[RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(Animator))]
public class EnemyAI : MonoBehaviour
{
// 敵の状態を定義する列挙型FSM
public enum AIState { Wander, Chase }
/// 敵AIの状態を表すステート
public enum AIState
{
Wander, // 徘徊
Chase, // 追跡
Attack // 攻撃
}
[Header("現在の状態")]
[SerializeField] private AIState currentState = AIState.Wander;
[Header("索敵設定")]
[Tooltip("プレイヤーを検知する距離")]
[SerializeField] private float detectionRange = 10f;
[Tooltip("プレイヤーを見失う距離(検知距離より広く設定するのがコツ)")]
[SerializeField] private float loseRange = 15f;
[Tooltip("プレイヤーのタグ名")]
[SerializeField] private string playerTag = "Player";
[Header("攻撃設定")]
[Tooltip("この距離に近づいたら攻撃 (メートル)")]
[SerializeField] private float attackRange = 1.5f;
[Tooltip("パンチの攻撃力")]
[SerializeField] private float attackDamage = 10f;
[Tooltip("一度攻撃が終了してから、次の攻撃を開始するまでの待ち時間 (秒)")]
[SerializeField] private float attackCooldown = 2.0f;
[Tooltip("攻撃の予兆時間 (秒)")]
[SerializeField] private float telegraphDuration = 0.6f;
[Header("ループ攻撃の調整")]
[Tooltip("連続攻撃の間に挟む最低限のインターバル (秒)")]
[SerializeField] private float comboInterval = 0.2f;
[Header("徘徊Wander設定")]
[SerializeField] private float wanderRadius = 8f;
[SerializeField] private float minWaitTime = 0.1f;
[SerializeField] private float maxWaitTime = 1f;
[Tooltip("目的地に向けて移動を開始してから、何秒で諦めるか")]
[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 = false;
private bool isAttacking = false;
private bool isWandering = false;
void Start()
private void Start()
{
agent = GetComponent<NavMeshAgent>();
anim = GetComponent<Animator>();
// 負荷軽減のため、AIの思考ループコルーチン開始(毎フレーム Update で処理しない)
// AIのメインループコルーチン開始
StartCoroutine(AILoop());
}
void Update()
private void Update()
{
// 1. NavMeshAgentの現在の速度Vector3の「大きさ長さ」を計算してフロート値にする
// これにより、前後左右どの向きに動いていても「進んでいるスピード」が正の数値として取れます
float currentSpeed = agent.velocity.magnitude;
// 毎フレームのアニメーションパラメータ更新処理
UpdateAnimation();
Debug.Log($"[アニメデバッグ] 敵の計算速度: {currentSpeed:F2} | Animatorコンポーネント: {anim != null}");
// 2. Animatorの"Speed"パラメーターに数値を送る
anim.SetFloat("Speed", currentSpeed);
// 攻撃ステートにおけるプレイヤーへの方向転換処理
UpdateRotationOnAttack();
}
// AIの意思決定を行うメインループ0.2秒ごとに実行して負荷を劇的に下げる)
#region AI ()
private IEnumerator AILoop()
{
while (true)
{
// 1. プレイヤーを探す(まだ見つけていない場合のみ)
if (playerTransform == null)
// プレイヤーが未検出の場合はタグから検索
if (playerTransform == null)
{
FindPlayer();
}
// 2. 現在のステート(状態)に応じて処理を分岐
switch (currentState)
// 攻撃アニメーションの再生中でない場合のみ、ステートに応じた行動を実行
if (!isAttacking)
{
case AIState.Wander:
WanderBehavior();
break;
case AIState.Chase:
ChaseBehavior();
break;
switch (currentState)
{
case AIState.Wander:
WanderBehavior();
break;
case AIState.Chase:
ChaseBehavior();
break;
case AIState.Attack:
AttackBehavior();
break;
}
}
// 毎フレームではなく、0.2秒1秒に5回だけ実行するQuest 3向けの最適化
// 毎フレーム実行を避け、0.2秒待機することでCPU負荷を軽減
yield return new WaitForSeconds(0.2f);
}
}
// シーン内からプレイヤー(タグ付き)を探すメソッド
private void FindPlayer()
{
GameObject playerObj = GameObject.FindGameObjectWithTag(playerTag);
if (playerObj != null)
if (playerObj != null)
{
playerTransform = playerObj.transform;
}
}
#endregion
// ─── 【徘徊状態】の行動 ───
#region
/// 【徘徊状態】の思考ロジック
private void WanderBehavior()
{
if (playerTransform != null)
{
// プレイヤーとの距離を計算
// プレイヤーとの距離を測定し、索敵範囲内なら追跡ステートへ遷移
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
// 索敵範囲内に入ったら「追跡状態」へ遷移
if (distanceToPlayer <= detectionRange)
{
currentState = AIState.Chase;
isWandering = false;
agent.ResetPath(); // 徘徊の目的地をリセット
agent.ResetPath();
Debug.Log($"👁️ {gameObject.name}: プレイヤーを発見! 追跡します。");
return;
}
}
// 徘徊の移動ロジック
if (!isWandering)
// すでに徘徊移動中でなければ、新しい目的地への移動コルーチンを開始
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();
Debug.Log($"❓ {gameObject.name}: プレイヤーを見失った。");
return;
}
// クールダウン中であっても、追跡ステートである限りはプレイヤーの現在位置を追い続ける
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;
}
// クールダウン中でなければ攻撃コルーチンを実行クールダウン中の場合は、Update内の処理によりプレイヤーの方向を向きながら待機します
if (!isCooldown)
{
StartCoroutine(PerformPunchAttack());
}
}
#endregion
#region
/// エージェントの移動状態に応じてAnimatorのSpeedパラメータを更新する
private void UpdateAnimation()
{
float targetSpeed = 0f;
// 攻撃中でなく、経路が存在し、かつ実際に一定以上の速度で移動している場合のみSpeedを1にする
if (!isAttacking && agent.hasPath && agent.velocity.magnitude > 0.1f)
{
targetSpeed = 1f;
}
// パラメータをなめらかに変化させて渡す
anim.SetFloat("Speed", targetSpeed, 0.1f, Time.deltaTime);
}
/// 攻撃ステートの際、プレイヤーの方向へ滑らかに旋回させる
private void UpdateRotationOnAttack()
{
if (currentState == AIState.Attack && playerTransform != null)
{
Vector3 direction = (playerTransform.position - transform.position).normalized;
direction.y = 0; // 上下方向の傾き(ピッチ回転)を無視
if (direction != Vector3.zero)
{
// Y軸の回転のみを滑らかに補間して適用
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), Time.deltaTime * 10f);
}
}
}
#endregion
#region
/// 徘徊時のランダム移動を制御するコルーチン
private IEnumerator WanderMoveRoutine()
{
isWandering = true;
Vector3 newPos = Vector3.zero;
bool isPathValid = false;
int maxAttempts = 10;
// ランダムな目的地を取得して移動開始
Vector3 newPos = GetRandomNavMeshPoint(transform.position, wanderRadius);
agent.SetDestination(newPos);
float elapsedTime = 0f; // 経過時間を計るカウンター
// 目的地に到着するまで待つ(ただしタイムアウト時間を超えたらループを抜ける)
while (agent.remainingDistance > agent.stoppingDistance)
// 有効な経路(壁などで分断されていないルート)が見つかるまで試行
for (int i = 0; i < maxAttempts; i++)
{
// 0.5秒ごとにチェック
yield return new WaitForSeconds(0.5f);
elapsedTime += 0.5f;
// 【スタック対策】設定した制限時間を超えた場合
if (elapsedTime >= wanderTimeout)
newPos = GetRandomNavMeshPoint(transform.position, wanderRadius);
NavMeshPath path = new NavMeshPath();
if (agent.CalculatePath(newPos, path))
{
Debug.Log($"⚠️ {gameObject.name}: 目的地({newPos})に到達できないため諦めました。");
agent.ResetPath(); // 動けない経路(バグ目的地)を一旦クリアする
break; // whileループを強制終了して次の目的地設定へ進める
if (path.status == NavMeshPathStatus.PathComplete)
{
isPathValid = true;
break;
}
}
}
// 到着後(または諦めた後)、ランダムな時間だけその場で待機して次の徘徊へ
// 有効な目的地が設定できた場合の移動処理
if (isPathValid)
{
agent.SetDestination(newPos);
float elapsedTime = 0f;
// 目的地に到達するか、タイムアウト時間を迎えるまで待機
while (agent.remainingDistance > agent.stoppingDistance)
{
yield return new WaitForSeconds(0.5f);
elapsedTime += 0.5f;
// スタック対策としてのタイムアウト判定
if (elapsedTime >= wanderTimeout)
{
agent.ResetPath();
break;
}
}
}
// 到着または諦めた後、ランダムな時間その場で待機
yield return new WaitForSeconds(Random.Range(minWaitTime, maxWaitTime));
isWandering = false;
}
// ─── 【追跡状態】の行動 ───
private void ChaseBehavior()
/// 予兆から攻撃アニメーションのトリガーまでを制御するコルーチン
private IEnumerator PerformPunchAttack()
{
isAttacking = true;
isCooldown = true; // ここで攻撃フラグをロックします
anim.SetTrigger("Telegraph");
Debug.Log($"⚠️ {gameObject.name}: 攻撃の予兆(溜め)開始");
yield return new WaitForSeconds(telegraphDuration);
anim.SetTrigger("Attack");
}
/// 連続攻撃成功時の、クイックなクールダウン解除処理
private IEnumerator ResetCooldownQuickly()
{
yield return new WaitForSeconds(comboInterval);
isCooldown = false;
}
/// コンボが途切れた際の、通常の攻撃クールダウン解除処理
private IEnumerator ResetCooldown()
{
// 指定された秒数待機する
yield return new WaitForSeconds(attackCooldown);
isCooldown = false;
Debug.Log($"✅ {gameObject.name}: 攻撃クールダウンが終了しました。再攻撃可能です。");
}
#endregion
#region
/// 攻撃アニメーションのヒットフレームで実行されるヒット判定Animation Eventから呼び出し
public void OnPunchHit()
{
if (playerTransform == null) return;
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
// ヒット判定の瞬間、プレイヤーが射程内に残っているか判定猶予として0.3m加算)
if (distanceToPlayer <= attackRange + 0.3f)
{
Debug.Log($"💥 敵のパンチがヒット!");
}
}
/// Recovery硬直アニメーションの終了時に実行される処理Animation Eventから呼び出し
public void OnRecoveryEnd()
{
isAttacking = false;
if (playerTransform == null)
{
currentState = AIState.Wander;
isCooldown = false;
return;
}
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
// 見失う距離loseRangeを超えたら「徘徊状態」に戻る
if (distanceToPlayer > loseRange)
// ループ判定:まだプレイヤーが射程内にいる場合はクイックな連続攻撃へ移行
if (distanceToPlayer <= attackRange)
{
currentState = AIState.Wander;
agent.ResetPath();
Debug.Log($"❓ {gameObject.name}: プレイヤーを見失った。徘徊に戻ります。");
return;
StartCoroutine(ResetCooldownQuickly());
Debug.Log($"🔄 {gameObject.name}: プレイヤーがまだ射程内です。連続攻撃(コンボ)を実行します。");
}
else
{
// プレイヤーが離れていた場合はコンボを抜け、追跡状態に戻ると同時に、通常のクールダウン(長めの待機)を開始
currentState = AIState.Chase;
StartCoroutine(ResetCooldown());
Debug.Log($"🏃 {gameObject.name}: プレイヤーが離れたため追跡に戻ります。({attackCooldown}秒の攻撃クールダウンを適用)");
}
// プレイヤーの位置を目的地に設定0.2秒ごとに更新されるため、滑らかに追従します)
agent.SetDestination(playerTransform.position);
}
#endregion
// NavMesh上のランダムな座標を取得する補助メソッ
#region
/// 指定された中心座標の範囲内から、NavMesh上の有効なランダム座標を取得する
private Vector3 GetRandomNavMeshPoint(Vector3 center, float radius)
{
Vector3 randomDirection = Random.insideUnitSphere * radius;
randomDirection += center;
NavMeshHit hit;
if (NavMesh.SamplePosition(randomDirection, out hit, radius, NavMesh.AllAreas))
if (NavMesh.SamplePosition(randomDirection, out hit, radius, NavMesh.AllAreas))
{
return hit.position;
}
return center;
}
// 【視覚的なデバッグ用】エディタ上で索敵範囲を線で表示する
/// エディタのSceneビューに索敵・攻撃範囲をギズモとして描画する
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);
}
#endregion
}