いろいろやってみた

Playerが蜘蛛にめり込まないようにしたい
This commit is contained in:
oogushiyuuga
2026-07-01 16:23:04 +09:00
parent 30ce9cbd0e
commit 4d3fee6ca5
473 changed files with 6618 additions and 24665 deletions

View 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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 60f7f85851ff9b544a35d5dbe1a21285

View 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次元の配列添字 (024) を割り出すヘルパーメソッド
private int GetIndex(int x, int y)
{
return y * 5 + x;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 945a90d6fc5ecb048849ec7b83634441

View 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.010.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);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 72dd993245dfeb94ca11fd104e817220

505
Assets/_Scripts/EnemyAI.cs Normal file
View 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
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 88407b9f9a6654436b641cce64611731

View 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);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4b4f072c824585843b120965516325b6

View 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);
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 42a222f4862dd2c46846a6be4dfe956a

View 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("押した瞬間");
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ae6f0d5ca1f0f42b6a38e38170777c16

View 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();
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5fc595679af414674be369cd3d5bc765

View 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;
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 45dd4c486e2a64ef5a8f38d756b9dc2d

View 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);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3c101f16cf16d4195ba45ae89a7f14d8

View 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); // 首の回転を設定。縦方向のみ回転させる
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 20e0bce59a58a401ca841697d32fb339

View 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バフの効果が終了しました。");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0215a75a0f67e1b49a1b823cfc7b8503

View 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("🏃 プレイヤーが糸の範囲から脱出。 鈍足効果解除。");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0926e85a7d8a1a349a8e4e6e2ddfa0a5

View 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.5m6m、または遠距離だけどクールダウン中の場合
// 💡 ここで初めて「通常歩き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>();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 67c8b05cbdd96694cbb2b2ef0a5efdd6

View 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("🔒 ドアが自動で閉じました。");
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7c3e120316425604197ba905dffcef0d

View 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;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ba5d2c8447b183c40ad8183575ee0b8e

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 432d44ce69f716a499b8e70a63583b9e

View 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);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 97e7b0ef50eb0934e84c6f6f7c3ed536