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_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96 m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1 m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 0 m_PresetInfoIsWorld: 1
--- !u!114 &4150137515110638401 --- !u!114 &4150137515110638401
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0

View File

@@ -732,9 +732,6 @@ MonoBehaviour:
<CeilingShotInterval>k__BackingField: 0.2 <CeilingShotInterval>k__BackingField: 0.2
<WebWarningDelay>k__BackingField: 0.8 <WebWarningDelay>k__BackingField: 0.8
<WebDuration>k__BackingField: 10 <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 --- !u!195 &139010308
NavMeshAgent: NavMeshAgent:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -817,6 +814,21 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: d2cd755b0c100204e833df85acb412e8, type: 3} m_Script: {fileID: 11500000, guid: d2cd755b0c100204e833df85acb412e8, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: Assembly-CSharp::BossDebugController 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 --- !u!1 &142265989
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -4447,6 +4459,9 @@ PrefabInstance:
- targetCorrespondingSourceObject: {fileID: 1987720806421918, guid: a2dfe588a42016e40892b72d31fd1cf6, type: 3} - targetCorrespondingSourceObject: {fileID: 1987720806421918, guid: a2dfe588a42016e40892b72d31fd1cf6, type: 3}
insertIndex: -1 insertIndex: -1
addedObject: {fileID: 139010320} addedObject: {fileID: 139010320}
- targetCorrespondingSourceObject: {fileID: 1987720806421918, guid: a2dfe588a42016e40892b72d31fd1cf6, type: 3}
insertIndex: -1
addedObject: {fileID: 139010321}
- targetCorrespondingSourceObject: {fileID: 1388461240955602, guid: a2dfe588a42016e40892b72d31fd1cf6, type: 3} - targetCorrespondingSourceObject: {fileID: 1388461240955602, guid: a2dfe588a42016e40892b72d31fd1cf6, type: 3}
insertIndex: -1 insertIndex: -1
addedObject: {fileID: 50675083} addedObject: {fileID: 50675083}

View File

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

View File

@@ -1,24 +1,29 @@
using System;
using System.Collections.Generic;
using UnityEngine; using UnityEngine;
using UnityEngine.AI; using UnityEngine.AI;
using System.Collections.Generic;
[RequireComponent(typeof(NavMeshAgent))] [RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(Animator))] [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 maxHp = 500f;
[SerializeField] private float currentHp = 500f; [SerializeField] private float currentHp = 500f;
[field: Header("移動・攻撃パラメータ")]
[field: SerializeField] public float ChaseRange { get; private set; } = 15f; [field: SerializeField] public float ChaseRange { get; private set; } = 15f;
[field: SerializeField] public float BiteRange { get; private set; } = 5f; [field: SerializeField] public float BiteRange { get; private set; } = 5f;
[field: SerializeField] public float ChargeSpeed { get; private set; } = 14f; [field: SerializeField] public float ChargeSpeed { get; private set; } = 14f;
[field: SerializeField] public float ChargeDuration { get; private set; } = 2f; [field: SerializeField] public float ChargeDuration { get; private set; } = 2f;
[field: SerializeField] public float ChargeMinimumDistance { get; private set; } = 6f; [field: SerializeField] public float ChargeMinimumDistance { get; private set; } = 6f;
[field: SerializeField] public float ChargeCooldownDuration { get; private set; } = 4f; [field: SerializeField] public float ChargeCooldownDuration { get; private set; } = 4f;
[field: SerializeField] public float ChargeRoarDuration { get; private set; } = 5f; [field: SerializeField] public float ChargeRoarDuration { get; private set; } = 5f;
[SerializeField] public Transform ceilingAnchor; [Header("アンカー・天井技設定")]
[SerializeField] public Transform groundAnchor; public Transform ceilingAnchor;
public Transform groundAnchor;
[field: SerializeField] public float AttackRadius { get; private set; } = 15f; [field: SerializeField] public float AttackRadius { get; private set; } = 15f;
[field: SerializeField] public float JumpRoarDuration { get; private set; } = 2f; [field: SerializeField] public float JumpRoarDuration { get; private set; } = 2f;
[field: SerializeField] public float CeilingAttackDuration { get; private set; } = 8.0f; [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 WebWarningDelay { get; private set; } = 0.8f;
[field: SerializeField] public float WebDuration { get; private set; } = 3.0f; [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 NavMeshAgent Agent { get; private set; }
public Animator Anim { get; private set; } public Animator Anim { get; private set; }
public Transform Player { 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 NormalSpeed { get; private set; }
public float NormalAcceleration { get; private set; } public float NormalAcceleration { get; private set; }
public bool PendingUltimate { get; set; }
private ISpiderState currentState; private ISpiderState currentState;
private Dictionary<System.Type, ISpiderState> stateCache; private Dictionary<Type, ISpiderState> stateCache;
// フェーズトリガー private bool triggered50;
private bool triggered50 = false; private bool triggered25;
private bool triggered25 = false; private bool isPaused;
public bool PendingUltimate { get; set; } = false;
private void Awake() private void Awake()
{ {
Agent = GetComponent<NavMeshAgent>(); Agent = GetComponent<NavMeshAgent>();
Anim = GetComponent<Animator>(); Anim = GetComponent<Animator>();
WebSpawner = GetComponent<BossWebSpawner>();
GameObject pObj = GameObject.FindGameObjectWithTag("Player"); GameObject pObj = GameObject.FindGameObjectWithTag("Player");
if (pObj != null) Player = pObj.transform; if (pObj != null) Player = pObj.transform;
@@ -67,11 +67,44 @@ public class BossController : MonoBehaviour, IDamageble
NormalSpeed = Agent.speed; NormalSpeed = Agent.speed;
NormalAcceleration = Agent.acceleration; 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() private void InitializeFSM()
{ {
stateCache = new Dictionary<System.Type, ISpiderState> stateCache = new Dictionary<Type, ISpiderState>
{ {
{ typeof(SpiderIdleState), new SpiderIdleState(this) }, { typeof(SpiderIdleState), new SpiderIdleState(this) },
{ typeof(SpiderChaseState), new SpiderChaseState(this) }, { typeof(SpiderChaseState), new SpiderChaseState(this) },
@@ -87,6 +120,8 @@ public class BossController : MonoBehaviour, IDamageble
private void Update() private void Update()
{ {
if (isPaused) return;
currentState?.UpdateState(); currentState?.UpdateState();
if (ChargeCooldownTimer > 0f) if (ChargeCooldownTimer > 0f)
@@ -97,26 +132,18 @@ public class BossController : MonoBehaviour, IDamageble
public void ChangeState<T>() where T : ISpiderState public void ChangeState<T>() where T : ISpiderState
{ {
if (currentState != null) currentState?.ExitState();
{
currentState.ExitState();
ResetAllAnimationTriggers(); ResetAllAnimationTriggers();
Debug.Log($"🕷️ BossSpider [Exit]: {currentState.GetType().Name}");
}
currentState = stateCache[typeof(T)]; currentState = stateCache[typeof(T)];
Debug.Log($"🕷️ BossSpider [Enter]: {currentState.GetType().Name}");
currentState.EnterState(); currentState.EnterState();
} }
public void TakeDamage(DamageInfo damageInfo) public void TakeDamage(DamageInfo damageInfo)
{ {
currentHp -= damageInfo.amount; currentHp = Mathf.Clamp(currentHp - damageInfo.amount, 0, maxHp);
currentHp = Mathf.Clamp(currentHp, 0, maxHp);
float hpPercentage = (currentHp / maxHp) * 100f; float hpPercentage = (currentHp / maxHp) * 100f;
Debug.Log($"💥 ボスに {damageInfo.amount} ダメージ。残りHP: {hpPercentage:F1}%");
if (hpPercentage <= 25f && !triggered25) if (hpPercentage <= 25f && !triggered25)
{ {
triggered25 = true; triggered25 = true;
@@ -143,7 +170,7 @@ public class BossController : MonoBehaviour, IDamageble
public bool TriggerParryStun() public bool TriggerParryStun()
{ {
if (currentState is SpiderBiteState || currentState is SpiderChargeState) if (currentState is SpiderBiteState or SpiderChargeState)
{ {
ChangeState<SpiderStunState>(); ChangeState<SpiderStunState>();
return true; return true;
@@ -151,49 +178,7 @@ public class BossController : MonoBehaviour, IDamageble
return false; return false;
} }
public void ResetChargeCooldown() public void ResetChargeCooldown() => ChargeCooldownTimer = ChargeCooldownDuration;
{
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 ResetAllAnimationTriggers() public void ResetAllAnimationTriggers()
{ {
@@ -206,46 +191,11 @@ public class BossController : MonoBehaviour, IDamageble
Anim.ResetTrigger("StunStart"); Anim.ResetTrigger("StunStart");
} }
#region デバッグ用機能 [ContextMenu("🛠️ デバッグ:強制必殺技")]
/// <summary>
/// Unityインスペクターの右上メニューから実行できるデバッグ用コマンドです
/// </summary>
[ContextMenu("🛠️ デバッグ:強制必殺技(天井ジャンプ)")]
public void DebugTriggerUltimate() public void DebugTriggerUltimate()
{ {
if (!Application.isPlaying) if (!Application.isPlaying) return;
{
Debug.LogWarning("⚠️ ゲームが再生中(Playモード)の時のみ実行可能です。");
return;
}
Debug.Log("🛠️ 【デバッグ操作】必殺技の予約券を発行し、強制的に天井ジャンプステートへ遷移します。");
// 必殺技の発動権をONにし、現在の行動をキャンセルして天井ジャンプへ移行
PendingUltimate = true; PendingUltimate = true;
ChangeState<SpiderJumpToCeilingState>(); 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;
using UnityEngine.SceneManagement; using UnityEngine.SceneManagement;
using UnityEngine.UI; using UnityEngine.UI;
using UnityEngine.XR; using UnityEngine.XR;
using System.Collections;
public class ElevatorSceneTransition : MonoBehaviour public class ElevatorSceneTransition : MonoBehaviour
{ {
[Header("遷移先の設定")] [Header("シーン設定")]
[SerializeField] private string bossSceneName = "BossStage"; [SerializeField] private string bossSceneName = "BossStage";
[SerializeField] private string playerTag = "Player"; [SerializeField] private string playerTag = "Player";
[Header("ローディングUI設定")] [Header("UI設定")]
[SerializeField] private GameObject loadingCanvas; [SerializeField] private GameObject loadingCanvas;
[SerializeField] private Text progressText; [SerializeField] private Text progressText;
[SerializeField] private Slider progressSlider; [SerializeField] private Slider progressSlider;
[Header("エレベーター下降演出設定")] [Header("演出設定")]
[Tooltip("エレベーターの部屋(壁や床、UIすべて)をまとめているオブジェクトを指定してください(※プレイヤーは外しておきます)")]
[SerializeField] private Transform elevatorCageTransform; [SerializeField] private Transform elevatorCageTransform;
[Tooltip("ガタガタ揺れる激しさ(0.01~0.03あたりが自然です)")]
[SerializeField] private float shakeMagnitude = 0.015f; [SerializeField] private float shakeMagnitude = 0.015f;
[Tooltip("下降中のコントローラーの振動の強さ(0.0 ~ 1.0)")]
[SerializeField] private float hapticAmplitude = 0.15f; [SerializeField] private float hapticAmplitude = 0.15f;
// ─── ★新設:親子関係にしないためのプレイヤー登録枠 ───
[Header("プレイヤーの同期設定")]
[Tooltip("XR Origin(またはXR Rig)のTransformを直接ドラッグ&ドロップしてください")]
[SerializeField] private Transform playerTransform; [SerializeField] private Transform playerTransform;
// ─────────────────────────────────────────────────────
private bool isLoadingStarted = false; private bool isLoadingStarted;
private Vector3 originalCagePosition; private Vector3 originalCagePosition;
private void Start() private void Start()
@@ -40,7 +30,6 @@ public class ElevatorSceneTransition : MonoBehaviour
if (elevatorCageTransform != null) if (elevatorCageTransform != null)
{ {
// ワールド空間での初期位置を記憶(localPositionからpositionに変更し、より確実に)
originalCagePosition = elevatorCageTransform.position; originalCagePosition = elevatorCageTransform.position;
} }
} }
@@ -53,13 +42,11 @@ public class ElevatorSceneTransition : MonoBehaviour
{ {
isLoadingStarted = true; isLoadingStarted = true;
// ★安全対策:接触したプレイヤーのTransformを自動でキャッシュする
if (playerTransform == null) if (playerTransform == null)
{ {
playerTransform = other.transform; playerTransform = other.transform;
} }
Debug.Log("🛗 プレイヤー乗車:親子関係なしのデルタ同期で下降演出を開始します。");
StartCoroutine(LoadSceneAsyncRoutine()); StartCoroutine(LoadSceneAsyncRoutine());
} }
} }
@@ -79,28 +66,21 @@ public class ElevatorSceneTransition : MonoBehaviour
if (progressText != null) progressText.text = $"LOADING: {(currentProgress * 100f):F0}%"; if (progressText != null) progressText.text = $"LOADING: {(currentProgress * 100f):F0}%";
if (progressSlider != null) progressSlider.value = currentProgress; if (progressSlider != null) progressSlider.value = currentProgress;
// ─── ★【超重要】親子関係なしでプレイヤーを完全同期させるロジック ───
// 1. 動かす前の、このフレームにおけるエレベーターの現在位置を記録
Vector3 previousCagePosition = elevatorCageTransform.position; Vector3 previousCagePosition = elevatorCageTransform.position;
// 2. エレベーターの部屋だけをランダムにガタガタ動かす
if (elevatorCageTransform != null) if (elevatorCageTransform != null)
{ {
Vector3 randomOffset = Random.insideUnitSphere * shakeMagnitude; Vector3 randomOffset = Random.insideUnitSphere * shakeMagnitude;
randomOffset.y = 0; // 横揺れのみ randomOffset.y = 0;
elevatorCageTransform.position = originalCagePosition + randomOffset; elevatorCageTransform.position = originalCagePosition + randomOffset;
} }
// 3. 「この1フレームでエレベーターが実際に動いた移動差分(デルタ)」を計算
Vector3 cageDelta = elevatorCageTransform.position - previousCagePosition; Vector3 cageDelta = elevatorCageTransform.position - previousCagePosition;
// 4. その移動差分を、プレイヤーの座標にもそのまま上乗せして足し算する
if (playerTransform != null && cageDelta != Vector3.zero) if (playerTransform != null && cageDelta != Vector3.zero)
{ {
playerTransform.position += cageDelta; playerTransform.position += cageDelta;
} }
// ───────────────────────────────────────────────────────────────────
hapticTimer += Time.deltaTime; hapticTimer += Time.deltaTime;
if (hapticTimer >= 0.1f) if (hapticTimer >= 0.1f)
@@ -112,10 +92,8 @@ public class ElevatorSceneTransition : MonoBehaviour
yield return null; yield return null;
} }
// 到着後、エレベーターの位置を元の位置に綺麗に戻す
if (elevatorCageTransform != null) if (elevatorCageTransform != null)
{ {
// 最後の移動差分を計算してプレイヤーを戻す
Vector3 previousCagePosition = elevatorCageTransform.position; Vector3 previousCagePosition = elevatorCageTransform.position;
elevatorCageTransform.position = originalCagePosition; elevatorCageTransform.position = originalCagePosition;
Vector3 cageDelta = elevatorCageTransform.position - previousCagePosition; Vector3 cageDelta = elevatorCageTransform.position - previousCagePosition;
@@ -127,7 +105,6 @@ public class ElevatorSceneTransition : MonoBehaviour
yield return new WaitForSeconds(1.0f); yield return new WaitForSeconds(1.0f);
Debug.Log("🎉 ボスステージに到着。シーンを切り替えます。");
asyncLoad.allowSceneActivation = true; asyncLoad.allowSceneActivation = true;
} }

View File

@@ -1,20 +1,12 @@
using System.Collections;
using UnityEngine; using UnityEngine;
using UnityEngine.AI; using UnityEngine.AI;
using System.Collections;
using System;
[RequireComponent(typeof(NavMeshAgent))] [RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(Animator))] [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("現在の状態")] [Header("現在の状態")]
[SerializeField] private AIState currentState = AIState.Wander; [SerializeField] private AIState currentState = AIState.Wander;
@@ -25,21 +17,14 @@ public class EnemyAI : MonoBehaviour
[SerializeField] private string playerTag = "Player"; [SerializeField] private string playerTag = "Player";
[Header("攻撃設定")] [Header("攻撃設定")]
[Tooltip("この距離に近づいたら攻撃 (メートル)")]
[SerializeField] private float attackRange = 1.5f; [SerializeField] private float attackRange = 1.5f;
[Tooltip("パンチの攻撃力")]
[SerializeField] private float attackDamage = 10f; [SerializeField] private float attackDamage = 10f;
[Tooltip("一度攻撃が終了してから、次の攻撃を開始するまでの待ち時間 (秒)")]
[SerializeField] private float attackCooldown = 2.0f; [SerializeField] private float attackCooldown = 2.0f;
[Tooltip("攻撃の予兆時間 (秒)")]
[SerializeField] private float telegraphDuration = 0.6f; [SerializeField] private float telegraphDuration = 0.6f;
[SerializeField] private Collider rightHandCollider; [SerializeField] private Collider rightHandCollider;
[Header("ループ攻撃の調整")]
[Tooltip("連続攻撃の間に挟む最低限のインターバル (秒)")]
[SerializeField] private float comboInterval = 0.2f; [SerializeField] private float comboInterval = 0.2f;
[Header("徘徊(Wander)設定")] [Header("徘徊設定")]
[SerializeField] private float wanderRadius = 8f; [SerializeField] private float wanderRadius = 8f;
[SerializeField] private float minWaitTime = 2f; [SerializeField] private float minWaitTime = 2f;
[SerializeField] private float maxWaitTime = 4f; [SerializeField] private float maxWaitTime = 4f;
@@ -49,116 +34,142 @@ public class EnemyAI : MonoBehaviour
private Animator anim; private Animator anim;
private Transform playerTransform; private Transform playerTransform;
private bool isCooldown = false; private bool isCooldown;
private bool isAttacking = false; private bool isAttacking;
private bool isWandering = false; private bool isWandering;
private bool isKnockbacking = false; private bool isKnockbacking;
private bool isDead = false; 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>(); agent = GetComponent<NavMeshAgent>();
anim = GetComponent<Animator>(); anim = GetComponent<Animator>();
}
private void Start()
{
SetFistCollider(false); SetFistCollider(false);
if (WorldPauseManager.Instance != null)
// AIのメインループコルーチンを開始 {
WorldPauseManager.Instance.Register(this);
}
StartCoroutine(AILoop()); 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() private void Update()
{ {
if(isDead) return; if (isDead || isPaused) return;
// 毎フレームのアニメーションパラメータ更新処理
UpdateAnimation(); UpdateAnimation();
// 攻撃ステートにおけるプレイヤーへの方向転換処理
UpdateRotationOnAttack(); UpdateRotationOnAttack();
} }
#region AI メインループ (思考ロジック)
private IEnumerator AILoop() private IEnumerator AILoop()
{ {
while (true) while (!isDead)
{ {
if(isDead) yield break; if (isPaused)
// プレイヤーが未検出の場合はタグから検索
if (playerTransform == null)
{ {
FindPlayer(); yield return null;
continue;
} }
// 攻撃、のけぞりアニメーションの再生中でない場合のみ、ステートに応じた行動を実行 if (playerTransform == null) FindPlayer();
if (!isAttacking && !isKnockbacking && currentState != AIState.Stun) if (!isAttacking && !isKnockbacking && currentState != AIState.Stun)
{ {
switch (currentState) switch (currentState)
{ {
case AIState.Wander: case AIState.Wander: WanderBehavior(); break;
WanderBehavior(); case AIState.Chase: ChaseBehavior(); break;
break; case AIState.Attack: AttackBehavior(); break;
case AIState.Chase:
ChaseBehavior();
break;
case AIState.Attack:
AttackBehavior();
break;
} }
} }
// 毎フレーム実行を避け、0.2秒待機することでCPU負荷を軽減 yield return WaitForUnpausedSeconds(0.2f);
yield return new WaitForSeconds(0.2f);
} }
} }
private void FindPlayer() private void FindPlayer()
{ {
GameObject playerObj = GameObject.FindGameObjectWithTag(playerTag); GameObject playerObj = GameObject.FindGameObjectWithTag(playerTag);
if (playerObj != null) if (playerObj != null) playerTransform = playerObj.transform;
{
playerTransform = playerObj.transform;
} }
}
#endregion
#region 各ステートの固有行動
/// 【徘徊状態】の思考ロジック
private void WanderBehavior() private void WanderBehavior()
{ {
if (playerTransform != null) if (playerTransform != null)
{ {
// プレイヤーとの距離を測定し、索敵範囲内なら追跡ステートへ遷移
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position); float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
if (distanceToPlayer <= detectionRange) if (distanceToPlayer <= detectionRange)
{ {
currentState = AIState.Chase; currentState = AIState.Chase;
isWandering = false; isWandering = false;
agent.ResetPath(); agent.ResetPath();
Debug.Log($"👁️ {gameObject.name}: プレイヤーを発見! 追跡します。");
return; return;
} }
} }
// すでに徘徊移動中でなければ、新しい目的地への移動コルーチンを開始
if (!isWandering) if (!isWandering)
{ {
StartCoroutine(WanderMoveRoutine()); StartCoroutine(WanderMoveRoutine());
} }
} }
/// 【追跡状態】の思考ロジック
private void ChaseBehavior() private void ChaseBehavior()
{ {
if (playerTransform == null) if (playerTransform == null) { currentState = AIState.Wander; return; }
{
currentState = AIState.Wander;
return;
}
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position); float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
// 攻撃射程内に入ったら攻撃ステートへ遷移し、その場に停止
if (distanceToPlayer <= attackRange) if (distanceToPlayer <= attackRange)
{ {
currentState = AIState.Attack; currentState = AIState.Attack;
@@ -166,106 +177,77 @@ public class EnemyAI : MonoBehaviour
return; return;
} }
// プレイヤーが見失う範囲を超えたら徘徊ステートにリセット
if (distanceToPlayer > loseRange) if (distanceToPlayer > loseRange)
{ {
currentState = AIState.Wander; currentState = AIState.Wander;
agent.ResetPath(); agent.ResetPath();
Debug.Log($"❓ {gameObject.name}: プレイヤーを見失った。");
return; return;
} }
// のけぞり中でない場合、追跡の目的地を更新する if (!isKnockbacking)
if(!isKnockbacking)
{ {
agent.SetDestination(playerTransform.position); agent.SetDestination(playerTransform.position);
} }
} }
/// 【攻撃状態】の思考ロジック
private void AttackBehavior() private void AttackBehavior()
{ {
if (playerTransform == null) if (playerTransform == null) { currentState = AIState.Wander; return; }
{
currentState = AIState.Wander;
return;
}
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position); float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
// 攻撃射程から外れていたら、再び追跡ステートに戻る
if (distanceToPlayer > attackRange) if (distanceToPlayer > attackRange)
{ {
currentState = AIState.Chase; currentState = AIState.Chase;
return; return;
} }
// クールダウン中でなければ攻撃コルーチンを実行(クールダウン中の場合は、Update内の処理によりプレイヤーの方向を向きながら待機します)
if (!isCooldown) if (!isCooldown)
{ {
StartCoroutine(PerformPunchAttack()); StartCoroutine(PerformPunchAttack());
} }
} }
#endregion
#region アニメーション・回転の制御メソッド
/// エージェントの移動状態に応じてAnimatorのSpeedパラメータを更新する
private void UpdateAnimation() private void UpdateAnimation()
{ {
float targetSpeed = 0f; float targetSpeed = 0f;
// 攻撃中でなく、経路が存在し、かつ実際に一定以上の速度で移動している場合のみSpeedを1にする
if (!isAttacking && !isKnockbacking && currentState != AIState.Stun && agent.hasPath && agent.velocity.magnitude > 0.1f) if (!isAttacking && !isKnockbacking && currentState != AIState.Stun && agent.hasPath && agent.velocity.magnitude > 0.1f)
{ {
targetSpeed = 1f; targetSpeed = 1f;
} }
anim.SetFloat(SpeedHash, targetSpeed, 0.1f, Time.deltaTime);
// パラメータをなめらかに変化させて渡す
anim.SetFloat("Speed", targetSpeed, 0.1f, Time.deltaTime);
} }
/// 攻撃ステートの際、プレイヤーの方向へ滑らかに旋回させる
private void UpdateRotationOnAttack() private void UpdateRotationOnAttack()
{ {
if (currentState == AIState.Attack && playerTransform != null && !isKnockbacking && currentState != AIState.Stun) if (currentState == AIState.Attack && playerTransform != null && !isKnockbacking && currentState != AIState.Stun)
{ {
Vector3 direction = (playerTransform.position - transform.position).normalized; Vector3 direction = (playerTransform.position - transform.position).normalized;
direction.y = 0; // 上下方向の傾き(ピッチ回転)を無視 direction.y = 0;
if (direction != Vector3.zero) if (direction != Vector3.zero)
{ {
// Y軸の回転のみを滑らかに補間して適用
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), Time.deltaTime * 10f); transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), Time.deltaTime * 10f);
} }
} }
} }
#endregion
// ダメージを受けた際に、EnemyhHealthから呼び出されるのけぞり開始メソッド
public void TriggerKnockback(float duration) public void TriggerKnockback(float duration)
{ {
if(isDead) return; if (isDead || currentState == AIState.Stun) return;
if(currentState == AIState.Stun) return;
SetFistCollider(false); SetFistCollider(false);
StopAllCoroutines(); StopAllCoroutines();
isAttacking = false; isAttacking = false;
isWandering = false; isWandering = false;
if(agent.isOnNavMesh) if (agent.isOnNavMesh) agent.isStopped = false;
{
agent.isStopped = false;
}
StartCoroutine(KnockbackRoutine(duration)); StartCoroutine(KnockbackRoutine(duration));
} }
public void TriggerParryStun(float duration) public void TriggerParryStun(float duration)
{ {
if(isDead) return; if (isDead) return;
Debug.Log($"{gameObject.name}: parry = true Stunnow");
SetFistCollider(false); SetFistCollider(false);
StopAllCoroutines(); StopAllCoroutines();
@@ -280,7 +262,7 @@ public class EnemyAI : MonoBehaviour
{ {
isDead = true; isDead = true;
StopAllCoroutines(); StopAllCoroutines();
if(agent != null && agent.isOnNavMesh) if (agent != null && agent.isOnNavMesh)
{ {
agent.isStopped = true; agent.isStopped = true;
agent.ResetPath(); agent.ResetPath();
@@ -291,24 +273,19 @@ public class EnemyAI : MonoBehaviour
{ {
currentState = AIState.Stun; currentState = AIState.Stun;
if(agent.isOnNavMesh) if (agent.isOnNavMesh)
{ {
agent.isStopped = true; agent.isStopped = true;
agent.ResetPath(); 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; currentState = AIState.Chase;
isCooldown = false; isCooldown = false;
StartCoroutine(AILoop()); StartCoroutine(AILoop());
} }
@@ -316,65 +293,48 @@ public class EnemyAI : MonoBehaviour
{ {
isKnockbacking = true; isKnockbacking = true;
if(agent.isOnNavMesh) if (agent.isOnNavMesh)
{ {
agent.isStopped = true; agent.isStopped = true;
agent.ResetPath(); 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; isKnockbacking = false;
isCooldown = false; isCooldown = false;
StartCoroutine(AILoop()); StartCoroutine(AILoop());
} }
#region 攻撃・移動のコルーチン処理
/// 徘徊時のランダム移動を制御するコルーチン
private IEnumerator WanderMoveRoutine() private IEnumerator WanderMoveRoutine()
{ {
isWandering = true; isWandering = true;
Vector3 newPos = Vector3.zero; Vector3 newPos = Vector3.zero;
bool isPathValid = false; bool isPathValid = false;
int maxAttempts = 10;
// 有効な経路(壁などで分断されていないルート)が見つかるまで試行 for (int i = 0; i < 10; i++)
for (int i = 0; i < maxAttempts; i++)
{ {
newPos = GetRandomNavMeshPoint(transform.position, wanderRadius); newPos = GetRandomNavMeshPoint(transform.position, wanderRadius);
NavMeshPath path = new NavMeshPath(); NavMeshPath path = new NavMeshPath();
if (agent.CalculatePath(newPos, path) && path.status == NavMeshPathStatus.PathComplete)
if (agent.CalculatePath(newPos, path))
{
if (path.status == NavMeshPathStatus.PathComplete)
{ {
isPathValid = true; isPathValid = true;
break; break;
} }
} }
}
// 有効な目的地が設定できた場合の移動処理
if (isPathValid) if (isPathValid)
{ {
agent.SetDestination(newPos); agent.SetDestination(newPos);
float elapsedTime = 0f; float elapsedTime = 0f;
// 目的地に到達するか、タイムアウト時間を迎えるまで待機
while (agent.remainingDistance > agent.stoppingDistance) while (agent.remainingDistance > agent.stoppingDistance)
{ {
yield return new WaitForSeconds(0.5f); yield return WaitForUnpausedSeconds(0.5f);
elapsedTime += 0.5f; elapsedTime += 0.5f;
// スタック対策としてのタイムアウト判定
if (elapsedTime >= wanderTimeout) if (elapsedTime >= wanderTimeout)
{ {
agent.ResetPath(); agent.ResetPath();
@@ -383,57 +343,36 @@ public class EnemyAI : MonoBehaviour
} }
} }
// 到着または諦めた後、ランダムな時間その場で待機 yield return WaitForUnpausedSeconds(Random.Range(minWaitTime, maxWaitTime));
yield return new WaitForSeconds(UnityEngine.Random.Range(minWaitTime, maxWaitTime));
isWandering = false; isWandering = false;
} }
/// 予兆から攻撃アニメーションのトリガーまでを制御するコルーチン
private IEnumerator PerformPunchAttack() private IEnumerator PerformPunchAttack()
{ {
isAttacking = true; isAttacking = true;
isCooldown = true; // ここで攻撃フラグをロックします isCooldown = true;
anim.SetTrigger("Telegraph"); anim.SetTrigger(TelegraphHash);
Debug.Log($"⚠️ {gameObject.name}: 攻撃の予兆(溜め)開始"); yield return WaitForUnpausedSeconds(telegraphDuration);
yield return new WaitForSeconds(telegraphDuration); if (isKnockbacking || isDead)
if(isKnockbacking) yield break;
anim.SetTrigger("Attack");
}
/// 連続攻撃成功時の、クイックなクールダウン解除処理
private IEnumerator ResetCooldownQuickly()
{ {
yield return new WaitForSeconds(comboInterval); isAttacking = false;
isCooldown = false; yield break;
} }
/// コンボが途切れた際の、通常の攻撃クールダウン解除処理 anim.SetTrigger(AttackHash);
private IEnumerator ResetCooldown()
{
// 指定された秒数待機する
yield return new WaitForSeconds(attackCooldown);
isCooldown = false;
Debug.Log($"✅ {gameObject.name}: 攻撃クールダウンが終了しました。再攻撃可能です。");
} }
#endregion
#region アニメーションイベント(外部からのコールバック)
/// 攻撃アニメーションのヒットフレームで実行されるヒット判定(Animation Eventから呼び出し)
public void OnPunchHit() public void OnPunchHit()
{ {
if (isKnockbacking || currentState == AIState.Stun) return; if (isKnockbacking || currentState == AIState.Stun || isPaused) return;
SetFistCollider(true); SetFistCollider(true);
} }
/// Recovery(硬直)アニメーションの終了時に実行される処理(Animation Eventから呼び出し)
public void OnRecoveryEnd() public void OnRecoveryEnd()
{ {
if(isKnockbacking || currentState == AIState.Stun) return; if (isKnockbacking || currentState == AIState.Stun) return;
SetFistCollider(false); SetFistCollider(false);
isAttacking = false; isAttacking = false;
@@ -447,59 +386,51 @@ public class EnemyAI : MonoBehaviour
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position); float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
// ループ判定:まだプレイヤーが射程内にいる場合はクイックな連続攻撃へ移行
if (distanceToPlayer <= attackRange) if (distanceToPlayer <= attackRange)
{ {
StartCoroutine(ResetCooldownQuickly()); StartCoroutine(ResetCooldownQuickly());
Debug.Log($"🔄 {gameObject.name}: プレイヤーがまだ射程内です。連続攻撃(コンボ)を実行します。");
} }
else else
{ {
// プレイヤーが離れていた場合はコンボを抜け、追跡状態に戻ると同時に、通常のクールダウン(長めの待機)を開始
currentState = AIState.Chase; currentState = AIState.Chase;
StartCoroutine(ResetCooldown()); StartCoroutine(ResetCooldown());
Debug.Log($"🏃 {gameObject.name}: プレイヤーが離れたため追跡に戻ります。({attackCooldown}秒の攻撃クールダウンを適用)");
} }
} }
#endregion
#region ヘルパー・デバッグ用メソッド private IEnumerator ResetCooldownQuickly()
/// 指定された中心座標の範囲内から、NavMesh上の有効なランダム座標を取得する {
yield return WaitForUnpausedSeconds(comboInterval);
isCooldown = false;
}
private IEnumerator ResetCooldown()
{
yield return WaitForUnpausedSeconds(attackCooldown);
isCooldown = false;
}
private void SetFistCollider(bool enabledState) private void SetFistCollider(bool enabledState)
{ {
if(rightHandCollider != null) if (rightHandCollider != null) rightHandCollider.enabled = enabledState;
{
rightHandCollider.enabled = enabledState;
}
} }
private Vector3 GetRandomNavMeshPoint(Vector3 center, float radius) private Vector3 GetRandomNavMeshPoint(Vector3 center, float radius)
{ {
Vector3 randomDirection = UnityEngine.Random.insideUnitSphere * radius; Vector3 randomDirection = Random.insideUnitSphere * radius + center;
randomDirection += center; if (NavMesh.SamplePosition(randomDirection, out NavMeshHit hit, radius, NavMesh.AllAreas))
NavMeshHit hit;
if (NavMesh.SamplePosition(randomDirection, out hit, radius, NavMesh.AllAreas))
{ {
return hit.position; return hit.position;
} }
return center; return center;
} }
/// エディタのSceneビューに索敵・攻撃範囲をギズモとして描画する
private void OnDrawGizmosSelected() private void OnDrawGizmosSelected()
{ {
// 索敵範囲(赤)
Gizmos.color = Color.red; Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, detectionRange); Gizmos.DrawWireSphere(transform.position, detectionRange);
// 見失う範囲(黄)
Gizmos.color = Color.yellow; Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, loseRange); Gizmos.DrawWireSphere(transform.position, loseRange);
// 攻撃範囲(青)
Gizmos.color = Color.blue; Gizmos.color = Color.blue;
Gizmos.DrawWireSphere(transform.position, attackRange); Gizmos.DrawWireSphere(transform.position, attackRange);
} }
#endregion
} }

View File

@@ -1,68 +1,74 @@
using System; using System;
using UnityEngine; using UnityEngine;
public class EnemyHealth : MonoBehaviour, IDamageble public class EnemyHealth : MonoBehaviour, IDamageable
{ {
[SerializeField] private float maxHealth = 100f; [SerializeField] private float maxHealth = 100f;
private float currentHealth;
[SerializeField] private float knockbackDuration = 0.5f; [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 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>(); 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) 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) if (CurrentHealth <= 0f)
{
Debug.Log("構えボーナス");
}
if(currentHealth <= 0)
{ {
Die(); Die();
} }
else else
{ {
if(enemyAI != null) OnKnockbackRequested?.Invoke(knockbackDuration);
{
enemyAI.TriggerKnockback(knockbackDuration);
}
} }
} }
private void Die() private void Die()
{ {
if(isDead) return; if (IsDead) return;
isDead = true; IsDead = true;
Animator anim = GetComponent<Animator>(); if (anim != null) anim.SetTrigger(DeadHash);
if(anim != null) if (col != null) col.enabled = false;
{
anim.SetTrigger("Dead");
}
Collider col = GetComponent<Collider>();
if(col != null)
{
col.enabled = false;
}
if(enemyAI != null)
{
enemyAI.DisableAIOnDeath();
}
OnDeath?.Invoke();
Destroy(gameObject, 3.0f); Destroy(gameObject, 3.0f);
} }
} }

View File

@@ -2,12 +2,13 @@ using UnityEngine;
public struct DamageInfo public struct DamageInfo
{ {
public float amount; //ダメージ量 public float amount;
public Vector3 hitPosition; //殴られた位置 public Vector3 hitPosition;
public Vector3 punchDirection; //パンチが飛んできた方向(ノックバック用) public Vector3 punchDirection;
public bool isPoseBonus; //構えから攻撃かどうか public bool isPoseBonus;
} }
public interface IDamageble
public interface IDamageable
{ {
void TakeDamage(DamageInfo damageInfo); 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 public class InputManagerLR : MonoBehaviour
{ {
static InputManagerLR instance; private static InputManagerLR instance;
[SerializeField] [SerializeField] private InputActionAsset actionAsset;
InputActionAsset ActionAsset;
InputActionMap ActionMap; private InputActionMap actionMap;
InputAction m_PrimaryButtonR; private InputAction primaryButtonR;
InputAction m_SecondaryButtonR; private InputAction secondaryButtonR;
InputAction m_PrimaryButtonL; private InputAction primaryButtonL;
InputAction m_SecondaryButtonL; private InputAction secondaryButtonL;
private void Awake() private void Awake()
{ {
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this; instance = this;
GameObject.DontDestroyOnLoad(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() if (actionAsset != null)
{ {
ActionMap?.Enable(); actionMap = actionAsset.FindActionMap("Test");
} if (actionMap != null)
private void OnDisable()
{ {
ActionMap?.Disable(); 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);
}
}
} }
// Aボタン private void OnEnable() => actionMap?.Enable();
public static bool PrimaryButtonR() => instance.m_PrimaryButtonR.IsPressed(); private void OnDisable() => actionMap?.Disable();
public static bool PrimaryButtonR_OnPress() => instance.m_PrimaryButtonR.WasPressedThisFrame();
public static bool PrimaryButtonR_OnRelease() => instance.m_PrimaryButtonR.WasReleasedThisFrame();
// Xボタン public static bool PrimaryButtonR() => instance?.primaryButtonR?.IsPressed() ?? false;
public static bool PrimaryButtonL() => instance.m_PrimaryButtonL.IsPressed(); public static bool PrimaryButtonR_OnPress() => instance?.primaryButtonR?.WasPressedThisFrame() ?? false;
public static bool PrimaryButtonL_OnPress() => instance.m_PrimaryButtonL.WasPressedThisFrame(); public static bool PrimaryButtonR_OnRelease() => instance?.primaryButtonR?.WasReleasedThisFrame() ?? false;
public static bool PrimaryButtonL_OnRelease() => instance.m_PrimaryButtonL.WasReleasedThisFrame();
// Bボタン public static bool PrimaryButtonL() => instance?.primaryButtonL?.IsPressed() ?? false;
public static bool SecondaryButtonR() => instance.m_SecondaryButtonR.IsPressed(); public static bool PrimaryButtonL_OnPress() => instance?.primaryButtonL?.WasPressedThisFrame() ?? false;
public static bool SecondaryButtonR_OnPress() => instance.m_SecondaryButtonR.WasPressedThisFrame(); public static bool PrimaryButtonL_OnRelease() => instance?.primaryButtonL?.WasReleasedThisFrame() ?? false;
public static bool SecondaryButtonR_OnRelease() => instance.m_SecondaryButtonR.WasReleasedThisFrame();
// Yボタン public static bool SecondaryButtonR() => instance?.secondaryButtonR?.IsPressed() ?? false;
public static bool SecondaryButtonL() => instance.m_SecondaryButtonL.IsPressed(); public static bool SecondaryButtonR_OnPress() => instance?.secondaryButtonR?.WasPressedThisFrame() ?? false;
public static bool SecondaryButtonL_OnPress() => instance.m_SecondaryButtonL.WasPressedThisFrame(); public static bool SecondaryButtonR_OnRelease() => instance?.secondaryButtonR?.WasReleasedThisFrame() ?? false;
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;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
using UnityEngine.AI;
using System.Collections.Generic;
public class PlayerStatusManager : MonoBehaviour public class PlayerStatusManager : MonoBehaviour
{ {
@@ -10,83 +8,66 @@ public class PlayerStatusManager : MonoBehaviour
[Header("デバッグ設定")] [Header("デバッグ設定")]
[SerializeField] private bool ignoreGaugeForDebug = true; [SerializeField] private bool ignoreGaugeForDebug = true;
[Header("Codingモード用 入力設定")] [Header("入力設定")]
[SerializeField] private InputActionProperty zlButtonAction; [SerializeField] private InputActionProperty zlButtonAction;
[SerializeField] private InputActionProperty zrButtonAction; [SerializeField] private InputActionProperty zrButtonAction;
[Header("プレイヤー移動・操作制御用 入力設定")]
[SerializeField] private InputActionProperty moveAction; [SerializeField] private InputActionProperty moveAction;
[SerializeField] private InputActionProperty turnAction; [SerializeField] private InputActionProperty turnAction;
[Header("パズル制限時間設定")] [Header("パズル制限時間")]
[SerializeField] private float codingLimitDuration = 15f; [SerializeField] private float codingLimitDuration = 15f;
private float codingTimer = 0f;
[Header("ゲージ設定")] [Header("ゲージ設定")]
[SerializeField] private float maxGauge = 100f; [SerializeField] private float maxGauge = 100f;
[SerializeField] private float currentGauge = 0f; [SerializeField] private float currentGauge = 0f;
[Header("表示UI設定")] [Header("UI & エフェクト")]
[SerializeField] private GameObject codingModeUI; [SerializeField] private GameObject codingModeUI;
[SerializeField] private GameObject successEffectPrefab;
[SerializeField] private Transform effectSpawnPoint;
[Header("バフパラメータ設定")] [Header("バフ機能")]
[Tooltip("攻撃力UP・防御力UPの持続時間 (秒)")] [SerializeField] private BuffHandler buffHandler = new BuffHandler();
[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;
private float attackBuffTimer = 0f; private float codingTimer;
private float defenseBuffTimer = 0f; public bool IsCodingMode { get; private set; }
private bool isCodingMode = false; public BuffHandler Buffs => buffHandler;
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 void Awake() private void Awake()
{ {
if (Instance == null) { Instance = this; } if (Instance == null)
else { Destroy(gameObject); } {
Instance = this;
}
else
{
Destroy(gameObject);
}
} }
private void OnEnable() private void OnEnable()
{ {
EnableAction(zlButtonAction); SetActionsEnabled(true);
EnableAction(zrButtonAction);
EnableAction(moveAction);
EnableAction(turnAction);
} }
private void OnDisable() private void OnDisable()
{ {
DisableAction(zlButtonAction); SetActionsEnabled(false);
DisableAction(zrButtonAction);
DisableAction(moveAction);
DisableAction(turnAction);
} }
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() private void Start()
@@ -98,13 +79,12 @@ public class PlayerStatusManager : MonoBehaviour
{ {
CheckComboInput(); CheckComboInput();
if (!isCodingMode) if (!IsCodingMode)
{ {
UpdateBuffTimers(); buffHandler.UpdateTimers(Time.deltaTime);
} }
else else
{ {
// CodingMode中の制限時間カウントダウン
codingTimer -= Time.deltaTime; codingTimer -= Time.deltaTime;
if (codingTimer <= 0f) if (codingTimer <= 0f)
{ {
@@ -115,33 +95,25 @@ public class PlayerStatusManager : MonoBehaviour
public void AddGauge(float amount) public void AddGauge(float amount)
{ {
if (isCodingMode) return; if (IsCodingMode) return;
currentGauge += amount; currentGauge = Mathf.Clamp(currentGauge + amount, 0f, maxGauge);
currentGauge = Mathf.Clamp(currentGauge, 0f, maxGauge);
} }
private void CheckComboInput() private void CheckComboInput()
{ {
if (zlButtonAction.action == null || zrButtonAction.action == null) return; if (zlButtonAction.action == null || zrButtonAction.action == null) return;
float zlVal = zlButtonAction.action.ReadValue<float>(); bool isBothHeld = zlButtonAction.action.ReadValue<float>() > 0.5f && zrButtonAction.action.ReadValue<float>() > 0.5f;
float zrVal = zrButtonAction.action.ReadValue<float>();
bool isBothHeld = zlVal > 0.5f && zrVal > 0.5f;
bool isAnyTriggered = zlButtonAction.action.triggered || zrButtonAction.action.triggered; bool isAnyTriggered = zlButtonAction.action.triggered || zrButtonAction.action.triggered;
if (isBothHeld && isAnyTriggered) if (isBothHeld && isAnyTriggered)
{ {
if (!isCodingMode) if (!IsCodingMode)
{ {
if (ignoreGaugeForDebug || currentGauge >= maxGauge) if (ignoreGaugeForDebug || currentGauge >= maxGauge)
{ {
StartCodingMode(); StartCodingMode();
} }
else
{
Debug.LogWarning($"⚠️ ゲージ不足のためCodingModeを開始できません。現在: {currentGauge} / 必要: {maxGauge}");
}
} }
else else
{ {
@@ -152,90 +124,49 @@ public class PlayerStatusManager : MonoBehaviour
private void StartCodingMode() private void StartCodingMode()
{ {
isCodingMode = true; IsCodingMode = true;
codingTimer = codingLimitDuration; codingTimer = codingLimitDuration;
// 1. プレイヤーの移動制御をロック SetActionState(moveAction, false);
DisableAction(moveAction); SetActionState(turnAction, false);
DisableAction(turnAction);
// 2. パズルブロックの状態・位置を全リセット
PuzzleBlock.ResetAllBlocksInScene(); PuzzleBlock.ResetAllBlocksInScene();
// 3. 敵のアニメーション・NavMeshを停止 if (WorldPauseManager.Instance != null)
foreach (var anim in FindObjectsByType<Animator>(FindObjectsSortMode.None))
{ {
if (anim == null || anim.gameObject == this.gameObject || anim.CompareTag("Player")) continue; WorldPauseManager.Instance.PauseWorld();
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); }
} }
if (codingModeUI != null) codingModeUI.SetActive(true); if (codingModeUI != null) codingModeUI.SetActive(true);
Debug.Log($"⏳ Codingモード開始(制限時間: {codingLimitDuration}秒)");
} }
public void ApplyBuffsAndResume(string buff1, string buff2) public void ApplyBuffsAndResume(BuffType buff1, BuffType buff2)
{
if (successEffectPrefab != null && effectSpawnPoint != null)
{ {
Instantiate(successEffectPrefab, effectSpawnPoint.position, effectSpawnPoint.rotation); Instantiate(successEffectPrefab, effectSpawnPoint.position, effectSpawnPoint.rotation);
ActivateBuffByName(buff1);
ActivateBuffByName(buff2);
ResumeWorldState(consumeGauge: true);
Debug.Log("🎉 パズル成功!バフを適用してゲームを再開しました。");
} }
private void FailCodingModeByTimeout() buffHandler.ActivateBuff(buff1);
{ buffHandler.ActivateBuff(buff2);
ResumeWorldState(consumeGauge: true); ResumeWorldState(consumeGauge: true);
Debug.Log("💀 制限時間切れ! Codingモード失敗(バフなし・ゲージ消費)。");
} }
public void CancelCodingMode() private void FailCodingModeByTimeout() => ResumeWorldState(consumeGauge: true);
{ public void CancelCodingMode() => ResumeWorldState(consumeGauge: false);
ResumeWorldState(consumeGauge: false);
Debug.Log("↩️ Codingモードを手動キャンセルしました。");
}
private void ResumeWorldState(bool consumeGauge) private void ResumeWorldState(bool consumeGauge)
{ {
foreach (var anim in pausedAnimators) { if (anim != null) anim.speed = 1f; } if (WorldPauseManager.Instance != null)
pausedAnimators.Clear(); {
WorldPauseManager.Instance.ResumeWorld();
}
foreach (var agent in pausedAgents) { if (agent != null && agent.enabled) agent.isStopped = false; } SetActionState(moveAction, true);
pausedAgents.Clear(); SetActionState(turnAction, true);
EnableAction(moveAction); IsCodingMode = false;
EnableAction(turnAction);
isCodingMode = false;
if (codingModeUI != null) codingModeUI.SetActive(false); if (codingModeUI != null) codingModeUI.SetActive(false);
if (consumeGauge) currentGauge = 0f; 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;
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 BlockColor { Red, Blue, Green }
public enum BlockShapeType { TShape, CShape } // ト字, コ字 public enum BlockShapeType { TShape, CShape }
public class PuzzleBlock : MonoBehaviour public class PuzzleBlock : MonoBehaviour
{ {
[field: SerializeField] public BlockColor Color { get; private set; } [field: SerializeField] public BlockColor Color { get; private set; }
[field: SerializeField] public BlockShapeType ShapeType { get; private set; } [field: SerializeField] public BlockShapeType ShapeType { get; private set; }
[Header("スナップ(合体)位置の設定")]
[Tooltip("相手と合体する基準位置(空のGameObject)を指定。未設定でも中心位置でスナップします")]
[SerializeField] private Transform snapPoint; [SerializeField] private Transform snapPoint;
private PuzzleBlock nearbyPartner; // 近くにある結合対象のブロック public static event Action<BlockColor> OnPuzzleCompleted;
private bool isConnected = false; // 結合済みフラグ
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 Rigidbody rb;
private XRGrabInteractable grabInteractable; private XRGrabInteractable grabInteractable;
@@ -27,9 +35,21 @@ public class PuzzleBlock : MonoBehaviour
rb = GetComponent<Rigidbody>(); rb = GetComponent<Rigidbody>();
grabInteractable = GetComponent<XRGrabInteractable>(); grabInteractable = GetComponent<XRGrabInteractable>();
// 初期スポーン位置・回転を記憶しておく // ★開始時の「親」と「ローカル座標・回転・スケール」を保持
initialPosition = transform.position; initialParent = transform.parent;
initialRotation = transform.rotation; 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) private void OnTriggerEnter(Collider other)
@@ -40,7 +60,6 @@ public class PuzzleBlock : MonoBehaviour
if (partner != null && IsValidPartner(partner)) if (partner != null && IsValidPartner(partner))
{ {
nearbyPartner = partner; nearbyPartner = partner;
Debug.Log($"🧩 パートナー検知: {partner.Color} の {partner.ShapeType}");
} }
} }
@@ -56,8 +75,7 @@ public class PuzzleBlock : MonoBehaviour
private bool IsValidPartner(PuzzleBlock partner) private bool IsValidPartner(PuzzleBlock partner)
{ {
if (partner == null || partner.isConnected) return false; if (partner == null || partner.isConnected) return false;
// 同一色 & 異なる形状(ト字 + コ字)のみ許可 return partner.Color == Color && partner.ShapeType != ShapeType;
return partner.Color == this.Color && partner.ShapeType != this.ShapeType;
} }
public void OnReleased() public void OnReleased()
@@ -75,7 +93,6 @@ public class PuzzleBlock : MonoBehaviour
isConnected = true; isConnected = true;
partner.isConnected = true; partner.isConnected = true;
// 1. 位置・回転のスナップ補正
if (snapPoint != null && partner.snapPoint != null) if (snapPoint != null && partner.snapPoint != null)
{ {
transform.position = partner.snapPoint.position; transform.position = partner.snapPoint.position;
@@ -87,15 +104,16 @@ public class PuzzleBlock : MonoBehaviour
transform.rotation = partner.transform.rotation; transform.rotation = partner.transform.rotation;
} }
// 2. 物理&VR掴み動作の完全ロック(完成後に崩れないようにする)
LockBlockState(); LockBlockState();
partner.LockBlockState(); partner.LockBlockState();
Debug.Log($"🎉 マグネット合体成功! 色: {Color}"); OnPuzzleCompleted?.Invoke(Color);
// 3. バフ適用してCodingMode完了へ BuffType buff = ConvertColorToBuffType(Color);
string buffName = ConvertColorToBuffName(Color); if (PlayerStatusManager.Instance != null)
PlayerStatusManager.Instance.ApplyBuffsAndResume(buffName, buffName); {
PlayerStatusManager.Instance.ApplyBuffsAndResume(buff, buff);
}
} }
private void LockBlockState() private void LockBlockState()
@@ -106,43 +124,72 @@ public class PuzzleBlock : MonoBehaviour
rb.angularVelocity = Vector3.zero; rb.angularVelocity = Vector3.zero;
rb.isKinematic = true; rb.isKinematic = true;
} }
// 掴みコンポーネントをオフにして手から切り離す
if (grabInteractable != null) grabInteractable.enabled = false; if (grabInteractable != null) grabInteractable.enabled = false;
} }
public void ResetBlock() public void ResetBlock()
{ {
// 1. 手で掴んでいる場合は手離し処理
ForceDrop();
isConnected = false; isConnected = false;
nearbyPartner = null; nearbyPartner = null;
transform.position = initialPosition; // 2. ★親オブジェクト(Canvas)の子へ戻す
transform.rotation = initialRotation; 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) if (rb != null)
{ {
rb.isKinematic = false; rb.isKinematic = false;
rb.linearVelocity = Vector3.zero; rb.linearVelocity = Vector3.zero;
rb.angularVelocity = 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() 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 return color switch
{ {
BlockColor.Red => "AttackUp", BlockColor.Red => BuffType.AttackUp,
BlockColor.Blue => "DefenseUp", BlockColor.Blue => BuffType.DefenseUp,
BlockColor.Green => "Heal", BlockColor.Green => BuffType.Heal,
_ => "AttackUp" _ => BuffType.AttackUp
}; };
} }
} }

View File

@@ -3,24 +3,25 @@ using UnityEngine;
public class SlowWebArea : MonoBehaviour public class SlowWebArea : MonoBehaviour
{ {
[SerializeField] private float damagePerSecond = 5f; [SerializeField] private float damagePerSecond = 5f;
private float damageTimer = 0f; private float damageTimer;
private void OnTriggerStay(Collider other) private void OnTriggerStay(Collider other)
{ {
if (other.CompareTag("Player")) if (other.CompareTag("Player"))
{ {
// ★鈍足効果の適用演出
// プレイヤーの移動スクリプトに対し、一時的な速度デバフ(例: Speed * 0.3f)を掛けます
// ここでは設計の切り離しのため、ログ出力のみとしています
Debug.LogWarning("🕸️ プレイヤーが蜘蛛の糸に囚われている! 【鈍足化デバフ発動中】");
// スリップダメージ(1秒ごとに継続ダメージ)
damageTimer += Time.deltaTime; damageTimer += Time.deltaTime;
if (damageTimer >= 1.0f) if (damageTimer >= 1.0f)
{ {
// プレイヤーのHealthコンポーネントにダメージを通知 IDamageable damageable = other.GetComponentInParent<IDamageable>();
// other.GetComponent<PlayerHealth>()?.TakeDamage(damagePerSecond); if (damageable != null)
Debug.Log($"🕸️ 糸によるスリップダメージ: {damagePerSecond}"); {
DamageInfo info = new DamageInfo
{
amount = damagePerSecond,
hitPosition = other.transform.position
};
damageable.TakeDamage(info);
}
damageTimer = 0f; damageTimer = 0f;
} }
} }
@@ -30,7 +31,7 @@ public class SlowWebArea : MonoBehaviour
{ {
if (other.CompareTag("Player")) if (other.CompareTag("Player"))
{ {
Debug.Log("🏃 プレイヤーが糸の範囲から脱出。 鈍足効果解除。"); damageTimer = 0f;
} }
} }
} }

View File

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

View File

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

View File

@@ -1,72 +1,48 @@
using UnityEngine;
using System.Collections; using System.Collections;
using UnityEngine;
/// <summary>
/// 1つのトリガーで左右のドアを反対方向にスライド開閉させ、
/// 一定時間後に自動で閉じるクラス
/// </summary>
public class VRDoorController : MonoBehaviour public class VRDoorController : MonoBehaviour
{ {
[Header("ドアのオブジェクト登録")] [Header("ドアオブジェクト")]
[Tooltip("左側にスライドさせたいドアのTransformを指定してください")]
[SerializeField] private Transform leftDoor; [SerializeField] private Transform leftDoor;
[Tooltip("右側にスライドさせたいドアのTransformを指定してください")]
[SerializeField] private Transform rightDoor; [SerializeField] private Transform rightDoor;
[Header("スライド量と速度の設定")] [Header("動作設定")]
[Tooltip("ドアが横に何メートルずれるか(移動量)")]
[SerializeField] private float slideDistance = 1.0f; [SerializeField] private float slideDistance = 1.0f;
[Tooltip("ドアが完全に開ききる(閉じきる)までにかかる時間(秒)")]
[SerializeField] private float openSpeed = 1.2f; [SerializeField] private float openSpeed = 1.2f;
[Tooltip("ドアが開ききった後、自動で閉まり始めるまでの待ち時間(秒)")]
[SerializeField] private float autoCloseDelay = 4.0f; [SerializeField] private float autoCloseDelay = 4.0f;
// ドアの「閉じた位置」と「開いた位置」の座標を記憶する変数
private Vector3 leftDoorClosedPos; private Vector3 leftDoorClosedPos;
private Vector3 rightDoorClosedPos; private Vector3 rightDoorClosedPos;
private Vector3 leftDoorOpenPos; private Vector3 leftDoorOpenPos;
private Vector3 rightDoorOpenPos; private Vector3 rightDoorOpenPos;
// ドアの状態を管理するフラグ(バグ連打の防止用) private bool isOpen;
private bool isOpen = false; private bool isMoving;
private bool isMoving = false;
private void Start() private void Start()
{ {
// 1. ゲーム開始時の位置を「閉じた状態」として記憶
if (leftDoor != null) if (leftDoor != null)
{ {
leftDoorClosedPos = leftDoor.localPosition; leftDoorClosedPos = leftDoor.localPosition;
// 左ドアはローカルのX軸のマイナス方向へスライドさせる
leftDoorOpenPos = leftDoorClosedPos + new Vector3(-slideDistance, 0, 0); leftDoorOpenPos = leftDoorClosedPos + new Vector3(-slideDistance, 0, 0);
} }
if (rightDoor != null) if (rightDoor != null)
{ {
rightDoorClosedPos = rightDoor.localPosition; rightDoorClosedPos = rightDoor.localPosition;
// 右ドアはローカルのX軸のプラス方向へスライドさせる
rightDoorOpenPos = rightDoorClosedPos + new Vector3(slideDistance, 0, 0); rightDoorOpenPos = rightDoorClosedPos + new Vector3(slideDistance, 0, 0);
} }
} }
/// <summary>
/// 【外部連携用】VRのボタンが押されたときに呼び出す公開メソッド
/// </summary>
public void OnButtonPress() public void OnButtonPress()
{ {
// すでに移動中、またはすでに開いている場合はボタンの入力を完全に無視する
if (isMoving || isOpen) return; if (isMoving || isOpen) return;
// 開いてから自動で閉まるまでの一連のコルーチンを始動
StartCoroutine(OpenAndAutoCloseRoutine()); StartCoroutine(OpenAndAutoCloseRoutine());
} }
/// <summary>
/// ドアを開け、待機し、自動で閉める一連の流れを管理するコルーチン
/// </summary>
private IEnumerator OpenAndAutoCloseRoutine() private IEnumerator OpenAndAutoCloseRoutine()
{ {
// ─── 1. ドアを開ける処理 ───
isMoving = true; isMoving = true;
float elapsedTime = 0f; float elapsedTime = 0f;
@@ -74,28 +50,22 @@ public class VRDoorController : MonoBehaviour
{ {
elapsedTime += Time.deltaTime; elapsedTime += Time.deltaTime;
float t = elapsedTime / openSpeed; float t = elapsedTime / openSpeed;
// ★VR酔い・チープさ対策:数値を滑らかに補間して、リアルな加減速(イージング)をかける
float easedT = Mathf.SmoothStep(0f, 1f, t); float easedT = Mathf.SmoothStep(0f, 1f, t);
if (leftDoor != null) leftDoor.localPosition = Vector3.Lerp(leftDoorClosedPos, leftDoorOpenPos, easedT); if (leftDoor != null) leftDoor.localPosition = Vector3.Lerp(leftDoorClosedPos, leftDoorOpenPos, easedT);
if (rightDoor != null) rightDoor.localPosition = Vector3.Lerp(rightDoorClosedPos, rightDoorOpenPos, 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 (leftDoor != null) leftDoor.localPosition = leftDoorOpenPos;
if (rightDoor != null) rightDoor.localPosition = rightDoorOpenPos; if (rightDoor != null) rightDoor.localPosition = rightDoorOpenPos;
isOpen = true; isOpen = true;
isMoving = false; isMoving = false;
Debug.Log("🚪 ドアが完全に開きました。");
// ─── 2. 開いた状態での待機(自動クローズのカウントダウン) ───
yield return new WaitForSeconds(autoCloseDelay); yield return new WaitForSeconds(autoCloseDelay);
// ─── 3. ドアを閉じる処理 ───
isMoving = true; isMoving = true;
isOpen = false; isOpen = false;
elapsedTime = 0f; elapsedTime = 0f;
@@ -112,11 +82,9 @@ public class VRDoorController : MonoBehaviour
yield return null; yield return null;
} }
// 完全に閉じた座標で固定
if (leftDoor != null) leftDoor.localPosition = leftDoorClosedPos; if (leftDoor != null) leftDoor.localPosition = leftDoorClosedPos;
if (rightDoor != null) rightDoor.localPosition = rightDoorClosedPos; if (rightDoor != null) rightDoor.localPosition = rightDoorClosedPos;
isMoving = false; isMoving = false;
Debug.Log("🔒 ドアが自動で閉じました。");
} }
} }

View File

@@ -2,79 +2,65 @@ using UnityEngine;
public class VRInputPoseDetector : MonoBehaviour public class VRInputPoseDetector : MonoBehaviour
{ {
[Header("必須コンポーネント登録")] [Header("参照")]
[SerializeField] private Transform headTransform; [SerializeField] private Transform headTransform;
[SerializeField] private Transform rightHandTransform; [SerializeField] private Transform rightHandTransform;
[SerializeField] private Transform leftHandTransform; [SerializeField] private Transform leftHandTransform;
[Header("構えの判定範囲設定")] [Header("判定パラメータ")]
[SerializeField] private float minForwardDistance = 0.15f; [SerializeField] private float minForwardDistance = 0.15f;
[SerializeField] private float maxForwardDistance = 0.5f; [SerializeField] private float maxForwardDistance = 0.5f;
[SerializeField] private float minHeightOffset = -0.35f; [SerializeField] private float minHeightOffset = -0.35f;
[SerializeField] private float maxHeightOffset = -0.1f; [SerializeField] private float maxHeightOffset = -0.1f;
[Header("タイマー・バッファ設定")] [Header("タイマーパラメータ")]
[SerializeField] private float requiredPoseTime = 0.3f; [SerializeField] private float requiredPoseTime = 0.3f;
[SerializeField] private float poseExitDelay = 0.15f; [SerializeField] private float poseExitDelay = 0.15f;
[Header("視覚フィードバック(ガイドUI設定)")] [Header("視覚フィードバック")]
[SerializeField] private Renderer guideRenderer; [SerializeField] private Renderer guideRenderer;
[SerializeField] private Color defaultColor = new Color(0.5f, 0.5f, 0.5f, 0.05f); // 普段(超薄いグレー) [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 chargingColor = new Color(1f, 0.8f, 0f, 0.2f);
[SerializeField] private Color successColor = new Color(0f, 0.6f, 1f, 0.4f); // 構え完了(綺麗な青シールド) [SerializeField] private Color successColor = new Color(0f, 0.6f, 1f, 0.4f);
private float poseTimer = 0f; private float poseTimer;
private float exitTimer = 0f; private float exitTimer;
private bool isPoseMaintained = false;
public bool IsPosing => isPoseMaintained; public bool IsPosing { get; private set; }
void Update() private void Update()
{ {
if (headTransform == null || rightHandTransform == null || leftHandTransform == null) return; if (headTransform == null || rightHandTransform == null || leftHandTransform == null) return;
// 両手の世界座標を頭を中心としたローカル座標に変換
Vector3 localRightHand = headTransform.InverseTransformPoint(rightHandTransform.position); Vector3 localRightHand = headTransform.InverseTransformPoint(rightHandTransform.position);
Vector3 localLeftHand = headTransform.InverseTransformPoint(leftHandTransform.position); Vector3 localLeftHand = headTransform.InverseTransformPoint(leftHandTransform.position);
// 両手が構え範囲に入っているかチェック bool isBothHandsGuarding = CheckHandPosition(localRightHand) && CheckHandPosition(localLeftHand);
bool isRightHandGuarding = CheckHandPosition(localRightHand);
bool isLeftHandGuarding = CheckHandPosition(localLeftHand);
bool isBothHandsGuarding = isRightHandGuarding && isLeftHandGuarding;
// ─── 🛡️ チャタリング防止付き・構え判定ロジック ───
if (isBothHandsGuarding) if (isBothHandsGuarding)
{ {
// 範囲内に手が戻ったので、解除カウントダウンを即座にリセット
exitTimer = 0f; exitTimer = 0f;
if (!isPoseMaintained) if (!IsPosing)
{ {
poseTimer += Time.deltaTime; poseTimer += Time.deltaTime;
if (poseTimer >= requiredPoseTime) if (poseTimer >= requiredPoseTime)
{ {
isPoseMaintained = true; IsPosing = true;
Debug.Log("🛡️ 【構え成功】 パリィ受付がアクティブになりました。");
} }
} }
} }
else else
{ {
// 範囲外に出た場合 poseTimer = 0f;
poseTimer = 0f; // まだ構えが完了していない段階(溜め中)なら即リセット
if (isPoseMaintained) if (IsPosing)
{ {
// すでに構えが成功している状態なら、猶予タイマーを開始(ここで即解除しない!)
exitTimer += Time.deltaTime; exitTimer += Time.deltaTime;
if (exitTimer >= poseExitDelay) if (exitTimer >= poseExitDelay)
{ {
isPoseMaintained = false; IsPosing = false;
exitTimer = 0f; exitTimer = 0f;
Debug.Log("❌ 【構え解除】 猶予時間を超えたためガードが解けました。");
} }
} }
} }
@@ -84,9 +70,8 @@ public class VRInputPoseDetector : MonoBehaviour
private bool CheckHandPosition(Vector3 localHandPos) private bool CheckHandPosition(Vector3 localHandPos)
{ {
bool inForwardRange = localHandPos.z >= minForwardDistance && localHandPos.z <= maxForwardDistance; // Z軸:頭より前に手があるか bool inForwardRange = localHandPos.z >= minForwardDistance && localHandPos.z <= maxForwardDistance;
bool inHeightRange = localHandPos.y >= minHeightOffset && localHandPos.y <= maxHeightOffset; // Y軸:手の高さが適切か bool inHeightRange = localHandPos.y >= minHeightOffset && localHandPos.y <= maxHeightOffset;
return inForwardRange && inHeightRange; return inForwardRange && inHeightRange;
} }
@@ -94,18 +79,17 @@ public class VRInputPoseDetector : MonoBehaviour
{ {
if (guideRenderer == null) return; if (guideRenderer == null) return;
// ボスの攻撃を見やすくするため、現在の状態に応じてガイドシールドのカラーを3段階可変させる if (IsPosing)
if (isPoseMaintained)
{ {
guideRenderer.material.color = successColor; // 成功:青シールド guideRenderer.material.color = successColor;
} }
else if (isBothHandsGuarding) else if (isBothHandsGuarding)
{ {
guideRenderer.material.color = chargingColor; // 溜め中:黄色 guideRenderer.material.color = chargingColor;
} }
else 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;
using UnityEngine.InputSystem; // Input Systemを使うために必要 using UnityEngine.InputSystem;
using UnityEngine.XR; // 振動機能のために必要 using UnityEngine.XR;
[RequireComponent(typeof(Collider))] [RequireComponent(typeof(Collider))]
[RequireComponent(typeof(Rigidbody))] [RequireComponent(typeof(Rigidbody))]
@@ -10,37 +10,30 @@ public class VRInputPunch : MonoBehaviour
[SerializeField] private XRNode controllerNode = XRNode.RightHand; [SerializeField] private XRNode controllerNode = XRNode.RightHand;
[SerializeField] private VRInputPoseDetector poseDetector; [SerializeField] private VRInputPoseDetector poseDetector;
[Header("攻撃(パンチ)設定")] [Header("パンチパラメータ")]
[SerializeField] private float minPunchSpeed = 1.0f; // パンチとして判定する最低速度 (m/s) [SerializeField] private float minPunchSpeed = 1.0f;
[SerializeField] private float damageMultiplier = 10.0f; // ダメージ倍率 [SerializeField] private float damageMultiplier = 10.0f;
[SerializeField] private float hapticAmplitude = 0.5f; // 通常ヒットの振動の強さ [SerializeField] private float hapticAmplitude = 0.5f;
[SerializeField] private float hapticDuration = 0.1f; // 通常ヒットの振動の長さ [SerializeField] private float hapticDuration = 0.1f;
[Header("パリィ設定")] [Header("パリィパラメータ")]
[SerializeField] private float parryStunDuration = 3.0f; // スタン時間(秒) [SerializeField] private float parryStunDuration = 3.0f;
[SerializeField] private float parryHapticAmplitude = 0.9f; // パリィ成功時の振動の強さ [SerializeField] private float parryHapticAmplitude = 0.9f;
[SerializeField] private float parryHapticDuration = 0.25f; // パリィ成功時の振動の長さ [SerializeField] private float parryHapticDuration = 0.25f;
// ─── 多段ヒット・連続暴発防止用タイマー ───
private float punchHitCooldown = 0f; // 次のパンチが当たるまでのインターバル private const string EnemyFistTag = "EnemyFist";
private float parryTriggerCooldown = 0f; // 次のパリィが成功するまでのインターバル
private void OnEnable() private void OnEnable()
{ {
if (velocityAction.action != null) if (velocityAction.action != null) velocityAction.action.Enable();
{
velocityAction.action.Enable();
}
} }
private void OnDisable() private void OnDisable()
{ {
if (velocityAction.action != null) if (velocityAction.action != null) velocityAction.action.Disable();
{
velocityAction.action.Disable();
}
} }
void Start() private void Start()
{ {
Rigidbody rb = GetComponent<Rigidbody>(); Rigidbody rb = GetComponent<Rigidbody>();
rb.isKinematic = true; rb.isKinematic = true;
@@ -50,104 +43,71 @@ public class VRInputPunch : MonoBehaviour
private void OnTriggerEnter(Collider other) private void OnTriggerEnter(Collider other)
{ {
// コントローラーの現在の移動速度を取得 Vector3 velocity = velocityAction.action != null ? velocityAction.action.ReadValue<Vector3>() : Vector3.zero;
Vector3 velocity = velocityAction.action.ReadValue<Vector3>();
float speed = velocity.magnitude; float speed = velocity.magnitude;
// ======================================================================== if (other.CompareTag(EnemyFistTag))
// 【パリィ判定】当たったオブジェクトが敵の拳だった場合
// ========================================================================
if (other.CompareTag("EnemyFist"))
{ {
Debug.Log($"🛡️ [パリィチェック] 敵の拳と接触。構え状態: {poseDetector.IsPosing}"); if (poseDetector != null && poseDetector.IsPosing)
{
if (TryProcessParry(other)) return;
}
}
// 自分が「構え状態」のときだけパリィの成立チェックを行う if (speed >= minPunchSpeed)
if (poseDetector.IsPosing) {
ProcessPunchAttack(other, speed, velocity);
}
}
private bool TryProcessParry(Collider other)
{ {
// 1. ザコ敵(EnemyAI)へのパリィ判定
EnemyAI enemyAI = other.GetComponentInParent<EnemyAI>(); EnemyAI enemyAI = other.GetComponentInParent<EnemyAI>();
if (enemyAI != null) if (enemyAI != null)
{ {
enemyAI.TriggerParryStun(parryStunDuration); enemyAI.TriggerParryStun(parryStunDuration);
TriggerHaptics(parryHapticAmplitude, parryHapticDuration); TriggerHaptics(parryHapticAmplitude, parryHapticDuration);
Debug.Log($"🛡️ 【ジャストパリィ成功!】 敵の体勢を崩しました。"); return true;
return; // ★本当のパリィ成功時のみ、ここで処理を終了する
} }
// 2. ボス(BossController)へのパリィ判定
BossController boss = other.GetComponentInParent<BossController>(); BossController boss = other.GetComponentInParent<BossController>();
if (boss != null) if (boss != null && boss.TriggerParryStun())
{
// ★リファクタリング:ボスが本当に攻撃中(Bite/Charge)で、パリィが成功した時だけ終了する
if (boss.TriggerParryStun())
{ {
TriggerHaptics(parryHapticAmplitude, parryHapticDuration); TriggerHaptics(parryHapticAmplitude, parryHapticDuration);
Debug.Log("🛡️ 【ジャストパリィ成功!】 ボスの体勢を崩しました!"); return true;
return; // ★本当のパリィ成功時のみ、ここで処理を終了する
}
}
} }
// ─── ★ここが今回の核心(超重要リファクタリング) ─── return false;
// 構え状態ではなかった場合、またはボスが攻撃中でなくパリィが不成立だった場合は、
// 「プレイヤーが隙を狙って殴りに行った」とみなして、returnせずにそのまま下のパンチ処理へ流す!
Debug.Log("🥊 パリィ不成立、または攻撃チャンス:通常のパンチダメージ処理へ移行します。");
} }
// ────────────────────────────────────────────────────────────────────────
private void ProcessPunchAttack(Collider other, float speed, Vector3 velocity)
// ========================================================================
// 【パンチ攻撃の処理】
// ========================================================================
Debug.Log($"[InputSystem接触] {other.name} に触れました。センサー速度: {speed:F2} m/s");
if (speed >= minPunchSpeed)
{ {
IDamageble damageble = other.GetComponentInParent<IDamageble>(); IDamageable damageable = other.GetComponentInParent<IDamageable>();
if (damageable == null) return;
if (damageble != null) bool isPosing = poseDetector != null && poseDetector.IsPosing;
{
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); DamageInfo info = new DamageInfo
}
else
{ {
info.amount = speed * damageMultiplier; amount = speed * damageMultiplier * (isPosing ? 2f : 1f),
info.isPoseBonus = false; isPoseBonus = isPosing,
hitPosition = other.transform.position,
if (PlayerStatusManager.Instance != null) PlayerStatusManager.Instance.AddGauge(10f); punchDirection = velocity.normalized
} };
info.hitPosition = other.transform.position;
info.punchDirection = velocity.normalized;
if (PlayerStatusManager.Instance != null) if (PlayerStatusManager.Instance != null)
{ {
info.amount *= PlayerStatusManager.Instance.CurrentAttackMultiplier; PlayerStatusManager.Instance.AddGauge(isPosing ? 20f : 10f);
info.amount *= PlayerStatusManager.Instance.Buffs.CurrentAttackMultiplier;
} }
damageble.TakeDamage(info); damageable.TakeDamage(info);
Debug.Log($"パンチ成功 ダメージ: {info.amount:F1}");
// 通常ヒットの振動を発生
TriggerHaptics(hapticAmplitude, hapticDuration); TriggerHaptics(hapticAmplitude, hapticDuration);
} }
}
else
{
Debug.Log($"スピードが足りません。必要: {minPunchSpeed} / 現在: {speed:F2}");
}
}
/// 引数に応じて指定された強さと長さでコントローラーを振動させる
private void TriggerHaptics(float amplitude, float duration) private void TriggerHaptics(float amplitude, float duration)
{ {
// 明示的に UnityEngine.XR.InputDevice と指定して型衝突を回避
UnityEngine.XR.InputDevice device = InputDevices.GetDeviceAtXRNode(controllerNode); UnityEngine.XR.InputDevice device = InputDevices.GetDeviceAtXRNode(controllerNode);
if (device.isValid) if (device.isValid)
{ {

View File

@@ -1,78 +1,60 @@
using UnityEngine; using UnityEngine;
// World Space Canvas をプレイヤーの真正面に移動させ、開いている間はプレイヤーの視点に滑らかに追従させるスクリプト
public class VRUIDoorFaceFollow : MonoBehaviour public class VRUIDoorFaceFollow : MonoBehaviour
{ {
[SerializeField] private Transform vrCameraTransform; [SerializeField] private Transform vrCameraTransform;
[Header("配置・追従のパラメータ")] [Header("配置・追従パラメータ")]
[SerializeField] private float forwardDistance = 0.5f; [SerializeField] private float forwardDistance = 0.5f;
[SerializeField] private float heightOffset = -0.1f; [SerializeField] private float heightOffset = -0.1f;
[SerializeField] private float followSpeed = 5.0f; [SerializeField] private float followSpeed = 5.0f;
// 画面がアクティブ(表示)になった瞬間を検知するためのフラグ private bool wasActiveLastFrame;
private bool wasActiveLastFrame = false;
private void Start() private void Start()
{ {
// カメラが未設定の場合は、シーン内からメインカメラを自動取得
if (vrCameraTransform == null && Camera.main != null) if (vrCameraTransform == null && Camera.main != null)
{ {
vrCameraTransform = Camera.main.transform; vrCameraTransform = Camera.main.transform;
} }
} }
// 全体の時間が止まっている(Time.timeScale = 0)間でも動作させるため、カメラ移動の後に実行される LateUpdate を使用します
private void LateUpdate() private void LateUpdate()
{ {
if (vrCameraTransform == null) return; if (vrCameraTransform == null) return;
// このオブジェクト(UI画面)が有効化された「最初のフレーム」の処理
if (!wasActiveLastFrame) if (!wasActiveLastFrame)
{ {
// 開いた瞬間は、プレイヤーの今の向きの「完全な正面」に瞬間移動させる(遅れてついてくるのを防ぐため)
SetInitialPosition(); SetInitialPosition();
wasActiveLastFrame = true; wasActiveLastFrame = true;
return; return;
} }
// 毎フレームの滑らかな追従処理
SmoothFollowPlayer(); SmoothFollowPlayer();
} }
private void OnDisable() private void OnDisable()
{ {
// 画面が閉じられたらフラグをリセットする
wasActiveLastFrame = false; wasActiveLastFrame = false;
} }
/// 画面が開いた瞬間に、プレイヤーの正面の正しい位置・向きに強制配置する
private void SetInitialPosition() private void SetInitialPosition()
{ {
// カメラの位置から、カメラが向いている正面方向に一定距離進んだ座標を計算
Vector3 targetPosition = vrCameraTransform.position + (vrCameraTransform.forward * forwardDistance); Vector3 targetPosition = vrCameraTransform.position + (vrCameraTransform.forward * forwardDistance);
targetPosition.y += heightOffset; // 高さを微調整 targetPosition.y += heightOffset;
transform.position = targetPosition; transform.position = targetPosition;
// 画面の向きをプレイヤー(カメラ)の方向に向ける(背を向けないようにする)
// UIパネルがプレイヤー側を正面にするよう、LookAtの後に180度回転させて裏返りを防ぎます
transform.LookAt(vrCameraTransform.position); transform.LookAt(vrCameraTransform.position);
transform.Rotate(0, 180, 0); transform.Rotate(0, 180, 0);
} }
/// プレイヤーが首を動かしたり移動したりした際、画面を滑らかについてこさせる
private void SmoothFollowPlayer() private void SmoothFollowPlayer()
{ {
// 目的地(目標座標)の計算
Vector3 targetPosition = vrCameraTransform.position + (vrCameraTransform.forward * forwardDistance); Vector3 targetPosition = vrCameraTransform.position + (vrCameraTransform.forward * forwardDistance);
targetPosition.y += heightOffset; targetPosition.y += heightOffset;
// Time.timeScale = 0 でも動かすため、Time.unscaledDeltaTime を使用
// 現在位置から目標位置まで滑らかに補間移動(Lerp)させる
transform.position = Vector3.Lerp(transform.position, targetPosition, Time.unscaledDeltaTime * followSpeed); transform.position = Vector3.Lerp(transform.position, targetPosition, Time.unscaledDeltaTime * followSpeed);
// 向きも滑らかに補間(Slerp)してプレイヤーの方を向かせ続ける
Quaternion targetRotation = Quaternion.LookRotation(transform.position - vrCameraTransform.position); Quaternion targetRotation = Quaternion.LookRotation(transform.position - vrCameraTransform.position);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.unscaledDeltaTime * followSpeed); 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