This commit is contained in:
ohgushiyuga
2026-08-27 12:51:15 +09:00
parent 1441c96f74
commit 52341100ed
32 changed files with 840 additions and 1076 deletions

View File

@@ -84,7 +84,7 @@ MonoBehaviour:
m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 0
m_PresetInfoIsWorld: 1
--- !u!114 &4150137515110638401
MonoBehaviour:
m_ObjectHideFlags: 0

View File

@@ -732,9 +732,6 @@ MonoBehaviour:
<CeilingShotInterval>k__BackingField: 0.2
<WebWarningDelay>k__BackingField: 0.8
<WebDuration>k__BackingField: 10
<WebAvoidanceRadius>k__BackingField: 2.5
warningMarkerPrefab: {fileID: 780381182997121746, guid: ad8909b4e4ba9424794a9e9054ea9e43, type: 3}
webPrefab: {fileID: 7127456385706922585, guid: 6cee7cb816e34c1478e31e98e258a22a, type: 3}
--- !u!195 &139010308
NavMeshAgent:
m_ObjectHideFlags: 0
@@ -817,6 +814,21 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: d2cd755b0c100204e833df85acb412e8, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::BossDebugController
--- !u!114 &139010321
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 139010306}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: aef8ddafc1c1d9e448ca7631fa034d60, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::BossWebSpawner
warningMarkerPrefab: {fileID: 780381182997121746, guid: ad8909b4e4ba9424794a9e9054ea9e43, type: 3}
webPrefab: {fileID: 7127456385706922585, guid: 6cee7cb816e34c1478e31e98e258a22a, type: 3}
<WebAvoidanceRadius>k__BackingField: 2.5
--- !u!1 &142265989
GameObject:
m_ObjectHideFlags: 0
@@ -4447,6 +4459,9 @@ PrefabInstance:
- targetCorrespondingSourceObject: {fileID: 1987720806421918, guid: a2dfe588a42016e40892b72d31fd1cf6, type: 3}
insertIndex: -1
addedObject: {fileID: 139010320}
- targetCorrespondingSourceObject: {fileID: 1987720806421918, guid: a2dfe588a42016e40892b72d31fd1cf6, type: 3}
insertIndex: -1
addedObject: {fileID: 139010321}
- targetCorrespondingSourceObject: {fileID: 1388461240955602, guid: a2dfe588a42016e40892b72d31fd1cf6, type: 3}
insertIndex: -1
addedObject: {fileID: 50675083}

View File

@@ -756,7 +756,7 @@ GameObject:
- component: {fileID: 119118690}
- component: {fileID: 119118689}
- component: {fileID: 119118688}
- component: {fileID: 119118691}
- component: {fileID: 119118692}
m_Layer: 5
m_Name: Button (Legacy)
m_TagString: Untagged
@@ -866,18 +866,27 @@ CanvasRenderer:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 119118686}
m_CullTransparentMesh: 1
--- !u!114 &119118691
MonoBehaviour:
--- !u!65 &119118692
BoxCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 119118686}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bca5237dce7df3543ba5a9c686d545d6, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::VRButtonTest
serializedVersion: 3
m_Size: {x: 1, y: 1, z: 1}
m_Center: {x: 0, y: 0, z: 0}
--- !u!1001 &130511407
PrefabInstance:
m_ObjectHideFlags: 0
@@ -14741,7 +14750,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
m_IsActive: 0
--- !u!224 &591090780
RectTransform:
m_ObjectHideFlags: 0
@@ -646636,6 +646645,7 @@ GameObject:
m_Component:
- component: {fileID: 1455781481}
- component: {fileID: 1455781480}
- component: {fileID: 1455781482}
m_Layer: 0
m_Name: PlayerManager
m_TagString: Untagged
@@ -646655,7 +646665,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 0215a75a0f67e1b49a1b823cfc7b8503, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::PlayerStatusManager
ignoreGaugeForDebug: 1
ignoreGaugeForDebug: 0
zlButtonAction:
m_UseReference: 1
m_Action:
@@ -646716,12 +646726,13 @@ MonoBehaviour:
maxGauge: 10
currentGauge: 0
codingModeUI: {fileID: 797096988}
buffDuration: 10
attackBuffMultiplier: 1.5
defenseBuffMultiplier: 0.5
hpHealAmount: 30
successEffectPrefab: {fileID: 3075638675573572061, guid: 59334a8a54b4cbc48996b04618ef137c, type: 3}
effectSpawnPoint: {fileID: 797096992}
buffHandler:
buffDuration: 10
attackBuffMultiplier: 1.5
defenseBuffMultiplier: 0.5
hpHealAmount: 30
--- !u!4 &1455781481
Transform:
m_ObjectHideFlags: 0
@@ -646737,6 +646748,18 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1455781482
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1455781479}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6f00095a3e63cc440be35394ad49b505, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::WorldPauseManager
--- !u!1001 &1455907610
PrefabInstance:
m_ObjectHideFlags: 0
@@ -656775,7 +656798,8 @@ PrefabInstance:
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedComponents:
- {fileID: 3150725272858075539, guid: 469b8fdf008b844589dcc60b237284c5, type: 3}
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []

View File

@@ -1,24 +1,29 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using System.Collections.Generic;
[RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(Animator))]
public class BossController : MonoBehaviour, IDamageble
[RequireComponent(typeof(BossWebSpawner))]
public class BossController : MonoBehaviour, IDamageable, IPauseable
{
[Header("ステータス")]
[SerializeField] private float maxHp = 500f;
[SerializeField] private float currentHp = 500f;
[field: Header("移動・攻撃パラメータ")]
[field: SerializeField] public float ChaseRange { get; private set; } = 15f;
[field: SerializeField] public float BiteRange { get; private set; } = 5f;
[field: SerializeField] public float ChargeSpeed { get; private set; } = 14f;
[field: SerializeField] public float ChargeDuration { get; private set; } = 2f;
[field: SerializeField] public float ChargeMinimumDistance { get; private set; } = 6f;
[field: SerializeField] public float ChargeCooldownDuration { get; private set; } = 4f;
[field: SerializeField] public float ChargeRoarDuration { get; private set; } = 5f;
[SerializeField] public Transform ceilingAnchor;
[SerializeField] public Transform groundAnchor;
[Header("アンカー・天井技設定")]
public Transform ceilingAnchor;
public Transform groundAnchor;
[field: SerializeField] public float AttackRadius { get; private set; } = 15f;
[field: SerializeField] public float JumpRoarDuration { get; private set; } = 2f;
[field: SerializeField] public float CeilingAttackDuration { get; private set; } = 8.0f;
@@ -26,33 +31,28 @@ public class BossController : MonoBehaviour, IDamageble
[field: SerializeField] public float WebWarningDelay { get; private set; } = 0.8f;
[field: SerializeField] public float WebDuration { get; private set; } = 3.0f;
[Tooltip("★新設:すでに糸がある場所から、最低何メートル離すか(被り防止の半径)")]
[field: SerializeField] public float WebAvoidanceRadius { get; private set; } = 2.5f;
// ★新設:現在ステージ上に存在している(または予兆がある)糸の座標リスト
public List<Vector3> ActiveWebPositions { get; private set; } = new List<Vector3>();
[SerializeField] private GameObject warningMarkerPrefab;
[SerializeField] private GameObject webPrefab;
public NavMeshAgent Agent { get; private set; }
public Animator Anim { get; private set; }
public Transform Player { get; private set; }
public float ChargeCooldownTimer { get; private set; } = 0f;
public BossWebSpawner WebSpawner { get; private set; }
public float ChargeCooldownTimer { get; private set; }
public float NormalSpeed { get; private set; }
public float NormalAcceleration { get; private set; }
public bool PendingUltimate { get; set; }
private ISpiderState currentState;
private Dictionary<System.Type, ISpiderState> stateCache;
private Dictionary<Type, ISpiderState> stateCache;
// フェーズトリガー
private bool triggered50 = false;
private bool triggered25 = false;
public bool PendingUltimate { get; set; } = false;
private bool triggered50;
private bool triggered25;
private bool isPaused;
private void Awake()
{
Agent = GetComponent<NavMeshAgent>();
Anim = GetComponent<Animator>();
WebSpawner = GetComponent<BossWebSpawner>();
GameObject pObj = GameObject.FindGameObjectWithTag("Player");
if (pObj != null) Player = pObj.transform;
@@ -67,11 +67,44 @@ public class BossController : MonoBehaviour, IDamageble
NormalSpeed = Agent.speed;
NormalAcceleration = Agent.acceleration;
}
if (WorldPauseManager.Instance != null)
{
WorldPauseManager.Instance.Register(this);
}
}
private void OnDestroy()
{
if (WorldPauseManager.Instance != null)
{
WorldPauseManager.Instance.Unregister(this);
}
}
public void Pause()
{
isPaused = true;
if (Anim != null) Anim.speed = 0f;
if (Agent != null && Agent.enabled && Agent.isOnNavMesh)
{
Agent.isStopped = true;
}
}
public void Resume()
{
isPaused = false;
if (Anim != null) Anim.speed = 1f;
if (Agent != null && Agent.enabled && Agent.isOnNavMesh)
{
Agent.isStopped = false;
}
}
private void InitializeFSM()
{
stateCache = new Dictionary<System.Type, ISpiderState>
stateCache = new Dictionary<Type, ISpiderState>
{
{ typeof(SpiderIdleState), new SpiderIdleState(this) },
{ typeof(SpiderChaseState), new SpiderChaseState(this) },
@@ -87,6 +120,8 @@ public class BossController : MonoBehaviour, IDamageble
private void Update()
{
if (isPaused) return;
currentState?.UpdateState();
if (ChargeCooldownTimer > 0f)
@@ -97,26 +132,18 @@ public class BossController : MonoBehaviour, IDamageble
public void ChangeState<T>() where T : ISpiderState
{
if (currentState != null)
{
currentState.ExitState();
ResetAllAnimationTriggers();
Debug.Log($"🕷️ BossSpider [Exit]: {currentState.GetType().Name}");
}
currentState?.ExitState();
ResetAllAnimationTriggers();
currentState = stateCache[typeof(T)];
Debug.Log($"🕷️ BossSpider [Enter]: {currentState.GetType().Name}");
currentState.EnterState();
}
public void TakeDamage(DamageInfo damageInfo)
{
currentHp -= damageInfo.amount;
currentHp = Mathf.Clamp(currentHp, 0, maxHp);
currentHp = Mathf.Clamp(currentHp - damageInfo.amount, 0, maxHp);
float hpPercentage = (currentHp / maxHp) * 100f;
Debug.Log($"💥 ボスに {damageInfo.amount} ダメージ。残りHP: {hpPercentage:F1}%");
if (hpPercentage <= 25f && !triggered25)
{
triggered25 = true;
@@ -143,7 +170,7 @@ public class BossController : MonoBehaviour, IDamageble
public bool TriggerParryStun()
{
if (currentState is SpiderBiteState || currentState is SpiderChargeState)
if (currentState is SpiderBiteState or SpiderChargeState)
{
ChangeState<SpiderStunState>();
return true;
@@ -151,49 +178,7 @@ public class BossController : MonoBehaviour, IDamageble
return false;
}
public void ResetChargeCooldown()
{
ChargeCooldownTimer = ChargeCooldownDuration;
}
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. 生成する座標をアクティブリストに登録(この場所への他の割り込みを禁止する)
ActiveWebPositions.Add(pos);
// 2. 地面に予兆マークを生成
GameObject marker = null;
if (warningMarkerPrefab != null)
{
marker = Instantiate(warningMarkerPrefab, pos + new Vector3(0, 0.5f, 0), Quaternion.identity);
}
yield return new WaitForSeconds(delay);
if (marker != null) Destroy(marker);
// 3. 本物の糸オブジェクトを生成(X軸90度回転)
if (webPrefab != null)
{
Quaternion webRotation = Quaternion.Euler(270f, 0f, 0f);
GameObject web = Instantiate(webPrefab, pos + new Vector3(0, 0.5f, 0), webRotation);
// ★持続時間(duration)が終わるまで待機してからDestroyする形に変更
yield return new WaitForSeconds(duration);
if (web != null) Destroy(web);
}
else
{
yield return new WaitForSeconds(duration);
}
// 4. 糸が消滅したので、リストから座標を削除してスペースを解放する
ActiveWebPositions.Remove(pos);
}
public void ResetChargeCooldown() => ChargeCooldownTimer = ChargeCooldownDuration;
public void ResetAllAnimationTriggers()
{
@@ -206,46 +191,11 @@ public class BossController : MonoBehaviour, IDamageble
Anim.ResetTrigger("StunStart");
}
#region デバッグ用機能
/// <summary>
/// Unityインスペクターの右上メニューから実行できるデバッグ用コマンドです
/// </summary>
[ContextMenu("🛠️ デバッグ:強制必殺技(天井ジャンプ)")]
[ContextMenu("🛠️ デバッグ:強制必殺技")]
public void DebugTriggerUltimate()
{
if (!Application.isPlaying)
{
Debug.LogWarning("⚠️ ゲームが再生中(Playモード)の時のみ実行可能です。");
return;
}
Debug.Log("🛠️ 【デバッグ操作】必殺技の予約券を発行し、強制的に天井ジャンプステートへ遷移します。");
// 必殺技の発動権をONにし、現在の行動をキャンセルして天井ジャンプへ移行
if (!Application.isPlaying) return;
PendingUltimate = true;
ChangeState<SpiderJumpToCeilingState>();
}
#endregion
}
/// 共通データ(boss参照)を自動保持し、各子ステートの無駄なコードを極限まで削ります。
public abstract class SpiderStateBase : ISpiderState
{
protected readonly BossController boss;
protected SpiderStateBase(BossController owner)
{
boss = owner;
}
public virtual void EnterState() { }
public abstract void UpdateState();
public virtual void ExitState() { }
}
public interface ISpiderState
{
void EnterState();
void UpdateState();
void ExitState();
}

View File

@@ -0,0 +1,81 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BossWebSpawner : MonoBehaviour, IPauseable
{
[SerializeField] private GameObject warningMarkerPrefab;
[SerializeField] private GameObject webPrefab;
[field: SerializeField] public float WebAvoidanceRadius { get; private set; } = 2.5f;
public List<Vector3> ActiveWebPositions { get; private set; } = new List<Vector3>();
private bool isPaused;
private void Start()
{
if (WorldPauseManager.Instance != null)
{
WorldPauseManager.Instance.Register(this);
}
}
private void OnDestroy()
{
if (WorldPauseManager.Instance != null)
{
WorldPauseManager.Instance.Unregister(this);
}
}
public void Pause() => isPaused = true;
public void Resume() => isPaused = false;
private IEnumerator WaitForUnpausedSeconds(float duration)
{
float timer = 0f;
while (timer < duration)
{
if (!isPaused)
{
timer += Time.deltaTime;
}
yield return null;
}
}
public void SpawnWarningAndWeb(Vector3 targetGroundPos, float delay, float webDuration)
{
StartCoroutine(SpawnWebRoutine(targetGroundPos, delay, webDuration));
}
private IEnumerator SpawnWebRoutine(Vector3 pos, float delay, float duration)
{
ActiveWebPositions.Add(pos);
GameObject marker = null;
if (warningMarkerPrefab != null)
{
marker = Instantiate(warningMarkerPrefab, pos + new Vector3(0, 0.5f, 0), Quaternion.identity);
}
yield return WaitForUnpausedSeconds(delay);
if (marker != null) Destroy(marker);
if (webPrefab != null)
{
Quaternion webRotation = Quaternion.Euler(270f, 0f, 0f);
GameObject web = Instantiate(webPrefab, pos + new Vector3(0, 0.5f, 0), webRotation);
yield return WaitForUnpausedSeconds(duration);
if (web != null) Destroy(web);
}
else
{
yield return WaitForUnpausedSeconds(duration);
}
ActiveWebPositions.Remove(pos);
}
}

View File

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

View File

@@ -0,0 +1,54 @@
using System;
using UnityEngine;
public enum BuffType
{
None,
AttackUp,
DefenseUp,
Heal
}
[Serializable]
public class BuffHandler
{
[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;
private float defenseBuffTimer;
public bool IsAttackBuffActive => attackBuffTimer > 0f;
public bool IsDefenseBuffActive => defenseBuffTimer > 0f;
public float CurrentAttackMultiplier => IsAttackBuffActive ? attackBuffMultiplier : 1.0f;
public float CurrentDefenseMultiplier => IsDefenseBuffActive ? defenseBuffMultiplier : 1.0f;
public void UpdateTimers(float deltaTime)
{
if (attackBuffTimer > 0f) attackBuffTimer -= deltaTime;
if (defenseBuffTimer > 0f) defenseBuffTimer -= deltaTime;
}
public void ActivateBuff(BuffType type)
{
switch (type)
{
case BuffType.AttackUp:
attackBuffTimer = buffDuration;
break;
case BuffType.DefenseUp:
defenseBuffTimer = buffDuration;
break;
case BuffType.Heal:
ExecuteHeal();
break;
}
}
private void ExecuteHeal()
{
Debug.Log($"💚 HP回復バフ発動:回復量 {hpHealAmount}");
}
}

View File

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

View File

@@ -1,37 +1,27 @@
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using UnityEngine.XR;
using System.Collections;
public class ElevatorSceneTransition : MonoBehaviour
{
[Header("遷移先の設定")]
[Header("シーン設定")]
[SerializeField] private string bossSceneName = "BossStage";
[SerializeField] private string playerTag = "Player";
[Header("ローディングUI設定")]
[Header("UI設定")]
[SerializeField] private GameObject loadingCanvas;
[SerializeField] private Text progressText;
[SerializeField] private Slider progressSlider;
[Header("エレベーター下降演出設定")]
[Tooltip("エレベーターの部屋(壁や床、UIすべて)をまとめているオブジェクトを指定してください(※プレイヤーは外しておきます)")]
[Header("演出設定")]
[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 bool isLoadingStarted;
private Vector3 originalCagePosition;
private void Start()
@@ -40,7 +30,6 @@ public class ElevatorSceneTransition : MonoBehaviour
if (elevatorCageTransform != null)
{
// ワールド空間での初期位置を記憶(localPositionからpositionに変更し、より確実に)
originalCagePosition = elevatorCageTransform.position;
}
}
@@ -53,13 +42,11 @@ public class ElevatorSceneTransition : MonoBehaviour
{
isLoadingStarted = true;
// ★安全対策:接触したプレイヤーのTransformを自動でキャッシュする
if (playerTransform == null)
{
playerTransform = other.transform;
}
Debug.Log("🛗 プレイヤー乗車:親子関係なしのデルタ同期で下降演出を開始します。");
StartCoroutine(LoadSceneAsyncRoutine());
}
}
@@ -79,28 +66,21 @@ public class ElevatorSceneTransition : MonoBehaviour
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; // 横揺れのみ
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)
@@ -112,10 +92,8 @@ public class ElevatorSceneTransition : MonoBehaviour
yield return null;
}
// 到着後、エレベーターの位置を元の位置に綺麗に戻す
if (elevatorCageTransform != null)
{
// 最後の移動差分を計算してプレイヤーを戻す
Vector3 previousCagePosition = elevatorCageTransform.position;
elevatorCageTransform.position = originalCagePosition;
Vector3 cageDelta = elevatorCageTransform.position - previousCagePosition;
@@ -127,7 +105,6 @@ public class ElevatorSceneTransition : MonoBehaviour
yield return new WaitForSeconds(1.0f);
Debug.Log("🎉 ボスステージに到着。シーンを切り替えます。");
asyncLoad.allowSceneActivation = true;
}

View File

@@ -1,20 +1,12 @@
using System.Collections;
using UnityEngine;
using UnityEngine.AI;
using System.Collections;
using System;
[RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(Animator))]
public class EnemyAI : MonoBehaviour
public class EnemyAI : MonoBehaviour, IPauseable
{
/// 敵AIの状態を表すステート
public enum AIState
{
Wander, // 徘徊
Chase, // 追跡
Attack, // 攻撃
Stun // 怯み
}
public enum AIState { Wander, Chase, Attack, Stun }
[Header("現在の状態")]
[SerializeField] private AIState currentState = AIState.Wander;
@@ -25,21 +17,14 @@ public class EnemyAI : MonoBehaviour
[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)設定")]
[Header("徘徊設定")]
[SerializeField] private float wanderRadius = 8f;
[SerializeField] private float minWaitTime = 2f;
[SerializeField] private float maxWaitTime = 4f;
@@ -49,116 +34,142 @@ public class EnemyAI : MonoBehaviour
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 bool isCooldown;
private bool isAttacking;
private bool isWandering;
private bool isKnockbacking;
private bool isDead;
private bool isPaused;
private void Start()
private static readonly int SpeedHash = Animator.StringToHash("Speed");
private static readonly int StunHash = Animator.StringToHash("Stun");
private static readonly int HitHash = Animator.StringToHash("Hit");
private static readonly int TelegraphHash = Animator.StringToHash("Telegraph");
private static readonly int AttackHash = Animator.StringToHash("Attack");
private void Awake()
{
agent = GetComponent<NavMeshAgent>();
anim = GetComponent<Animator>();
}
private void Start()
{
SetFistCollider(false);
// AIのメインループコルーチンを開始
if (WorldPauseManager.Instance != null)
{
WorldPauseManager.Instance.Register(this);
}
StartCoroutine(AILoop());
}
private void OnDestroy()
{
if (WorldPauseManager.Instance != null)
{
WorldPauseManager.Instance.Unregister(this);
}
}
public void Pause()
{
isPaused = true;
if (anim != null) anim.speed = 0f;
if (agent != null && agent.enabled && agent.isOnNavMesh)
{
agent.isStopped = true;
}
}
public void Resume()
{
isPaused = false;
if (anim != null) anim.speed = 1f;
if (agent != null && agent.enabled && agent.isOnNavMesh && !isDead && !isKnockbacking && currentState != AIState.Stun)
{
agent.isStopped = false;
}
}
private IEnumerator WaitForUnpausedSeconds(float duration)
{
float timer = 0f;
while (timer < duration)
{
if (!isPaused)
{
timer += Time.deltaTime;
}
yield return null;
}
}
private void Update()
{
if(isDead) return;
if (isDead || isPaused) return;
// 毎フレームのアニメーションパラメータ更新処理
UpdateAnimation();
// 攻撃ステートにおけるプレイヤーへの方向転換処理
UpdateRotationOnAttack();
}
#region AI メインループ (思考ロジック)
private IEnumerator AILoop()
{
while (true)
while (!isDead)
{
if(isDead) yield break;
// プレイヤーが未検出の場合はタグから検索
if (playerTransform == null)
if (isPaused)
{
FindPlayer();
yield return null;
continue;
}
// 攻撃、のけぞりアニメーションの再生中でない場合のみ、ステートに応じた行動を実行
if (playerTransform == null) FindPlayer();
if (!isAttacking && !isKnockbacking && currentState != AIState.Stun)
{
switch (currentState)
{
case AIState.Wander:
WanderBehavior();
break;
case AIState.Chase:
ChaseBehavior();
break;
case AIState.Attack:
AttackBehavior();
break;
case AIState.Wander: WanderBehavior(); break;
case AIState.Chase: ChaseBehavior(); break;
case AIState.Attack: AttackBehavior(); break;
}
}
// 毎フレーム実行を避け、0.2秒待機することでCPU負荷を軽減
yield return new WaitForSeconds(0.2f);
yield return WaitForUnpausedSeconds(0.2f);
}
}
private void FindPlayer()
{
GameObject playerObj = GameObject.FindGameObjectWithTag(playerTag);
if (playerObj != null)
{
playerTransform = playerObj.transform;
}
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;
}
if (playerTransform == null) { currentState = AIState.Wander; return; }
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
// 攻撃射程内に入ったら攻撃ステートへ遷移し、その場に停止
if (distanceToPlayer <= attackRange)
{
currentState = AIState.Attack;
@@ -166,106 +177,77 @@ public class EnemyAI : MonoBehaviour
return;
}
// プレイヤーが見失う範囲を超えたら徘徊ステートにリセット
if (distanceToPlayer > loseRange)
{
currentState = AIState.Wander;
agent.ResetPath();
Debug.Log($"❓ {gameObject.name}: プレイヤーを見失った。");
return;
}
// のけぞり中でない場合、追跡の目的地を更新する
if(!isKnockbacking)
if (!isKnockbacking)
{
agent.SetDestination(playerTransform.position);
}
}
/// 【攻撃状態】の思考ロジック
private void AttackBehavior()
{
if (playerTransform == null)
{
currentState = AIState.Wander;
return;
}
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);
anim.SetFloat(SpeedHash, targetSpeed, 0.1f, Time.deltaTime);
}
/// 攻撃ステートの際、プレイヤーの方向へ滑らかに旋回させる
private void UpdateRotationOnAttack()
{
if (currentState == AIState.Attack && playerTransform != null && !isKnockbacking && currentState != AIState.Stun)
{
Vector3 direction = (playerTransform.position - transform.position).normalized;
direction.y = 0; // 上下方向の傾き(ピッチ回転)を無視
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;
if (isDead || currentState == AIState.Stun) return;
SetFistCollider(false);
StopAllCoroutines();
isAttacking = false;
isWandering = false;
if(agent.isOnNavMesh)
{
agent.isStopped = 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");
if (isDead) return;
SetFistCollider(false);
StopAllCoroutines();
@@ -280,7 +262,7 @@ public class EnemyAI : MonoBehaviour
{
isDead = true;
StopAllCoroutines();
if(agent != null && agent.isOnNavMesh)
if (agent != null && agent.isOnNavMesh)
{
agent.isStopped = true;
agent.ResetPath();
@@ -291,24 +273,19 @@ public class EnemyAI : MonoBehaviour
{
currentState = AIState.Stun;
if(agent.isOnNavMesh)
if (agent.isOnNavMesh)
{
agent.isStopped = true;
agent.ResetPath();
}
anim.SetTrigger("Stun");
anim.SetTrigger(StunHash);
yield return WaitForUnpausedSeconds(duration);
yield return new WaitForSeconds(duration);
if(agent.isOnNavMesh)
{
agent.isStopped = false;
}
if (agent.isOnNavMesh) agent.isStopped = false;
currentState = AIState.Chase;
isCooldown = false;
StartCoroutine(AILoop());
}
@@ -316,65 +293,48 @@ public class EnemyAI : MonoBehaviour
{
isKnockbacking = true;
if(agent.isOnNavMesh)
if (agent.isOnNavMesh)
{
agent.isStopped = true;
agent.ResetPath();
}
anim.SetTrigger("Hit");
anim.SetTrigger(HitHash);
yield return WaitForUnpausedSeconds(duration);
yield return new WaitForSeconds(duration);
if(agent.isOnNavMesh)
{
agent.isStopped = false;
}
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++)
for (int i = 0; i < 10; i++)
{
newPos = GetRandomNavMeshPoint(transform.position, wanderRadius);
NavMeshPath path = new NavMeshPath();
if (agent.CalculatePath(newPos, path))
if (agent.CalculatePath(newPos, path) && path.status == NavMeshPathStatus.PathComplete)
{
if (path.status == NavMeshPathStatus.PathComplete)
{
isPathValid = true;
break;
}
isPathValid = true;
break;
}
}
// 有効な目的地が設定できた場合の移動処理
if (isPathValid)
{
agent.SetDestination(newPos);
float elapsedTime = 0f;
// 目的地に到達するか、タイムアウト時間を迎えるまで待機
while (agent.remainingDistance > agent.stoppingDistance)
{
yield return new WaitForSeconds(0.5f);
yield return WaitForUnpausedSeconds(0.5f);
elapsedTime += 0.5f;
// スタック対策としてのタイムアウト判定
if (elapsedTime >= wanderTimeout)
{
agent.ResetPath();
@@ -383,57 +343,36 @@ public class EnemyAI : MonoBehaviour
}
}
// 到着または諦めた後、ランダムな時間その場で待機
yield return new WaitForSeconds(UnityEngine.Random.Range(minWaitTime, maxWaitTime));
yield return WaitForUnpausedSeconds(Random.Range(minWaitTime, maxWaitTime));
isWandering = false;
}
/// 予兆から攻撃アニメーションのトリガーまでを制御するコルーチン
private IEnumerator PerformPunchAttack()
{
isAttacking = true;
isCooldown = true; // ここで攻撃フラグをロックします
isCooldown = true;
anim.SetTrigger("Telegraph");
Debug.Log($"⚠️ {gameObject.name}: 攻撃の予兆(溜め)開始");
anim.SetTrigger(TelegraphHash);
yield return WaitForUnpausedSeconds(telegraphDuration);
yield return new WaitForSeconds(telegraphDuration);
if (isKnockbacking || isDead)
{
isAttacking = false;
yield break;
}
if(isKnockbacking) yield break;
anim.SetTrigger("Attack");
anim.SetTrigger(AttackHash);
}
/// 連続攻撃成功時の、クイックなクールダウン解除処理
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;
if (isKnockbacking || currentState == AIState.Stun || isPaused) return;
SetFistCollider(true);
}
/// Recovery(硬直)アニメーションの終了時に実行される処理(Animation Eventから呼び出し)
public void OnRecoveryEnd()
{
if(isKnockbacking || currentState == AIState.Stun) return;
if (isKnockbacking || currentState == AIState.Stun) return;
SetFistCollider(false);
isAttacking = false;
@@ -447,59 +386,51 @@ public class EnemyAI : MonoBehaviour
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 IEnumerator ResetCooldownQuickly()
{
yield return WaitForUnpausedSeconds(comboInterval);
isCooldown = false;
}
private IEnumerator ResetCooldown()
{
yield return WaitForUnpausedSeconds(attackCooldown);
isCooldown = false;
}
private void SetFistCollider(bool enabledState)
{
if(rightHandCollider != null)
{
rightHandCollider.enabled = enabledState;
}
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))
Vector3 randomDirection = Random.insideUnitSphere * radius + center;
if (NavMesh.SamplePosition(randomDirection, out NavMeshHit 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

@@ -1,68 +1,74 @@
using System;
using UnityEngine;
public class EnemyHealth : MonoBehaviour, IDamageble
public class EnemyHealth : MonoBehaviour, IDamageable
{
[SerializeField] private float maxHealth = 100f;
private float currentHealth;
[SerializeField] private float knockbackDuration = 0.5f;
public float CurrentHealth { get; private set; }
public bool IsDead { get; private set; }
public event Action<float> OnKnockbackRequested;
public event Action OnDeath;
private EnemyAI enemyAI;
private Animator anim;
private Collider col;
private bool isDead = false;
private static readonly int DeadHash = Animator.StringToHash("Dead");
void Start()
private void Awake()
{
currentHealth = maxHealth; //HPを代入
CurrentHealth = maxHealth;
enemyAI = GetComponent<EnemyAI>();
anim = GetComponent<Animator>();
col = GetComponent<Collider>();
}
private void OnEnable()
{
if (enemyAI != null)
{
OnKnockbackRequested += enemyAI.TriggerKnockback;
OnDeath += enemyAI.DisableAIOnDeath;
}
}
private void OnDisable()
{
if (enemyAI != null)
{
OnKnockbackRequested -= enemyAI.TriggerKnockback;
OnDeath -= enemyAI.DisableAIOnDeath;
}
}
public void TakeDamage(DamageInfo damageInfo)
{
if(currentHealth <= 0) return;
if (IsDead) return;
currentHealth -= damageInfo.amount; //ダメージ計算
CurrentHealth = Mathf.Max(0f, CurrentHealth - damageInfo.amount);
if(damageInfo.isPoseBonus)
{
Debug.Log("構えボーナス");
}
if(currentHealth <= 0)
if (CurrentHealth <= 0f)
{
Die();
}
else
{
if(enemyAI != null)
{
enemyAI.TriggerKnockback(knockbackDuration);
}
OnKnockbackRequested?.Invoke(knockbackDuration);
}
}
private void Die()
{
if(isDead) return;
isDead = true;
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();
}
if (anim != null) anim.SetTrigger(DeadHash);
if (col != null) col.enabled = false;
OnDeath?.Invoke();
Destroy(gameObject, 3.0f);
}
}

View File

@@ -2,12 +2,13 @@ using UnityEngine;
public struct DamageInfo
{
public float amount; //ダメージ量
public Vector3 hitPosition; //殴られた位置
public Vector3 punchDirection; //パンチが飛んできた方向(ノックバック用)
public bool isPoseBonus; //構えから攻撃かどうか
public float amount;
public Vector3 hitPosition;
public Vector3 punchDirection;
public bool isPoseBonus;
}
public interface IDamageble
public interface IDamageable
{
void TakeDamage(DamageInfo damageInfo);
}

View File

@@ -0,0 +1,5 @@
public interface IPauseable
{
void Pause();
void Resume();
}

View File

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

View File

@@ -1,28 +0,0 @@
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

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

View File

@@ -3,55 +3,56 @@ using UnityEngine.InputSystem;
public class InputManagerLR : MonoBehaviour
{
static InputManagerLR instance;
private static InputManagerLR instance;
[SerializeField]
InputActionAsset ActionAsset;
[SerializeField] private InputActionAsset actionAsset;
InputActionMap ActionMap;
InputAction m_PrimaryButtonR;
InputAction m_SecondaryButtonR;
InputAction m_PrimaryButtonL;
InputAction m_SecondaryButtonL;
private InputActionMap actionMap;
private InputAction primaryButtonR;
private InputAction secondaryButtonR;
private InputAction primaryButtonL;
private InputAction secondaryButtonL;
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
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);
DontDestroyOnLoad(gameObject);
if (actionAsset != null)
{
actionMap = actionAsset.FindActionMap("Test");
if (actionMap != null)
{
primaryButtonR = actionMap.FindAction("XR_PrimaryButtonR", throwIfNotFound: false);
primaryButtonL = actionMap.FindAction("XR_PrimaryButtonL", throwIfNotFound: false);
secondaryButtonR = actionMap.FindAction("XR_SecondaryButtonR", throwIfNotFound: false);
secondaryButtonL = actionMap.FindAction("XR_SecondaryButtonL", throwIfNotFound: false);
}
}
}
private void OnEnable()
{
ActionMap?.Enable();
}
private void OnEnable() => actionMap?.Enable();
private void OnDisable() => actionMap?.Disable();
private void OnDisable()
{
ActionMap?.Disable();
}
public static bool PrimaryButtonR() => instance?.primaryButtonR?.IsPressed() ?? false;
public static bool PrimaryButtonR_OnPress() => instance?.primaryButtonR?.WasPressedThisFrame() ?? false;
public static bool PrimaryButtonR_OnRelease() => instance?.primaryButtonR?.WasReleasedThisFrame() ?? false;
// 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();
public static bool PrimaryButtonL() => instance?.primaryButtonL?.IsPressed() ?? false;
public static bool PrimaryButtonL_OnPress() => instance?.primaryButtonL?.WasPressedThisFrame() ?? false;
public static bool PrimaryButtonL_OnRelease() => instance?.primaryButtonL?.WasReleasedThisFrame() ?? false;
// 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();
public static bool SecondaryButtonR() => instance?.secondaryButtonR?.IsPressed() ?? false;
public static bool SecondaryButtonR_OnPress() => instance?.secondaryButtonR?.WasPressedThisFrame() ?? false;
public static bool SecondaryButtonR_OnRelease() => instance?.secondaryButtonR?.WasReleasedThisFrame() ?? false;
// 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();
public static bool SecondaryButtonL() => instance?.secondaryButtonL?.IsPressed() ?? false;
public static bool SecondaryButtonL_OnPress() => instance?.secondaryButtonL?.WasPressedThisFrame() ?? false;
public static bool SecondaryButtonL_OnRelease() => instance?.secondaryButtonL?.WasReleasedThisFrame() ?? false;
}

View File

@@ -1,74 +0,0 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
/// <summary>
/// XR UIが反応しない原因を自動判定する超安全な診断スクリプト
/// </summary>
public class XRUIDiagnoser : MonoBehaviour
{
private void Start()
{
Debug.Log("============== 🔍 XR UI 自動診断スタート ==============");
// 1. EventSystem の重複・存在チェック
var eventSystems = FindObjectsByType<EventSystem>(FindObjectsInactive.Include, FindObjectsSortMode.None);
if (eventSystems.Length == 0)
{
Debug.LogError("🚨【原因】シーンに EventSystem が存在しません!");
}
else if (eventSystems.Length > 1)
{
Debug.LogError($"🚨【原因】EventSystem がシーンに {eventSystems.Length} 個あります!これが最大の原因になりやすいです。不要な方を削除してください。");
foreach (var es in eventSystems) Debug.Log($" ・配置場所: {es.gameObject.name}", es.gameObject);
}
else
{
var module = eventSystems[0].GetComponent<BaseInputModule>();
Debug.Log($"⭕ EventSystem: OK [{eventSystems[0].name}] | Module: {(module != null ? module.GetType().Name : "なし")}");
}
// 2. Ray Interactor (ビーム発射体) の存在チェック (名前で動的探索するためバージョンエラーになりません)
var allComponents = FindObjectsByType<Component>(FindObjectsInactive.Include, FindObjectsSortMode.None);
int rayCount = 0;
foreach (var c in allComponents)
{
if (c != null && c.GetType().Name.Contains("RayInteractor"))
{
rayCount++;
bool active = c.gameObject.activeInHierarchy;
if (!active)
Debug.LogError($"🚨【原因】[{c.gameObject.name}] の RayInteractor オブジェクトが非アクティブ(OFF)になっています!", c.gameObject);
else
Debug.Log($"⭕ Ray Interactor 発見: [{c.gameObject.name}] ({c.GetType().Name}) | Active: OK");
}
}
if (rayCount == 0)
{
Debug.LogError("🚨【原因】コントローラーの手のオブジェクトに RayInteractor コンポーネントが見つかりません!");
}
// 3. Canvas の設定チェック
var canvases = FindObjectsByType<Canvas>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
if (canvases.Length == 0)
{
Debug.LogError("🚨【原因】アクティブな Canvas がシーン内に存在しません!");
}
foreach (var canvas in canvases)
{
var raycaster = canvas.GetComponent<GraphicRaycaster>();
if (raycaster == null)
{
Debug.LogError($"🚨【原因】Canvas [{canvas.name}] に Raycaster コンポーネントが付いていません!", canvas.gameObject);
}
else
{
Debug.Log($"⭕ Canvas: OK [{canvas.name}] | RenderMode: {canvas.renderMode} | Raycaster: {raycaster.GetType().Name}");
}
}
Debug.Log("========================================================");
}
}

View File

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

View File

@@ -1,7 +1,5 @@
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.AI;
using System.Collections.Generic;
public class PlayerStatusManager : MonoBehaviour
{
@@ -10,83 +8,66 @@ public class PlayerStatusManager : MonoBehaviour
[Header("デバッグ設定")]
[SerializeField] private bool ignoreGaugeForDebug = true;
[Header("Codingモード用 入力設定")]
[Header("入力設定")]
[SerializeField] private InputActionProperty zlButtonAction;
[SerializeField] private InputActionProperty zrButtonAction;
[Header("プレイヤー移動・操作制御用 入力設定")]
[SerializeField] private InputActionProperty moveAction;
[SerializeField] private InputActionProperty turnAction;
[Header("パズル制限時間設定")]
[Header("パズル制限時間")]
[SerializeField] private float codingLimitDuration = 15f;
private float codingTimer = 0f;
[Header("ゲージ設定")]
[SerializeField] private float maxGauge = 100f;
[SerializeField] private float currentGauge = 0f;
[Header("表示UI設定")]
[Header("UI & エフェクト")]
[SerializeField] private GameObject codingModeUI;
[SerializeField] private GameObject successEffectPrefab;
[SerializeField] private Transform effectSpawnPoint;
[Header("バフパラメータ設定")]
[Tooltip("攻撃力UP・防御力UPの持続時間 (秒)")]
[SerializeField] private float buffDuration = 10f;
[Tooltip("攻撃力UP時のダメージ倍率")]
[SerializeField] private float attackBuffMultiplier = 1.5f;
[Tooltip("防御力UP時の被ダメージ軽減率")]
[SerializeField] private float defenseBuffMultiplier = 0.5f;
[Tooltip("HP回復バフ発動時の回復量")]
[SerializeField] private float hpHealAmount = 30f;
[Header("バフ機能")]
[SerializeField] private BuffHandler buffHandler = new BuffHandler();
private float attackBuffTimer = 0f;
private float defenseBuffTimer = 0f;
private bool isCodingMode = false;
public GameObject successEffectPrefab;
// エフェクトを出す位置(合体したブロックの少し上など)
public Transform effectSpawnPoint;
private List<Animator> pausedAnimators = new List<Animator>();
private List<NavMeshAgent> pausedAgents = new List<NavMeshAgent>();
#region プロパティ
public bool IsAttackBuffActive => attackBuffTimer > 0f;
public bool IsDefenseBuffActive => defenseBuffTimer > 0f;
public float CurrentAttackMultiplier => IsAttackBuffActive ? attackBuffMultiplier : 1.0f;
public float CurrentDefenseMultiplier => IsDefenseBuffActive ? defenseBuffMultiplier : 1.0f;
#endregion
private float codingTimer;
public bool IsCodingMode { get; private set; }
public BuffHandler Buffs => buffHandler;
private void Awake()
{
if (Instance == null) { Instance = this; }
else { Destroy(gameObject); }
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject);
}
}
private void OnEnable()
{
EnableAction(zlButtonAction);
EnableAction(zrButtonAction);
EnableAction(moveAction);
EnableAction(turnAction);
SetActionsEnabled(true);
}
private void OnDisable()
{
DisableAction(zlButtonAction);
DisableAction(zrButtonAction);
DisableAction(moveAction);
DisableAction(turnAction);
SetActionsEnabled(false);
}
private void EnableAction(InputActionProperty property)
private void SetActionsEnabled(bool enable)
{
if (property.action != null) property.action.Enable();
SetActionState(zlButtonAction, enable);
SetActionState(zrButtonAction, enable);
SetActionState(moveAction, enable);
SetActionState(turnAction, enable);
}
private void DisableAction(InputActionProperty property)
private void SetActionState(InputActionProperty property, bool enable)
{
if (property.action != null) property.action.Disable();
if (property.action == null) return;
if (enable) property.action.Enable();
else property.action.Disable();
}
private void Start()
@@ -98,13 +79,12 @@ public class PlayerStatusManager : MonoBehaviour
{
CheckComboInput();
if (!isCodingMode)
if (!IsCodingMode)
{
UpdateBuffTimers();
buffHandler.UpdateTimers(Time.deltaTime);
}
else
{
// CodingMode中の制限時間カウントダウン
codingTimer -= Time.deltaTime;
if (codingTimer <= 0f)
{
@@ -115,33 +95,25 @@ public class PlayerStatusManager : MonoBehaviour
public void AddGauge(float amount)
{
if (isCodingMode) return;
currentGauge += amount;
currentGauge = Mathf.Clamp(currentGauge, 0f, maxGauge);
if (IsCodingMode) return;
currentGauge = Mathf.Clamp(currentGauge + amount, 0f, maxGauge);
}
private void CheckComboInput()
{
if (zlButtonAction.action == null || zrButtonAction.action == null) return;
float zlVal = zlButtonAction.action.ReadValue<float>();
float zrVal = zrButtonAction.action.ReadValue<float>();
bool isBothHeld = zlVal > 0.5f && zrVal > 0.5f;
bool isBothHeld = zlButtonAction.action.ReadValue<float>() > 0.5f && zrButtonAction.action.ReadValue<float>() > 0.5f;
bool isAnyTriggered = zlButtonAction.action.triggered || zrButtonAction.action.triggered;
if (isBothHeld && isAnyTriggered)
{
if (!isCodingMode)
if (!IsCodingMode)
{
if (ignoreGaugeForDebug || currentGauge >= maxGauge)
{
StartCodingMode();
}
else
{
Debug.LogWarning($"⚠️ ゲージ不足のためCodingModeを開始できません。現在: {currentGauge} / 必要: {maxGauge}");
}
}
else
{
@@ -152,90 +124,49 @@ public class PlayerStatusManager : MonoBehaviour
private void StartCodingMode()
{
isCodingMode = true;
IsCodingMode = true;
codingTimer = codingLimitDuration;
// 1. プレイヤーの移動制御をロック
DisableAction(moveAction);
DisableAction(turnAction);
SetActionState(moveAction, false);
SetActionState(turnAction, false);
// 2. パズルブロックの状態・位置を全リセット
PuzzleBlock.ResetAllBlocksInScene();
// 3. 敵のアニメーション・NavMeshを停止
foreach (var anim in FindObjectsByType<Animator>(FindObjectsSortMode.None))
if (WorldPauseManager.Instance != null)
{
if (anim == null || anim.gameObject == this.gameObject || anim.CompareTag("Player")) continue;
anim.speed = 0f;
pausedAnimators.Add(anim);
}
foreach (var agent in FindObjectsByType<NavMeshAgent>(FindObjectsSortMode.None))
{
if (agent == null || !agent.enabled || !agent.gameObject.activeInHierarchy || agent.CompareTag("Player")) continue;
if (!agent.isStopped) { agent.isStopped = true; pausedAgents.Add(agent); }
WorldPauseManager.Instance.PauseWorld();
}
if (codingModeUI != null) codingModeUI.SetActive(true);
Debug.Log($"⏳ Codingモード開始(制限時間: {codingLimitDuration}秒)");
}
public void ApplyBuffsAndResume(string buff1, string buff2)
public void ApplyBuffsAndResume(BuffType buff1, BuffType buff2)
{
Instantiate(successEffectPrefab, effectSpawnPoint.position, effectSpawnPoint.rotation);
ActivateBuffByName(buff1);
ActivateBuffByName(buff2);
if (successEffectPrefab != null && effectSpawnPoint != null)
{
Instantiate(successEffectPrefab, effectSpawnPoint.position, effectSpawnPoint.rotation);
}
buffHandler.ActivateBuff(buff1);
buffHandler.ActivateBuff(buff2);
ResumeWorldState(consumeGauge: true);
Debug.Log("🎉 パズル成功!バフを適用してゲームを再開しました。");
}
private void FailCodingModeByTimeout()
{
ResumeWorldState(consumeGauge: true);
Debug.Log("💀 制限時間切れ! Codingモード失敗(バフなし・ゲージ消費)。");
}
public void CancelCodingMode()
{
ResumeWorldState(consumeGauge: false);
Debug.Log("↩️ Codingモードを手動キャンセルしました。");
}
private void FailCodingModeByTimeout() => ResumeWorldState(consumeGauge: true);
public void CancelCodingMode() => ResumeWorldState(consumeGauge: false);
private void ResumeWorldState(bool consumeGauge)
{
foreach (var anim in pausedAnimators) { if (anim != null) anim.speed = 1f; }
pausedAnimators.Clear();
if (WorldPauseManager.Instance != null)
{
WorldPauseManager.Instance.ResumeWorld();
}
foreach (var agent in pausedAgents) { if (agent != null && agent.enabled) agent.isStopped = false; }
pausedAgents.Clear();
SetActionState(moveAction, true);
SetActionState(turnAction, true);
EnableAction(moveAction);
EnableAction(turnAction);
isCodingMode = false;
IsCodingMode = false;
if (codingModeUI != null) codingModeUI.SetActive(false);
if (consumeGauge) currentGauge = 0f;
}
private void ActivateBuffByName(string buffName)
{
switch (buffName)
{
case "AttackUp": attackBuffTimer = buffDuration; break;
case "DefenseUp": defenseBuffTimer = buffDuration; break;
case "Heal": ExecuteHeal(); break;
}
}
private void ExecuteHeal()
{
Debug.Log($"💚 HP回復バフ発動! 回復量: {hpHealAmount}");
}
private void UpdateBuffTimers()
{
if (attackBuffTimer > 0f) attackBuffTimer -= Time.deltaTime;
if (defenseBuffTimer > 0f) defenseBuffTimer -= Time.deltaTime;
}
}

View File

@@ -1,24 +1,32 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit.Interactables; // XR Toolkit用
using UnityEngine.XR.Interaction.Toolkit;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
public enum BlockColor { Red, Blue, Green }
public enum BlockShapeType { TShape, CShape } // ト字, コ字
public enum BlockShapeType { TShape, CShape }
public class PuzzleBlock : MonoBehaviour
{
[field: SerializeField] public BlockColor Color { get; private set; }
[field: SerializeField] public BlockShapeType ShapeType { get; private set; }
[Header("スナップ(合体)位置の設定")]
[Tooltip("相手と合体する基準位置(空のGameObject)を指定。未設定でも中心位置でスナップします")]
[SerializeField] private Transform snapPoint;
private PuzzleBlock nearbyPartner; // 近くにある結合対象のブロック
private bool isConnected = false; // 結合済みフラグ
public static event Action<BlockColor> OnPuzzleCompleted;
private static readonly HashSet<PuzzleBlock> allBlocks = new HashSet<PuzzleBlock>();
private PuzzleBlock nearbyPartner;
private bool isConnected;
// ★ワールド情報ではなく「親階層」と「ローカル情報」を記憶
private Transform initialParent;
private Vector3 initialLocalPosition;
private Quaternion initialLocalRotation;
private Vector3 initialLocalScale;
// リセット用の初期座標・回転保持用
private Vector3 initialPosition;
private Quaternion initialRotation;
private Rigidbody rb;
private XRGrabInteractable grabInteractable;
@@ -27,9 +35,21 @@ public class PuzzleBlock : MonoBehaviour
rb = GetComponent<Rigidbody>();
grabInteractable = GetComponent<XRGrabInteractable>();
// 初期スポーン位置・回転を記憶しておく
initialPosition = transform.position;
initialRotation = transform.rotation;
// ★開始時の「親」と「ローカル座標・回転・スケール」を保持
initialParent = transform.parent;
initialLocalPosition = transform.localPosition;
initialLocalRotation = transform.localRotation;
initialLocalScale = transform.localScale;
}
private void OnEnable()
{
allBlocks.Add(this);
}
private void OnDisable()
{
allBlocks.Remove(this);
}
private void OnTriggerEnter(Collider other)
@@ -40,7 +60,6 @@ public class PuzzleBlock : MonoBehaviour
if (partner != null && IsValidPartner(partner))
{
nearbyPartner = partner;
Debug.Log($"🧩 パートナー検知: {partner.Color} の {partner.ShapeType}");
}
}
@@ -56,8 +75,7 @@ public class PuzzleBlock : MonoBehaviour
private bool IsValidPartner(PuzzleBlock partner)
{
if (partner == null || partner.isConnected) return false;
// 同一色 & 異なる形状(ト字 + コ字)のみ許可
return partner.Color == this.Color && partner.ShapeType != this.ShapeType;
return partner.Color == Color && partner.ShapeType != ShapeType;
}
public void OnReleased()
@@ -75,7 +93,6 @@ public class PuzzleBlock : MonoBehaviour
isConnected = true;
partner.isConnected = true;
// 1. 位置・回転のスナップ補正
if (snapPoint != null && partner.snapPoint != null)
{
transform.position = partner.snapPoint.position;
@@ -87,15 +104,16 @@ public class PuzzleBlock : MonoBehaviour
transform.rotation = partner.transform.rotation;
}
// 2. 物理&VR掴み動作の完全ロック(完成後に崩れないようにする)
LockBlockState();
partner.LockBlockState();
Debug.Log($"🎉 マグネット合体成功! 色: {Color}");
OnPuzzleCompleted?.Invoke(Color);
// 3. バフ適用してCodingMode完了へ
string buffName = ConvertColorToBuffName(Color);
PlayerStatusManager.Instance.ApplyBuffsAndResume(buffName, buffName);
BuffType buff = ConvertColorToBuffType(Color);
if (PlayerStatusManager.Instance != null)
{
PlayerStatusManager.Instance.ApplyBuffsAndResume(buff, buff);
}
}
private void LockBlockState()
@@ -106,43 +124,72 @@ public class PuzzleBlock : MonoBehaviour
rb.angularVelocity = Vector3.zero;
rb.isKinematic = true;
}
// 掴みコンポーネントをオフにして手から切り離す
if (grabInteractable != null) grabInteractable.enabled = false;
}
public void ResetBlock()
{
// 1. 手で掴んでいる場合は手離し処理
ForceDrop();
isConnected = false;
nearbyPartner = null;
transform.position = initialPosition;
transform.rotation = initialRotation;
// 2. ★親オブジェクト(Canvas)の子へ戻す
if (initialParent != null && transform.parent != initialParent)
{
transform.SetParent(initialParent, false);
}
// 3. ★Canvas内のローカル位置・回転・スケールを完全復元
transform.localPosition = initialLocalPosition;
transform.localRotation = initialLocalRotation;
transform.localScale = initialLocalScale;
// 4. 物理状態の再初期化
if (rb != null)
{
rb.isKinematic = false;
rb.linearVelocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
}
if (grabInteractable != null) grabInteractable.enabled = true;
// 5. 掴み機能の再有効化
if (grabInteractable != null)
{
grabInteractable.enabled = true;
}
}
private void ForceDrop()
{
if (grabInteractable == null || !grabInteractable.isSelected) return;
if (grabInteractable.interactionManager != null && grabInteractable.firstInteractorSelecting != null)
{
grabInteractable.interactionManager.SelectExit(grabInteractable.firstInteractorSelecting, grabInteractable);
}
}
public static void ResetAllBlocksInScene()
{
foreach (var block in FindObjectsByType<PuzzleBlock>(FindObjectsSortMode.None))
foreach (var block in allBlocks)
{
if (block != null) block.ResetBlock();
if (block != null)
{
block.ResetBlock();
}
}
}
private string ConvertColorToBuffName(BlockColor color)
private BuffType ConvertColorToBuffType(BlockColor color)
{
return color switch
{
BlockColor.Red => "AttackUp",
BlockColor.Blue => "DefenseUp",
BlockColor.Green => "Heal",
_ => "AttackUp"
BlockColor.Red => BuffType.AttackUp,
BlockColor.Blue => BuffType.DefenseUp,
BlockColor.Green => BuffType.Heal,
_ => BuffType.AttackUp
};
}
}

View File

@@ -3,24 +3,25 @@ using UnityEngine;
public class SlowWebArea : MonoBehaviour
{
[SerializeField] private float damagePerSecond = 5f;
private float damageTimer = 0f;
private float damageTimer;
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}");
IDamageable damageable = other.GetComponentInParent<IDamageable>();
if (damageable != null)
{
DamageInfo info = new DamageInfo
{
amount = damagePerSecond,
hitPosition = other.transform.position
};
damageable.TakeDamage(info);
}
damageTimer = 0f;
}
}
@@ -30,7 +31,7 @@ public class SlowWebArea : MonoBehaviour
{
if (other.CompareTag("Player"))
{
Debug.Log("🏃 プレイヤーが糸の範囲から脱出。 鈍足効果解除。");
damageTimer = 0f;
}
}
}

View File

@@ -1,5 +1,26 @@
using UnityEngine;
public interface ISpiderState
{
void EnterState();
void UpdateState();
void ExitState();
}
public abstract class SpiderStateBase : ISpiderState
{
protected readonly BossController boss;
protected SpiderStateBase(BossController owner)
{
boss = owner;
}
public virtual void EnterState() { }
public abstract void UpdateState();
public virtual void ExitState() { }
}
public class SpiderIdleState : SpiderStateBase
{
public SpiderIdleState(BossController owner) : base(owner) { }
@@ -29,14 +50,12 @@ public class SpiderChaseState : SpiderStateBase
float distance = Vector3.Distance(boss.transform.position, boss.Player.position);
// 遠距離突進判定(カプセル化されたプロパティを使用)
if (distance >= boss.ChargeMinimumDistance && boss.ChargeCooldownTimer <= 0f)
{
boss.ChangeState<SpiderChargeState>();
return;
}
// 至近距離噛みつき判定
if (distance <= boss.BiteRange)
{
boss.ChangeState<SpiderBiteState>();
@@ -63,9 +82,7 @@ public class SpiderBiteState : SpiderStateBase
boss.Agent.velocity = Vector3.zero;
boss.Anim.SetFloat("Speed", 0f);
boss.Anim.SetTrigger("Attack_Bite");
Debug.Log("🎯 蜘蛛ボス:通常噛みつき開始。攻撃モーション中にゆっくり向き直ります。");
}
public override void ExitState()
@@ -121,7 +138,6 @@ public class SpiderChargeState : SpiderStateBase
boss.ResetChargeCooldown();
boss.Anim.SetTrigger("Attack_Roar");
Debug.Log($"🔊 蜘蛛ボス:遠距離を検知。その場でピタッと完全停止。咆哮(溜め {roarTimer}秒)を開始!");
}
public override void ExitState()
@@ -134,11 +150,10 @@ public class SpiderChargeState : SpiderStateBase
public override void UpdateState()
{
// フェーズ1:咆哮・溜めロックオン
if (!isChargingPhase)
{
roarTimer -= Time.deltaTime;
boss.Agent.velocity = Vector3.zero; // 完全固定
boss.Agent.velocity = Vector3.zero;
if (boss.Player != null)
{
@@ -168,13 +183,10 @@ public class SpiderChargeState : SpiderStateBase
boss.Agent.speed = boss.ChargeSpeed;
boss.Agent.acceleration = 120f;
boss.Agent.SetDestination(targetDestination);
Debug.Log("🚀 蜘蛛ボス:ロックオン完了。全速力で突進ダッシュを発射!");
}
return;
}
// フェーズ2:突進移動中
chargeTimer -= Time.deltaTime;
if (chargeTimer <= 0f || (boss.Agent.velocity.magnitude < 0.2f && chargeTimer < (boss.ChargeDuration - 0.3f)))
@@ -202,12 +214,10 @@ public class SpiderJumpToCeilingState : SpiderStateBase
boss.Agent.enabled = false;
boss.Anim.SetTrigger("Attack_Roar");
Debug.Log($"🚨 蜘蛛ボス:必殺技前の一大威嚇(咆哮 {roarTimer}秒)!");
}
public override void UpdateState()
{
// フェーズ1:飛び立つ前の威嚇
if (!isJumpingPhase)
{
roarTimer -= Time.deltaTime;
@@ -219,7 +229,6 @@ public class SpiderJumpToCeilingState : SpiderStateBase
return;
}
// フェーズ2:ジャンプ空中移動
if (boss.ceilingAnchor == null) return;
jumpProgress += Time.unscaledDeltaTime * 1.5f;
@@ -237,25 +246,20 @@ public class SpiderJumpToCeilingState : SpiderStateBase
}
}
// ==========================================
// ■ STATE: CEILING ATTACK (インスペクター無段階調整版)
// ==========================================
public class SpiderCeilingAttackState : SpiderStateBase
{
private float timer = 0f;
private float shotTimer = 0f;
private float timer;
private float shotTimer;
public SpiderCeilingAttackState(BossController owner) : base(owner) { }
public override void EnterState()
{
// ★リファクタリング:インスペクターで設定した必殺技の長さを適用
timer = boss.CeilingAttackDuration;
shotTimer = 0f;
boss.Anim.SetBool("IsOnCeiling", true);
boss.PendingUltimate = false;
Debug.Log($"🕸️ 蜘蛛ボス:天井からの乱射開始! 持続時間: {timer}秒 / 発射間隔: {boss.CeilingShotInterval}秒");
}
public override void ExitState()
@@ -271,12 +275,9 @@ public class SpiderCeilingAttackState : SpiderStateBase
timer -= Time.deltaTime;
shotTimer -= Time.deltaTime;
// タイマーがゼロになったら発射
if (shotTimer <= 0f)
{
ExecuteRandomWebAttack();
// ★リファクタリング:インスペクターで設定した間隔(秒)でリセットして弾数を制御
shotTimer = boss.CeilingShotInterval;
}
@@ -286,14 +287,13 @@ public class SpiderCeilingAttackState : SpiderStateBase
}
}
private void ExecuteRandomWebAttack()
private void ExecuteRandomWebAttack()
{
Vector3 targetGroundPos = Vector3.zero;
int maxAttempts = 15; // 無限ループ(フリーズ)を防止するための最大再抽選回数
int maxAttempts = 15;
for (int i = 0; i < maxAttempts; i++)
{
// 1. 一旦ランダムな座標を算出
Vector2 randomCircle = Random.insideUnitCircle * boss.AttackRadius;
targetGroundPos = new Vector3(
boss.groundAnchor.position.x + randomCircle.x,
@@ -301,29 +301,23 @@ public class SpiderCeilingAttackState : SpiderStateBase
boss.groundAnchor.position.z + randomCircle.y
);
// 2. 現在アクティブなすべての糸との距離を計測する
bool isTooClose = false;
foreach (Vector3 existingPos in boss.ActiveWebPositions)
foreach (Vector3 existingPos in boss.WebSpawner.ActiveWebPositions)
{
// 設定された回避半径(WebAvoidanceRadius)より近い既存の糸があるか?
if (Vector3.Distance(targetGroundPos, existingPos) < boss.WebAvoidanceRadius)
if (Vector3.Distance(targetGroundPos, existingPos) < boss.WebSpawner.WebAvoidanceRadius)
{
isTooClose = true;
break; // 被り検知、この座標は使えないのでチェックを抜けて再抽選へ
break;
}
}
// 3. 誰とも被っていなければ、このクリーンな座標で確定
if (!isTooClose)
{
break;
}
if (!isTooClose) break;
}
// 確定した(または上限に達して安全が担保された)座標へ向けて発射命令
boss.SpawnWarningAndWeb(targetGroundPos, boss.WebWarningDelay, boss.WebDuration);
boss.WebSpawner.SpawnWarningAndWeb(targetGroundPos, boss.WebWarningDelay, boss.WebDuration);
}
}
public class SpiderStunState : SpiderStateBase
{
private float stunTimer;
@@ -336,8 +330,6 @@ public class SpiderStunState : SpiderStateBase
boss.Anim.SetTrigger("StunStart");
boss.Anim.SetBool("IsStunned", true);
if (boss.Agent.enabled) boss.Agent.isStopped = true;
Debug.Log("🛡️ パリィ成功! チャンスタイム!");
}
public override void ExitState()

View File

@@ -1,9 +1,9 @@
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class SimpleTitleManager : MonoBehaviour
public class TitleManager : MonoBehaviour
{
[Header("UI Buttons")]
[SerializeField] private Button startButton;
@@ -14,14 +14,13 @@ public class SimpleTitleManager : MonoBehaviour
private void Awake()
{
// テンプレートの手でボタンを押した時に実行される処理
startButton.onClick.AddListener(OnStartButtonClicked);
exitButton.onClick.AddListener(OnExitButtonClicked);
if (startButton != null) startButton.onClick.AddListener(OnStartButtonClicked);
if (exitButton != null) exitButton.onClick.AddListener(OnExitButtonClicked);
}
private void OnStartButtonClicked()
{
startButton.interactable = false; // 二重押し防止
if (startButton != null) startButton.interactable = false;
StartCoroutine(LoadSceneRoutine());
}

View File

@@ -1,72 +1,48 @@
using UnityEngine;
using System.Collections;
using UnityEngine;
/// <summary>
/// 1つのトリガーで左右のドアを反対方向にスライド開閉させ、
/// 一定時間後に自動で閉じるクラス
/// </summary>
public class VRDoorController : MonoBehaviour
{
[Header("ドアのオブジェクト登録")]
[Tooltip("左側にスライドさせたいドアのTransformを指定してください")]
[Header("ドアオブジェクト")]
[SerializeField] private Transform leftDoor;
[Tooltip("右側にスライドさせたいドアのTransformを指定してください")]
[SerializeField] private Transform rightDoor;
[Header("スライド量と速度の設定")]
[Tooltip("ドアが横に何メートルずれるか(移動量)")]
[Header("動作設定")]
[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 bool isOpen;
private bool isMoving;
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;
@@ -74,28 +50,22 @@ public class VRDoorController : MonoBehaviour
{
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フレーム待機
yield return null;
}
// 完全に開ききった座標で固定
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;
@@ -112,11 +82,9 @@ public class VRDoorController : MonoBehaviour
yield return null;
}
// 完全に閉じた座標で固定
if (leftDoor != null) leftDoor.localPosition = leftDoorClosedPos;
if (rightDoor != null) rightDoor.localPosition = rightDoorClosedPos;
isMoving = false;
Debug.Log("🔒 ドアが自動で閉じました。");
}
}

View File

@@ -2,79 +2,65 @@ using UnityEngine;
public class VRInputPoseDetector : MonoBehaviour
{
[Header("必須コンポーネント登録")]
[Header("参照")]
[SerializeField] private Transform headTransform;
[SerializeField] private Transform rightHandTransform;
[SerializeField] private Transform leftHandTransform;
[Header("構えの判定範囲設定")]
[Header("判定パラメータ")]
[SerializeField] private float minForwardDistance = 0.15f;
[SerializeField] private float maxForwardDistance = 0.5f;
[SerializeField] private float minHeightOffset = -0.35f;
[SerializeField] private float maxHeightOffset = -0.1f;
[Header("タイマー・バッファ設定")]
[Header("タイマーパラメータ")]
[SerializeField] private float requiredPoseTime = 0.3f;
[SerializeField] private float poseExitDelay = 0.15f;
[Header("視覚フィードバック(ガイドUI設定)")]
[Header("視覚フィードバック")]
[SerializeField] private Renderer guideRenderer;
[SerializeField] private Color defaultColor = new Color(0.5f, 0.5f, 0.5f, 0.05f); // 普段(超薄いグレー)
[SerializeField] private Color chargingColor = new Color(1f, 0.8f, 0f, 0.2f); // 溜め中(うっすら黄色)
[SerializeField] private Color successColor = new Color(0f, 0.6f, 1f, 0.4f); // 構え完了(綺麗な青シールド)
[SerializeField] private Color defaultColor = new Color(0.5f, 0.5f, 0.5f, 0.05f);
[SerializeField] private Color chargingColor = new Color(1f, 0.8f, 0f, 0.2f);
[SerializeField] private Color successColor = new Color(0f, 0.6f, 1f, 0.4f);
private float poseTimer = 0f;
private float exitTimer = 0f;
private bool isPoseMaintained = false;
private float poseTimer;
private float exitTimer;
public bool IsPosing => isPoseMaintained;
public bool IsPosing { get; private set; }
void Update()
private 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);
bool isBothHandsGuarding = CheckHandPosition(localRightHand) && CheckHandPosition(localLeftHand);
bool isBothHandsGuarding = isRightHandGuarding && isLeftHandGuarding;
// ─── 🛡️ チャタリング防止付き・構え判定ロジック ───
if (isBothHandsGuarding)
{
// 範囲内に手が戻ったので、解除カウントダウンを即座にリセット
exitTimer = 0f;
if (!isPoseMaintained)
if (!IsPosing)
{
poseTimer += Time.deltaTime;
if (poseTimer >= requiredPoseTime)
{
isPoseMaintained = true;
Debug.Log("🛡️ 【構え成功】 パリィ受付がアクティブになりました。");
IsPosing = true;
}
}
}
else
{
// 範囲外に出た場合
poseTimer = 0f; // まだ構えが完了していない段階(溜め中)なら即リセット
poseTimer = 0f;
if (isPoseMaintained)
if (IsPosing)
{
// すでに構えが成功している状態なら、猶予タイマーを開始(ここで即解除しない!)
exitTimer += Time.deltaTime;
if (exitTimer >= poseExitDelay)
{
isPoseMaintained = false;
IsPosing = false;
exitTimer = 0f;
Debug.Log("❌ 【構え解除】 猶予時間を超えたためガードが解けました。");
}
}
}
@@ -84,9 +70,8 @@ public class VRInputPoseDetector : MonoBehaviour
private bool CheckHandPosition(Vector3 localHandPos)
{
bool inForwardRange = localHandPos.z >= minForwardDistance && localHandPos.z <= maxForwardDistance; // Z軸:頭より前に手があるか
bool inHeightRange = localHandPos.y >= minHeightOffset && localHandPos.y <= maxHeightOffset; // Y軸:手の高さが適切か
bool inForwardRange = localHandPos.z >= minForwardDistance && localHandPos.z <= maxForwardDistance;
bool inHeightRange = localHandPos.y >= minHeightOffset && localHandPos.y <= maxHeightOffset;
return inForwardRange && inHeightRange;
}
@@ -94,18 +79,17 @@ public class VRInputPoseDetector : MonoBehaviour
{
if (guideRenderer == null) return;
// ボスの攻撃を見やすくするため、現在の状態に応じてガイドシールドのカラーを3段階可変させる
if (isPoseMaintained)
if (IsPosing)
{
guideRenderer.material.color = successColor; // 成功:青シールド
guideRenderer.material.color = successColor;
}
else if (isBothHandsGuarding)
{
guideRenderer.material.color = chargingColor; // 溜め中:黄色
guideRenderer.material.color = chargingColor;
}
else
{
guideRenderer.material.color = defaultColor; // 構えなし:目立たない透明グレー
guideRenderer.material.color = defaultColor;
}
}
}

View File

@@ -1,92 +0,0 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
namespace VRTitle.Input
{
/// <summary>
/// VRコントローラーからのポインター入力とUI操作を仲介するコンポーネント
/// </summary>
[RequireComponent(typeof(LineRenderer))]
public class VRLaserPointer : MonoBehaviour
{
[Header("Ray Settings")]
[SerializeField] private float maxDistance = 10f;
[SerializeField] private LayerMask uiLayerMask = ~0;
[Header("Visual")]
[SerializeField] private LineRenderer lineRenderer;
private GameObject currentHoveredObject;
private void Reset()
{
lineRenderer = GetComponent<LineRenderer>();
lineRenderer.positionCount = 2;
lineRenderer.startWidth = 0.005f;
lineRenderer.endWidth = 0.001f;
}
private void Update()
{
ProcessPointer();
}
private void ProcessPointer()
{
Ray ray = new Ray(transform.position, transform.forward);
PointerEventData pointerData = new PointerEventData(EventSystem.current)
{
position = new Vector2(Screen.width / 2f, Screen.height / 2f)
};
List<RaycastResult> results = new List<RaycastResult>();
EventSystem.current?.RaycastAll(pointerData, results);
Vector3 targetPosition = transform.position + transform.forward * maxDistance;
if (results.Count > 0)
{
RaycastResult hit = results[0];
targetPosition = hit.worldPosition;
UpdateHoverState(hit.gameObject, pointerData);
}
else
{
UpdateHoverState(null, pointerData);
}
// ポインター描画の更新
lineRenderer.SetPosition(0, transform.position);
lineRenderer.SetPosition(1, targetPosition);
}
private void UpdateHoverState(GameObject newHover, PointerEventData pointerData)
{
if (currentHoveredObject == newHover) return;
if (currentHoveredObject != null)
{
ExecuteEvents.Execute(currentHoveredObject, pointerData, ExecuteEvents.pointerExitHandler);
}
currentHoveredObject = newHover;
if (currentHoveredObject != null)
{
ExecuteEvents.Execute(currentHoveredObject, pointerData, ExecuteEvents.pointerEnterHandler);
}
}
/// <summary>
/// トリガーボタンが押された時に呼び出す(Input System側から呼び出し可能)
/// </summary>
public void OnPrimaryTriggerDown()
{
if (currentHoveredObject == null) return;
PointerEventData pointerData = new PointerEventData(EventSystem.current);
ExecuteEvents.Execute(currentHoveredObject, pointerData, ExecuteEvents.pointerClickHandler);
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 6159a60476d605c4494622c0790e9b02

View File

@@ -1,6 +1,6 @@
using UnityEngine;
using UnityEngine.InputSystem; // Input Systemを使うために必要
using UnityEngine.XR; // 振動機能のために必要
using UnityEngine.InputSystem;
using UnityEngine.XR;
[RequireComponent(typeof(Collider))]
[RequireComponent(typeof(Rigidbody))]
@@ -10,37 +10,30 @@ public class VRInputPunch : MonoBehaviour
[SerializeField] private XRNode controllerNode = XRNode.RightHand;
[SerializeField] private VRInputPoseDetector poseDetector;
[Header("攻撃(パンチ)設定")]
[SerializeField] private float minPunchSpeed = 1.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 minPunchSpeed = 1.0f;
[SerializeField] private float damageMultiplier = 10.0f;
[SerializeField] private float hapticAmplitude = 0.5f;
[SerializeField] private float hapticDuration = 0.1f;
[Header("パリィ設定")]
[SerializeField] private float parryStunDuration = 3.0f; // スタン時間(秒)
[SerializeField] private float parryHapticAmplitude = 0.9f; // パリィ成功時の振動の強さ
[SerializeField] private float parryHapticDuration = 0.25f; // パリィ成功時の振動の長さ
// ─── 多段ヒット・連続暴発防止用タイマー ───
private float punchHitCooldown = 0f; // 次のパンチが当たるまでのインターバル
private float parryTriggerCooldown = 0f; // 次のパリィが成功するまでのインターバル
[Header("パリィパラメータ")]
[SerializeField] private float parryStunDuration = 3.0f;
[SerializeField] private float parryHapticAmplitude = 0.9f;
[SerializeField] private float parryHapticDuration = 0.25f;
private const string EnemyFistTag = "EnemyFist";
private void OnEnable()
{
if (velocityAction.action != null)
{
velocityAction.action.Enable();
}
if (velocityAction.action != null) velocityAction.action.Enable();
}
private void OnDisable()
{
if (velocityAction.action != null)
{
velocityAction.action.Disable();
}
if (velocityAction.action != null) velocityAction.action.Disable();
}
void Start()
private void Start()
{
Rigidbody rb = GetComponent<Rigidbody>();
rb.isKinematic = true;
@@ -50,104 +43,71 @@ public class VRInputPunch : MonoBehaviour
private void OnTriggerEnter(Collider other)
{
// コントローラーの現在の移動速度を取得
Vector3 velocity = velocityAction.action.ReadValue<Vector3>();
Vector3 velocity = velocityAction.action != null ? velocityAction.action.ReadValue<Vector3>() : Vector3.zero;
float speed = velocity.magnitude;
// ========================================================================
// 【パリィ判定】当たったオブジェクトが敵の拳だった場合
// ========================================================================
if (other.CompareTag("EnemyFist"))
if (other.CompareTag(EnemyFistTag))
{
Debug.Log($"🛡️ [パリィチェック] 敵の拳と接触。構え状態: {poseDetector.IsPosing}");
// 自分が「構え状態」のときだけパリィの成立チェックを行う
if (poseDetector.IsPosing)
if (poseDetector != null && poseDetector.IsPosing)
{
// 1. ザコ敵(EnemyAI)へのパリィ判定
EnemyAI enemyAI = other.GetComponentInParent<EnemyAI>();
if (enemyAI != null)
{
enemyAI.TriggerParryStun(parryStunDuration);
TriggerHaptics(parryHapticAmplitude, parryHapticDuration);
Debug.Log($"🛡️ 【ジャストパリィ成功!】 敵の体勢を崩しました。");
return; // ★本当のパリィ成功時のみ、ここで処理を終了する
}
// 2. ボス(BossController)へのパリィ判定
BossController boss = other.GetComponentInParent<BossController>();
if (boss != null)
{
// ★リファクタリング:ボスが本当に攻撃中(Bite/Charge)で、パリィが成功した時だけ終了する
if (boss.TriggerParryStun())
{
TriggerHaptics(parryHapticAmplitude, parryHapticDuration);
Debug.Log("🛡️ 【ジャストパリィ成功!】 ボスの体勢を崩しました!");
return; // ★本当のパリィ成功時のみ、ここで処理を終了する
}
}
if (TryProcessParry(other)) return;
}
// ─── ★ここが今回の核心(超重要リファクタリング) ───
// 構え状態ではなかった場合、またはボスが攻撃中でなくパリィが不成立だった場合は、
// 「プレイヤーが隙を狙って殴りに行った」とみなして、returnせずにそのまま下のパンチ処理へ流す!
Debug.Log("🥊 パリィ不成立、または攻撃チャンス:通常のパンチダメージ処理へ移行します。");
}
// ────────────────────────────────────────────────────────────────────────
// ========================================================================
// 【パンチ攻撃の処理】
// ========================================================================
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}");
ProcessPunchAttack(other, speed, velocity);
}
}
/// 引数に応じて指定された強さと長さでコントローラーを振動させる
private bool TryProcessParry(Collider other)
{
EnemyAI enemyAI = other.GetComponentInParent<EnemyAI>();
if (enemyAI != null)
{
enemyAI.TriggerParryStun(parryStunDuration);
TriggerHaptics(parryHapticAmplitude, parryHapticDuration);
return true;
}
BossController boss = other.GetComponentInParent<BossController>();
if (boss != null && boss.TriggerParryStun())
{
TriggerHaptics(parryHapticAmplitude, parryHapticDuration);
return true;
}
return false;
}
private void ProcessPunchAttack(Collider other, float speed, Vector3 velocity)
{
IDamageable damageable = other.GetComponentInParent<IDamageable>();
if (damageable == null) return;
bool isPosing = poseDetector != null && poseDetector.IsPosing;
DamageInfo info = new DamageInfo
{
amount = speed * damageMultiplier * (isPosing ? 2f : 1f),
isPoseBonus = isPosing,
hitPosition = other.transform.position,
punchDirection = velocity.normalized
};
if (PlayerStatusManager.Instance != null)
{
PlayerStatusManager.Instance.AddGauge(isPosing ? 20f : 10f);
info.amount *= PlayerStatusManager.Instance.Buffs.CurrentAttackMultiplier;
}
damageable.TakeDamage(info);
TriggerHaptics(hapticAmplitude, hapticDuration);
}
private void TriggerHaptics(float amplitude, float duration)
{
// 明示的に UnityEngine.XR.InputDevice と指定して型衝突を回避
UnityEngine.XR.InputDevice device = InputDevices.GetDeviceAtXRNode(controllerNode);
if (device.isValid)
{

View File

@@ -1,78 +1,60 @@
using UnityEngine;
// World Space Canvas をプレイヤーの真正面に移動させ、開いている間はプレイヤーの視点に滑らかに追従させるスクリプト
public class VRUIDoorFaceFollow : MonoBehaviour
{
[SerializeField] private Transform vrCameraTransform;
[Header("配置・追従のパラメータ")]
[Header("配置・追従パラメータ")]
[SerializeField] private float forwardDistance = 0.5f;
[SerializeField] private float heightOffset = -0.1f;
[SerializeField] private float followSpeed = 5.0f;
// 画面がアクティブ(表示)になった瞬間を検知するためのフラグ
private bool wasActiveLastFrame = false;
private bool wasActiveLastFrame;
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; // 高さを微調整
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,47 @@
using System.Collections.Generic;
using UnityEngine;
public class WorldPauseManager : MonoBehaviour
{
public static WorldPauseManager Instance { get; private set; }
private readonly HashSet<IPauseable> pauseables = new HashSet<IPauseable>();
private void Awake()
{
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject);
}
}
public void Register(IPauseable pauseable)
{
if (pauseable != null) pauseables.Add(pauseable);
}
public void Unregister(IPauseable pauseable)
{
if (pauseable != null) pauseables.Remove(pauseable);
}
public void PauseWorld()
{
foreach (var p in pauseables)
{
p?.Pause();
}
}
public void ResumeWorld()
{
foreach (var p in pauseables)
{
p?.Resume();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6f00095a3e63cc440be35394ad49b505