diff --git a/Assets/XR/Settings/OpenXRPackageSettings.asset b/Assets/XR/Settings/OpenXRPackageSettings.asset index 060a7a5..50424f1 100644 --- a/Assets/XR/Settings/OpenXRPackageSettings.asset +++ b/Assets/XR/Settings/OpenXRPackageSettings.asset @@ -1273,8 +1273,8 @@ MonoBehaviour: features: - {fileID: 7011443450567333355} - {fileID: -671281728444007487} - - {fileID: 6956056911768161135} - - {fileID: 5091991234558418143} + - {fileID: -901939997048047619} + - {fileID: -1734471456787174204} - {fileID: 8980318060753866109} - {fileID: -6559185765804338520} - {fileID: -4756202077021157709} @@ -1285,7 +1285,7 @@ MonoBehaviour: - {fileID: 348827886137839178} - {fileID: -7499073031166464939} - {fileID: 7666556398442971420} - - {fileID: 1118878472687837173} + - {fileID: 431653824124869831} - {fileID: 3985858319333531581} - {fileID: -6511185071715886993} - {fileID: -4363231183632145407} diff --git a/Assets/_Scripts/BossController.cs b/Assets/_Scripts/BossController.cs index 48ea971..daa21f2 100644 --- a/Assets/_Scripts/BossController.cs +++ b/Assets/_Scripts/BossController.cs @@ -6,64 +6,40 @@ using System.Collections.Generic; [RequireComponent(typeof(Animator))] public class BossController : MonoBehaviour { - #region インスペクター設定項目 - [Header("ステータス設定")] [SerializeField] private float maxHp = 500f; [SerializeField] private float currentHp = 500f; - [SerializeField] public float chaseRange = 15f; - [SerializeField] public float biteRange = 5f; + [field: SerializeField] public float ChaseRange { get; private set; } = 15f; + [field: SerializeField] public float BiteRange { get; private set; } = 5f; + + [field: SerializeField] public float ChargeSpeed { get; private set; } = 14f; + [field: SerializeField] public float ChargeDuration { get; private set; } = 2f; + [field: SerializeField] public float ChargeMinimumDistance { get; private set; } = 6f; + [field: SerializeField] public float ChargeCooldownDuration { get; private set; } = 4f; + [Tooltip("突進する前の、その場での咆哮・溜め時間(秒)")] + [field: SerializeField] public float ChargeRoarDuration { get; private set; } = 5f; // ★インスペクターに逃がして保守性を向上 - [Header("必殺技(天井)設定")] - [Tooltip("天井の待機座標となる空のGameObjectのTransformを指定してください")] [SerializeField] public Transform ceilingAnchor; - [Tooltip("地上に戻る際の、中央の着地座標のTransformを指定してください")] [SerializeField] public Transform groundAnchor; - [Tooltip("天井から糸を発射する範囲の広さ")] - [SerializeField] public float attackRadius = 10f; - [Tooltip("糸が降る前の予兆マークのPrefab")] + [field: SerializeField] public float AttackRadius { get; private set; } = 10f; + [field: SerializeField] public float JumpRoarDuration { get; private set; } = 2f; + [SerializeField] private GameObject warningMarkerPrefab; - [Tooltip("地面に残る糸(コライダー&鈍足スクリプト付き)のPrefab")] [SerializeField] private GameObject webPrefab; - [Header("突進(体当たり)設定")] - [SerializeField] public float chargeSpeed = 14f; - [SerializeField] public float chargeDuration = 2f; - [SerializeField] public float chargeMinimumDistance = 6f; - [SerializeField] public float chargeCooldownDuration = 4f; - - // ─── ボス固有の通常移動パラメーター(Start時に自動キャッシュされます) ─── + public NavMeshAgent Agent { get; private set; } + public Animator Anim { get; private set; } + public Transform Player { get; private set; } + public float ChargeCooldownTimer { get; private set; } = 0f; public float NormalSpeed { get; private set; } public float NormalAcceleration { get; private set; } - private void Start() - { - // インセペクターで設定されたNavMeshAgentの初期値を記憶しておく - if (Agent != null) - { - NormalSpeed = Agent.speed; - NormalAcceleration = Agent.acceleration; - } - } - #endregion - - #region コンポーネント・キャッシュ用(ステートからアクセス可能) - [HideInInspector] public NavMeshAgent Agent { get; private set; } - [HideInInspector] public Animator Anim { get; private set; } - [HideInInspector] public Transform Player { get; private set; } - public float ChargeCooldownTimer { get; private set; } = 0f; - #endregion - - #region FSM(状態管理)システム private ISpiderState currentState; - - // ステートのインスタンスをキャッシュ(ガベージコレクションの発生を抑える) private Dictionary stateCache; - // フェーズ管理用フラグ(50%と25%のトリガー管理) + // フェーズトリガー private bool triggered50 = false; private bool triggered25 = false; - public bool PendingUltimate { get; set; } = false; // 必殺技の発動権があるか - #endregion + public bool PendingUltimate { get; set; } = false; private void Awake() { @@ -76,6 +52,15 @@ public class BossController : MonoBehaviour InitializeFSM(); } + private void Start() + { + if (Agent != null) + { + NormalSpeed = Agent.speed; + NormalAcceleration = Agent.acceleration; + } + } + private void InitializeFSM() { stateCache = new Dictionary @@ -89,7 +74,6 @@ public class BossController : MonoBehaviour { typeof(SpiderStunState), new SpiderStunState(this) } }; - // 初期ステートをIdleに設定 ChangeState(); } @@ -108,6 +92,7 @@ public class BossController : MonoBehaviour if (currentState != null) { currentState.ExitState(); + ResetAllAnimationTriggers(); Debug.Log($"🕷️ BossSpider [Exit]: {currentState.GetType().Name}"); } @@ -116,9 +101,6 @@ public class BossController : MonoBehaviour currentState.EnterState(); } - /// - /// ダメージを受け取る外部窓口。HPトリガーによるフェーズ遷移をここで監視する - /// public void TakeDamage(float damage) { currentHp -= damage; @@ -127,14 +109,12 @@ public class BossController : MonoBehaviour Debug.Log($"💥 ボスに {damage} ダメージ。残りHP: {hpPercentage:F1}%"); - // 25%以下カットのトリガー if (hpPercentage <= 25f && !triggered25) { triggered25 = true; PendingUltimate = true; TriggerPhaseTransition(); } - // 50%以下カットのトリガー else if (hpPercentage <= 50f && !triggered50 && hpPercentage > 25f) { triggered50 = true; @@ -145,7 +125,6 @@ public class BossController : MonoBehaviour private void TriggerPhaseTransition() { - // 咆哮、ジャンプ、スタン中以外であれば、現在の行動をキャンセルして即座に天井へエスケープ if (currentState is not SpiderJumpToCeilingState && currentState is not SpiderCeilingAttackState && currentState is not SpiderStunState) @@ -154,12 +133,8 @@ public class BossController : MonoBehaviour } } - /// - /// 【パリィ連携窓口】プレイヤーのVRInputPunchなどから、パリィ成功時にこのメソッドが叩かれる - /// public void TriggerParryStun() { - // パリィを受け付ける攻撃ステート(噛みつき、体当たり)のときのみスタンに移行する if (currentState is SpiderBiteState || currentState is SpiderChargeState) { ChangeState(); @@ -168,10 +143,9 @@ public class BossController : MonoBehaviour public void ResetChargeCooldown() { - ChargeCooldownTimer = chargeCooldownDuration; + ChargeCooldownTimer = ChargeCooldownDuration; } - #region ファクトリメソッド(エフェクトやオブジェクト生成の保守性を担保) public void SpawnWarningAndWeb(Vector3 targetGroundPos, float delay, float webDuration) { StartCoroutine(SpawnWebRoutine(targetGroundPos, delay, webDuration)); @@ -179,7 +153,6 @@ public class BossController : MonoBehaviour private System.Collections.IEnumerator SpawnWebRoutine(Vector3 pos, float delay, float duration) { - // 1. 地面に予兆マークを生成 GameObject marker = null; if (warningMarkerPrefab != null) { @@ -187,21 +160,15 @@ public class BossController : MonoBehaviour } yield return new WaitForSeconds(delay); - - // 2. 予兆マークを消し、本物の糸オブジェクトを生成 if (marker != null) Destroy(marker); if (webPrefab != null) { GameObject web = Instantiate(webPrefab, pos, Quaternion.identity); - Destroy(web, duration); // 数秒後に自動消滅 + Destroy(web, duration); } } - /// - /// ★超重要:アニメーターに残ってしまった不要なトリガーをすべて強制消去(リセット)する - /// これを各ステートのExit時に呼ぶことで、アニメーションが固まる・遅れて暴発するバグを根絶します - /// public void ResetAllAnimationTriggers() { if (Anim == null) return; @@ -212,14 +179,26 @@ public class BossController : MonoBehaviour Anim.ResetTrigger("Land"); Anim.ResetTrigger("StunStart"); } - #endregion } -#region 1. ステート共通インターフェース +/// 共通データ(boss参照)を自動保持し、各子ステートの無駄なコードを極限まで削ります。 +public abstract class SpiderStateBase : ISpiderState +{ + protected readonly BossController boss; + + protected SpiderStateBase(BossController owner) + { + boss = owner; + } + + public virtual void EnterState() { } + public abstract void UpdateState(); + public virtual void ExitState() { } +} + public interface ISpiderState { void EnterState(); void UpdateState(); void ExitState(); -} -#endregion \ No newline at end of file +} \ No newline at end of file diff --git a/Assets/_Scripts/SpiderIdleState.cs b/Assets/_Scripts/SpiderIdleState.cs index 1e219c6..435d3b0 100644 --- a/Assets/_Scripts/SpiderIdleState.cs +++ b/Assets/_Scripts/SpiderIdleState.cs @@ -1,53 +1,43 @@ using UnityEngine; -public class SpiderIdleState : ISpiderState +public class SpiderIdleState : SpiderStateBase { - private BossController boss; - public SpiderIdleState(BossController owner) => boss = owner; + public SpiderIdleState(BossController owner) : base(owner) { } - public void EnterState() => boss.Anim.SetTrigger("Idle"); - public void ExitState() { } + public override void EnterState() => boss.Anim.SetTrigger("Idle"); - public void UpdateState() + public override void UpdateState() { if (boss.Player == null) return; - // プレイヤーを検知したら即座に追跡へ - if (Vector3.Distance(boss.transform.position, boss.Player.position) <= boss.chaseRange) + if (Vector3.Distance(boss.transform.position, boss.Player.position) <= boss.ChaseRange) { boss.ChangeState(); } } } -public class SpiderChaseState : ISpiderState +public class SpiderChaseState : SpiderStateBase { - private BossController boss; + public SpiderChaseState(BossController owner) : base(owner) { } - public SpiderChaseState(BossController owner) => boss = owner; + public override void EnterState() => boss.Agent.isStopped = false; - public void EnterState() => boss.Agent.isStopped = false; - public void ExitState() - { - boss.Agent.isStopped = true; - boss.ResetAllAnimationTriggers(); - } - - public void UpdateState() + public override void UpdateState() { if (boss.Player == null) return; float distance = Vector3.Distance(boss.transform.position, boss.Player.position); - // 6m以上かつクールダウン終了 ➔ 1歩も歩かずに即突進 - if (distance >= boss.chargeMinimumDistance && boss.ChargeCooldownTimer <= 0f) + // 遠距離突進判定(カプセル化されたプロパティを使用) + if (distance >= boss.ChargeMinimumDistance && boss.ChargeCooldownTimer <= 0f) { boss.ChangeState(); - return; // 移行したので以下の歩き処理は実行しない + return; } - // 判定2:至近距離(2.5m以内)➔ 通常噛みつき - if (distance <= boss.biteRange) + // 至近距離噛みつき判定 + if (distance <= boss.BiteRange) { boss.ChangeState(); return; @@ -58,44 +48,27 @@ public class SpiderChaseState : ISpiderState } } -// ========================================== -// ■ STATE: BITE (完全静止・滑り移動ゼロ修正版) -// ========================================== -public class SpiderBiteState : ISpiderState +public class SpiderBiteState : SpiderStateBase { - private BossController boss; private float stateTimer; - public SpiderBiteState(BossController owner) => boss = owner; + public SpiderBiteState(BossController owner) : base(owner) { } - public void EnterState() + public override void EnterState() { - stateTimer = 1.5f; // 噛みつきアニメーションの時間(環境に合わせて調整してください) + stateTimer = 1.5f; - // 1. 【物理ブレーキ】これまでの移動経路を完全に忘却させ、慣性速度も強制ゼロにする boss.Agent.ResetPath(); boss.Agent.isStopped = true; boss.Agent.velocity = Vector3.zero; - // 2. 【アニメーションブレーキ】直前の走っていた速度(Speed)を強制的に0リセット - // これをしないと、速度の減衰が遅れて「歩きながら噛み付く」ブレンドモーションになります boss.Anim.SetFloat("Speed", 0f); - - // 3. 噛みつきモーションを発動 boss.Anim.SetTrigger("Attack_Bite"); - Debug.Log("🎯 蜘蛛ボス:噛みつき攻撃! 経路を完全消去してその場に固定しました。"); + Debug.Log("🎯 蜘蛛ボス:噛みつき攻撃! その場に完全固定。"); } - public void ExitState() + public override void UpdateState() { - // 次のステート(Chaseなど)に安全に移行できるよう、ナビゲーションの停止を解除 - boss.Agent.isStopped = false; - boss.ResetAllAnimationTriggers(); - } - - public void UpdateState() - { - // 4. 【徹底防衛】アニメーションの再生中に予期せぬ物理反発や滑りが発生しないよう、毎フレーム速度をゼロに固定 boss.Agent.velocity = Vector3.zero; stateTimer -= Time.deltaTime; @@ -107,55 +80,44 @@ public class SpiderBiteState : ISpiderState } } -// ========================================== -// ■ STATE: CHARGE (慣性ブレーキ完全修正版) -// ========================================== -public class SpiderChargeState : ISpiderState +public class SpiderChargeState : SpiderStateBase { - private BossController boss; private float roarTimer; private float chargeTimer; private Vector3 chargeDirection; private bool isChargingPhase; - public SpiderChargeState(BossController owner) => boss = owner; + public SpiderChargeState(BossController owner) : base(owner) { } - public void EnterState() + public override void EnterState() { - roarTimer = 5f; // 溜め(咆哮)時間 - chargeTimer = boss.chargeDuration; + roarTimer = boss.ChargeRoarDuration; + chargeTimer = boss.ChargeDuration; isChargingPhase = false; - // ★超重要:ナビゲーションを止め、さらに残っている移動速度(慣性)を完全にゼロリセットする! boss.Agent.isStopped = true; - boss.Agent.velocity = Vector3.zero; // これにより滑り込みを100%防止します + boss.Agent.velocity = Vector3.zero; boss.ResetChargeCooldown(); - - // その場に完全静止した状態で、満を持して咆哮アニメーションを再生 boss.Anim.SetTrigger("Attack_Roar"); - Debug.Log("🔊 蜘蛛ボス:遠距離を検知してその場でピタッと完全停止。咆哮(溜め)を開始!"); + Debug.Log($"🔊 蜘蛛ボス:遠距離を検知。その場でピタッと完全停止。咆哮(溜め {roarTimer}秒)を開始!"); } - public void ExitState() + public override void ExitState() { boss.Anim.SetBool("IsCharging", false); - boss.Agent.speed = boss.NormalSpeed; boss.Agent.acceleration = boss.NormalAcceleration; boss.Agent.isStopped = false; - boss.ResetAllAnimationTriggers(); } - public void UpdateState() + public override void UpdateState() { - // ─── フェーズ1:咆哮・溜めロックオン(その場で旋回のみ) ─── + // フェーズ1:咆哮・溜めロックオン if (!isChargingPhase) { roarTimer -= Time.deltaTime; - - // ★安全対策:溜め期間中、予期せぬ物理の反発などで1ミリも動かないように速度をゼロに固定し続ける - boss.Agent.velocity = Vector3.zero; + boss.Agent.velocity = Vector3.zero; // 完全固定 if (boss.Player != null) { @@ -167,7 +129,6 @@ public class SpiderChargeState : ISpiderState } } - // 溜め終了 ➔ 「発射」の瞬間 if (roarTimer <= 0f) { isChargingPhase = true; @@ -180,13 +141,11 @@ public class SpiderChargeState : ISpiderState chargeDirection.y = 0; } - // 20m先の目的地に向かって、NavMeshAgentによる超高速突進ダッシュを発射 Vector3 targetDestination = boss.transform.position + chargeDirection * 20f; boss.Agent.isStopped = false; - boss.Agent.speed = boss.chargeSpeed; + boss.Agent.speed = boss.ChargeSpeed; boss.Agent.acceleration = 120f; - boss.Agent.SetDestination(targetDestination); Debug.Log("🚀 蜘蛛ボス:ロックオン完了。全速力で突進ダッシュを発射!"); @@ -194,58 +153,52 @@ public class SpiderChargeState : ISpiderState return; } - // ─── フェーズ2:突進中 ─── + // フェーズ2:突進移動中 chargeTimer -= Time.deltaTime; - // 壁やプレイヤーに激突して急停止したか、設定時間が経過したら通常状態へ戻る - if (chargeTimer <= 0f || (boss.Agent.velocity.magnitude < 0.2f && chargeTimer < (boss.chargeDuration - 0.3f))) + if (chargeTimer <= 0f || (boss.Agent.velocity.magnitude < 0.2f && chargeTimer < (boss.ChargeDuration - 0.3f))) { boss.ChangeState(); } } } -public class SpiderJumpToCeilingState : ISpiderState +public class SpiderJumpToCeilingState : SpiderStateBase { - private BossController boss; private float roarTimer; private float jumpProgress; private Vector3 startPos; private bool isJumpingPhase; - public SpiderJumpToCeilingState(BossController owner) => boss = owner; + public SpiderJumpToCeilingState(BossController owner) : base(owner) { } - public void EnterState() + public override void EnterState() { - roarTimer = 2.0f; // ★天井に飛ぶ前の大威嚇の時間 (2.0秒) + roarTimer = boss.JumpRoarDuration; jumpProgress = 0f; startPos = boss.transform.position; isJumpingPhase = false; boss.Agent.enabled = false; - - // 1. 必殺技移行を知らせる威嚇の咆哮 boss.Anim.SetTrigger("Attack_Roar"); - Debug.Log("🚨 蜘蛛ボス:必殺技前の一大威嚇(咆哮)! このあと天井へ飛びます。"); + Debug.Log($"🚨 蜘蛛ボス:必殺技前の一大威嚇(咆哮 {roarTimer}秒)!"); } - public void ExitState() { } - - public void UpdateState() + public override void UpdateState() { - // フェーズ1:飛び立つ前の威嚇ポーズ(その場で足止め) + // フェーズ1:飛び立つ前の威嚇 if (!isJumpingPhase) { roarTimer -= Time.deltaTime; if (roarTimer <= 0f) { isJumpingPhase = true; - boss.Anim.SetTrigger("JumpUp"); // ジャンプアニメーションへ + boss.Anim.SetTrigger("JumpUp"); } return; } - // フェーズ2:本来の大ジャンプ空中移動 + // フェーズ2:ジャンプ空中移動 if (boss.ceilingAnchor == null) return; jumpProgress += Time.unscaledDeltaTime * 1.5f; @@ -263,35 +216,33 @@ public class SpiderJumpToCeilingState : ISpiderState } } -public class SpiderCeilingAttackState : ISpiderState +public class SpiderCeilingAttackState : SpiderStateBase { - private BossController boss; - private float stateDuration = 7.0f; // 必殺技全体の長さ(秒) - private float shotInterval = 0.4f; // 糸を降らせる間隔 + private float stateDuration = 7.0f; + private float shotInterval = 0.4f; private float timer = 0f; private float shotTimer = 0f; - public SpiderCeilingAttackState(BossController owner) => boss = owner; + public SpiderCeilingAttackState(BossController owner) : base(owner) { } - public void EnterState() + public override void EnterState() { timer = stateDuration; shotTimer = 0f; boss.Anim.SetBool("IsOnCeiling", true); - boss.PendingUltimate = false; // 発動権を消化 + boss.PendingUltimate = false; Debug.Log("🕸️ 蜘蛛ボス:天井から無数の糸を乱射中!"); } - public void ExitState() + public override void ExitState() { boss.Anim.SetBool("IsOnCeiling", false); - // 地上の着地座標へ位置をリセットしてNavMeshを復帰 if (boss.groundAnchor != null) boss.transform.position = boss.groundAnchor.position; boss.Agent.enabled = true; boss.Anim.SetTrigger("Land"); } - public void UpdateState() + public override void UpdateState() { timer -= Time.deltaTime; shotTimer -= Time.deltaTime; @@ -302,7 +253,6 @@ public class SpiderCeilingAttackState : ISpiderState shotTimer = shotInterval; } - // 時間が来たら、地上へ復帰 if (timer <= 0f) { boss.ChangeState(); @@ -311,50 +261,43 @@ public class SpiderCeilingAttackState : ISpiderState private void ExecuteRandomWebAttack() { - // ボスの中央(地上座標)を基準に、ランダムな円内に糸を降らせる - Vector2 randomCircle = Random.insideUnitCircle * boss.attackRadius; + Vector2 randomCircle = Random.insideUnitCircle * boss.AttackRadius; Vector3 targetGroundPos = new Vector3( boss.groundAnchor.position.x + randomCircle.x, boss.groundAnchor.position.y, boss.groundAnchor.position.z + randomCircle.y ); - // 予兆を出し、1.0秒後に2.5秒間残る糸を生成する boss.SpawnWarningAndWeb(targetGroundPos, 1.0f, 2.5f); } } -// ========================================== -// ■ STATE: STUN (チャンスタイム - パリィ成功時) -// ========================================== -public class SpiderStunState : ISpiderState +public class SpiderStunState : SpiderStateBase { - private BossController boss; private float stunTimer; - public SpiderStunState(BossController owner) => boss = owner; + public SpiderStunState(BossController owner) : base(owner) { } - public void EnterState() + public override void EnterState() { - stunTimer = 4.0f; // 4秒間の完全な無防備状態(チャンスタイム) + stunTimer = 4.0f; boss.Anim.SetTrigger("StunStart"); boss.Anim.SetBool("IsStunned", true); if (boss.Agent.enabled) boss.Agent.isStopped = true; - Debug.Log("🛡️ パリィ成功! 蜘蛛ボスがひっくり返ってスタン中! チャンスタイム!"); + Debug.Log("🛡️ パリィ成功! チャンスタイム!"); } - public void ExitState() + public override void ExitState() { boss.Anim.SetBool("IsStunned", false); if (boss.Agent.enabled) boss.Agent.isStopped = false; } - public void UpdateState() + public override void UpdateState() { stunTimer -= Time.deltaTime; if (stunTimer <= 0f) { - // スンが明けたら、もしHPトリガーが裏で引かれていれば優先して天井へ if (boss.PendingUltimate) boss.ChangeState(); else boss.ChangeState(); }