いろいろやってみた
Playerが蜘蛛にめり込まないようにしたい
This commit is contained in:
200
Assets/_Scripts/BossController.cs
Normal file
200
Assets/_Scripts/BossController.cs
Normal file
@@ -0,0 +1,200 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
using System.Collections.Generic;
|
||||
|
||||
[RequireComponent(typeof(NavMeshAgent))]
|
||||
[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 = 2.5f;
|
||||
|
||||
[Header("必殺技(天井)設定")]
|
||||
[Tooltip("天井の待機座標となる空のGameObjectのTransformを指定してください")]
|
||||
[SerializeField] public Transform ceilingAnchor;
|
||||
[Tooltip("地上に戻る際の、中央の着地座標のTransformを指定してください")]
|
||||
[SerializeField] public Transform groundAnchor;
|
||||
[Tooltip("天井から糸を発射する範囲の広さ")]
|
||||
[SerializeField] public float attackRadius = 10f;
|
||||
[Tooltip("糸が降る前の予兆マークのPrefab")]
|
||||
[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;
|
||||
#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<System.Type, ISpiderState> stateCache;
|
||||
|
||||
// フェーズ管理用フラグ(50%と25%のトリガー管理)
|
||||
private bool triggered50 = false;
|
||||
private bool triggered25 = false;
|
||||
public bool PendingUltimate { get; set; } = false; // 必殺技の発動権があるか
|
||||
#endregion
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Agent = GetComponent<NavMeshAgent>();
|
||||
Anim = GetComponent<Animator>();
|
||||
|
||||
GameObject pObj = GameObject.FindGameObjectWithTag("Player");
|
||||
if (pObj != null) Player = pObj.transform;
|
||||
|
||||
InitializeFSM();
|
||||
}
|
||||
|
||||
private void InitializeFSM()
|
||||
{
|
||||
stateCache = new Dictionary<System.Type, ISpiderState>
|
||||
{
|
||||
{ typeof(SpiderIdleState), new SpiderIdleState(this) },
|
||||
{ typeof(SpiderChaseState), new SpiderChaseState(this) },
|
||||
{ typeof(SpiderBiteState), new SpiderBiteState(this) },
|
||||
{ typeof(SpiderChargeState), new SpiderChargeState(this) },
|
||||
{ typeof(SpiderJumpToCeilingState), new SpiderJumpToCeilingState(this) },
|
||||
{ typeof(SpiderCeilingAttackState), new SpiderCeilingAttackState(this) },
|
||||
{ typeof(SpiderStunState), new SpiderStunState(this) }
|
||||
};
|
||||
|
||||
// 初期ステートをIdleに設定
|
||||
ChangeState<SpiderIdleState>();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
currentState?.UpdateState();
|
||||
|
||||
// ★新設:突進のクールダウンのみ、通常のUpdateで常に独立して減算する
|
||||
if (ChargeCooldownTimer > 0f)
|
||||
{
|
||||
ChargeCooldownTimer -= Time.deltaTime;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// オブジェクト指向に基づいた、型安全なステート遷移メソッド
|
||||
/// </summary>
|
||||
public void ChangeState<T>() where T : ISpiderState
|
||||
{
|
||||
if (currentState != null)
|
||||
{
|
||||
currentState.ExitState();
|
||||
Debug.Log($"🕷️ BossSpider [Exit]: {currentState.GetType().Name}");
|
||||
}
|
||||
|
||||
currentState = stateCache[typeof(T)];
|
||||
Debug.Log($"🕷️ BossSpider [Enter]: {currentState.GetType().Name}");
|
||||
currentState.EnterState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ダメージを受け取る外部窓口。HPトリガーによるフェーズ遷移をここで監視する
|
||||
/// </summary>
|
||||
public void TakeDamage(float damage)
|
||||
{
|
||||
currentHp -= damage;
|
||||
currentHp = Mathf.Clamp(currentHp, 0, maxHp);
|
||||
float hpPercentage = (currentHp / maxHp) * 100f;
|
||||
|
||||
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;
|
||||
PendingUltimate = true;
|
||||
TriggerPhaseTransition();
|
||||
}
|
||||
}
|
||||
|
||||
private void TriggerPhaseTransition()
|
||||
{
|
||||
// 咆哮、ジャンプ、スタン中以外であれば、現在の行動をキャンセルして即座に天井へエスケープ
|
||||
if (currentState is not SpiderJumpToCeilingState &&
|
||||
currentState is not SpiderCeilingAttackState &&
|
||||
currentState is not SpiderStunState)
|
||||
{
|
||||
ChangeState<SpiderJumpToCeilingState>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 【パリィ連携窓口】プレイヤーのVRInputPunchなどから、パリィ成功時にこのメソッドが叩かれる
|
||||
/// </summary>
|
||||
public void TriggerParryStun()
|
||||
{
|
||||
// パリィを受け付ける攻撃ステート(噛みつき、体当たり)のときのみスタンに移行する
|
||||
if (currentState is SpiderBiteState || currentState is SpiderChargeState)
|
||||
{
|
||||
ChangeState<SpiderStunState>();
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetChargeCooldown()
|
||||
{
|
||||
ChargeCooldownTimer = chargeCooldownDuration;
|
||||
}
|
||||
|
||||
#region ファクトリメソッド(エフェクトやオブジェクト生成の保守性を担保)
|
||||
public void SpawnWarningAndWeb(Vector3 targetGroundPos, float delay, float webDuration)
|
||||
{
|
||||
StartCoroutine(SpawnWebRoutine(targetGroundPos, delay, webDuration));
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator SpawnWebRoutine(Vector3 pos, float delay, float duration)
|
||||
{
|
||||
// 1. 地面に予兆マークを生成
|
||||
GameObject marker = null;
|
||||
if (warningMarkerPrefab != null)
|
||||
{
|
||||
marker = Instantiate(warningMarkerPrefab, pos + new Vector3(0, 0.02f, 0), Quaternion.identity);
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(delay);
|
||||
|
||||
// 2. 予兆マークを消し、本物の糸オブジェクトを生成
|
||||
if (marker != null) Destroy(marker);
|
||||
|
||||
if (webPrefab != null)
|
||||
{
|
||||
GameObject web = Instantiate(webPrefab, pos, Quaternion.identity);
|
||||
Destroy(web, duration); // 数秒後に自動消滅
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
#region 1. ステート共通インターフェース
|
||||
public interface ISpiderState
|
||||
{
|
||||
void EnterState();
|
||||
void UpdateState();
|
||||
void ExitState();
|
||||
}
|
||||
#endregion
|
||||
2
Assets/_Scripts/BossController.cs.meta
Normal file
2
Assets/_Scripts/BossController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 60f7f85851ff9b544a35d5dbe1a21285
|
||||
223
Assets/_Scripts/CodingModeGridManager.cs
Normal file
223
Assets/_Scripts/CodingModeGridManager.cs
Normal file
@@ -0,0 +1,223 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.InputSystem;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class CodingModeGridManager : MonoBehaviour
|
||||
{
|
||||
[Header("スティック入力設定")]
|
||||
[Tooltip("移動に使用するコントローラーのスティックのInputActionPropertyを指定してください")]
|
||||
[SerializeField] private InputActionProperty thumbstickAction;
|
||||
[Range(0.1f, 0.9f)] [SerializeField] private float inputThreshold = 0.5f; // スティックをどれくらい倒したら動くか
|
||||
|
||||
[Header("5x5 マス目(UI Image)の登録")]
|
||||
[Tooltip("ヒエラルキー上の25個のマス目のImageコンポーネントを、左上(0)から右下(24)の順番で格納してください")]
|
||||
[SerializeField] private List<Image> gridCells = new List<Image>();
|
||||
|
||||
[Header("マスの配色設定")]
|
||||
[SerializeField] private Color colorNormal = Color.gray;
|
||||
[SerializeField] private Color colorPlayer = Color.green; // 自機(プレイヤー)
|
||||
[SerializeField] private Color colorGoal = Color.yellow; // ゴール
|
||||
[SerializeField] private Color colorFailZone = Color.red; // 失敗ゾーン
|
||||
[SerializeField] private Color colorCheckpoint = Color.cyan; // チェックポイント
|
||||
|
||||
// 内部で使用するマスの種類定義
|
||||
private enum CellType { Normal, Start, Goal, FailZone, Checkpoint }
|
||||
private CellType[] cellLogicArray = new CellType[25];
|
||||
private string[] checkpointBuffTypeArray = new string[25]; // 各チェックポイントに割り振るバフ名
|
||||
|
||||
// プレイヤーの現在位置(X:0~4, Y:0~4)
|
||||
private int playerX = 0;
|
||||
private int playerY = 0;
|
||||
|
||||
// ゴールの位置
|
||||
private int goalX = 4;
|
||||
private int goalY = 4;
|
||||
|
||||
// スティックの連続移動を防ぐためのフラグ(一度スティックを中央に戻さないと連続で動かない仕様)
|
||||
private bool isStickNeutral = true;
|
||||
|
||||
// 道中で踏んで現在蓄積されているバフのリスト
|
||||
private List<string> accumulatedBuffs = new List<string>();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (thumbstickAction.action != null) thumbstickAction.action.Enable();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (thumbstickAction.action != null) thumbstickAction.action.Disable();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// ─── ★Time.timeScale = 0 でも動くスティック入力移動ロジック ───
|
||||
if (thumbstickAction.action == null) return;
|
||||
|
||||
Vector2 stickInput = thumbstickAction.action.ReadValue<Vector2>();
|
||||
|
||||
// スティックが中央付近に戻ったらニュートラル状態を復帰
|
||||
if (stickInput.magnitude < inputThreshold)
|
||||
{
|
||||
isStickNeutral = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ニュートラル状態のときのみ、倒された方向に応じて1マスだけ移動させる
|
||||
if (isStickNeutral)
|
||||
{
|
||||
if (Mathf.Abs(stickInput.x) > Mathf.Abs(stickInput.y))
|
||||
{
|
||||
// 横移動
|
||||
if (stickInput.x > inputThreshold) MovePlayer(1, 0); // 右
|
||||
else if (stickInput.x < -inputThreshold) MovePlayer(-1, 0); // 左
|
||||
}
|
||||
else
|
||||
{
|
||||
// 縦移動 (UIの配列構造上、上に行くとYマイナス、下に行くとYプラスとなるよう計算)
|
||||
if (stickInput.y > inputThreshold) MovePlayer(0, -1); // 上
|
||||
else if (stickInput.y < -inputThreshold) MovePlayer(0, 1); // 下
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 盤面を完全に初期化し、失敗ゾーンやチェックポイントをランダムに再配置する(PlayerStatusManagerから呼ばれる)
|
||||
/// </summary>
|
||||
public void ResetAndGenerateMinigame()
|
||||
{
|
||||
accumulatedBuffs.Clear();
|
||||
|
||||
// 1. 全マスを通常マスとして初期化
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
cellLogicArray[i] = CellType.Normal;
|
||||
checkpointBuffTypeArray[i] = "";
|
||||
}
|
||||
|
||||
// 2. スタートとゴールの固定配置 (左上スタート、右下ゴール)
|
||||
playerX = 0; playerY = 0;
|
||||
goalX = 4; goalY = 4;
|
||||
cellLogicArray[GetIndex(0, 0)] = CellType.Start;
|
||||
cellLogicArray[GetIndex(4, 4)] = CellType.Goal;
|
||||
|
||||
// 3. 失敗ゾーン(赤マス)をランダムに5個配置 (スタート・ゴールには被らせない)
|
||||
int failZonesPlaced = 0;
|
||||
while (failZonesPlaced < 5)
|
||||
{
|
||||
int randIndex = Random.Range(1, 24); // 0と24を避ける
|
||||
if (cellLogicArray[randIndex] == CellType.Normal)
|
||||
{
|
||||
cellLogicArray[randIndex] = CellType.FailZone;
|
||||
failZonesPlaced++;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. チェックポイント(青マス)を3つのバフに対応させてランダムに3個配置
|
||||
string[] buffPool = { "AttackUp", "DefenseUp", "Heal" };
|
||||
int checkpointsPlaced = 0;
|
||||
while (checkpointsPlaced < 3)
|
||||
{
|
||||
int randIndex = Random.Range(1, 24);
|
||||
if (cellLogicArray[randIndex] == CellType.Normal)
|
||||
{
|
||||
cellLogicArray[randIndex] = CellType.Checkpoint;
|
||||
checkpointBuffTypeArray[randIndex] = buffPool[checkpointsPlaced];
|
||||
checkpointsPlaced++;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 視覚的(UIの色)に反映
|
||||
RedrawGridGraphic();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// プレイヤーの位置を指定された方向へ動かし、移動先のイベントを判定する
|
||||
/// </summary>
|
||||
private void MovePlayer(int xDirection, int yDirection)
|
||||
{
|
||||
int nextX = Mathf.Clamp(playerX + xDirection, 0, 4);
|
||||
int nextY = Mathf.Clamp(playerY + yDirection, 0, 4);
|
||||
|
||||
// 壁にぶつかって座標が変わらなかった場合は移動不発とし、連続移動防止もかけない
|
||||
if (nextX == playerX && nextY == playerY) return;
|
||||
|
||||
playerX = nextX;
|
||||
playerY = nextY;
|
||||
isStickNeutral = false; // 1回動いたのでロックをかける
|
||||
|
||||
int currentIndex = GetIndex(playerX, playerY);
|
||||
CellType landedCell = cellLogicArray[currentIndex];
|
||||
|
||||
Debug.Log($"👾 カーソル移動: ({playerX}, {playerY}) | マス属性: {landedCell}");
|
||||
|
||||
// 移動先のマスに応じた処理
|
||||
switch (landedCell)
|
||||
{
|
||||
case CellType.FailZone:
|
||||
PlayerStatusManager.Instance.FailCodingMode(); // 強制終了
|
||||
return;
|
||||
|
||||
case CellType.Checkpoint:
|
||||
string buff = checkpointBuffTypeArray[currentIndex];
|
||||
if (!accumulatedBuffs.Contains(buff))
|
||||
{
|
||||
accumulatedBuffs.Add(buff);
|
||||
Debug.Log($"💚 バフコード抽出成功: [{buff}] を一時キープ中!");
|
||||
}
|
||||
break;
|
||||
|
||||
case CellType.Goal:
|
||||
PlayerStatusManager.Instance.CompleteCodingMode(accumulatedBuffs); // クリア!バフ適用
|
||||
return;
|
||||
}
|
||||
|
||||
// 画面の色を最新状態に更新
|
||||
RedrawGridGraphic();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 現在のロジック配列に基づいて、25個のUI Imageの色をまとめて上書き変更する
|
||||
/// </summary>
|
||||
private void RedrawGridGraphic()
|
||||
{
|
||||
for (int y = 0; y < 5; y++)
|
||||
{
|
||||
for (int x = 0; x < 5; x++)
|
||||
{
|
||||
int index = GetIndex(x, y);
|
||||
if (gridCells[index] == null) continue;
|
||||
|
||||
// 基本の色を決定
|
||||
switch (cellLogicArray[index])
|
||||
{
|
||||
case CellType.Start:
|
||||
case CellType.Normal: gridCells[index].color = colorNormal; break;
|
||||
case CellType.FailZone: gridCells[index].color = colorFailZone; break;
|
||||
case CellType.Checkpoint: gridCells[index].color = colorCheckpoint; break;
|
||||
case CellType.Goal: gridCells[index].color = colorGoal; break;
|
||||
}
|
||||
|
||||
// チェックポイントをすでに踏み抜いている場合は、獲得済みとして通常色に戻す演出(任意)
|
||||
if (cellLogicArray[index] == CellType.Checkpoint && accumulatedBuffs.Contains(checkpointBuffTypeArray[index]))
|
||||
{
|
||||
gridCells[index].color = colorNormal * 0.7f; // 少し暗いグレーにして区別
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 最後にプレイヤーのいるマスを「自機の色」で最優先に塗り替える
|
||||
int playerIndex = GetIndex(playerX, playerY);
|
||||
if (gridCells[playerIndex] != null)
|
||||
{
|
||||
gridCells[playerIndex].color = colorPlayer;
|
||||
}
|
||||
}
|
||||
|
||||
// 2次元座標 (X, Y) から 1次元の配列添字 (0~24) を割り出すヘルパーメソッド
|
||||
private int GetIndex(int x, int y)
|
||||
{
|
||||
return y * 5 + x;
|
||||
}
|
||||
}
|
||||
2
Assets/_Scripts/CodingModeGridManager.cs.meta
Normal file
2
Assets/_Scripts/CodingModeGridManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 945a90d6fc5ecb048849ec7b83634441
|
||||
142
Assets/_Scripts/ElevatorSceneTransition.cs
Normal file
142
Assets/_Scripts/ElevatorSceneTransition.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.XR;
|
||||
using System.Collections;
|
||||
|
||||
public class ElevatorSceneTransition : MonoBehaviour
|
||||
{
|
||||
[Header("遷移先の設定")]
|
||||
[SerializeField] private string bossSceneName = "BossStage";
|
||||
[SerializeField] private string playerTag = "Player";
|
||||
|
||||
[Header("ローディングUI設定")]
|
||||
[SerializeField] private GameObject loadingCanvas;
|
||||
[SerializeField] private Text progressText;
|
||||
[SerializeField] private Slider progressSlider;
|
||||
|
||||
[Header("エレベーター下降演出設定")]
|
||||
[Tooltip("エレベーターの部屋(壁や床、UIすべて)をまとめているオブジェクトを指定してください(※プレイヤーは外しておきます)")]
|
||||
[SerializeField] private Transform elevatorCageTransform;
|
||||
|
||||
[Tooltip("ガタガタ揺れる激しさ(0.01~0.03あたりが自然です)")]
|
||||
[SerializeField] private float shakeMagnitude = 0.015f;
|
||||
|
||||
[Tooltip("下降中のコントローラーの振動の強さ(0.0 ~ 1.0)")]
|
||||
[SerializeField] private float hapticAmplitude = 0.15f;
|
||||
|
||||
// ─── ★新設:親子関係にしないためのプレイヤー登録枠 ───
|
||||
[Header("プレイヤーの同期設定")]
|
||||
[Tooltip("XR Origin(またはXR Rig)のTransformを直接ドラッグ&ドロップしてください")]
|
||||
[SerializeField] private Transform playerTransform;
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
private bool isLoadingStarted = false;
|
||||
private Vector3 originalCagePosition;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (loadingCanvas != null) loadingCanvas.SetActive(false);
|
||||
|
||||
if (elevatorCageTransform != null)
|
||||
{
|
||||
// ワールド空間での初期位置を記憶(localPositionからpositionに変更し、より確実に)
|
||||
originalCagePosition = elevatorCageTransform.position;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (isLoadingStarted) return;
|
||||
|
||||
if (other.CompareTag(playerTag))
|
||||
{
|
||||
isLoadingStarted = true;
|
||||
|
||||
// ★安全対策:接触したプレイヤーのTransformを自動でキャッシュする
|
||||
if (playerTransform == null)
|
||||
{
|
||||
playerTransform = other.transform;
|
||||
}
|
||||
|
||||
Debug.Log("🛗 プレイヤー乗車:親子関係なしのデルタ同期で下降演出を開始します。");
|
||||
StartCoroutine(LoadSceneAsyncRoutine());
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator LoadSceneAsyncRoutine()
|
||||
{
|
||||
if (loadingCanvas != null) loadingCanvas.SetActive(true);
|
||||
|
||||
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(bossSceneName);
|
||||
asyncLoad.allowSceneActivation = false;
|
||||
|
||||
float hapticTimer = 0f;
|
||||
|
||||
while (asyncLoad.progress < 0.9f)
|
||||
{
|
||||
float currentProgress = Mathf.Clamp01(asyncLoad.progress / 0.9f);
|
||||
if (progressText != null) progressText.text = $"LOADING: {(currentProgress * 100f):F0}%";
|
||||
if (progressSlider != null) progressSlider.value = currentProgress;
|
||||
|
||||
// ─── ★【超重要】親子関係なしでプレイヤーを完全同期させるロジック ───
|
||||
|
||||
// 1. 動かす前の、このフレームにおけるエレベーターの現在位置を記録
|
||||
Vector3 previousCagePosition = elevatorCageTransform.position;
|
||||
|
||||
// 2. エレベーターの部屋だけをランダムにガタガタ動かす
|
||||
if (elevatorCageTransform != null)
|
||||
{
|
||||
Vector3 randomOffset = Random.insideUnitSphere * shakeMagnitude;
|
||||
randomOffset.y = 0; // 横揺れのみ
|
||||
elevatorCageTransform.position = originalCagePosition + randomOffset;
|
||||
}
|
||||
|
||||
// 3. 「この1フレームでエレベーターが実際に動いた移動差分(デルタ)」を計算
|
||||
Vector3 cageDelta = elevatorCageTransform.position - previousCagePosition;
|
||||
|
||||
// 4. その移動差分を、プレイヤーの座標にもそのまま上乗せして足し算する
|
||||
if (playerTransform != null && cageDelta != Vector3.zero)
|
||||
{
|
||||
playerTransform.position += cageDelta;
|
||||
}
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
|
||||
hapticTimer += Time.deltaTime;
|
||||
if (hapticTimer >= 0.1f)
|
||||
{
|
||||
TriggerBothHandHaptics(hapticAmplitude, 0.08f);
|
||||
hapticTimer = 0f;
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// 到着後、エレベーターの位置を元の位置に綺麗に戻す
|
||||
if (elevatorCageTransform != null)
|
||||
{
|
||||
// 最後の移動差分を計算してプレイヤーを戻す
|
||||
Vector3 previousCagePosition = elevatorCageTransform.position;
|
||||
elevatorCageTransform.position = originalCagePosition;
|
||||
Vector3 cageDelta = elevatorCageTransform.position - previousCagePosition;
|
||||
if (playerTransform != null) playerTransform.position += cageDelta;
|
||||
}
|
||||
|
||||
if (progressText != null) progressText.text = "ARRIVED";
|
||||
if (progressSlider != null) progressSlider.value = 1f;
|
||||
|
||||
yield return new WaitForSeconds(1.0f);
|
||||
|
||||
Debug.Log("🎉 ボスステージに到着。シーンを切り替えます。");
|
||||
asyncLoad.allowSceneActivation = true;
|
||||
}
|
||||
|
||||
private void TriggerBothHandHaptics(float amplitude, float duration)
|
||||
{
|
||||
InputDevice leftDevice = InputDevices.GetDeviceAtXRNode(XRNode.LeftHand);
|
||||
if (leftDevice.isValid) leftDevice.SendHapticImpulse(0, amplitude, duration);
|
||||
|
||||
InputDevice rightDevice = InputDevices.GetDeviceAtXRNode(XRNode.RightHand);
|
||||
if (rightDevice.isValid) rightDevice.SendHapticImpulse(0, amplitude, duration);
|
||||
}
|
||||
}
|
||||
2
Assets/_Scripts/ElevatorSceneTransition.cs.meta
Normal file
2
Assets/_Scripts/ElevatorSceneTransition.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 72dd993245dfeb94ca11fd104e817220
|
||||
505
Assets/_Scripts/EnemyAI.cs
Normal file
505
Assets/_Scripts/EnemyAI.cs
Normal file
@@ -0,0 +1,505 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
using System.Collections;
|
||||
using System;
|
||||
|
||||
[RequireComponent(typeof(NavMeshAgent))]
|
||||
[RequireComponent(typeof(Animator))]
|
||||
public class EnemyAI : MonoBehaviour
|
||||
{
|
||||
/// 敵AIの状態を表すステート
|
||||
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("攻撃設定")]
|
||||
[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;
|
||||
[SerializeField] private Collider rightHandCollider;
|
||||
|
||||
[Header("ループ攻撃の調整")]
|
||||
[Tooltip("連続攻撃の間に挟む最低限のインターバル (秒)")]
|
||||
[SerializeField] private float comboInterval = 0.2f;
|
||||
|
||||
[Header("徘徊(Wander)設定")]
|
||||
[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 = false;
|
||||
private bool isAttacking = false;
|
||||
private bool isWandering = false;
|
||||
private bool isKnockbacking = false;
|
||||
private bool isDead = false;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
agent = GetComponent<NavMeshAgent>();
|
||||
anim = GetComponent<Animator>();
|
||||
|
||||
SetFistCollider(false);
|
||||
|
||||
// AIのメインループコルーチンを開始
|
||||
StartCoroutine(AILoop());
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if(isDead) return;
|
||||
|
||||
// 毎フレームのアニメーションパラメータ更新処理
|
||||
UpdateAnimation();
|
||||
|
||||
// 攻撃ステートにおけるプレイヤーへの方向転換処理
|
||||
UpdateRotationOnAttack();
|
||||
}
|
||||
|
||||
#region AI メインループ (思考ロジック)
|
||||
private IEnumerator AILoop()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if(isDead) yield break;
|
||||
|
||||
// プレイヤーが未検出の場合はタグから検索
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 毎フレーム実行を避け、0.2秒待機することでCPU負荷を軽減
|
||||
yield return new WaitForSeconds(0.2f);
|
||||
}
|
||||
}
|
||||
|
||||
private void FindPlayer()
|
||||
{
|
||||
GameObject playerObj = GameObject.FindGameObjectWithTag(playerTag);
|
||||
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();
|
||||
Debug.Log($"👁️ {gameObject.name}: プレイヤーを発見! 追跡します。");
|
||||
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();
|
||||
Debug.Log($"❓ {gameObject.name}: プレイヤーを見失った。");
|
||||
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;
|
||||
}
|
||||
|
||||
// クールダウン中でなければ攻撃コルーチンを実行(クールダウン中の場合は、Update内の処理によりプレイヤーの方向を向きながら待機します)
|
||||
if (!isCooldown)
|
||||
{
|
||||
StartCoroutine(PerformPunchAttack());
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region アニメーション・回転の制御メソッド
|
||||
/// エージェントの移動状態に応じてAnimatorのSpeedパラメータを更新する
|
||||
private void UpdateAnimation()
|
||||
{
|
||||
float targetSpeed = 0f;
|
||||
|
||||
// 攻撃中でなく、経路が存在し、かつ実際に一定以上の速度で移動している場合のみSpeedを1にする
|
||||
if (!isAttacking && !isKnockbacking && currentState != AIState.Stun && 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 && !isKnockbacking && currentState != AIState.Stun)
|
||||
{
|
||||
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
|
||||
|
||||
// ダメージを受けた際に、EnemyhHealthから呼び出されるのけぞり開始メソッド
|
||||
public void TriggerKnockback(float duration)
|
||||
{
|
||||
if(isDead) return;
|
||||
|
||||
if(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;
|
||||
|
||||
Debug.Log($"{gameObject.name}: parry = true Stunnow");
|
||||
|
||||
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("Stun");
|
||||
|
||||
yield return new WaitForSeconds(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("Hit");
|
||||
|
||||
yield return new WaitForSeconds(duration);
|
||||
|
||||
if(agent.isOnNavMesh)
|
||||
{
|
||||
agent.isStopped = false;
|
||||
}
|
||||
|
||||
isKnockbacking = false;
|
||||
isCooldown = false;
|
||||
|
||||
StartCoroutine(AILoop());
|
||||
}
|
||||
|
||||
#region 攻撃・移動のコルーチン処理
|
||||
/// 徘徊時のランダム移動を制御するコルーチン
|
||||
private IEnumerator WanderMoveRoutine()
|
||||
{
|
||||
isWandering = true;
|
||||
Vector3 newPos = Vector3.zero;
|
||||
bool isPathValid = false;
|
||||
int maxAttempts = 10;
|
||||
|
||||
// 有効な経路(壁などで分断されていないルート)が見つかるまで試行
|
||||
for (int i = 0; i < maxAttempts; i++)
|
||||
{
|
||||
newPos = GetRandomNavMeshPoint(transform.position, wanderRadius);
|
||||
NavMeshPath path = new NavMeshPath();
|
||||
|
||||
if (agent.CalculatePath(newPos, path))
|
||||
{
|
||||
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(UnityEngine.Random.Range(minWaitTime, maxWaitTime));
|
||||
isWandering = false;
|
||||
}
|
||||
|
||||
/// 予兆から攻撃アニメーションのトリガーまでを制御するコルーチン
|
||||
private IEnumerator PerformPunchAttack()
|
||||
{
|
||||
isAttacking = true;
|
||||
isCooldown = true; // ここで攻撃フラグをロックします
|
||||
|
||||
anim.SetTrigger("Telegraph");
|
||||
Debug.Log($"⚠️ {gameObject.name}: 攻撃の予兆(溜め)開始");
|
||||
|
||||
yield return new WaitForSeconds(telegraphDuration);
|
||||
|
||||
if(isKnockbacking) yield break;
|
||||
|
||||
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 (isKnockbacking || currentState == AIState.Stun) return;
|
||||
|
||||
SetFistCollider(true);
|
||||
}
|
||||
|
||||
/// Recovery(硬直)アニメーションの終了時に実行される処理(Animation Eventから呼び出し)
|
||||
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());
|
||||
Debug.Log($"🔄 {gameObject.name}: プレイヤーがまだ射程内です。連続攻撃(コンボ)を実行します。");
|
||||
}
|
||||
else
|
||||
{
|
||||
// プレイヤーが離れていた場合はコンボを抜け、追跡状態に戻ると同時に、通常のクールダウン(長めの待機)を開始
|
||||
currentState = AIState.Chase;
|
||||
StartCoroutine(ResetCooldown());
|
||||
Debug.Log($"🏃 {gameObject.name}: プレイヤーが離れたため追跡に戻ります。({attackCooldown}秒の攻撃クールダウンを適用)");
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ヘルパー・デバッグ用メソッド
|
||||
/// 指定された中心座標の範囲内から、NavMesh上の有効なランダム座標を取得する
|
||||
|
||||
private void SetFistCollider(bool enabledState)
|
||||
{
|
||||
if(rightHandCollider != null)
|
||||
{
|
||||
rightHandCollider.enabled = enabledState;
|
||||
}
|
||||
}
|
||||
private Vector3 GetRandomNavMeshPoint(Vector3 center, float radius)
|
||||
{
|
||||
Vector3 randomDirection = UnityEngine.Random.insideUnitSphere * radius;
|
||||
randomDirection += center;
|
||||
NavMeshHit hit;
|
||||
|
||||
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
|
||||
}
|
||||
2
Assets/_Scripts/EnemyAI.cs.meta
Normal file
2
Assets/_Scripts/EnemyAI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 88407b9f9a6654436b641cce64611731
|
||||
68
Assets/_Scripts/EnemyHealth.cs
Normal file
68
Assets/_Scripts/EnemyHealth.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
public class EnemyHealth : MonoBehaviour, IDamageble
|
||||
{
|
||||
[SerializeField] private float maxHealth = 100f;
|
||||
private float currentHealth;
|
||||
|
||||
[SerializeField] private float knockbackDuration = 0.5f;
|
||||
private EnemyAI enemyAI;
|
||||
|
||||
private bool isDead = false;
|
||||
|
||||
void Start()
|
||||
{
|
||||
currentHealth = maxHealth; //HPを代入
|
||||
enemyAI = GetComponent<EnemyAI>();
|
||||
}
|
||||
|
||||
public void TakeDamage(DamageInfo damageInfo)
|
||||
{
|
||||
if(currentHealth <= 0) return;
|
||||
|
||||
currentHealth -= damageInfo.amount; //ダメージ計算
|
||||
|
||||
if(damageInfo.isPoseBonus)
|
||||
{
|
||||
Debug.Log("構えボーナス");
|
||||
}
|
||||
|
||||
if(currentHealth <= 0)
|
||||
{
|
||||
Die();
|
||||
}
|
||||
else
|
||||
{
|
||||
if(enemyAI != null)
|
||||
{
|
||||
enemyAI.TriggerKnockback(knockbackDuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Die()
|
||||
{
|
||||
if(isDead) return;
|
||||
isDead = true;
|
||||
|
||||
Animator anim = GetComponent<Animator>();
|
||||
if(anim != null)
|
||||
{
|
||||
anim.SetTrigger("Dead");
|
||||
}
|
||||
|
||||
Collider col = GetComponent<Collider>();
|
||||
if(col != null)
|
||||
{
|
||||
col.enabled = false;
|
||||
}
|
||||
|
||||
if(enemyAI != null)
|
||||
{
|
||||
enemyAI.DisableAIOnDeath();
|
||||
}
|
||||
|
||||
Destroy(gameObject, 3.0f);
|
||||
}
|
||||
}
|
||||
2
Assets/_Scripts/EnemyHealth.cs.meta
Normal file
2
Assets/_Scripts/EnemyHealth.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b4f072c824585843b120965516325b6
|
||||
13
Assets/_Scripts/IDamageble.cs
Normal file
13
Assets/_Scripts/IDamageble.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using UnityEngine;
|
||||
|
||||
public struct DamageInfo
|
||||
{
|
||||
public float amount; //ダメージ量
|
||||
public Vector3 hitPosition; //殴られた位置
|
||||
public Vector3 punchDirection; //パンチが飛んできた方向(ノックバック用)
|
||||
public bool isPoseBonus; //構えから攻撃かどうか
|
||||
}
|
||||
public interface IDamageble
|
||||
{
|
||||
void TakeDamage(DamageInfo damageInfo);
|
||||
}
|
||||
2
Assets/_Scripts/IDamageble.cs.meta
Normal file
2
Assets/_Scripts/IDamageble.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 42a222f4862dd2c46846a6be4dfe956a
|
||||
28
Assets/_Scripts/InputLR.cs
Normal file
28
Assets/_Scripts/InputLR.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using UnityEngine;
|
||||
using Unity.VRTemplate;
|
||||
|
||||
public class InputLR : MonoBehaviour
|
||||
{
|
||||
void Update()
|
||||
{
|
||||
if(InputManagerLR.PrimaryButtonR())
|
||||
{
|
||||
OnPrimaryButtonR();
|
||||
}
|
||||
|
||||
if(InputManagerLR.PrimaryButtonR_OnPress())
|
||||
{
|
||||
OnPressPrimaryButtonR();
|
||||
}
|
||||
}
|
||||
|
||||
void OnPrimaryButtonR()
|
||||
{
|
||||
//邪魔になるためなし
|
||||
}
|
||||
|
||||
void OnPressPrimaryButtonR()
|
||||
{
|
||||
Debug.Log("押した瞬間");
|
||||
}
|
||||
}
|
||||
2
Assets/_Scripts/InputLR.cs.meta
Normal file
2
Assets/_Scripts/InputLR.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ae6f0d5ca1f0f42b6a38e38170777c16
|
||||
57
Assets/_Scripts/InputManagerLR.cs
Normal file
57
Assets/_Scripts/InputManagerLR.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
public class InputManagerLR : MonoBehaviour
|
||||
{
|
||||
static InputManagerLR instance;
|
||||
|
||||
[SerializeField]
|
||||
InputActionAsset ActionAsset;
|
||||
|
||||
InputActionMap ActionMap;
|
||||
InputAction m_PrimaryButtonR;
|
||||
InputAction m_SecondaryButtonR;
|
||||
InputAction m_PrimaryButtonL;
|
||||
InputAction m_SecondaryButtonL;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
instance = this;
|
||||
GameObject.DontDestroyOnLoad(gameObject);
|
||||
ActionMap = ActionAsset.FindActionMap("Test");
|
||||
m_PrimaryButtonR = ActionMap.FindAction("XR_PrimaryButtonR", throwIfNotFound: true);
|
||||
m_PrimaryButtonL = ActionMap.FindAction("XR_PrimaryButtonL", throwIfNotFound: true);
|
||||
m_SecondaryButtonR = ActionMap.FindAction("XR_SecondaryButtonR", throwIfNotFound: true);
|
||||
m_SecondaryButtonL = ActionMap.FindAction("XR_SecondaryButtonL", throwIfNotFound: true);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
ActionMap?.Enable();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
ActionMap?.Disable();
|
||||
}
|
||||
|
||||
// Aボタン
|
||||
public static bool PrimaryButtonR() => instance.m_PrimaryButtonR.IsPressed();
|
||||
public static bool PrimaryButtonR_OnPress() => instance.m_PrimaryButtonR.WasPressedThisFrame();
|
||||
public static bool PrimaryButtonR_OnRelease() => instance.m_PrimaryButtonR.WasReleasedThisFrame();
|
||||
|
||||
// Xボタン
|
||||
public static bool PrimaryButtonL() => instance.m_PrimaryButtonL.IsPressed();
|
||||
public static bool PrimaryButtonL_OnPress() => instance.m_PrimaryButtonL.WasPressedThisFrame();
|
||||
public static bool PrimaryButtonL_OnRelease() => instance.m_PrimaryButtonL.WasReleasedThisFrame();
|
||||
|
||||
// Bボタン
|
||||
public static bool SecondaryButtonR() => instance.m_SecondaryButtonR.IsPressed();
|
||||
public static bool SecondaryButtonR_OnPress() => instance.m_SecondaryButtonR.WasPressedThisFrame();
|
||||
public static bool SecondaryButtonR_OnRelease() => instance.m_SecondaryButtonR.WasReleasedThisFrame();
|
||||
|
||||
// Yボタン
|
||||
public static bool SecondaryButtonL() => instance.m_SecondaryButtonL.IsPressed();
|
||||
public static bool SecondaryButtonL_OnPress() => instance.m_SecondaryButtonL.WasPressedThisFrame();
|
||||
public static bool SecondaryButtonL_OnRelease() => instance.m_SecondaryButtonL.WasReleasedThisFrame();
|
||||
}
|
||||
2
Assets/_Scripts/InputManagerLR.cs.meta
Normal file
2
Assets/_Scripts/InputManagerLR.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5fc595679af414674be369cd3d5bc765
|
||||
62
Assets/_Scripts/ItemCountDown.cs
Normal file
62
Assets/_Scripts/ItemCountDown.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class ItemCountDown : MonoBehaviour
|
||||
{
|
||||
|
||||
public GameObject gameObjectRestore; //ゲームオブフェクトの復帰する座標を指定する
|
||||
|
||||
private bool isGrabbed = false;
|
||||
private bool isTouching = false;
|
||||
|
||||
public float grabItemTimeLimit = 5.0f; //ゲームオブジェクトの位置がリセットされるまでのタイマー
|
||||
private float timer; //スクリプト内のタイマーに用いる変数
|
||||
|
||||
void Start()
|
||||
{
|
||||
timer = 0.0f;
|
||||
}
|
||||
|
||||
public void GetGrab()
|
||||
{
|
||||
isGrabbed = true;
|
||||
isTouching = true;
|
||||
}
|
||||
|
||||
public void ExitGrab()
|
||||
{
|
||||
isTouching = false;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
//制限時間が0秒の場合は、位置のリセットを実行しない
|
||||
if (grabItemTimeLimit != 0)
|
||||
{
|
||||
if (isGrabbed == true)
|
||||
{
|
||||
if (isTouching == false)
|
||||
{
|
||||
timer += Time.deltaTime;
|
||||
|
||||
if (timer > grabItemTimeLimit)
|
||||
{
|
||||
//ゲームオブジェクトの速度をリセット
|
||||
var rigidbody = GetComponent<Rigidbody>();
|
||||
rigidbody.linearVelocity = Vector3.zero;
|
||||
//ゲームオブジェクトを指定位置に配置する
|
||||
rigidbody.transform.position = gameObjectRestore.transform.position;
|
||||
rigidbody.transform.rotation = gameObjectRestore.transform.rotation;
|
||||
//ゲームオブジェクトは不動の状態に戻る
|
||||
isGrabbed = false;
|
||||
timer = 0.0f;
|
||||
}
|
||||
}
|
||||
//プレイヤーが触っている場合はカウントダウンをリセット
|
||||
else
|
||||
{
|
||||
timer = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Scripts/ItemCountDown.cs.meta
Normal file
2
Assets/_Scripts/ItemCountDown.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 45dd4c486e2a64ef5a8f38d756b9dc2d
|
||||
39
Assets/_Scripts/PlayerMove.cs
Normal file
39
Assets/_Scripts/PlayerMove.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class PlayerMove : MonoBehaviour
|
||||
{
|
||||
// パラメータ
|
||||
public float moveSpeed = 5f; // 移動速度
|
||||
public float gravity = -9.8f; // 重力加速度
|
||||
public CharacterController controller; // 移動に使うCharacterController
|
||||
|
||||
// 演算用変数
|
||||
private Vector3 velocity; // 加速度を保持する変数
|
||||
private bool isGrounded; // 地面に着地しているかどうかのフラグ変数
|
||||
|
||||
// ゲーム中実行されるUpdate関数
|
||||
void Update()
|
||||
{
|
||||
// 着地状態のチェック
|
||||
isGrounded = controller.isGrounded;
|
||||
|
||||
// 着地している場合は落下速度をリセット
|
||||
if (isGrounded && velocity.y < 0)
|
||||
{
|
||||
velocity.y = -2f; // 地面に着いた場合、速度をリセット
|
||||
}
|
||||
|
||||
// 入力の取得
|
||||
float h = Input.GetAxis("Horizontal");
|
||||
float v = Input.GetAxis("Vertical");
|
||||
|
||||
// ローカル座標をワールド座標に変換して移動方向を計算
|
||||
Vector3 moveDirection = transform.TransformDirection(new Vector3(h, 0, v)) * moveSpeed;
|
||||
|
||||
// 重力を加算
|
||||
velocity.y += gravity * Time.deltaTime;
|
||||
|
||||
// 移動と重力を一度のcontroller.Moveで処理
|
||||
controller.Move((moveDirection + velocity) * Time.deltaTime);
|
||||
}
|
||||
}
|
||||
2
Assets/_Scripts/PlayerMove.cs.meta
Normal file
2
Assets/_Scripts/PlayerMove.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c101f16cf16d4195ba45ae89a7f14d8
|
||||
37
Assets/_Scripts/PlayerPOV.cs
Normal file
37
Assets/_Scripts/PlayerPOV.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class PlayerPOV : MonoBehaviour
|
||||
{
|
||||
// パラメータ
|
||||
public Transform neck; // プレイヤーの首のTransformを指定
|
||||
public float sensitivity = 2.0f; // マウス感度(視点の移動の速さを調整)
|
||||
public float minVertical = -90.0f; // 視点の最小角度(縦の回転制限)
|
||||
public float maxVertical = 90.0f; // 視点の最大角度(縦の回転制限)
|
||||
|
||||
// 演算用変数
|
||||
private float rotationX = 0f; // 縦方向の回転角度(首の回転)
|
||||
|
||||
// ゲーム開始時に呼ばれる
|
||||
void Start()
|
||||
{
|
||||
// カーソルを非表示&ロック
|
||||
Cursor.lockState = CursorLockMode.Locked; // カーソルを画面中央に固定
|
||||
Cursor.visible = false; // カーソルを非表示にする
|
||||
}
|
||||
|
||||
// 毎フレーム実行される
|
||||
void Update()
|
||||
{
|
||||
// マウス入力の取得
|
||||
float mouseX = Input.GetAxis("Mouse X") * sensitivity; // 横のマウス移動量を取得し、感度で調整
|
||||
float mouseY = Input.GetAxis("Mouse Y") * sensitivity; // 縦のマウス移動量を取得し、感度で調整
|
||||
|
||||
// Player(体)の回転(左右)
|
||||
transform.Rotate(0, mouseX, 0); // プレイヤー(体)の左右の回転をマウスX方向の入力に合わせて行う
|
||||
|
||||
// Neck(首)の回転(上下)
|
||||
rotationX -= mouseY; // マウスY方向の入力によって縦方向の回転を更新
|
||||
rotationX = Mathf.Clamp(rotationX, minVertical, maxVertical); // 回転角度を指定された範囲に制限
|
||||
neck.localRotation = Quaternion.Euler(rotationX, 0, 0); // 首の回転を設定。縦方向のみ回転させる
|
||||
}
|
||||
}
|
||||
2
Assets/_Scripts/PlayerPOV.cs.meta
Normal file
2
Assets/_Scripts/PlayerPOV.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20e0bce59a58a401ca841697d32fb339
|
||||
174
Assets/_Scripts/PlayerStatusManager.cs
Normal file
174
Assets/_Scripts/PlayerStatusManager.cs
Normal file
@@ -0,0 +1,174 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class PlayerStatusManager : MonoBehaviour
|
||||
{
|
||||
public static PlayerStatusManager Instance { get; private set; }
|
||||
|
||||
[Header("Codingモード用 入力設定")]
|
||||
[SerializeField] private InputActionProperty zlButtonAction;
|
||||
[SerializeField] private InputActionProperty zrButtonAction;
|
||||
|
||||
[Header("ゲージ設定")]
|
||||
[SerializeField] private float maxGauge = 100f;
|
||||
[SerializeField] private float currentGauge = 0f;
|
||||
|
||||
[Header("表示UI設定")]
|
||||
[Tooltip("ミニゲーム画面をまとめている親オブジェクトを指定してください")]
|
||||
[SerializeField] private GameObject codingModeUI;
|
||||
|
||||
[Header("バフパラメータ設定")]
|
||||
[SerializeField] private float buffDuration = 10f;
|
||||
[SerializeField] private float attackBuffMultiplier = 1.5f;
|
||||
[SerializeField] private float defenseBuffMultiplier = 0.5f;
|
||||
[SerializeField] private float hpHealAmount = 30f;
|
||||
|
||||
private float attackBuffTimer = 0f;
|
||||
private float defenseBuffTimer = 0f;
|
||||
private bool isCodingMode = false;
|
||||
|
||||
public bool IsAttackBuffActive => attackBuffTimer > 0f;
|
||||
public bool IsDefenseBuffActive => defenseBuffTimer > 0f;
|
||||
public float CurrentAttackMultiplier => IsAttackBuffActive ? attackBuffMultiplier : 1.0f;
|
||||
public float CurrentDefenseMultiplier => IsDefenseBuffActive ? defenseBuffMultiplier : 1.0f;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null) { Instance = this; }
|
||||
else { Destroy(gameObject); }
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (zlButtonAction.action != null) zlButtonAction.action.Enable();
|
||||
if (zrButtonAction.action != null) zrButtonAction.action.Enable();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (zlButtonAction.action != null) zlButtonAction.action.Disable();
|
||||
if (zrButtonAction.action != null) zrButtonAction.action.Disable();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (codingModeUI != null) codingModeUI.SetActive(false);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
CheckComboInput();
|
||||
|
||||
if (!isCodingMode)
|
||||
{
|
||||
UpdateBuffTimers();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddGauge(float amount)
|
||||
{
|
||||
if (isCodingMode) return;
|
||||
currentGauge += amount;
|
||||
currentGauge = Mathf.Clamp(currentGauge, 0f, maxGauge);
|
||||
Debug.Log($"🔋 共通ゲージ増加: {currentGauge} / {maxGauge}");
|
||||
}
|
||||
|
||||
private void CheckComboInput()
|
||||
{
|
||||
if (zlButtonAction.action == null || zrButtonAction.action == null) return;
|
||||
|
||||
bool zlPressed = zlButtonAction.action.triggered && zlButtonAction.action.ReadValue<float>() > 0.5f;
|
||||
bool zrPressed = zrButtonAction.action.triggered && zrButtonAction.action.ReadValue<float>() > 0.5f;
|
||||
|
||||
if (!isCodingMode && currentGauge >= maxGauge)
|
||||
{
|
||||
if ((zlPressed && zrButtonAction.action.ReadValue<float>() > 0.5f) ||
|
||||
(zrPressed && zlButtonAction.action.ReadValue<float>() > 0.5f))
|
||||
{
|
||||
StartCodingMode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void StartCodingMode()
|
||||
{
|
||||
isCodingMode = true;
|
||||
Time.timeScale = 0f; // 時間停止
|
||||
|
||||
if (codingModeUI != null)
|
||||
{
|
||||
codingModeUI.SetActive(true);
|
||||
// ★新設:画面が開いた瞬間にミニゲームの盤面をランダム生成してリセットする
|
||||
CodingModeGridManager gridManager = codingModeUI.GetComponentInChildren<CodingModeGridManager>();
|
||||
if (gridManager != null) gridManager.ResetAndGenerateMinigame();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ★ミニゲームクリア時:獲得したバフリストをすべて適用して現実世界を再開する
|
||||
/// </summary>
|
||||
public void CompleteCodingMode(List<string> earnedBuffs)
|
||||
{
|
||||
Debug.Log("💻 【ハッキング成功】 ミニゲームクリア!現実世界に復帰します。");
|
||||
|
||||
foreach (string buff in earnedBuffs)
|
||||
{
|
||||
ActivateBuffByName(buff);
|
||||
}
|
||||
|
||||
ResumeGameplay();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ★ミニゲーム失敗時:失敗ゾーン接触による強制終了処理(バフなしで復帰)
|
||||
/// </summary>
|
||||
public void FailCodingMode()
|
||||
{
|
||||
Debug.Log("🚨 【ハッキング強制失敗】 失敗ゾーンに接触しました。バフなしで復帰します。");
|
||||
ResumeGameplay();
|
||||
}
|
||||
|
||||
private void ResumeGameplay()
|
||||
{
|
||||
isCodingMode = false;
|
||||
Time.timeScale = 1f; // 時間再開
|
||||
|
||||
if (codingModeUI != null) codingModeUI.SetActive(false);
|
||||
currentGauge = 0f; // ゲージ消費
|
||||
}
|
||||
|
||||
private void ActivateBuffByName(string buffName)
|
||||
{
|
||||
switch (buffName)
|
||||
{
|
||||
case "AttackUp":
|
||||
attackBuffTimer = buffDuration;
|
||||
Debug.Log($"⚔️ 攻撃力UPが適用されました。効果時間: {buffDuration}秒");
|
||||
break;
|
||||
case "DefenseUp":
|
||||
defenseBuffTimer = buffDuration;
|
||||
Debug.Log($"🛡️ 防御力UPが適用されました。効果時間: {buffDuration}秒");
|
||||
break;
|
||||
case "Heal":
|
||||
Debug.Log($"💚 HP回復が適用されました。回復量: {hpHealAmount}");
|
||||
// ※将来の拡張:PlayerHealth.Instance.Heal(hpHealAmount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateBuffTimers()
|
||||
{
|
||||
if (attackBuffTimer > 0f)
|
||||
{
|
||||
attackBuffTimer -= Time.deltaTime;
|
||||
if (attackBuffTimer <= 0f) Debug.Log("⚔️ 攻撃力UPバフの効果が終了しました。");
|
||||
}
|
||||
|
||||
if (defenseBuffTimer > 0f)
|
||||
{
|
||||
defenseBuffTimer -= Time.deltaTime;
|
||||
if (defenseBuffTimer <= 0f) Debug.Log("🛡️ 防御力UPバフの効果が終了しました。");
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Scripts/PlayerStatusManager.cs.meta
Normal file
2
Assets/_Scripts/PlayerStatusManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0215a75a0f67e1b49a1b823cfc7b8503
|
||||
36
Assets/_Scripts/SlowWebArea.cs
Normal file
36
Assets/_Scripts/SlowWebArea.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class SlowWebArea : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float damagePerSecond = 5f;
|
||||
private float damageTimer = 0f;
|
||||
|
||||
private void OnTriggerStay(Collider other)
|
||||
{
|
||||
if (other.CompareTag("Player"))
|
||||
{
|
||||
// ★鈍足効果の適用演出
|
||||
// プレイヤーの移動スクリプトに対し、一時的な速度デバフ(例: Speed * 0.3f)を掛けます
|
||||
// ここでは設計の切り離しのため、ログ出力のみとしています
|
||||
Debug.LogWarning("🕸️ プレイヤーが蜘蛛の糸に囚われている! 【鈍足化デバフ発動中】");
|
||||
|
||||
// スリップダメージ(1秒ごとに継続ダメージ)
|
||||
damageTimer += Time.deltaTime;
|
||||
if (damageTimer >= 1.0f)
|
||||
{
|
||||
// プレイヤーのHealthコンポーネントにダメージを通知
|
||||
// other.GetComponent<PlayerHealth>()?.TakeDamage(damagePerSecond);
|
||||
Debug.Log($"🕸️ 糸によるスリップダメージ: {damagePerSecond}");
|
||||
damageTimer = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerExit(Collider other)
|
||||
{
|
||||
if (other.CompareTag("Player"))
|
||||
{
|
||||
Debug.Log("🏃 プレイヤーが糸の範囲から脱出。 鈍足効果解除。");
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Scripts/SlowWebArea.cs.meta
Normal file
2
Assets/_Scripts/SlowWebArea.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0926e85a7d8a1a349a8e4e6e2ddfa0a5
|
||||
360
Assets/_Scripts/SpiderIdleState.cs
Normal file
360
Assets/_Scripts/SpiderIdleState.cs
Normal file
@@ -0,0 +1,360 @@
|
||||
using UnityEngine;
|
||||
|
||||
// ==========================================
|
||||
// ■ STATE: IDLE (待機状態)
|
||||
// ==========================================
|
||||
public class SpiderIdleState : ISpiderState
|
||||
{
|
||||
private BossController boss;
|
||||
public SpiderIdleState(BossController owner) => boss = owner;
|
||||
|
||||
public void EnterState() => boss.Anim.SetTrigger("Idle");
|
||||
public void ExitState() { }
|
||||
|
||||
public void UpdateState()
|
||||
{
|
||||
if (boss.Player == null) return;
|
||||
|
||||
// プレイヤーを検知したら即座に追跡へ
|
||||
if (Vector3.Distance(boss.transform.position, boss.Player.position) <= boss.chaseRange)
|
||||
{
|
||||
boss.ChangeState<SpiderChaseState>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ■ STATE: CHASE (追跡・行動分岐状態)
|
||||
// ==========================================
|
||||
// ==========================================
|
||||
// ■ STATE: CHASE (追跡・行動分岐状態) - 13x13ステージ最適化版
|
||||
// ==========================================
|
||||
// ==========================================
|
||||
// ■ STATE: CHASE (遠距離即座突進・最適化版)
|
||||
// ==========================================
|
||||
public class SpiderChaseState : ISpiderState
|
||||
{
|
||||
private BossController boss;
|
||||
|
||||
public SpiderChaseState(BossController owner) => boss = owner;
|
||||
|
||||
public void EnterState() => boss.Agent.isStopped = false;
|
||||
public void ExitState() => boss.Agent.isStopped = true;
|
||||
|
||||
public 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)
|
||||
{
|
||||
boss.ChangeState<SpiderChargeState>();
|
||||
return; // 移行したので以下の歩き処理は実行しない
|
||||
}
|
||||
|
||||
// 判定2:至近距離(2.5m以内)➔ 通常噛みつき
|
||||
if (distance <= boss.biteRange)
|
||||
{
|
||||
boss.ChangeState<SpiderBiteState>();
|
||||
return;
|
||||
}
|
||||
|
||||
// 判定3:中間距離(2.5m~6m)、または遠距離だけどクールダウン中の場合
|
||||
// 💡 ここで初めて「通常歩き(Walk)」でじりじりとプレイヤーに近づきます
|
||||
boss.Agent.SetDestination(boss.Player.position);
|
||||
boss.Anim.SetFloat("Speed", boss.Agent.velocity.magnitude);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ■ STATE: BITE (通常噛みつき - パリィ可)
|
||||
// ==========================================
|
||||
public class SpiderBiteState : ISpiderState
|
||||
{
|
||||
private BossController boss;
|
||||
private float stateTimer;
|
||||
|
||||
public SpiderBiteState(BossController owner) => boss = owner;
|
||||
|
||||
public void EnterState()
|
||||
{
|
||||
stateTimer = 1.5f; // 攻撃アニメーションの想定想定想定
|
||||
boss.Anim.SetTrigger("Attack_Bite");
|
||||
Debug.Log("🎯 蜘蛛ボス:噛みつき攻撃!(パリィ受付中)");
|
||||
}
|
||||
|
||||
public void ExitState() { }
|
||||
|
||||
public void UpdateState()
|
||||
{
|
||||
stateTimer -= Time.deltaTime;
|
||||
if (stateTimer <= 0f)
|
||||
{
|
||||
// 攻撃が終わったら、フェーズ移行チェックを挟みつつ追跡に戻る
|
||||
if (boss.PendingUltimate) boss.ChangeState<SpiderJumpToCeilingState>();
|
||||
else boss.ChangeState<SpiderChaseState>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ■ STATE: CHARGE (走って体当たり突進 - パリィ可)
|
||||
// ==========================================
|
||||
// ==========================================
|
||||
// ■ STATE: CHARGE (咆哮・溜めロック ➔ ロケット発射)
|
||||
// ==========================================
|
||||
public class SpiderChargeState : ISpiderState
|
||||
{
|
||||
private BossController boss;
|
||||
private float roarTimer;
|
||||
private float chargeTimer;
|
||||
private Vector3 chargeDirection;
|
||||
private bool isChargingPhase;
|
||||
|
||||
public SpiderChargeState(BossController owner) => boss = owner;
|
||||
|
||||
public void EnterState()
|
||||
{
|
||||
roarTimer = 1.5f; // 咆哮・溜めロックの時間(1.5秒)
|
||||
chargeTimer = boss.chargeDuration;
|
||||
isChargingPhase = false;
|
||||
|
||||
boss.Agent.enabled = false; // その場に固定するためNavMeshを即オフ
|
||||
boss.ResetChargeCooldown(); // 突進のクールダウンを開始
|
||||
|
||||
// 1. 遠距離を検知した「その場」で、即座に咆哮威嚇を再生
|
||||
boss.Anim.SetTrigger("Attack_Roar");
|
||||
Debug.Log("🔊 蜘蛛ボス:【遠距離検知】その場で咆哮・突進の溜めを開始!");
|
||||
}
|
||||
|
||||
public void ExitState()
|
||||
{
|
||||
boss.Anim.SetBool("IsCharging", false);
|
||||
boss.Agent.enabled = true;
|
||||
}
|
||||
|
||||
public void UpdateState()
|
||||
{
|
||||
// ─── フェーズ1:咆哮・溜め(動かずプレイヤーの方向を完全にロック) ───
|
||||
if (!isChargingPhase)
|
||||
{
|
||||
roarTimer -= Time.deltaTime;
|
||||
|
||||
// プレイヤーが動いても、突進直前までずーーっとその方向を向いてロックオンし続ける
|
||||
if (boss.Player != null)
|
||||
{
|
||||
Vector3 lookDir = (boss.Player.position - boss.transform.position).normalized;
|
||||
lookDir.y = 0;
|
||||
if (lookDir != Vector3.zero)
|
||||
{
|
||||
// 凄まじい速度でプレイヤーを正面に捉え続ける
|
||||
boss.transform.rotation = Quaternion.Slerp(boss.transform.rotation, Quaternion.LookRotation(lookDir), Time.deltaTime * 10f);
|
||||
}
|
||||
}
|
||||
|
||||
// 溜め時間が終わったら「発射」
|
||||
if (roarTimer <= 0f)
|
||||
{
|
||||
isChargingPhase = true;
|
||||
boss.Anim.SetTrigger("ChargeStart");
|
||||
boss.Anim.SetBool("IsCharging", true);
|
||||
|
||||
// 発射した瞬間のプレイヤーのベクトルを最終確定(ロック完了)
|
||||
if (boss.Player != null)
|
||||
{
|
||||
chargeDirection = (boss.Player.position - boss.transform.position).normalized;
|
||||
chargeDirection.y = 0;
|
||||
boss.transform.rotation = Quaternion.LookRotation(chargeDirection);
|
||||
}
|
||||
Debug.Log("🚀 蜘蛛ボス:ロックオン完了、全速力で発射!");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── フェーズ2:発射(端から端まで全速力ダッシュ) ───
|
||||
|
||||
// 【安全対策:レイキャストによる壁検知】
|
||||
// 13x13ステージの端(壁や柱)に激突してめり込むのを防ぐため、ボスの目の前1.5mに障害物がないかチェック
|
||||
Ray ray = new Ray(boss.transform.position + Vector3.up * 0.5f, chargeDirection);
|
||||
if (Physics.Raycast(ray, 1.5f))
|
||||
{
|
||||
Debug.Log("🔒 蜘蛛ボス:ステージの端(壁)を検知したため、突進を安全に終了します。");
|
||||
boss.ChangeState<SpiderChaseState>();
|
||||
return;
|
||||
}
|
||||
|
||||
// 全速力で直線移動
|
||||
boss.transform.position += chargeDirection * boss.chargeSpeed * Time.deltaTime;
|
||||
|
||||
chargeTimer -= Time.deltaTime;
|
||||
if (chargeTimer <= 0f)
|
||||
{
|
||||
// 時間を駆け抜けたら追跡に戻る
|
||||
boss.ChangeState<SpiderChaseState>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ■ STATE: JUMP TO CEILING (天井への大ジャンプ)
|
||||
// ==========================================
|
||||
public class SpiderJumpToCeilingState : ISpiderState
|
||||
{
|
||||
private BossController boss;
|
||||
private float roarTimer;
|
||||
private float jumpProgress;
|
||||
private Vector3 startPos;
|
||||
private bool isJumpingPhase;
|
||||
|
||||
public SpiderJumpToCeilingState(BossController owner) => boss = owner;
|
||||
|
||||
public void EnterState()
|
||||
{
|
||||
roarTimer = 2.0f; // ★天井に飛ぶ前の大威嚇の時間 (2.0秒)
|
||||
jumpProgress = 0f;
|
||||
startPos = boss.transform.position;
|
||||
isJumpingPhase = false;
|
||||
|
||||
boss.Agent.enabled = false;
|
||||
|
||||
// 1. 必殺技移行を知らせる威嚇の咆哮
|
||||
boss.Anim.SetTrigger("Attack_Roar");
|
||||
Debug.Log("🚨 蜘蛛ボス:必殺技前の一大威嚇(咆哮)! このあと天井へ飛びます。");
|
||||
}
|
||||
|
||||
public void ExitState() { }
|
||||
|
||||
public void UpdateState()
|
||||
{
|
||||
// フェーズ1:飛び立つ前の威嚇ポーズ(その場で足止め)
|
||||
if (!isJumpingPhase)
|
||||
{
|
||||
roarTimer -= Time.deltaTime;
|
||||
if (roarTimer <= 0f)
|
||||
{
|
||||
isJumpingPhase = true;
|
||||
boss.Anim.SetTrigger("JumpUp"); // ジャンプアニメーションへ
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// フェーズ2:本来の大ジャンプ空中移動
|
||||
if (boss.ceilingAnchor == null) return;
|
||||
|
||||
jumpProgress += Time.unscaledDeltaTime * 1.5f;
|
||||
|
||||
Vector3 currentPos = Vector3.Lerp(startPos, boss.ceilingAnchor.position, jumpProgress);
|
||||
currentPos.y += Mathf.Sin(jumpProgress * Mathf.PI) * 3f;
|
||||
|
||||
boss.transform.position = currentPos;
|
||||
|
||||
if (jumpProgress >= 1f)
|
||||
{
|
||||
boss.transform.position = boss.ceilingAnchor.position;
|
||||
boss.ChangeState<SpiderCeilingAttackState>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ■ STATE: CEILING ATTACK (必殺技:天井糸吐き)
|
||||
// ==========================================
|
||||
public class SpiderCeilingAttackState : ISpiderState
|
||||
{
|
||||
private BossController boss;
|
||||
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 void EnterState()
|
||||
{
|
||||
timer = stateDuration;
|
||||
shotTimer = 0f;
|
||||
boss.Anim.SetBool("IsOnCeiling", true);
|
||||
boss.PendingUltimate = false; // 発動権を消化
|
||||
Debug.Log("🕸️ 蜘蛛ボス:天井から無数の糸を乱射中!");
|
||||
}
|
||||
|
||||
public 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()
|
||||
{
|
||||
timer -= Time.deltaTime;
|
||||
shotTimer -= Time.deltaTime;
|
||||
|
||||
if (shotTimer <= 0f)
|
||||
{
|
||||
ExecuteRandomWebAttack();
|
||||
shotTimer = shotInterval;
|
||||
}
|
||||
|
||||
// 時間が来たら、地上へ復帰
|
||||
if (timer <= 0f)
|
||||
{
|
||||
boss.ChangeState<SpiderChaseState>();
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteRandomWebAttack()
|
||||
{
|
||||
// ボスの中央(地上座標)を基準に、ランダムな円内に糸を降らせる
|
||||
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
|
||||
{
|
||||
private BossController boss;
|
||||
private float stunTimer;
|
||||
|
||||
public SpiderStunState(BossController owner) => boss = owner;
|
||||
|
||||
public void EnterState()
|
||||
{
|
||||
stunTimer = 4.0f; // 4秒間の完全な無防備状態(チャンスタイム)
|
||||
boss.Anim.SetTrigger("StunStart");
|
||||
boss.Anim.SetBool("IsStunned", true);
|
||||
if (boss.Agent.enabled) boss.Agent.isStopped = true;
|
||||
Debug.Log("🛡️ パリィ成功! 蜘蛛ボスがひっくり返ってスタン中! チャンスタイム!");
|
||||
}
|
||||
|
||||
public void ExitState()
|
||||
{
|
||||
boss.Anim.SetBool("IsStunned", false);
|
||||
if (boss.Agent.enabled) boss.Agent.isStopped = false;
|
||||
}
|
||||
|
||||
public void UpdateState()
|
||||
{
|
||||
stunTimer -= Time.deltaTime;
|
||||
if (stunTimer <= 0f)
|
||||
{
|
||||
// スンが明けたら、もしHPトリガーが裏で引かれていれば優先して天井へ
|
||||
if (boss.PendingUltimate) boss.ChangeState<SpiderJumpToCeilingState>();
|
||||
else boss.ChangeState<SpiderChaseState>();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Scripts/SpiderIdleState.cs.meta
Normal file
2
Assets/_Scripts/SpiderIdleState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 67c8b05cbdd96694cbb2b2ef0a5efdd6
|
||||
122
Assets/_Scripts/VRDoorController.cs
Normal file
122
Assets/_Scripts/VRDoorController.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// 1つのトリガーで左右のドアを反対方向にスライド開閉させ、
|
||||
/// 一定時間後に自動で閉じるクラス
|
||||
/// </summary>
|
||||
public class VRDoorController : MonoBehaviour
|
||||
{
|
||||
[Header("ドアのオブジェクト登録")]
|
||||
[Tooltip("左側にスライドさせたいドアのTransformを指定してください")]
|
||||
[SerializeField] private Transform leftDoor;
|
||||
[Tooltip("右側にスライドさせたいドアのTransformを指定してください")]
|
||||
[SerializeField] private Transform rightDoor;
|
||||
|
||||
[Header("スライド量と速度の設定")]
|
||||
[Tooltip("ドアが横に何メートルずれるか(移動量)")]
|
||||
[SerializeField] private float slideDistance = 1.0f;
|
||||
[Tooltip("ドアが完全に開ききる(閉じきる)までにかかる時間(秒)")]
|
||||
[SerializeField] private float openSpeed = 1.2f;
|
||||
[Tooltip("ドアが開ききった後、自動で閉まり始めるまでの待ち時間(秒)")]
|
||||
[SerializeField] private float autoCloseDelay = 4.0f;
|
||||
|
||||
// ドアの「閉じた位置」と「開いた位置」の座標を記憶する変数
|
||||
private Vector3 leftDoorClosedPos;
|
||||
private Vector3 rightDoorClosedPos;
|
||||
private Vector3 leftDoorOpenPos;
|
||||
private Vector3 rightDoorOpenPos;
|
||||
|
||||
// ドアの状態を管理するフラグ(バグ連打の防止用)
|
||||
private bool isOpen = false;
|
||||
private bool isMoving = false;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// 1. ゲーム開始時の位置を「閉じた状態」として記憶
|
||||
if (leftDoor != null)
|
||||
{
|
||||
leftDoorClosedPos = leftDoor.localPosition;
|
||||
// 左ドアはローカルのX軸のマイナス方向へスライドさせる
|
||||
leftDoorOpenPos = leftDoorClosedPos + new Vector3(-slideDistance, 0, 0);
|
||||
}
|
||||
|
||||
if (rightDoor != null)
|
||||
{
|
||||
rightDoorClosedPos = rightDoor.localPosition;
|
||||
// 右ドアはローカルのX軸のプラス方向へスライドさせる
|
||||
rightDoorOpenPos = rightDoorClosedPos + new Vector3(slideDistance, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 【外部連携用】VRのボタンが押されたときに呼び出す公開メソッド
|
||||
/// </summary>
|
||||
public void OnButtonPress()
|
||||
{
|
||||
// すでに移動中、またはすでに開いている場合はボタンの入力を完全に無視する
|
||||
if (isMoving || isOpen) return;
|
||||
|
||||
// 開いてから自動で閉まるまでの一連のコルーチンを始動
|
||||
StartCoroutine(OpenAndAutoCloseRoutine());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ドアを開け、待機し、自動で閉める一連の流れを管理するコルーチン
|
||||
/// </summary>
|
||||
private IEnumerator OpenAndAutoCloseRoutine()
|
||||
{
|
||||
// ─── 1. ドアを開ける処理 ───
|
||||
isMoving = true;
|
||||
float elapsedTime = 0f;
|
||||
|
||||
while (elapsedTime < openSpeed)
|
||||
{
|
||||
elapsedTime += Time.deltaTime;
|
||||
float t = elapsedTime / openSpeed;
|
||||
|
||||
// ★VR酔い・チープさ対策:数値を滑らかに補間して、リアルな加減速(イージング)をかける
|
||||
float easedT = Mathf.SmoothStep(0f, 1f, t);
|
||||
|
||||
if (leftDoor != null) leftDoor.localPosition = Vector3.Lerp(leftDoorClosedPos, leftDoorOpenPos, easedT);
|
||||
if (rightDoor != null) rightDoor.localPosition = Vector3.Lerp(rightDoorClosedPos, rightDoorOpenPos, easedT);
|
||||
|
||||
yield return null; // 1フレーム待機
|
||||
}
|
||||
|
||||
// 完全に開ききった座標で固定
|
||||
if (leftDoor != null) leftDoor.localPosition = leftDoorOpenPos;
|
||||
if (rightDoor != null) rightDoor.localPosition = rightDoorOpenPos;
|
||||
|
||||
isOpen = true;
|
||||
isMoving = false;
|
||||
Debug.Log("🚪 ドアが完全に開きました。");
|
||||
|
||||
// ─── 2. 開いた状態での待機(自動クローズのカウントダウン) ───
|
||||
yield return new WaitForSeconds(autoCloseDelay);
|
||||
|
||||
// ─── 3. ドアを閉じる処理 ───
|
||||
isMoving = true;
|
||||
isOpen = false;
|
||||
elapsedTime = 0f;
|
||||
|
||||
while (elapsedTime < openSpeed)
|
||||
{
|
||||
elapsedTime += Time.deltaTime;
|
||||
float t = elapsedTime / openSpeed;
|
||||
float easedT = Mathf.SmoothStep(0f, 1f, t);
|
||||
|
||||
if (leftDoor != null) leftDoor.localPosition = Vector3.Lerp(leftDoorOpenPos, leftDoorClosedPos, easedT);
|
||||
if (rightDoor != null) rightDoor.localPosition = Vector3.Lerp(rightDoorOpenPos, rightDoorClosedPos, easedT);
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// 完全に閉じた座標で固定
|
||||
if (leftDoor != null) leftDoor.localPosition = leftDoorClosedPos;
|
||||
if (rightDoor != null) rightDoor.localPosition = rightDoorClosedPos;
|
||||
|
||||
isMoving = false;
|
||||
Debug.Log("🔒 ドアが自動で閉じました。");
|
||||
}
|
||||
}
|
||||
2
Assets/_Scripts/VRDoorController.cs.meta
Normal file
2
Assets/_Scripts/VRDoorController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c3e120316425604197ba905dffcef0d
|
||||
58
Assets/_Scripts/VRInputPoseDetector.cs
Normal file
58
Assets/_Scripts/VRInputPoseDetector.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class VRInputPoseDetector : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Transform headTransform;
|
||||
[SerializeField] private Transform rightHandTransform;
|
||||
[SerializeField] private Transform leftHandTransform;
|
||||
[SerializeField] private float minForwardDistance = 0.15f;
|
||||
[SerializeField] private float maxForwardDistance = 0.5f;
|
||||
[SerializeField] private float minHeightOffset = -0.35f;
|
||||
[SerializeField] private float maxHeightOffset = -0.1f;
|
||||
[SerializeField] private float requiredPoseTime = -0.3f;
|
||||
private float poseTimer = 0f;
|
||||
private bool isPoseMainTained = false;
|
||||
public bool IsPosing => isPoseMainTained;
|
||||
|
||||
void Update()
|
||||
{
|
||||
if(headTransform == null || rightHandTransform == null || leftHandTransform == null) return;
|
||||
|
||||
//両手の世界座標を頭を中心としたローカル座標に変換
|
||||
Vector3 localRightHand = headTransform.InverseTransformPoint(rightHandTransform.position);
|
||||
Vector3 localLeftHand = headTransform.InverseTransformPoint(leftHandTransform.position);
|
||||
|
||||
//両手が構え範囲に入っているかチェック
|
||||
bool isRightHandGuarding = CheckHandPosition(localRightHand);
|
||||
bool isLeftHandGuarding = CheckHandPosition(localLeftHand);
|
||||
|
||||
//両手が構え範囲にある場合のタイマー処理
|
||||
if(isRightHandGuarding && isLeftHandGuarding)
|
||||
{
|
||||
poseTimer += Time.deltaTime;
|
||||
if(poseTimer >= requiredPoseTime && !isPoseMainTained)
|
||||
{
|
||||
isPoseMainTained = true;
|
||||
Debug.Log("kamaeseikou");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
poseTimer = 0f;
|
||||
if(isPoseMainTained)
|
||||
{
|
||||
isPoseMainTained = false;
|
||||
Debug.Log("kamaesippai");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
private bool CheckHandPosition(Vector3 localHandPos)
|
||||
{
|
||||
bool inForwardRange = localHandPos.z >= minForwardDistance && localHandPos.z <= maxForwardDistance; //Z軸:頭より前に手があるか
|
||||
bool inHeightRange = localHandPos.y >= minHeightOffset && localHandPos.y <= maxHeightOffset; //Y軸:手の高さが適切か
|
||||
|
||||
return inForwardRange && inHeightRange;
|
||||
}
|
||||
}
|
||||
2
Assets/_Scripts/VRInputPoseDetector.cs.meta
Normal file
2
Assets/_Scripts/VRInputPoseDetector.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba5d2c8447b183c40ad8183575ee0b8e
|
||||
149
Assets/_Scripts/VRPunchAttack.cs
Normal file
149
Assets/_Scripts/VRPunchAttack.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem; // Input Systemを使うために必要
|
||||
using UnityEngine.XR; // 振動機能のために必要
|
||||
|
||||
[RequireComponent(typeof(Collider))]
|
||||
[RequireComponent(typeof(Rigidbody))]
|
||||
public class VRInputPunch : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private InputActionProperty velocityAction;
|
||||
[SerializeField] private XRNode controllerNode = XRNode.RightHand;
|
||||
[SerializeField] private VRInputPoseDetector poseDetector;
|
||||
|
||||
[Header("攻撃(パンチ)設定")]
|
||||
[SerializeField] private float minPunchSpeed = 2.0f; // パンチとして判定する最低速度 (m/s)
|
||||
[SerializeField] private float damageMultiplier = 10.0f; // ダメージ倍率
|
||||
[SerializeField] private float hapticAmplitude = 0.5f; // 通常ヒットの振動の強さ
|
||||
[SerializeField] private float hapticDuration = 0.1f; // 通常ヒットの振動の長さ
|
||||
|
||||
[Header("パリィ設定")]
|
||||
[SerializeField] private float minParrySpeed = 1.0f; // パリィできるパンチとして判定する最低速度 (m/s)
|
||||
[SerializeField] private float parryStunDuration = 3.0f; // スタン時間(秒)
|
||||
[SerializeField] private float parryHapticAmplitude = 0.9f; // パリィ成功時の振動の強さ
|
||||
[SerializeField] private float parryHapticDuration = 0.25f; // パリィ成功時の振動の長さ
|
||||
// ─────────────────────────────────────────
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (velocityAction.action != null)
|
||||
{
|
||||
velocityAction.action.Enable();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (velocityAction.action != null)
|
||||
{
|
||||
velocityAction.action.Disable();
|
||||
}
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
Rigidbody rb = GetComponent<Rigidbody>();
|
||||
rb.isKinematic = true;
|
||||
rb.useGravity = false;
|
||||
GetComponent<Collider>().isTrigger = true;
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
// コントローラーの現在の移動速度を取得
|
||||
Vector3 velocity = velocityAction.action.ReadValue<Vector3>();
|
||||
float speed = velocity.magnitude;
|
||||
|
||||
//【パリィ判定】当たったオブジェクトが敵の拳だった場合
|
||||
if (other.CompareTag("EnemyFist"))
|
||||
{
|
||||
Debug.Log($"🛡️ [パリィチェック] 敵の拳と接触。プレイヤーの手の速度: {speed:F2} m/s / 構え状態: {poseDetector.IsPosing}");
|
||||
|
||||
// 条件:構え状態(IsPosing)である、もしくは、弾くための速度が一定以上出ている場合
|
||||
if (poseDetector.IsPosing || speed >= minParrySpeed)
|
||||
{
|
||||
// 敵の親オブジェクトからEnemyAIスクリプトを検索
|
||||
EnemyAI enemyAI = other.GetComponentInParent<EnemyAI>();
|
||||
if (enemyAI != null)
|
||||
{
|
||||
// 敵の攻撃コンボを強制中断し、スタン状態にする
|
||||
enemyAI.TriggerParryStun(parryStunDuration);
|
||||
|
||||
// パリィ成功用の強力な振動をプレイヤーに返す
|
||||
TriggerHaptics(parryHapticAmplitude, parryHapticDuration);
|
||||
|
||||
Debug.Log($"🛡️ 【ジャストパリィ成功!】 敵の体勢を崩しました。スタン時間: {parryStunDuration}秒");
|
||||
return; // パリィが成立した場合は以降のパンチ処理は行わない
|
||||
}
|
||||
}
|
||||
BossController boss = other.GetComponentInParent<BossController>();
|
||||
if (boss != null)
|
||||
{
|
||||
// ボスにパリィ成立を通知(噛みつき・突進中であれば自動でスタンへ移行)
|
||||
boss.TriggerParryStun();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("🧱 ガード(または速度不足):敵の攻撃は防いだが、弾き返せなかった。");
|
||||
return;
|
||||
}
|
||||
}
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
// パンチ攻撃の処理
|
||||
Debug.Log($"[InputSystem接触] {other.name} に触れました。センサー速度: {speed:F2} m/s");
|
||||
|
||||
if (speed >= minPunchSpeed)
|
||||
{
|
||||
IDamageble damageble = other.GetComponentInParent<IDamageble>();
|
||||
|
||||
if (damageble != null)
|
||||
{
|
||||
DamageInfo info = new DamageInfo();
|
||||
// 構えからパンチするとダメージ2倍、ゲージ取得度2倍
|
||||
if (poseDetector.IsPosing)
|
||||
{
|
||||
info.amount = (speed * damageMultiplier) * 2f;
|
||||
info.isPoseBonus = true;
|
||||
|
||||
if (PlayerStatusManager.Instance != null) PlayerStatusManager.Instance.AddGauge(20f);
|
||||
}
|
||||
else
|
||||
{
|
||||
info.amount = speed * damageMultiplier;
|
||||
info.isPoseBonus = false;
|
||||
|
||||
if (PlayerStatusManager.Instance != null) PlayerStatusManager.Instance.AddGauge(10f);
|
||||
}
|
||||
info.hitPosition = other.transform.position;
|
||||
info.punchDirection = velocity.normalized;
|
||||
|
||||
if (PlayerStatusManager.Instance != null)
|
||||
{
|
||||
info.amount *= PlayerStatusManager.Instance.CurrentAttackMultiplier;
|
||||
}
|
||||
|
||||
damageble.TakeDamage(info);
|
||||
|
||||
Debug.Log($"パンチ成功 ダメージ: {info.amount:F1}");
|
||||
|
||||
// 通常ヒットの振動を発生
|
||||
TriggerHaptics(hapticAmplitude, hapticDuration);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"スピードが足りません。必要: {minPunchSpeed} / 現在: {speed:F2}");
|
||||
}
|
||||
}
|
||||
|
||||
/// 引数に応じて指定された強さと長さでコントローラーを振動させる
|
||||
private void TriggerHaptics(float amplitude, float duration)
|
||||
{
|
||||
UnityEngine.XR.InputDevice device = InputDevices.GetDeviceAtXRNode(controllerNode);
|
||||
if (device.isValid)
|
||||
{
|
||||
device.SendHapticImpulse(0, amplitude, duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Scripts/VRPunchAttack.cs.meta
Normal file
2
Assets/_Scripts/VRPunchAttack.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 432d44ce69f716a499b8e70a63583b9e
|
||||
79
Assets/_Scripts/VRUIDoorFaceFollow.cs
Normal file
79
Assets/_Scripts/VRUIDoorFaceFollow.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using UnityEngine;
|
||||
|
||||
// World Space Canvas をプレイヤーの真正面に移動させ、開いている間はプレイヤーの視点に滑らかに追従させるスクリプト
|
||||
public class VRUIDoorFaceFollow : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Transform vrCameraTransform;
|
||||
|
||||
[Header("配置・追従のパラメータ")]
|
||||
[SerializeField] private float forwardDistance = 0.5f;
|
||||
[SerializeField] private float heightOffset = -0.1f;
|
||||
[SerializeField] private float followSpeed = 5.0f;
|
||||
|
||||
// 画面がアクティブ(表示)になった瞬間を検知するためのフラグ
|
||||
private bool wasActiveLastFrame = false;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// カメラが未設定の場合は、シーン内からメインカメラを自動取得
|
||||
if (vrCameraTransform == null && Camera.main != null)
|
||||
{
|
||||
vrCameraTransform = Camera.main.transform;
|
||||
}
|
||||
}
|
||||
|
||||
// 全体の時間が止まっている(Time.timeScale = 0)間でも動作させるため、カメラ移動の後に実行される LateUpdate を使用します
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (vrCameraTransform == null) return;
|
||||
|
||||
// このオブジェクト(UI画面)が有効化された「最初のフレーム」の処理
|
||||
if (!wasActiveLastFrame)
|
||||
{
|
||||
// 開いた瞬間は、プレイヤーの今の向きの「完全な正面」に瞬間移動させる(遅れてついてくるのを防ぐため)
|
||||
SetInitialPosition();
|
||||
wasActiveLastFrame = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 毎フレームの滑らかな追従処理
|
||||
SmoothFollowPlayer();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
// 画面が閉じられたらフラグをリセットする
|
||||
wasActiveLastFrame = false;
|
||||
}
|
||||
|
||||
/// 画面が開いた瞬間に、プレイヤーの正面の正しい位置・向きに強制配置する
|
||||
private void SetInitialPosition()
|
||||
{
|
||||
// カメラの位置から、カメラが向いている正面方向に一定距離進んだ座標を計算
|
||||
Vector3 targetPosition = vrCameraTransform.position + (vrCameraTransform.forward * forwardDistance);
|
||||
targetPosition.y += heightOffset; // 高さを微調整
|
||||
|
||||
transform.position = targetPosition;
|
||||
|
||||
// 画面の向きをプレイヤー(カメラ)の方向に向ける(背を向けないようにする)
|
||||
// UIパネルがプレイヤー側を正面にするよう、LookAtの後に180度回転させて裏返りを防ぎます
|
||||
transform.LookAt(vrCameraTransform.position);
|
||||
transform.Rotate(0, 180, 0);
|
||||
}
|
||||
|
||||
/// プレイヤーが首を動かしたり移動したりした際、画面を滑らかについてこさせる
|
||||
private void SmoothFollowPlayer()
|
||||
{
|
||||
// 目的地(目標座標)の計算
|
||||
Vector3 targetPosition = vrCameraTransform.position + (vrCameraTransform.forward * forwardDistance);
|
||||
targetPosition.y += heightOffset;
|
||||
|
||||
// Time.timeScale = 0 でも動かすため、Time.unscaledDeltaTime を使用
|
||||
// 現在位置から目標位置まで滑らかに補間移動(Lerp)させる
|
||||
transform.position = Vector3.Lerp(transform.position, targetPosition, Time.unscaledDeltaTime * followSpeed);
|
||||
|
||||
// 向きも滑らかに補間(Slerp)してプレイヤーの方を向かせ続ける
|
||||
Quaternion targetRotation = Quaternion.LookRotation(transform.position - vrCameraTransform.position);
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.unscaledDeltaTime * followSpeed);
|
||||
}
|
||||
}
|
||||
2
Assets/_Scripts/VRUIDoorFaceFollow.cs.meta
Normal file
2
Assets/_Scripts/VRUIDoorFaceFollow.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97e7b0ef50eb0934e84c6f6f7c3ed536
|
||||
Reference in New Issue
Block a user