otamesi
This commit is contained in:
@@ -84,7 +84,7 @@ MonoBehaviour:
|
||||
m_FallbackScreenDPI: 96
|
||||
m_DefaultSpriteDPI: 96
|
||||
m_DynamicPixelsPerUnit: 1
|
||||
m_PresetInfoIsWorld: 0
|
||||
m_PresetInfoIsWorld: 1
|
||||
--- !u!114 &4150137515110638401
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@@ -732,9 +732,6 @@ MonoBehaviour:
|
||||
<CeilingShotInterval>k__BackingField: 0.2
|
||||
<WebWarningDelay>k__BackingField: 0.8
|
||||
<WebDuration>k__BackingField: 10
|
||||
<WebAvoidanceRadius>k__BackingField: 2.5
|
||||
warningMarkerPrefab: {fileID: 780381182997121746, guid: ad8909b4e4ba9424794a9e9054ea9e43, type: 3}
|
||||
webPrefab: {fileID: 7127456385706922585, guid: 6cee7cb816e34c1478e31e98e258a22a, type: 3}
|
||||
--- !u!195 &139010308
|
||||
NavMeshAgent:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -817,6 +814,21 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: d2cd755b0c100204e833df85acb412e8, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::BossDebugController
|
||||
--- !u!114 &139010321
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 139010306}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: aef8ddafc1c1d9e448ca7631fa034d60, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::BossWebSpawner
|
||||
warningMarkerPrefab: {fileID: 780381182997121746, guid: ad8909b4e4ba9424794a9e9054ea9e43, type: 3}
|
||||
webPrefab: {fileID: 7127456385706922585, guid: 6cee7cb816e34c1478e31e98e258a22a, type: 3}
|
||||
<WebAvoidanceRadius>k__BackingField: 2.5
|
||||
--- !u!1 &142265989
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -4447,6 +4459,9 @@ PrefabInstance:
|
||||
- targetCorrespondingSourceObject: {fileID: 1987720806421918, guid: a2dfe588a42016e40892b72d31fd1cf6, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 139010320}
|
||||
- targetCorrespondingSourceObject: {fileID: 1987720806421918, guid: a2dfe588a42016e40892b72d31fd1cf6, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 139010321}
|
||||
- targetCorrespondingSourceObject: {fileID: 1388461240955602, guid: a2dfe588a42016e40892b72d31fd1cf6, type: 3}
|
||||
insertIndex: -1
|
||||
addedObject: {fileID: 50675083}
|
||||
|
||||
@@ -756,7 +756,7 @@ GameObject:
|
||||
- component: {fileID: 119118690}
|
||||
- component: {fileID: 119118689}
|
||||
- component: {fileID: 119118688}
|
||||
- component: {fileID: 119118691}
|
||||
- component: {fileID: 119118692}
|
||||
m_Layer: 5
|
||||
m_Name: Button (Legacy)
|
||||
m_TagString: Untagged
|
||||
@@ -866,18 +866,27 @@ CanvasRenderer:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 119118686}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &119118691
|
||||
MonoBehaviour:
|
||||
--- !u!65 &119118692
|
||||
BoxCollider:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 119118686}
|
||||
m_Material: {fileID: 0}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_LayerOverridePriority: 0
|
||||
m_IsTrigger: 0
|
||||
m_ProvidesContacts: 0
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bca5237dce7df3543ba5a9c686d545d6, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::VRButtonTest
|
||||
serializedVersion: 3
|
||||
m_Size: {x: 1, y: 1, z: 1}
|
||||
m_Center: {x: 0, y: 0, z: 0}
|
||||
--- !u!1001 &130511407
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -14741,7 +14750,7 @@ GameObject:
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
m_IsActive: 0
|
||||
--- !u!224 &591090780
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -646636,6 +646645,7 @@ GameObject:
|
||||
m_Component:
|
||||
- component: {fileID: 1455781481}
|
||||
- component: {fileID: 1455781480}
|
||||
- component: {fileID: 1455781482}
|
||||
m_Layer: 0
|
||||
m_Name: PlayerManager
|
||||
m_TagString: Untagged
|
||||
@@ -646655,7 +646665,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 0215a75a0f67e1b49a1b823cfc7b8503, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::PlayerStatusManager
|
||||
ignoreGaugeForDebug: 1
|
||||
ignoreGaugeForDebug: 0
|
||||
zlButtonAction:
|
||||
m_UseReference: 1
|
||||
m_Action:
|
||||
@@ -646716,12 +646726,13 @@ MonoBehaviour:
|
||||
maxGauge: 10
|
||||
currentGauge: 0
|
||||
codingModeUI: {fileID: 797096988}
|
||||
buffDuration: 10
|
||||
attackBuffMultiplier: 1.5
|
||||
defenseBuffMultiplier: 0.5
|
||||
hpHealAmount: 30
|
||||
successEffectPrefab: {fileID: 3075638675573572061, guid: 59334a8a54b4cbc48996b04618ef137c, type: 3}
|
||||
effectSpawnPoint: {fileID: 797096992}
|
||||
buffHandler:
|
||||
buffDuration: 10
|
||||
attackBuffMultiplier: 1.5
|
||||
defenseBuffMultiplier: 0.5
|
||||
hpHealAmount: 30
|
||||
--- !u!4 &1455781481
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -646737,6 +646748,18 @@ Transform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &1455781482
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1455781479}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 6f00095a3e63cc440be35394ad49b505, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::WorldPauseManager
|
||||
--- !u!1001 &1455907610
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -656775,7 +656798,8 @@ PrefabInstance:
|
||||
propertyPath: m_LocalEulerAnglesHint.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
m_RemovedComponents: []
|
||||
m_RemovedComponents:
|
||||
- {fileID: 3150725272858075539, guid: 469b8fdf008b844589dcc60b237284c5, type: 3}
|
||||
m_RemovedGameObjects: []
|
||||
m_AddedGameObjects: []
|
||||
m_AddedComponents: []
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
using System.Collections.Generic;
|
||||
|
||||
[RequireComponent(typeof(NavMeshAgent))]
|
||||
[RequireComponent(typeof(Animator))]
|
||||
public class BossController : MonoBehaviour, IDamageble
|
||||
[RequireComponent(typeof(BossWebSpawner))]
|
||||
public class BossController : MonoBehaviour, IDamageable, IPauseable
|
||||
{
|
||||
[Header("ステータス")]
|
||||
[SerializeField] private float maxHp = 500f;
|
||||
[SerializeField] private float currentHp = 500f;
|
||||
|
||||
[field: Header("移動・攻撃パラメータ")]
|
||||
[field: SerializeField] public float ChaseRange { get; private set; } = 15f;
|
||||
[field: SerializeField] public float BiteRange { get; private set; } = 5f;
|
||||
|
||||
[field: SerializeField] public float ChargeSpeed { get; private set; } = 14f;
|
||||
[field: SerializeField] public float ChargeDuration { get; private set; } = 2f;
|
||||
[field: SerializeField] public float ChargeMinimumDistance { get; private set; } = 6f;
|
||||
[field: SerializeField] public float ChargeCooldownDuration { get; private set; } = 4f;
|
||||
[field: SerializeField] public float ChargeRoarDuration { get; private set; } = 5f;
|
||||
|
||||
[SerializeField] public Transform ceilingAnchor;
|
||||
[SerializeField] public Transform groundAnchor;
|
||||
[Header("アンカー・天井技設定")]
|
||||
public Transform ceilingAnchor;
|
||||
public Transform groundAnchor;
|
||||
[field: SerializeField] public float AttackRadius { get; private set; } = 15f;
|
||||
[field: SerializeField] public float JumpRoarDuration { get; private set; } = 2f;
|
||||
[field: SerializeField] public float CeilingAttackDuration { get; private set; } = 8.0f;
|
||||
@@ -26,33 +31,28 @@ public class BossController : MonoBehaviour, IDamageble
|
||||
[field: SerializeField] public float WebWarningDelay { get; private set; } = 0.8f;
|
||||
[field: SerializeField] public float WebDuration { get; private set; } = 3.0f;
|
||||
|
||||
[Tooltip("★新設:すでに糸がある場所から、最低何メートル離すか(被り防止の半径)")]
|
||||
[field: SerializeField] public float WebAvoidanceRadius { get; private set; } = 2.5f;
|
||||
|
||||
// ★新設:現在ステージ上に存在している(または予兆がある)糸の座標リスト
|
||||
public List<Vector3> ActiveWebPositions { get; private set; } = new List<Vector3>();
|
||||
[SerializeField] private GameObject warningMarkerPrefab;
|
||||
[SerializeField] private GameObject webPrefab;
|
||||
|
||||
public NavMeshAgent Agent { get; private set; }
|
||||
public Animator Anim { get; private set; }
|
||||
public Transform Player { get; private set; }
|
||||
public float ChargeCooldownTimer { get; private set; } = 0f;
|
||||
public BossWebSpawner WebSpawner { get; private set; }
|
||||
|
||||
public float ChargeCooldownTimer { get; private set; }
|
||||
public float NormalSpeed { get; private set; }
|
||||
public float NormalAcceleration { get; private set; }
|
||||
public bool PendingUltimate { get; set; }
|
||||
|
||||
private ISpiderState currentState;
|
||||
private Dictionary<System.Type, ISpiderState> stateCache;
|
||||
private Dictionary<Type, ISpiderState> stateCache;
|
||||
|
||||
// フェーズトリガー
|
||||
private bool triggered50 = false;
|
||||
private bool triggered25 = false;
|
||||
public bool PendingUltimate { get; set; } = false;
|
||||
private bool triggered50;
|
||||
private bool triggered25;
|
||||
private bool isPaused;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Agent = GetComponent<NavMeshAgent>();
|
||||
Anim = GetComponent<Animator>();
|
||||
WebSpawner = GetComponent<BossWebSpawner>();
|
||||
|
||||
GameObject pObj = GameObject.FindGameObjectWithTag("Player");
|
||||
if (pObj != null) Player = pObj.transform;
|
||||
@@ -67,11 +67,44 @@ public class BossController : MonoBehaviour, IDamageble
|
||||
NormalSpeed = Agent.speed;
|
||||
NormalAcceleration = Agent.acceleration;
|
||||
}
|
||||
|
||||
if (WorldPauseManager.Instance != null)
|
||||
{
|
||||
WorldPauseManager.Instance.Register(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (WorldPauseManager.Instance != null)
|
||||
{
|
||||
WorldPauseManager.Instance.Unregister(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void Pause()
|
||||
{
|
||||
isPaused = true;
|
||||
if (Anim != null) Anim.speed = 0f;
|
||||
if (Agent != null && Agent.enabled && Agent.isOnNavMesh)
|
||||
{
|
||||
Agent.isStopped = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Resume()
|
||||
{
|
||||
isPaused = false;
|
||||
if (Anim != null) Anim.speed = 1f;
|
||||
if (Agent != null && Agent.enabled && Agent.isOnNavMesh)
|
||||
{
|
||||
Agent.isStopped = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeFSM()
|
||||
{
|
||||
stateCache = new Dictionary<System.Type, ISpiderState>
|
||||
stateCache = new Dictionary<Type, ISpiderState>
|
||||
{
|
||||
{ typeof(SpiderIdleState), new SpiderIdleState(this) },
|
||||
{ typeof(SpiderChaseState), new SpiderChaseState(this) },
|
||||
@@ -87,6 +120,8 @@ public class BossController : MonoBehaviour, IDamageble
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (isPaused) return;
|
||||
|
||||
currentState?.UpdateState();
|
||||
|
||||
if (ChargeCooldownTimer > 0f)
|
||||
@@ -97,26 +132,18 @@ public class BossController : MonoBehaviour, IDamageble
|
||||
|
||||
public void ChangeState<T>() where T : ISpiderState
|
||||
{
|
||||
if (currentState != null)
|
||||
{
|
||||
currentState.ExitState();
|
||||
ResetAllAnimationTriggers();
|
||||
Debug.Log($"🕷️ BossSpider [Exit]: {currentState.GetType().Name}");
|
||||
}
|
||||
currentState?.ExitState();
|
||||
ResetAllAnimationTriggers();
|
||||
|
||||
currentState = stateCache[typeof(T)];
|
||||
Debug.Log($"🕷️ BossSpider [Enter]: {currentState.GetType().Name}");
|
||||
currentState.EnterState();
|
||||
}
|
||||
|
||||
public void TakeDamage(DamageInfo damageInfo)
|
||||
{
|
||||
currentHp -= damageInfo.amount;
|
||||
currentHp = Mathf.Clamp(currentHp, 0, maxHp);
|
||||
currentHp = Mathf.Clamp(currentHp - damageInfo.amount, 0, maxHp);
|
||||
float hpPercentage = (currentHp / maxHp) * 100f;
|
||||
|
||||
Debug.Log($"💥 ボスに {damageInfo.amount} ダメージ。残りHP: {hpPercentage:F1}%");
|
||||
|
||||
if (hpPercentage <= 25f && !triggered25)
|
||||
{
|
||||
triggered25 = true;
|
||||
@@ -143,7 +170,7 @@ public class BossController : MonoBehaviour, IDamageble
|
||||
|
||||
public bool TriggerParryStun()
|
||||
{
|
||||
if (currentState is SpiderBiteState || currentState is SpiderChargeState)
|
||||
if (currentState is SpiderBiteState or SpiderChargeState)
|
||||
{
|
||||
ChangeState<SpiderStunState>();
|
||||
return true;
|
||||
@@ -151,49 +178,7 @@ public class BossController : MonoBehaviour, IDamageble
|
||||
return false;
|
||||
}
|
||||
|
||||
public void ResetChargeCooldown()
|
||||
{
|
||||
ChargeCooldownTimer = ChargeCooldownDuration;
|
||||
}
|
||||
|
||||
public void SpawnWarningAndWeb(Vector3 targetGroundPos, float delay, float webDuration)
|
||||
{
|
||||
StartCoroutine(SpawnWebRoutine(targetGroundPos, delay, webDuration));
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator SpawnWebRoutine(Vector3 pos, float delay, float duration)
|
||||
{
|
||||
// 1. 生成する座標をアクティブリストに登録(この場所への他の割り込みを禁止する)
|
||||
ActiveWebPositions.Add(pos);
|
||||
|
||||
// 2. 地面に予兆マークを生成
|
||||
GameObject marker = null;
|
||||
if (warningMarkerPrefab != null)
|
||||
{
|
||||
marker = Instantiate(warningMarkerPrefab, pos + new Vector3(0, 0.5f, 0), Quaternion.identity);
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(delay);
|
||||
if (marker != null) Destroy(marker);
|
||||
|
||||
// 3. 本物の糸オブジェクトを生成(X軸90度回転)
|
||||
if (webPrefab != null)
|
||||
{
|
||||
Quaternion webRotation = Quaternion.Euler(270f, 0f, 0f);
|
||||
GameObject web = Instantiate(webPrefab, pos + new Vector3(0, 0.5f, 0), webRotation);
|
||||
|
||||
// ★持続時間(duration)が終わるまで待機してからDestroyする形に変更
|
||||
yield return new WaitForSeconds(duration);
|
||||
if (web != null) Destroy(web);
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new WaitForSeconds(duration);
|
||||
}
|
||||
|
||||
// 4. 糸が消滅したので、リストから座標を削除してスペースを解放する
|
||||
ActiveWebPositions.Remove(pos);
|
||||
}
|
||||
public void ResetChargeCooldown() => ChargeCooldownTimer = ChargeCooldownDuration;
|
||||
|
||||
public void ResetAllAnimationTriggers()
|
||||
{
|
||||
@@ -206,46 +191,11 @@ public class BossController : MonoBehaviour, IDamageble
|
||||
Anim.ResetTrigger("StunStart");
|
||||
}
|
||||
|
||||
#region デバッグ用機能
|
||||
/// <summary>
|
||||
/// Unityインスペクターの右上メニューから実行できるデバッグ用コマンドです
|
||||
/// </summary>
|
||||
[ContextMenu("🛠️ デバッグ:強制必殺技(天井ジャンプ)")]
|
||||
[ContextMenu("🛠️ デバッグ:強制必殺技")]
|
||||
public void DebugTriggerUltimate()
|
||||
{
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
Debug.LogWarning("⚠️ ゲームが再生中(Playモード)の時のみ実行可能です。");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log("🛠️ 【デバッグ操作】必殺技の予約券を発行し、強制的に天井ジャンプステートへ遷移します。");
|
||||
|
||||
// 必殺技の発動権をONにし、現在の行動をキャンセルして天井ジャンプへ移行
|
||||
if (!Application.isPlaying) return;
|
||||
PendingUltimate = true;
|
||||
ChangeState<SpiderJumpToCeilingState>();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// 共通データ(boss参照)を自動保持し、各子ステートの無駄なコードを極限まで削ります。
|
||||
public abstract class SpiderStateBase : ISpiderState
|
||||
{
|
||||
protected readonly BossController boss;
|
||||
|
||||
protected SpiderStateBase(BossController owner)
|
||||
{
|
||||
boss = owner;
|
||||
}
|
||||
|
||||
public virtual void EnterState() { }
|
||||
public abstract void UpdateState();
|
||||
public virtual void ExitState() { }
|
||||
}
|
||||
|
||||
public interface ISpiderState
|
||||
{
|
||||
void EnterState();
|
||||
void UpdateState();
|
||||
void ExitState();
|
||||
}
|
||||
81
Assets/_Scripts/BossWebSpawner.cs
Normal file
81
Assets/_Scripts/BossWebSpawner.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
2
Assets/_Scripts/BossWebSpawner.cs.meta
Normal file
2
Assets/_Scripts/BossWebSpawner.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aef8ddafc1c1d9e448ca7631fa034d60
|
||||
54
Assets/_Scripts/BuffHandler.cs
Normal file
54
Assets/_Scripts/BuffHandler.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
2
Assets/_Scripts/BuffHandler.cs.meta
Normal file
2
Assets/_Scripts/BuffHandler.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3db55a74838ece4418651e3d28d097cf
|
||||
@@ -1,37 +1,27 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.XR;
|
||||
using System.Collections;
|
||||
|
||||
public class ElevatorSceneTransition : MonoBehaviour
|
||||
{
|
||||
[Header("遷移先の設定")]
|
||||
[Header("シーン設定")]
|
||||
[SerializeField] private string bossSceneName = "BossStage";
|
||||
[SerializeField] private string playerTag = "Player";
|
||||
|
||||
[Header("ローディングUI設定")]
|
||||
[Header("UI設定")]
|
||||
[SerializeField] private GameObject loadingCanvas;
|
||||
[SerializeField] private Text progressText;
|
||||
[SerializeField] private Slider progressSlider;
|
||||
|
||||
[Header("エレベーター下降演出設定")]
|
||||
[Tooltip("エレベーターの部屋(壁や床、UIすべて)をまとめているオブジェクトを指定してください(※プレイヤーは外しておきます)")]
|
||||
[Header("演出設定")]
|
||||
[SerializeField] private Transform elevatorCageTransform;
|
||||
|
||||
[Tooltip("ガタガタ揺れる激しさ(0.01~0.03あたりが自然です)")]
|
||||
[SerializeField] private float shakeMagnitude = 0.015f;
|
||||
|
||||
[Tooltip("下降中のコントローラーの振動の強さ(0.0 ~ 1.0)")]
|
||||
[SerializeField] private float hapticAmplitude = 0.15f;
|
||||
|
||||
// ─── ★新設:親子関係にしないためのプレイヤー登録枠 ───
|
||||
[Header("プレイヤーの同期設定")]
|
||||
[Tooltip("XR Origin(またはXR Rig)のTransformを直接ドラッグ&ドロップしてください")]
|
||||
[SerializeField] private Transform playerTransform;
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
private bool isLoadingStarted = false;
|
||||
private bool isLoadingStarted;
|
||||
private Vector3 originalCagePosition;
|
||||
|
||||
private void Start()
|
||||
@@ -40,7 +30,6 @@ public class ElevatorSceneTransition : MonoBehaviour
|
||||
|
||||
if (elevatorCageTransform != null)
|
||||
{
|
||||
// ワールド空間での初期位置を記憶(localPositionからpositionに変更し、より確実に)
|
||||
originalCagePosition = elevatorCageTransform.position;
|
||||
}
|
||||
}
|
||||
@@ -53,13 +42,11 @@ public class ElevatorSceneTransition : MonoBehaviour
|
||||
{
|
||||
isLoadingStarted = true;
|
||||
|
||||
// ★安全対策:接触したプレイヤーのTransformを自動でキャッシュする
|
||||
if (playerTransform == null)
|
||||
{
|
||||
playerTransform = other.transform;
|
||||
}
|
||||
|
||||
Debug.Log("🛗 プレイヤー乗車:親子関係なしのデルタ同期で下降演出を開始します。");
|
||||
StartCoroutine(LoadSceneAsyncRoutine());
|
||||
}
|
||||
}
|
||||
@@ -79,28 +66,21 @@ public class ElevatorSceneTransition : MonoBehaviour
|
||||
if (progressText != null) progressText.text = $"LOADING: {(currentProgress * 100f):F0}%";
|
||||
if (progressSlider != null) progressSlider.value = currentProgress;
|
||||
|
||||
// ─── ★【超重要】親子関係なしでプレイヤーを完全同期させるロジック ───
|
||||
|
||||
// 1. 動かす前の、このフレームにおけるエレベーターの現在位置を記録
|
||||
Vector3 previousCagePosition = elevatorCageTransform.position;
|
||||
|
||||
// 2. エレベーターの部屋だけをランダムにガタガタ動かす
|
||||
if (elevatorCageTransform != null)
|
||||
{
|
||||
Vector3 randomOffset = Random.insideUnitSphere * shakeMagnitude;
|
||||
randomOffset.y = 0; // 横揺れのみ
|
||||
randomOffset.y = 0;
|
||||
elevatorCageTransform.position = originalCagePosition + randomOffset;
|
||||
}
|
||||
|
||||
// 3. 「この1フレームでエレベーターが実際に動いた移動差分(デルタ)」を計算
|
||||
Vector3 cageDelta = elevatorCageTransform.position - previousCagePosition;
|
||||
|
||||
// 4. その移動差分を、プレイヤーの座標にもそのまま上乗せして足し算する
|
||||
if (playerTransform != null && cageDelta != Vector3.zero)
|
||||
{
|
||||
playerTransform.position += cageDelta;
|
||||
}
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
|
||||
hapticTimer += Time.deltaTime;
|
||||
if (hapticTimer >= 0.1f)
|
||||
@@ -112,10 +92,8 @@ public class ElevatorSceneTransition : MonoBehaviour
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// 到着後、エレベーターの位置を元の位置に綺麗に戻す
|
||||
if (elevatorCageTransform != null)
|
||||
{
|
||||
// 最後の移動差分を計算してプレイヤーを戻す
|
||||
Vector3 previousCagePosition = elevatorCageTransform.position;
|
||||
elevatorCageTransform.position = originalCagePosition;
|
||||
Vector3 cageDelta = elevatorCageTransform.position - previousCagePosition;
|
||||
@@ -127,7 +105,6 @@ public class ElevatorSceneTransition : MonoBehaviour
|
||||
|
||||
yield return new WaitForSeconds(1.0f);
|
||||
|
||||
Debug.Log("🎉 ボスステージに到着。シーンを切り替えます。");
|
||||
asyncLoad.allowSceneActivation = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
using System.Collections;
|
||||
using System;
|
||||
|
||||
[RequireComponent(typeof(NavMeshAgent))]
|
||||
[RequireComponent(typeof(Animator))]
|
||||
public class EnemyAI : MonoBehaviour
|
||||
public class EnemyAI : MonoBehaviour, IPauseable
|
||||
{
|
||||
/// 敵AIの状態を表すステート
|
||||
public enum AIState
|
||||
{
|
||||
Wander, // 徘徊
|
||||
Chase, // 追跡
|
||||
Attack, // 攻撃
|
||||
Stun // 怯み
|
||||
}
|
||||
public enum AIState { Wander, Chase, Attack, Stun }
|
||||
|
||||
[Header("現在の状態")]
|
||||
[SerializeField] private AIState currentState = AIState.Wander;
|
||||
@@ -25,21 +17,14 @@ public class EnemyAI : MonoBehaviour
|
||||
[SerializeField] private string playerTag = "Player";
|
||||
|
||||
[Header("攻撃設定")]
|
||||
[Tooltip("この距離に近づいたら攻撃 (メートル)")]
|
||||
[SerializeField] private float attackRange = 1.5f;
|
||||
[Tooltip("パンチの攻撃力")]
|
||||
[SerializeField] private float attackDamage = 10f;
|
||||
[Tooltip("一度攻撃が終了してから、次の攻撃を開始するまでの待ち時間 (秒)")]
|
||||
[SerializeField] private float attackCooldown = 2.0f;
|
||||
[Tooltip("攻撃の予兆時間 (秒)")]
|
||||
[SerializeField] private float telegraphDuration = 0.6f;
|
||||
[SerializeField] private Collider rightHandCollider;
|
||||
|
||||
[Header("ループ攻撃の調整")]
|
||||
[Tooltip("連続攻撃の間に挟む最低限のインターバル (秒)")]
|
||||
[SerializeField] private float comboInterval = 0.2f;
|
||||
|
||||
[Header("徘徊(Wander)設定")]
|
||||
[Header("徘徊設定")]
|
||||
[SerializeField] private float wanderRadius = 8f;
|
||||
[SerializeField] private float minWaitTime = 2f;
|
||||
[SerializeField] private float maxWaitTime = 4f;
|
||||
@@ -49,116 +34,142 @@ public class EnemyAI : MonoBehaviour
|
||||
private Animator anim;
|
||||
private Transform playerTransform;
|
||||
|
||||
private bool isCooldown = false;
|
||||
private bool isAttacking = false;
|
||||
private bool isWandering = false;
|
||||
private bool isKnockbacking = false;
|
||||
private bool isDead = false;
|
||||
private bool isCooldown;
|
||||
private bool isAttacking;
|
||||
private bool isWandering;
|
||||
private bool isKnockbacking;
|
||||
private bool isDead;
|
||||
private bool isPaused;
|
||||
|
||||
private void Start()
|
||||
private static readonly int SpeedHash = Animator.StringToHash("Speed");
|
||||
private static readonly int StunHash = Animator.StringToHash("Stun");
|
||||
private static readonly int HitHash = Animator.StringToHash("Hit");
|
||||
private static readonly int TelegraphHash = Animator.StringToHash("Telegraph");
|
||||
private static readonly int AttackHash = Animator.StringToHash("Attack");
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
agent = GetComponent<NavMeshAgent>();
|
||||
anim = GetComponent<Animator>();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
SetFistCollider(false);
|
||||
|
||||
// AIのメインループコルーチンを開始
|
||||
if (WorldPauseManager.Instance != null)
|
||||
{
|
||||
WorldPauseManager.Instance.Register(this);
|
||||
}
|
||||
StartCoroutine(AILoop());
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (WorldPauseManager.Instance != null)
|
||||
{
|
||||
WorldPauseManager.Instance.Unregister(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void Pause()
|
||||
{
|
||||
isPaused = true;
|
||||
if (anim != null) anim.speed = 0f;
|
||||
if (agent != null && agent.enabled && agent.isOnNavMesh)
|
||||
{
|
||||
agent.isStopped = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Resume()
|
||||
{
|
||||
isPaused = false;
|
||||
if (anim != null) anim.speed = 1f;
|
||||
if (agent != null && agent.enabled && agent.isOnNavMesh && !isDead && !isKnockbacking && currentState != AIState.Stun)
|
||||
{
|
||||
agent.isStopped = false;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator WaitForUnpausedSeconds(float duration)
|
||||
{
|
||||
float timer = 0f;
|
||||
while (timer < duration)
|
||||
{
|
||||
if (!isPaused)
|
||||
{
|
||||
timer += Time.deltaTime;
|
||||
}
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if(isDead) return;
|
||||
if (isDead || isPaused) return;
|
||||
|
||||
// 毎フレームのアニメーションパラメータ更新処理
|
||||
UpdateAnimation();
|
||||
|
||||
// 攻撃ステートにおけるプレイヤーへの方向転換処理
|
||||
UpdateRotationOnAttack();
|
||||
}
|
||||
|
||||
#region AI メインループ (思考ロジック)
|
||||
private IEnumerator AILoop()
|
||||
{
|
||||
while (true)
|
||||
while (!isDead)
|
||||
{
|
||||
if(isDead) yield break;
|
||||
|
||||
// プレイヤーが未検出の場合はタグから検索
|
||||
if (playerTransform == null)
|
||||
if (isPaused)
|
||||
{
|
||||
FindPlayer();
|
||||
yield return null;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 攻撃、のけぞりアニメーションの再生中でない場合のみ、ステートに応じた行動を実行
|
||||
if (playerTransform == null) FindPlayer();
|
||||
|
||||
if (!isAttacking && !isKnockbacking && currentState != AIState.Stun)
|
||||
{
|
||||
switch (currentState)
|
||||
{
|
||||
case AIState.Wander:
|
||||
WanderBehavior();
|
||||
break;
|
||||
case AIState.Chase:
|
||||
ChaseBehavior();
|
||||
break;
|
||||
case AIState.Attack:
|
||||
AttackBehavior();
|
||||
break;
|
||||
case AIState.Wander: WanderBehavior(); break;
|
||||
case AIState.Chase: ChaseBehavior(); break;
|
||||
case AIState.Attack: AttackBehavior(); break;
|
||||
}
|
||||
}
|
||||
|
||||
// 毎フレーム実行を避け、0.2秒待機することでCPU負荷を軽減
|
||||
yield return new WaitForSeconds(0.2f);
|
||||
yield return WaitForUnpausedSeconds(0.2f);
|
||||
}
|
||||
}
|
||||
|
||||
private void FindPlayer()
|
||||
{
|
||||
GameObject playerObj = GameObject.FindGameObjectWithTag(playerTag);
|
||||
if (playerObj != null)
|
||||
{
|
||||
playerTransform = playerObj.transform;
|
||||
}
|
||||
if (playerObj != null) playerTransform = playerObj.transform;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 各ステートの固有行動
|
||||
/// 【徘徊状態】の思考ロジック
|
||||
private void WanderBehavior()
|
||||
{
|
||||
if (playerTransform != null)
|
||||
{
|
||||
// プレイヤーとの距離を測定し、索敵範囲内なら追跡ステートへ遷移
|
||||
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
|
||||
if (distanceToPlayer <= detectionRange)
|
||||
{
|
||||
currentState = AIState.Chase;
|
||||
isWandering = false;
|
||||
agent.ResetPath();
|
||||
Debug.Log($"👁️ {gameObject.name}: プレイヤーを発見! 追跡します。");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// すでに徘徊移動中でなければ、新しい目的地への移動コルーチンを開始
|
||||
if (!isWandering)
|
||||
{
|
||||
StartCoroutine(WanderMoveRoutine());
|
||||
}
|
||||
}
|
||||
|
||||
/// 【追跡状態】の思考ロジック
|
||||
private void ChaseBehavior()
|
||||
{
|
||||
if (playerTransform == null)
|
||||
{
|
||||
currentState = AIState.Wander;
|
||||
return;
|
||||
}
|
||||
if (playerTransform == null) { currentState = AIState.Wander; return; }
|
||||
|
||||
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
|
||||
|
||||
// 攻撃射程内に入ったら攻撃ステートへ遷移し、その場に停止
|
||||
if (distanceToPlayer <= attackRange)
|
||||
{
|
||||
currentState = AIState.Attack;
|
||||
@@ -166,106 +177,77 @@ public class EnemyAI : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
// プレイヤーが見失う範囲を超えたら徘徊ステートにリセット
|
||||
if (distanceToPlayer > loseRange)
|
||||
{
|
||||
currentState = AIState.Wander;
|
||||
agent.ResetPath();
|
||||
Debug.Log($"❓ {gameObject.name}: プレイヤーを見失った。");
|
||||
return;
|
||||
}
|
||||
|
||||
// のけぞり中でない場合、追跡の目的地を更新する
|
||||
if(!isKnockbacking)
|
||||
if (!isKnockbacking)
|
||||
{
|
||||
agent.SetDestination(playerTransform.position);
|
||||
}
|
||||
}
|
||||
|
||||
/// 【攻撃状態】の思考ロジック
|
||||
private void AttackBehavior()
|
||||
{
|
||||
if (playerTransform == null)
|
||||
{
|
||||
currentState = AIState.Wander;
|
||||
return;
|
||||
}
|
||||
if (playerTransform == null) { currentState = AIState.Wander; return; }
|
||||
|
||||
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
|
||||
|
||||
// 攻撃射程から外れていたら、再び追跡ステートに戻る
|
||||
if (distanceToPlayer > attackRange)
|
||||
{
|
||||
currentState = AIState.Chase;
|
||||
return;
|
||||
}
|
||||
|
||||
// クールダウン中でなければ攻撃コルーチンを実行(クールダウン中の場合は、Update内の処理によりプレイヤーの方向を向きながら待機します)
|
||||
if (!isCooldown)
|
||||
{
|
||||
StartCoroutine(PerformPunchAttack());
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region アニメーション・回転の制御メソッド
|
||||
/// エージェントの移動状態に応じてAnimatorのSpeedパラメータを更新する
|
||||
private void UpdateAnimation()
|
||||
{
|
||||
float targetSpeed = 0f;
|
||||
|
||||
// 攻撃中でなく、経路が存在し、かつ実際に一定以上の速度で移動している場合のみSpeedを1にする
|
||||
if (!isAttacking && !isKnockbacking && currentState != AIState.Stun && agent.hasPath && agent.velocity.magnitude > 0.1f)
|
||||
{
|
||||
targetSpeed = 1f;
|
||||
}
|
||||
|
||||
// パラメータをなめらかに変化させて渡す
|
||||
anim.SetFloat("Speed", targetSpeed, 0.1f, Time.deltaTime);
|
||||
anim.SetFloat(SpeedHash, targetSpeed, 0.1f, Time.deltaTime);
|
||||
}
|
||||
|
||||
/// 攻撃ステートの際、プレイヤーの方向へ滑らかに旋回させる
|
||||
private void UpdateRotationOnAttack()
|
||||
{
|
||||
if (currentState == AIState.Attack && playerTransform != null && !isKnockbacking && currentState != AIState.Stun)
|
||||
{
|
||||
Vector3 direction = (playerTransform.position - transform.position).normalized;
|
||||
direction.y = 0; // 上下方向の傾き(ピッチ回転)を無視
|
||||
direction.y = 0;
|
||||
|
||||
if (direction != Vector3.zero)
|
||||
{
|
||||
// Y軸の回転のみを滑らかに補間して適用
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), Time.deltaTime * 10f);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
// ダメージを受けた際に、EnemyhHealthから呼び出されるのけぞり開始メソッド
|
||||
public void TriggerKnockback(float duration)
|
||||
{
|
||||
if(isDead) return;
|
||||
|
||||
if(currentState == AIState.Stun) return;
|
||||
if (isDead || currentState == AIState.Stun) return;
|
||||
|
||||
SetFistCollider(false);
|
||||
StopAllCoroutines();
|
||||
isAttacking = false;
|
||||
isWandering = false;
|
||||
|
||||
if(agent.isOnNavMesh)
|
||||
{
|
||||
agent.isStopped = false;
|
||||
}
|
||||
|
||||
if (agent.isOnNavMesh) agent.isStopped = false;
|
||||
StartCoroutine(KnockbackRoutine(duration));
|
||||
}
|
||||
|
||||
public void TriggerParryStun(float duration)
|
||||
{
|
||||
if(isDead) return;
|
||||
|
||||
Debug.Log($"{gameObject.name}: parry = true Stunnow");
|
||||
if (isDead) return;
|
||||
|
||||
SetFistCollider(false);
|
||||
StopAllCoroutines();
|
||||
@@ -280,7 +262,7 @@ public class EnemyAI : MonoBehaviour
|
||||
{
|
||||
isDead = true;
|
||||
StopAllCoroutines();
|
||||
if(agent != null && agent.isOnNavMesh)
|
||||
if (agent != null && agent.isOnNavMesh)
|
||||
{
|
||||
agent.isStopped = true;
|
||||
agent.ResetPath();
|
||||
@@ -291,24 +273,19 @@ public class EnemyAI : MonoBehaviour
|
||||
{
|
||||
currentState = AIState.Stun;
|
||||
|
||||
if(agent.isOnNavMesh)
|
||||
if (agent.isOnNavMesh)
|
||||
{
|
||||
agent.isStopped = true;
|
||||
agent.ResetPath();
|
||||
}
|
||||
|
||||
anim.SetTrigger("Stun");
|
||||
anim.SetTrigger(StunHash);
|
||||
yield return WaitForUnpausedSeconds(duration);
|
||||
|
||||
yield return new WaitForSeconds(duration);
|
||||
|
||||
if(agent.isOnNavMesh)
|
||||
{
|
||||
agent.isStopped = false;
|
||||
}
|
||||
if (agent.isOnNavMesh) agent.isStopped = false;
|
||||
|
||||
currentState = AIState.Chase;
|
||||
isCooldown = false;
|
||||
|
||||
StartCoroutine(AILoop());
|
||||
}
|
||||
|
||||
@@ -316,65 +293,48 @@ public class EnemyAI : MonoBehaviour
|
||||
{
|
||||
isKnockbacking = true;
|
||||
|
||||
if(agent.isOnNavMesh)
|
||||
if (agent.isOnNavMesh)
|
||||
{
|
||||
agent.isStopped = true;
|
||||
agent.ResetPath();
|
||||
}
|
||||
|
||||
anim.SetTrigger("Hit");
|
||||
anim.SetTrigger(HitHash);
|
||||
yield return WaitForUnpausedSeconds(duration);
|
||||
|
||||
yield return new WaitForSeconds(duration);
|
||||
|
||||
if(agent.isOnNavMesh)
|
||||
{
|
||||
agent.isStopped = false;
|
||||
}
|
||||
if (agent.isOnNavMesh) agent.isStopped = false;
|
||||
|
||||
isKnockbacking = false;
|
||||
isCooldown = false;
|
||||
|
||||
StartCoroutine(AILoop());
|
||||
}
|
||||
|
||||
#region 攻撃・移動のコルーチン処理
|
||||
/// 徘徊時のランダム移動を制御するコルーチン
|
||||
private IEnumerator WanderMoveRoutine()
|
||||
{
|
||||
isWandering = true;
|
||||
Vector3 newPos = Vector3.zero;
|
||||
bool isPathValid = false;
|
||||
int maxAttempts = 10;
|
||||
|
||||
// 有効な経路(壁などで分断されていないルート)が見つかるまで試行
|
||||
for (int i = 0; i < maxAttempts; i++)
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
newPos = GetRandomNavMeshPoint(transform.position, wanderRadius);
|
||||
NavMeshPath path = new NavMeshPath();
|
||||
|
||||
if (agent.CalculatePath(newPos, path))
|
||||
if (agent.CalculatePath(newPos, path) && path.status == NavMeshPathStatus.PathComplete)
|
||||
{
|
||||
if (path.status == NavMeshPathStatus.PathComplete)
|
||||
{
|
||||
isPathValid = true;
|
||||
break;
|
||||
}
|
||||
isPathValid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 有効な目的地が設定できた場合の移動処理
|
||||
if (isPathValid)
|
||||
{
|
||||
agent.SetDestination(newPos);
|
||||
float elapsedTime = 0f;
|
||||
|
||||
// 目的地に到達するか、タイムアウト時間を迎えるまで待機
|
||||
while (agent.remainingDistance > agent.stoppingDistance)
|
||||
{
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
yield return WaitForUnpausedSeconds(0.5f);
|
||||
elapsedTime += 0.5f;
|
||||
|
||||
// スタック対策としてのタイムアウト判定
|
||||
if (elapsedTime >= wanderTimeout)
|
||||
{
|
||||
agent.ResetPath();
|
||||
@@ -383,57 +343,36 @@ public class EnemyAI : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// 到着または諦めた後、ランダムな時間その場で待機
|
||||
yield return new WaitForSeconds(UnityEngine.Random.Range(minWaitTime, maxWaitTime));
|
||||
yield return WaitForUnpausedSeconds(Random.Range(minWaitTime, maxWaitTime));
|
||||
isWandering = false;
|
||||
}
|
||||
|
||||
/// 予兆から攻撃アニメーションのトリガーまでを制御するコルーチン
|
||||
private IEnumerator PerformPunchAttack()
|
||||
{
|
||||
isAttacking = true;
|
||||
isCooldown = true; // ここで攻撃フラグをロックします
|
||||
isCooldown = true;
|
||||
|
||||
anim.SetTrigger("Telegraph");
|
||||
Debug.Log($"⚠️ {gameObject.name}: 攻撃の予兆(溜め)開始");
|
||||
anim.SetTrigger(TelegraphHash);
|
||||
yield return WaitForUnpausedSeconds(telegraphDuration);
|
||||
|
||||
yield return new WaitForSeconds(telegraphDuration);
|
||||
if (isKnockbacking || isDead)
|
||||
{
|
||||
isAttacking = false;
|
||||
yield break;
|
||||
}
|
||||
|
||||
if(isKnockbacking) yield break;
|
||||
|
||||
anim.SetTrigger("Attack");
|
||||
anim.SetTrigger(AttackHash);
|
||||
}
|
||||
|
||||
/// 連続攻撃成功時の、クイックなクールダウン解除処理
|
||||
private IEnumerator ResetCooldownQuickly()
|
||||
{
|
||||
yield return new WaitForSeconds(comboInterval);
|
||||
isCooldown = false;
|
||||
}
|
||||
|
||||
/// コンボが途切れた際の、通常の攻撃クールダウン解除処理
|
||||
private IEnumerator ResetCooldown()
|
||||
{
|
||||
// 指定された秒数待機する
|
||||
yield return new WaitForSeconds(attackCooldown);
|
||||
isCooldown = false;
|
||||
Debug.Log($"✅ {gameObject.name}: 攻撃クールダウンが終了しました。再攻撃可能です。");
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region アニメーションイベント(外部からのコールバック)
|
||||
/// 攻撃アニメーションのヒットフレームで実行されるヒット判定(Animation Eventから呼び出し)
|
||||
public void OnPunchHit()
|
||||
{
|
||||
if (isKnockbacking || currentState == AIState.Stun) return;
|
||||
|
||||
if (isKnockbacking || currentState == AIState.Stun || isPaused) return;
|
||||
SetFistCollider(true);
|
||||
}
|
||||
|
||||
/// Recovery(硬直)アニメーションの終了時に実行される処理(Animation Eventから呼び出し)
|
||||
public void OnRecoveryEnd()
|
||||
{
|
||||
if(isKnockbacking || currentState == AIState.Stun) return;
|
||||
if (isKnockbacking || currentState == AIState.Stun) return;
|
||||
|
||||
SetFistCollider(false);
|
||||
isAttacking = false;
|
||||
@@ -447,59 +386,51 @@ public class EnemyAI : MonoBehaviour
|
||||
|
||||
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
|
||||
|
||||
// ループ判定:まだプレイヤーが射程内にいる場合はクイックな連続攻撃へ移行
|
||||
if (distanceToPlayer <= attackRange)
|
||||
{
|
||||
StartCoroutine(ResetCooldownQuickly());
|
||||
Debug.Log($"🔄 {gameObject.name}: プレイヤーがまだ射程内です。連続攻撃(コンボ)を実行します。");
|
||||
}
|
||||
else
|
||||
{
|
||||
// プレイヤーが離れていた場合はコンボを抜け、追跡状態に戻ると同時に、通常のクールダウン(長めの待機)を開始
|
||||
currentState = AIState.Chase;
|
||||
StartCoroutine(ResetCooldown());
|
||||
Debug.Log($"🏃 {gameObject.name}: プレイヤーが離れたため追跡に戻ります。({attackCooldown}秒の攻撃クールダウンを適用)");
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ヘルパー・デバッグ用メソッド
|
||||
/// 指定された中心座標の範囲内から、NavMesh上の有効なランダム座標を取得する
|
||||
private IEnumerator ResetCooldownQuickly()
|
||||
{
|
||||
yield return WaitForUnpausedSeconds(comboInterval);
|
||||
isCooldown = false;
|
||||
}
|
||||
|
||||
private IEnumerator ResetCooldown()
|
||||
{
|
||||
yield return WaitForUnpausedSeconds(attackCooldown);
|
||||
isCooldown = false;
|
||||
}
|
||||
|
||||
private void SetFistCollider(bool enabledState)
|
||||
{
|
||||
if(rightHandCollider != null)
|
||||
{
|
||||
rightHandCollider.enabled = enabledState;
|
||||
}
|
||||
if (rightHandCollider != null) rightHandCollider.enabled = enabledState;
|
||||
}
|
||||
|
||||
private Vector3 GetRandomNavMeshPoint(Vector3 center, float radius)
|
||||
{
|
||||
Vector3 randomDirection = UnityEngine.Random.insideUnitSphere * radius;
|
||||
randomDirection += center;
|
||||
NavMeshHit hit;
|
||||
|
||||
if (NavMesh.SamplePosition(randomDirection, out hit, radius, NavMesh.AllAreas))
|
||||
Vector3 randomDirection = Random.insideUnitSphere * radius + center;
|
||||
if (NavMesh.SamplePosition(randomDirection, out NavMeshHit hit, radius, NavMesh.AllAreas))
|
||||
{
|
||||
return hit.position;
|
||||
}
|
||||
return center;
|
||||
}
|
||||
|
||||
/// エディタのSceneビューに索敵・攻撃範囲をギズモとして描画する
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
// 索敵範囲(赤)
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireSphere(transform.position, detectionRange);
|
||||
|
||||
// 見失う範囲(黄)
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawWireSphere(transform.position, loseRange);
|
||||
|
||||
// 攻撃範囲(青)
|
||||
Gizmos.color = Color.blue;
|
||||
Gizmos.DrawWireSphere(transform.position, attackRange);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -1,68 +1,74 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
public class EnemyHealth : MonoBehaviour, IDamageble
|
||||
public class EnemyHealth : MonoBehaviour, IDamageable
|
||||
{
|
||||
[SerializeField] private float maxHealth = 100f;
|
||||
private float currentHealth;
|
||||
|
||||
[SerializeField] private float knockbackDuration = 0.5f;
|
||||
|
||||
public float CurrentHealth { get; private set; }
|
||||
public bool IsDead { get; private set; }
|
||||
|
||||
public event Action<float> OnKnockbackRequested;
|
||||
public event Action OnDeath;
|
||||
|
||||
private EnemyAI enemyAI;
|
||||
private Animator anim;
|
||||
private Collider col;
|
||||
|
||||
private bool isDead = false;
|
||||
private static readonly int DeadHash = Animator.StringToHash("Dead");
|
||||
|
||||
void Start()
|
||||
private void Awake()
|
||||
{
|
||||
currentHealth = maxHealth; //HPを代入
|
||||
CurrentHealth = maxHealth;
|
||||
enemyAI = GetComponent<EnemyAI>();
|
||||
anim = GetComponent<Animator>();
|
||||
col = GetComponent<Collider>();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (enemyAI != null)
|
||||
{
|
||||
OnKnockbackRequested += enemyAI.TriggerKnockback;
|
||||
OnDeath += enemyAI.DisableAIOnDeath;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (enemyAI != null)
|
||||
{
|
||||
OnKnockbackRequested -= enemyAI.TriggerKnockback;
|
||||
OnDeath -= enemyAI.DisableAIOnDeath;
|
||||
}
|
||||
}
|
||||
|
||||
public void TakeDamage(DamageInfo damageInfo)
|
||||
{
|
||||
if(currentHealth <= 0) return;
|
||||
if (IsDead) return;
|
||||
|
||||
currentHealth -= damageInfo.amount; //ダメージ計算
|
||||
CurrentHealth = Mathf.Max(0f, CurrentHealth - damageInfo.amount);
|
||||
|
||||
if(damageInfo.isPoseBonus)
|
||||
{
|
||||
Debug.Log("構えボーナス");
|
||||
}
|
||||
|
||||
if(currentHealth <= 0)
|
||||
if (CurrentHealth <= 0f)
|
||||
{
|
||||
Die();
|
||||
}
|
||||
else
|
||||
{
|
||||
if(enemyAI != null)
|
||||
{
|
||||
enemyAI.TriggerKnockback(knockbackDuration);
|
||||
}
|
||||
OnKnockbackRequested?.Invoke(knockbackDuration);
|
||||
}
|
||||
}
|
||||
|
||||
private void Die()
|
||||
{
|
||||
if(isDead) return;
|
||||
isDead = true;
|
||||
if (IsDead) return;
|
||||
IsDead = true;
|
||||
|
||||
Animator anim = GetComponent<Animator>();
|
||||
if(anim != null)
|
||||
{
|
||||
anim.SetTrigger("Dead");
|
||||
}
|
||||
|
||||
Collider col = GetComponent<Collider>();
|
||||
if(col != null)
|
||||
{
|
||||
col.enabled = false;
|
||||
}
|
||||
|
||||
if(enemyAI != null)
|
||||
{
|
||||
enemyAI.DisableAIOnDeath();
|
||||
}
|
||||
if (anim != null) anim.SetTrigger(DeadHash);
|
||||
if (col != null) col.enabled = false;
|
||||
|
||||
OnDeath?.Invoke();
|
||||
Destroy(gameObject, 3.0f);
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,13 @@ using UnityEngine;
|
||||
|
||||
public struct DamageInfo
|
||||
{
|
||||
public float amount; //ダメージ量
|
||||
public Vector3 hitPosition; //殴られた位置
|
||||
public Vector3 punchDirection; //パンチが飛んできた方向(ノックバック用)
|
||||
public bool isPoseBonus; //構えから攻撃かどうか
|
||||
public float amount;
|
||||
public Vector3 hitPosition;
|
||||
public Vector3 punchDirection;
|
||||
public bool isPoseBonus;
|
||||
}
|
||||
public interface IDamageble
|
||||
|
||||
public interface IDamageable
|
||||
{
|
||||
void TakeDamage(DamageInfo damageInfo);
|
||||
}
|
||||
5
Assets/_Scripts/IPauseable.cs
Normal file
5
Assets/_Scripts/IPauseable.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
public interface IPauseable
|
||||
{
|
||||
void Pause();
|
||||
void Resume();
|
||||
}
|
||||
2
Assets/_Scripts/IPauseable.cs.meta
Normal file
2
Assets/_Scripts/IPauseable.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b7c4298a74840743a4bb184041b53f9
|
||||
@@ -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("押した瞬間");
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ae6f0d5ca1f0f42b6a38e38170777c16
|
||||
@@ -3,55 +3,56 @@ using UnityEngine.InputSystem;
|
||||
|
||||
public class InputManagerLR : MonoBehaviour
|
||||
{
|
||||
static InputManagerLR instance;
|
||||
private static InputManagerLR instance;
|
||||
|
||||
[SerializeField]
|
||||
InputActionAsset ActionAsset;
|
||||
[SerializeField] private InputActionAsset actionAsset;
|
||||
|
||||
InputActionMap ActionMap;
|
||||
InputAction m_PrimaryButtonR;
|
||||
InputAction m_SecondaryButtonR;
|
||||
InputAction m_PrimaryButtonL;
|
||||
InputAction m_SecondaryButtonL;
|
||||
private InputActionMap actionMap;
|
||||
private InputAction primaryButtonR;
|
||||
private InputAction secondaryButtonR;
|
||||
private InputAction primaryButtonL;
|
||||
private InputAction secondaryButtonL;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (instance != null && instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
instance = this;
|
||||
GameObject.DontDestroyOnLoad(gameObject);
|
||||
ActionMap = ActionAsset.FindActionMap("Test");
|
||||
m_PrimaryButtonR = ActionMap.FindAction("XR_PrimaryButtonR", throwIfNotFound: true);
|
||||
m_PrimaryButtonL = ActionMap.FindAction("XR_PrimaryButtonL", throwIfNotFound: true);
|
||||
m_SecondaryButtonR = ActionMap.FindAction("XR_SecondaryButtonR", throwIfNotFound: true);
|
||||
m_SecondaryButtonL = ActionMap.FindAction("XR_SecondaryButtonL", throwIfNotFound: true);
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
if (actionAsset != null)
|
||||
{
|
||||
actionMap = actionAsset.FindActionMap("Test");
|
||||
if (actionMap != null)
|
||||
{
|
||||
primaryButtonR = actionMap.FindAction("XR_PrimaryButtonR", throwIfNotFound: false);
|
||||
primaryButtonL = actionMap.FindAction("XR_PrimaryButtonL", throwIfNotFound: false);
|
||||
secondaryButtonR = actionMap.FindAction("XR_SecondaryButtonR", throwIfNotFound: false);
|
||||
secondaryButtonL = actionMap.FindAction("XR_SecondaryButtonL", throwIfNotFound: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
ActionMap?.Enable();
|
||||
}
|
||||
private void OnEnable() => actionMap?.Enable();
|
||||
private void OnDisable() => actionMap?.Disable();
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
ActionMap?.Disable();
|
||||
}
|
||||
public static bool PrimaryButtonR() => instance?.primaryButtonR?.IsPressed() ?? false;
|
||||
public static bool PrimaryButtonR_OnPress() => instance?.primaryButtonR?.WasPressedThisFrame() ?? false;
|
||||
public static bool PrimaryButtonR_OnRelease() => instance?.primaryButtonR?.WasReleasedThisFrame() ?? false;
|
||||
|
||||
// Aボタン
|
||||
public static bool PrimaryButtonR() => instance.m_PrimaryButtonR.IsPressed();
|
||||
public static bool PrimaryButtonR_OnPress() => instance.m_PrimaryButtonR.WasPressedThisFrame();
|
||||
public static bool PrimaryButtonR_OnRelease() => instance.m_PrimaryButtonR.WasReleasedThisFrame();
|
||||
public static bool PrimaryButtonL() => instance?.primaryButtonL?.IsPressed() ?? false;
|
||||
public static bool PrimaryButtonL_OnPress() => instance?.primaryButtonL?.WasPressedThisFrame() ?? false;
|
||||
public static bool PrimaryButtonL_OnRelease() => instance?.primaryButtonL?.WasReleasedThisFrame() ?? false;
|
||||
|
||||
// Xボタン
|
||||
public static bool PrimaryButtonL() => instance.m_PrimaryButtonL.IsPressed();
|
||||
public static bool PrimaryButtonL_OnPress() => instance.m_PrimaryButtonL.WasPressedThisFrame();
|
||||
public static bool PrimaryButtonL_OnRelease() => instance.m_PrimaryButtonL.WasReleasedThisFrame();
|
||||
public static bool SecondaryButtonR() => instance?.secondaryButtonR?.IsPressed() ?? false;
|
||||
public static bool SecondaryButtonR_OnPress() => instance?.secondaryButtonR?.WasPressedThisFrame() ?? false;
|
||||
public static bool SecondaryButtonR_OnRelease() => instance?.secondaryButtonR?.WasReleasedThisFrame() ?? false;
|
||||
|
||||
// Bボタン
|
||||
public static bool SecondaryButtonR() => instance.m_SecondaryButtonR.IsPressed();
|
||||
public static bool SecondaryButtonR_OnPress() => instance.m_SecondaryButtonR.WasPressedThisFrame();
|
||||
public static bool SecondaryButtonR_OnRelease() => instance.m_SecondaryButtonR.WasReleasedThisFrame();
|
||||
|
||||
// Yボタン
|
||||
public static bool SecondaryButtonL() => instance.m_SecondaryButtonL.IsPressed();
|
||||
public static bool SecondaryButtonL_OnPress() => instance.m_SecondaryButtonL.WasPressedThisFrame();
|
||||
public static bool SecondaryButtonL_OnRelease() => instance.m_SecondaryButtonL.WasReleasedThisFrame();
|
||||
public static bool SecondaryButtonL() => instance?.secondaryButtonL?.IsPressed() ?? false;
|
||||
public static bool SecondaryButtonL_OnPress() => instance?.secondaryButtonL?.WasPressedThisFrame() ?? false;
|
||||
public static bool SecondaryButtonL_OnRelease() => instance?.secondaryButtonL?.WasReleasedThisFrame() ?? false;
|
||||
}
|
||||
@@ -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("========================================================");
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bca5237dce7df3543ba5a9c686d545d6
|
||||
@@ -1,7 +1,5 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.AI;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class PlayerStatusManager : MonoBehaviour
|
||||
{
|
||||
@@ -10,83 +8,66 @@ public class PlayerStatusManager : MonoBehaviour
|
||||
[Header("デバッグ設定")]
|
||||
[SerializeField] private bool ignoreGaugeForDebug = true;
|
||||
|
||||
[Header("Codingモード用 入力設定")]
|
||||
[Header("入力設定")]
|
||||
[SerializeField] private InputActionProperty zlButtonAction;
|
||||
[SerializeField] private InputActionProperty zrButtonAction;
|
||||
|
||||
[Header("プレイヤー移動・操作制御用 入力設定")]
|
||||
[SerializeField] private InputActionProperty moveAction;
|
||||
[SerializeField] private InputActionProperty turnAction;
|
||||
|
||||
[Header("パズル制限時間設定")]
|
||||
[Header("パズル制限時間")]
|
||||
[SerializeField] private float codingLimitDuration = 15f;
|
||||
private float codingTimer = 0f;
|
||||
|
||||
[Header("ゲージ設定")]
|
||||
[SerializeField] private float maxGauge = 100f;
|
||||
[SerializeField] private float currentGauge = 0f;
|
||||
|
||||
[Header("表示UI設定")]
|
||||
[Header("UI & エフェクト")]
|
||||
[SerializeField] private GameObject codingModeUI;
|
||||
[SerializeField] private GameObject successEffectPrefab;
|
||||
[SerializeField] private Transform effectSpawnPoint;
|
||||
|
||||
[Header("バフパラメータ設定")]
|
||||
[Tooltip("攻撃力UP・防御力UPの持続時間 (秒)")]
|
||||
[SerializeField] private float buffDuration = 10f;
|
||||
[Tooltip("攻撃力UP時のダメージ倍率")]
|
||||
[SerializeField] private float attackBuffMultiplier = 1.5f;
|
||||
[Tooltip("防御力UP時の被ダメージ軽減率")]
|
||||
[SerializeField] private float defenseBuffMultiplier = 0.5f;
|
||||
[Tooltip("HP回復バフ発動時の回復量")]
|
||||
[SerializeField] private float hpHealAmount = 30f;
|
||||
[Header("バフ機能")]
|
||||
[SerializeField] private BuffHandler buffHandler = new BuffHandler();
|
||||
|
||||
private float attackBuffTimer = 0f;
|
||||
private float defenseBuffTimer = 0f;
|
||||
private bool isCodingMode = false;
|
||||
|
||||
public GameObject successEffectPrefab;
|
||||
// エフェクトを出す位置(合体したブロックの少し上など)
|
||||
public Transform effectSpawnPoint;
|
||||
|
||||
private List<Animator> pausedAnimators = new List<Animator>();
|
||||
private List<NavMeshAgent> pausedAgents = new List<NavMeshAgent>();
|
||||
|
||||
#region プロパティ
|
||||
public bool IsAttackBuffActive => attackBuffTimer > 0f;
|
||||
public bool IsDefenseBuffActive => defenseBuffTimer > 0f;
|
||||
public float CurrentAttackMultiplier => IsAttackBuffActive ? attackBuffMultiplier : 1.0f;
|
||||
public float CurrentDefenseMultiplier => IsDefenseBuffActive ? defenseBuffMultiplier : 1.0f;
|
||||
#endregion
|
||||
private float codingTimer;
|
||||
public bool IsCodingMode { get; private set; }
|
||||
public BuffHandler Buffs => buffHandler;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null) { Instance = this; }
|
||||
else { Destroy(gameObject); }
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
EnableAction(zlButtonAction);
|
||||
EnableAction(zrButtonAction);
|
||||
EnableAction(moveAction);
|
||||
EnableAction(turnAction);
|
||||
SetActionsEnabled(true);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
DisableAction(zlButtonAction);
|
||||
DisableAction(zrButtonAction);
|
||||
DisableAction(moveAction);
|
||||
DisableAction(turnAction);
|
||||
SetActionsEnabled(false);
|
||||
}
|
||||
|
||||
private void EnableAction(InputActionProperty property)
|
||||
private void SetActionsEnabled(bool enable)
|
||||
{
|
||||
if (property.action != null) property.action.Enable();
|
||||
SetActionState(zlButtonAction, enable);
|
||||
SetActionState(zrButtonAction, enable);
|
||||
SetActionState(moveAction, enable);
|
||||
SetActionState(turnAction, enable);
|
||||
}
|
||||
|
||||
private void DisableAction(InputActionProperty property)
|
||||
private void SetActionState(InputActionProperty property, bool enable)
|
||||
{
|
||||
if (property.action != null) property.action.Disable();
|
||||
if (property.action == null) return;
|
||||
if (enable) property.action.Enable();
|
||||
else property.action.Disable();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
@@ -98,13 +79,12 @@ public class PlayerStatusManager : MonoBehaviour
|
||||
{
|
||||
CheckComboInput();
|
||||
|
||||
if (!isCodingMode)
|
||||
if (!IsCodingMode)
|
||||
{
|
||||
UpdateBuffTimers();
|
||||
buffHandler.UpdateTimers(Time.deltaTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
// CodingMode中の制限時間カウントダウン
|
||||
codingTimer -= Time.deltaTime;
|
||||
if (codingTimer <= 0f)
|
||||
{
|
||||
@@ -115,33 +95,25 @@ public class PlayerStatusManager : MonoBehaviour
|
||||
|
||||
public void AddGauge(float amount)
|
||||
{
|
||||
if (isCodingMode) return;
|
||||
currentGauge += amount;
|
||||
currentGauge = Mathf.Clamp(currentGauge, 0f, maxGauge);
|
||||
if (IsCodingMode) return;
|
||||
currentGauge = Mathf.Clamp(currentGauge + amount, 0f, maxGauge);
|
||||
}
|
||||
|
||||
private void CheckComboInput()
|
||||
{
|
||||
if (zlButtonAction.action == null || zrButtonAction.action == null) return;
|
||||
|
||||
float zlVal = zlButtonAction.action.ReadValue<float>();
|
||||
float zrVal = zrButtonAction.action.ReadValue<float>();
|
||||
|
||||
bool isBothHeld = zlVal > 0.5f && zrVal > 0.5f;
|
||||
bool isBothHeld = zlButtonAction.action.ReadValue<float>() > 0.5f && zrButtonAction.action.ReadValue<float>() > 0.5f;
|
||||
bool isAnyTriggered = zlButtonAction.action.triggered || zrButtonAction.action.triggered;
|
||||
|
||||
if (isBothHeld && isAnyTriggered)
|
||||
{
|
||||
if (!isCodingMode)
|
||||
if (!IsCodingMode)
|
||||
{
|
||||
if (ignoreGaugeForDebug || currentGauge >= maxGauge)
|
||||
{
|
||||
StartCodingMode();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"⚠️ ゲージ不足のためCodingModeを開始できません。現在: {currentGauge} / 必要: {maxGauge}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -152,90 +124,49 @@ public class PlayerStatusManager : MonoBehaviour
|
||||
|
||||
private void StartCodingMode()
|
||||
{
|
||||
isCodingMode = true;
|
||||
IsCodingMode = true;
|
||||
codingTimer = codingLimitDuration;
|
||||
|
||||
// 1. プレイヤーの移動制御をロック
|
||||
DisableAction(moveAction);
|
||||
DisableAction(turnAction);
|
||||
SetActionState(moveAction, false);
|
||||
SetActionState(turnAction, false);
|
||||
|
||||
// 2. パズルブロックの状態・位置を全リセット
|
||||
PuzzleBlock.ResetAllBlocksInScene();
|
||||
|
||||
// 3. 敵のアニメーション・NavMeshを停止
|
||||
foreach (var anim in FindObjectsByType<Animator>(FindObjectsSortMode.None))
|
||||
if (WorldPauseManager.Instance != null)
|
||||
{
|
||||
if (anim == null || anim.gameObject == this.gameObject || anim.CompareTag("Player")) continue;
|
||||
anim.speed = 0f;
|
||||
pausedAnimators.Add(anim);
|
||||
}
|
||||
|
||||
foreach (var agent in FindObjectsByType<NavMeshAgent>(FindObjectsSortMode.None))
|
||||
{
|
||||
if (agent == null || !agent.enabled || !agent.gameObject.activeInHierarchy || agent.CompareTag("Player")) continue;
|
||||
if (!agent.isStopped) { agent.isStopped = true; pausedAgents.Add(agent); }
|
||||
WorldPauseManager.Instance.PauseWorld();
|
||||
}
|
||||
|
||||
if (codingModeUI != null) codingModeUI.SetActive(true);
|
||||
Debug.Log($"⏳ Codingモード開始(制限時間: {codingLimitDuration}秒)");
|
||||
}
|
||||
|
||||
public void ApplyBuffsAndResume(string buff1, string buff2)
|
||||
public void ApplyBuffsAndResume(BuffType buff1, BuffType buff2)
|
||||
{
|
||||
Instantiate(successEffectPrefab, effectSpawnPoint.position, effectSpawnPoint.rotation);
|
||||
ActivateBuffByName(buff1);
|
||||
ActivateBuffByName(buff2);
|
||||
if (successEffectPrefab != null && effectSpawnPoint != null)
|
||||
{
|
||||
Instantiate(successEffectPrefab, effectSpawnPoint.position, effectSpawnPoint.rotation);
|
||||
}
|
||||
|
||||
buffHandler.ActivateBuff(buff1);
|
||||
buffHandler.ActivateBuff(buff2);
|
||||
ResumeWorldState(consumeGauge: true);
|
||||
Debug.Log("🎉 パズル成功!バフを適用してゲームを再開しました。");
|
||||
}
|
||||
|
||||
private void FailCodingModeByTimeout()
|
||||
{
|
||||
ResumeWorldState(consumeGauge: true);
|
||||
Debug.Log("💀 制限時間切れ! Codingモード失敗(バフなし・ゲージ消費)。");
|
||||
}
|
||||
|
||||
public void CancelCodingMode()
|
||||
{
|
||||
ResumeWorldState(consumeGauge: false);
|
||||
Debug.Log("↩️ Codingモードを手動キャンセルしました。");
|
||||
}
|
||||
private void FailCodingModeByTimeout() => ResumeWorldState(consumeGauge: true);
|
||||
public void CancelCodingMode() => ResumeWorldState(consumeGauge: false);
|
||||
|
||||
private void ResumeWorldState(bool consumeGauge)
|
||||
{
|
||||
foreach (var anim in pausedAnimators) { if (anim != null) anim.speed = 1f; }
|
||||
pausedAnimators.Clear();
|
||||
if (WorldPauseManager.Instance != null)
|
||||
{
|
||||
WorldPauseManager.Instance.ResumeWorld();
|
||||
}
|
||||
|
||||
foreach (var agent in pausedAgents) { if (agent != null && agent.enabled) agent.isStopped = false; }
|
||||
pausedAgents.Clear();
|
||||
SetActionState(moveAction, true);
|
||||
SetActionState(turnAction, true);
|
||||
|
||||
EnableAction(moveAction);
|
||||
EnableAction(turnAction);
|
||||
|
||||
isCodingMode = false;
|
||||
IsCodingMode = false;
|
||||
if (codingModeUI != null) codingModeUI.SetActive(false);
|
||||
|
||||
if (consumeGauge) currentGauge = 0f;
|
||||
}
|
||||
|
||||
private void ActivateBuffByName(string buffName)
|
||||
{
|
||||
switch (buffName)
|
||||
{
|
||||
case "AttackUp": attackBuffTimer = buffDuration; break;
|
||||
case "DefenseUp": defenseBuffTimer = buffDuration; break;
|
||||
case "Heal": ExecuteHeal(); break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteHeal()
|
||||
{
|
||||
Debug.Log($"💚 HP回復バフ発動! 回復量: {hpHealAmount}");
|
||||
}
|
||||
|
||||
private void UpdateBuffTimers()
|
||||
{
|
||||
if (attackBuffTimer > 0f) attackBuffTimer -= Time.deltaTime;
|
||||
if (defenseBuffTimer > 0f) defenseBuffTimer -= Time.deltaTime;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Interactables; // XR Toolkit用
|
||||
using UnityEngine.XR.Interaction.Toolkit;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Interactables;
|
||||
|
||||
public enum BlockColor { Red, Blue, Green }
|
||||
public enum BlockShapeType { TShape, CShape } // ト字, コ字
|
||||
public enum BlockShapeType { TShape, CShape }
|
||||
|
||||
public class PuzzleBlock : MonoBehaviour
|
||||
{
|
||||
[field: SerializeField] public BlockColor Color { get; private set; }
|
||||
[field: SerializeField] public BlockShapeType ShapeType { get; private set; }
|
||||
|
||||
[Header("スナップ(合体)位置の設定")]
|
||||
[Tooltip("相手と合体する基準位置(空のGameObject)を指定。未設定でも中心位置でスナップします")]
|
||||
[SerializeField] private Transform snapPoint;
|
||||
|
||||
private PuzzleBlock nearbyPartner; // 近くにある結合対象のブロック
|
||||
private bool isConnected = false; // 結合済みフラグ
|
||||
public static event Action<BlockColor> OnPuzzleCompleted;
|
||||
|
||||
private static readonly HashSet<PuzzleBlock> allBlocks = new HashSet<PuzzleBlock>();
|
||||
|
||||
private PuzzleBlock nearbyPartner;
|
||||
private bool isConnected;
|
||||
|
||||
// ★ワールド情報ではなく「親階層」と「ローカル情報」を記憶
|
||||
private Transform initialParent;
|
||||
private Vector3 initialLocalPosition;
|
||||
private Quaternion initialLocalRotation;
|
||||
private Vector3 initialLocalScale;
|
||||
|
||||
// リセット用の初期座標・回転保持用
|
||||
private Vector3 initialPosition;
|
||||
private Quaternion initialRotation;
|
||||
private Rigidbody rb;
|
||||
private XRGrabInteractable grabInteractable;
|
||||
|
||||
@@ -27,9 +35,21 @@ public class PuzzleBlock : MonoBehaviour
|
||||
rb = GetComponent<Rigidbody>();
|
||||
grabInteractable = GetComponent<XRGrabInteractable>();
|
||||
|
||||
// 初期スポーン位置・回転を記憶しておく
|
||||
initialPosition = transform.position;
|
||||
initialRotation = transform.rotation;
|
||||
// ★開始時の「親」と「ローカル座標・回転・スケール」を保持
|
||||
initialParent = transform.parent;
|
||||
initialLocalPosition = transform.localPosition;
|
||||
initialLocalRotation = transform.localRotation;
|
||||
initialLocalScale = transform.localScale;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
allBlocks.Add(this);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
allBlocks.Remove(this);
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
@@ -40,7 +60,6 @@ public class PuzzleBlock : MonoBehaviour
|
||||
if (partner != null && IsValidPartner(partner))
|
||||
{
|
||||
nearbyPartner = partner;
|
||||
Debug.Log($"🧩 パートナー検知: {partner.Color} の {partner.ShapeType}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,8 +75,7 @@ public class PuzzleBlock : MonoBehaviour
|
||||
private bool IsValidPartner(PuzzleBlock partner)
|
||||
{
|
||||
if (partner == null || partner.isConnected) return false;
|
||||
// 同一色 & 異なる形状(ト字 + コ字)のみ許可
|
||||
return partner.Color == this.Color && partner.ShapeType != this.ShapeType;
|
||||
return partner.Color == Color && partner.ShapeType != ShapeType;
|
||||
}
|
||||
|
||||
public void OnReleased()
|
||||
@@ -75,7 +93,6 @@ public class PuzzleBlock : MonoBehaviour
|
||||
isConnected = true;
|
||||
partner.isConnected = true;
|
||||
|
||||
// 1. 位置・回転のスナップ補正
|
||||
if (snapPoint != null && partner.snapPoint != null)
|
||||
{
|
||||
transform.position = partner.snapPoint.position;
|
||||
@@ -87,15 +104,16 @@ public class PuzzleBlock : MonoBehaviour
|
||||
transform.rotation = partner.transform.rotation;
|
||||
}
|
||||
|
||||
// 2. 物理&VR掴み動作の完全ロック(完成後に崩れないようにする)
|
||||
LockBlockState();
|
||||
partner.LockBlockState();
|
||||
|
||||
Debug.Log($"🎉 マグネット合体成功! 色: {Color}");
|
||||
OnPuzzleCompleted?.Invoke(Color);
|
||||
|
||||
// 3. バフ適用してCodingMode完了へ
|
||||
string buffName = ConvertColorToBuffName(Color);
|
||||
PlayerStatusManager.Instance.ApplyBuffsAndResume(buffName, buffName);
|
||||
BuffType buff = ConvertColorToBuffType(Color);
|
||||
if (PlayerStatusManager.Instance != null)
|
||||
{
|
||||
PlayerStatusManager.Instance.ApplyBuffsAndResume(buff, buff);
|
||||
}
|
||||
}
|
||||
|
||||
private void LockBlockState()
|
||||
@@ -106,43 +124,72 @@ public class PuzzleBlock : MonoBehaviour
|
||||
rb.angularVelocity = Vector3.zero;
|
||||
rb.isKinematic = true;
|
||||
}
|
||||
// 掴みコンポーネントをオフにして手から切り離す
|
||||
if (grabInteractable != null) grabInteractable.enabled = false;
|
||||
}
|
||||
|
||||
public void ResetBlock()
|
||||
{
|
||||
// 1. 手で掴んでいる場合は手離し処理
|
||||
ForceDrop();
|
||||
|
||||
isConnected = false;
|
||||
nearbyPartner = null;
|
||||
|
||||
transform.position = initialPosition;
|
||||
transform.rotation = initialRotation;
|
||||
// 2. ★親オブジェクト(Canvas)の子へ戻す
|
||||
if (initialParent != null && transform.parent != initialParent)
|
||||
{
|
||||
transform.SetParent(initialParent, false);
|
||||
}
|
||||
|
||||
// 3. ★Canvas内のローカル位置・回転・スケールを完全復元
|
||||
transform.localPosition = initialLocalPosition;
|
||||
transform.localRotation = initialLocalRotation;
|
||||
transform.localScale = initialLocalScale;
|
||||
|
||||
// 4. 物理状態の再初期化
|
||||
if (rb != null)
|
||||
{
|
||||
rb.isKinematic = false;
|
||||
rb.linearVelocity = Vector3.zero;
|
||||
rb.angularVelocity = Vector3.zero;
|
||||
}
|
||||
if (grabInteractable != null) grabInteractable.enabled = true;
|
||||
|
||||
// 5. 掴み機能の再有効化
|
||||
if (grabInteractable != null)
|
||||
{
|
||||
grabInteractable.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void ForceDrop()
|
||||
{
|
||||
if (grabInteractable == null || !grabInteractable.isSelected) return;
|
||||
|
||||
if (grabInteractable.interactionManager != null && grabInteractable.firstInteractorSelecting != null)
|
||||
{
|
||||
grabInteractable.interactionManager.SelectExit(grabInteractable.firstInteractorSelecting, grabInteractable);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ResetAllBlocksInScene()
|
||||
{
|
||||
foreach (var block in FindObjectsByType<PuzzleBlock>(FindObjectsSortMode.None))
|
||||
foreach (var block in allBlocks)
|
||||
{
|
||||
if (block != null) block.ResetBlock();
|
||||
if (block != null)
|
||||
{
|
||||
block.ResetBlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string ConvertColorToBuffName(BlockColor color)
|
||||
private BuffType ConvertColorToBuffType(BlockColor color)
|
||||
{
|
||||
return color switch
|
||||
{
|
||||
BlockColor.Red => "AttackUp",
|
||||
BlockColor.Blue => "DefenseUp",
|
||||
BlockColor.Green => "Heal",
|
||||
_ => "AttackUp"
|
||||
BlockColor.Red => BuffType.AttackUp,
|
||||
BlockColor.Blue => BuffType.DefenseUp,
|
||||
BlockColor.Green => BuffType.Heal,
|
||||
_ => BuffType.AttackUp
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,24 +3,25 @@ using UnityEngine;
|
||||
public class SlowWebArea : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float damagePerSecond = 5f;
|
||||
private float damageTimer = 0f;
|
||||
private float damageTimer;
|
||||
|
||||
private void OnTriggerStay(Collider other)
|
||||
{
|
||||
if (other.CompareTag("Player"))
|
||||
{
|
||||
// ★鈍足効果の適用演出
|
||||
// プレイヤーの移動スクリプトに対し、一時的な速度デバフ(例: Speed * 0.3f)を掛けます
|
||||
// ここでは設計の切り離しのため、ログ出力のみとしています
|
||||
Debug.LogWarning("🕸️ プレイヤーが蜘蛛の糸に囚われている! 【鈍足化デバフ発動中】");
|
||||
|
||||
// スリップダメージ(1秒ごとに継続ダメージ)
|
||||
damageTimer += Time.deltaTime;
|
||||
if (damageTimer >= 1.0f)
|
||||
{
|
||||
// プレイヤーのHealthコンポーネントにダメージを通知
|
||||
// other.GetComponent<PlayerHealth>()?.TakeDamage(damagePerSecond);
|
||||
Debug.Log($"🕸️ 糸によるスリップダメージ: {damagePerSecond}");
|
||||
IDamageable damageable = other.GetComponentInParent<IDamageable>();
|
||||
if (damageable != null)
|
||||
{
|
||||
DamageInfo info = new DamageInfo
|
||||
{
|
||||
amount = damagePerSecond,
|
||||
hitPosition = other.transform.position
|
||||
};
|
||||
damageable.TakeDamage(info);
|
||||
}
|
||||
damageTimer = 0f;
|
||||
}
|
||||
}
|
||||
@@ -30,7 +31,7 @@ public class SlowWebArea : MonoBehaviour
|
||||
{
|
||||
if (other.CompareTag("Player"))
|
||||
{
|
||||
Debug.Log("🏃 プレイヤーが糸の範囲から脱出。 鈍足効果解除。");
|
||||
damageTimer = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,26 @@
|
||||
using UnityEngine;
|
||||
|
||||
public interface ISpiderState
|
||||
{
|
||||
void EnterState();
|
||||
void UpdateState();
|
||||
void ExitState();
|
||||
}
|
||||
|
||||
public abstract class SpiderStateBase : ISpiderState
|
||||
{
|
||||
protected readonly BossController boss;
|
||||
|
||||
protected SpiderStateBase(BossController owner)
|
||||
{
|
||||
boss = owner;
|
||||
}
|
||||
|
||||
public virtual void EnterState() { }
|
||||
public abstract void UpdateState();
|
||||
public virtual void ExitState() { }
|
||||
}
|
||||
|
||||
public class SpiderIdleState : SpiderStateBase
|
||||
{
|
||||
public SpiderIdleState(BossController owner) : base(owner) { }
|
||||
@@ -29,14 +50,12 @@ public class SpiderChaseState : SpiderStateBase
|
||||
|
||||
float distance = Vector3.Distance(boss.transform.position, boss.Player.position);
|
||||
|
||||
// 遠距離突進判定(カプセル化されたプロパティを使用)
|
||||
if (distance >= boss.ChargeMinimumDistance && boss.ChargeCooldownTimer <= 0f)
|
||||
{
|
||||
boss.ChangeState<SpiderChargeState>();
|
||||
return;
|
||||
}
|
||||
|
||||
// 至近距離噛みつき判定
|
||||
if (distance <= boss.BiteRange)
|
||||
{
|
||||
boss.ChangeState<SpiderBiteState>();
|
||||
@@ -63,9 +82,7 @@ public class SpiderBiteState : SpiderStateBase
|
||||
boss.Agent.velocity = Vector3.zero;
|
||||
|
||||
boss.Anim.SetFloat("Speed", 0f);
|
||||
|
||||
boss.Anim.SetTrigger("Attack_Bite");
|
||||
Debug.Log("🎯 蜘蛛ボス:通常噛みつき開始。攻撃モーション中にゆっくり向き直ります。");
|
||||
}
|
||||
|
||||
public override void ExitState()
|
||||
@@ -121,7 +138,6 @@ public class SpiderChargeState : SpiderStateBase
|
||||
|
||||
boss.ResetChargeCooldown();
|
||||
boss.Anim.SetTrigger("Attack_Roar");
|
||||
Debug.Log($"🔊 蜘蛛ボス:遠距離を検知。その場でピタッと完全停止。咆哮(溜め {roarTimer}秒)を開始!");
|
||||
}
|
||||
|
||||
public override void ExitState()
|
||||
@@ -134,11 +150,10 @@ public class SpiderChargeState : SpiderStateBase
|
||||
|
||||
public override void UpdateState()
|
||||
{
|
||||
// フェーズ1:咆哮・溜めロックオン
|
||||
if (!isChargingPhase)
|
||||
{
|
||||
roarTimer -= Time.deltaTime;
|
||||
boss.Agent.velocity = Vector3.zero; // 完全固定
|
||||
boss.Agent.velocity = Vector3.zero;
|
||||
|
||||
if (boss.Player != null)
|
||||
{
|
||||
@@ -168,13 +183,10 @@ public class SpiderChargeState : SpiderStateBase
|
||||
boss.Agent.speed = boss.ChargeSpeed;
|
||||
boss.Agent.acceleration = 120f;
|
||||
boss.Agent.SetDestination(targetDestination);
|
||||
|
||||
Debug.Log("🚀 蜘蛛ボス:ロックオン完了。全速力で突進ダッシュを発射!");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// フェーズ2:突進移動中
|
||||
chargeTimer -= Time.deltaTime;
|
||||
|
||||
if (chargeTimer <= 0f || (boss.Agent.velocity.magnitude < 0.2f && chargeTimer < (boss.ChargeDuration - 0.3f)))
|
||||
@@ -202,12 +214,10 @@ public class SpiderJumpToCeilingState : SpiderStateBase
|
||||
|
||||
boss.Agent.enabled = false;
|
||||
boss.Anim.SetTrigger("Attack_Roar");
|
||||
Debug.Log($"🚨 蜘蛛ボス:必殺技前の一大威嚇(咆哮 {roarTimer}秒)!");
|
||||
}
|
||||
|
||||
public override void UpdateState()
|
||||
{
|
||||
// フェーズ1:飛び立つ前の威嚇
|
||||
if (!isJumpingPhase)
|
||||
{
|
||||
roarTimer -= Time.deltaTime;
|
||||
@@ -219,7 +229,6 @@ public class SpiderJumpToCeilingState : SpiderStateBase
|
||||
return;
|
||||
}
|
||||
|
||||
// フェーズ2:ジャンプ空中移動
|
||||
if (boss.ceilingAnchor == null) return;
|
||||
|
||||
jumpProgress += Time.unscaledDeltaTime * 1.5f;
|
||||
@@ -237,25 +246,20 @@ public class SpiderJumpToCeilingState : SpiderStateBase
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ■ STATE: CEILING ATTACK (インスペクター無段階調整版)
|
||||
// ==========================================
|
||||
public class SpiderCeilingAttackState : SpiderStateBase
|
||||
{
|
||||
private float timer = 0f;
|
||||
private float shotTimer = 0f;
|
||||
private float timer;
|
||||
private float shotTimer;
|
||||
|
||||
public SpiderCeilingAttackState(BossController owner) : base(owner) { }
|
||||
|
||||
public override void EnterState()
|
||||
{
|
||||
// ★リファクタリング:インスペクターで設定した必殺技の長さを適用
|
||||
timer = boss.CeilingAttackDuration;
|
||||
shotTimer = 0f;
|
||||
|
||||
boss.Anim.SetBool("IsOnCeiling", true);
|
||||
boss.PendingUltimate = false;
|
||||
Debug.Log($"🕸️ 蜘蛛ボス:天井からの乱射開始! 持続時間: {timer}秒 / 発射間隔: {boss.CeilingShotInterval}秒");
|
||||
}
|
||||
|
||||
public override void ExitState()
|
||||
@@ -271,12 +275,9 @@ public class SpiderCeilingAttackState : SpiderStateBase
|
||||
timer -= Time.deltaTime;
|
||||
shotTimer -= Time.deltaTime;
|
||||
|
||||
// タイマーがゼロになったら発射
|
||||
if (shotTimer <= 0f)
|
||||
{
|
||||
ExecuteRandomWebAttack();
|
||||
|
||||
// ★リファクタリング:インスペクターで設定した間隔(秒)でリセットして弾数を制御
|
||||
shotTimer = boss.CeilingShotInterval;
|
||||
}
|
||||
|
||||
@@ -286,14 +287,13 @@ public class SpiderCeilingAttackState : SpiderStateBase
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteRandomWebAttack()
|
||||
private void ExecuteRandomWebAttack()
|
||||
{
|
||||
Vector3 targetGroundPos = Vector3.zero;
|
||||
int maxAttempts = 15; // 無限ループ(フリーズ)を防止するための最大再抽選回数
|
||||
int maxAttempts = 15;
|
||||
|
||||
for (int i = 0; i < maxAttempts; i++)
|
||||
{
|
||||
// 1. 一旦ランダムな座標を算出
|
||||
Vector2 randomCircle = Random.insideUnitCircle * boss.AttackRadius;
|
||||
targetGroundPos = new Vector3(
|
||||
boss.groundAnchor.position.x + randomCircle.x,
|
||||
@@ -301,29 +301,23 @@ public class SpiderCeilingAttackState : SpiderStateBase
|
||||
boss.groundAnchor.position.z + randomCircle.y
|
||||
);
|
||||
|
||||
// 2. 現在アクティブなすべての糸との距離を計測する
|
||||
bool isTooClose = false;
|
||||
foreach (Vector3 existingPos in boss.ActiveWebPositions)
|
||||
foreach (Vector3 existingPos in boss.WebSpawner.ActiveWebPositions)
|
||||
{
|
||||
// 設定された回避半径(WebAvoidanceRadius)より近い既存の糸があるか?
|
||||
if (Vector3.Distance(targetGroundPos, existingPos) < boss.WebAvoidanceRadius)
|
||||
if (Vector3.Distance(targetGroundPos, existingPos) < boss.WebSpawner.WebAvoidanceRadius)
|
||||
{
|
||||
isTooClose = true;
|
||||
break; // 被り検知、この座標は使えないのでチェックを抜けて再抽選へ
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 誰とも被っていなければ、このクリーンな座標で確定
|
||||
if (!isTooClose)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (!isTooClose) break;
|
||||
}
|
||||
|
||||
// 確定した(または上限に達して安全が担保された)座標へ向けて発射命令
|
||||
boss.SpawnWarningAndWeb(targetGroundPos, boss.WebWarningDelay, boss.WebDuration);
|
||||
boss.WebSpawner.SpawnWarningAndWeb(targetGroundPos, boss.WebWarningDelay, boss.WebDuration);
|
||||
}
|
||||
}
|
||||
|
||||
public class SpiderStunState : SpiderStateBase
|
||||
{
|
||||
private float stunTimer;
|
||||
@@ -336,8 +330,6 @@ public class SpiderStunState : SpiderStateBase
|
||||
boss.Anim.SetTrigger("StunStart");
|
||||
boss.Anim.SetBool("IsStunned", true);
|
||||
if (boss.Agent.enabled) boss.Agent.isStopped = true;
|
||||
Debug.Log("🛡️ パリィ成功! チャンスタイム!");
|
||||
|
||||
}
|
||||
|
||||
public override void ExitState()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class SimpleTitleManager : MonoBehaviour
|
||||
public class TitleManager : MonoBehaviour
|
||||
{
|
||||
[Header("UI Buttons")]
|
||||
[SerializeField] private Button startButton;
|
||||
@@ -14,14 +14,13 @@ public class SimpleTitleManager : MonoBehaviour
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// テンプレートの手でボタンを押した時に実行される処理
|
||||
startButton.onClick.AddListener(OnStartButtonClicked);
|
||||
exitButton.onClick.AddListener(OnExitButtonClicked);
|
||||
if (startButton != null) startButton.onClick.AddListener(OnStartButtonClicked);
|
||||
if (exitButton != null) exitButton.onClick.AddListener(OnExitButtonClicked);
|
||||
}
|
||||
|
||||
private void OnStartButtonClicked()
|
||||
{
|
||||
startButton.interactable = false; // 二重押し防止
|
||||
if (startButton != null) startButton.interactable = false;
|
||||
StartCoroutine(LoadSceneRoutine());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,72 +1,48 @@
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 1つのトリガーで左右のドアを反対方向にスライド開閉させ、
|
||||
/// 一定時間後に自動で閉じるクラス
|
||||
/// </summary>
|
||||
public class VRDoorController : MonoBehaviour
|
||||
{
|
||||
[Header("ドアのオブジェクト登録")]
|
||||
[Tooltip("左側にスライドさせたいドアのTransformを指定してください")]
|
||||
[Header("ドアオブジェクト")]
|
||||
[SerializeField] private Transform leftDoor;
|
||||
[Tooltip("右側にスライドさせたいドアのTransformを指定してください")]
|
||||
[SerializeField] private Transform rightDoor;
|
||||
|
||||
[Header("スライド量と速度の設定")]
|
||||
[Tooltip("ドアが横に何メートルずれるか(移動量)")]
|
||||
[Header("動作設定")]
|
||||
[SerializeField] private float slideDistance = 1.0f;
|
||||
[Tooltip("ドアが完全に開ききる(閉じきる)までにかかる時間(秒)")]
|
||||
[SerializeField] private float openSpeed = 1.2f;
|
||||
[Tooltip("ドアが開ききった後、自動で閉まり始めるまでの待ち時間(秒)")]
|
||||
[SerializeField] private float autoCloseDelay = 4.0f;
|
||||
|
||||
// ドアの「閉じた位置」と「開いた位置」の座標を記憶する変数
|
||||
private Vector3 leftDoorClosedPos;
|
||||
private Vector3 rightDoorClosedPos;
|
||||
private Vector3 leftDoorOpenPos;
|
||||
private Vector3 rightDoorOpenPos;
|
||||
|
||||
// ドアの状態を管理するフラグ(バグ連打の防止用)
|
||||
private bool isOpen = false;
|
||||
private bool isMoving = false;
|
||||
private bool isOpen;
|
||||
private bool isMoving;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// 1. ゲーム開始時の位置を「閉じた状態」として記憶
|
||||
if (leftDoor != null)
|
||||
{
|
||||
leftDoorClosedPos = leftDoor.localPosition;
|
||||
// 左ドアはローカルのX軸のマイナス方向へスライドさせる
|
||||
leftDoorOpenPos = leftDoorClosedPos + new Vector3(-slideDistance, 0, 0);
|
||||
}
|
||||
|
||||
if (rightDoor != null)
|
||||
{
|
||||
rightDoorClosedPos = rightDoor.localPosition;
|
||||
// 右ドアはローカルのX軸のプラス方向へスライドさせる
|
||||
rightDoorOpenPos = rightDoorClosedPos + new Vector3(slideDistance, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 【外部連携用】VRのボタンが押されたときに呼び出す公開メソッド
|
||||
/// </summary>
|
||||
public void OnButtonPress()
|
||||
{
|
||||
// すでに移動中、またはすでに開いている場合はボタンの入力を完全に無視する
|
||||
if (isMoving || isOpen) return;
|
||||
|
||||
// 開いてから自動で閉まるまでの一連のコルーチンを始動
|
||||
StartCoroutine(OpenAndAutoCloseRoutine());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ドアを開け、待機し、自動で閉める一連の流れを管理するコルーチン
|
||||
/// </summary>
|
||||
private IEnumerator OpenAndAutoCloseRoutine()
|
||||
{
|
||||
// ─── 1. ドアを開ける処理 ───
|
||||
isMoving = true;
|
||||
float elapsedTime = 0f;
|
||||
|
||||
@@ -74,28 +50,22 @@ public class VRDoorController : MonoBehaviour
|
||||
{
|
||||
elapsedTime += Time.deltaTime;
|
||||
float t = elapsedTime / openSpeed;
|
||||
|
||||
// ★VR酔い・チープさ対策:数値を滑らかに補間して、リアルな加減速(イージング)をかける
|
||||
float easedT = Mathf.SmoothStep(0f, 1f, t);
|
||||
|
||||
if (leftDoor != null) leftDoor.localPosition = Vector3.Lerp(leftDoorClosedPos, leftDoorOpenPos, easedT);
|
||||
if (rightDoor != null) rightDoor.localPosition = Vector3.Lerp(rightDoorClosedPos, rightDoorOpenPos, easedT);
|
||||
|
||||
yield return null; // 1フレーム待機
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// 完全に開ききった座標で固定
|
||||
if (leftDoor != null) leftDoor.localPosition = leftDoorOpenPos;
|
||||
if (rightDoor != null) rightDoor.localPosition = rightDoorOpenPos;
|
||||
|
||||
isOpen = true;
|
||||
isMoving = false;
|
||||
Debug.Log("🚪 ドアが完全に開きました。");
|
||||
|
||||
// ─── 2. 開いた状態での待機(自動クローズのカウントダウン) ───
|
||||
yield return new WaitForSeconds(autoCloseDelay);
|
||||
|
||||
// ─── 3. ドアを閉じる処理 ───
|
||||
isMoving = true;
|
||||
isOpen = false;
|
||||
elapsedTime = 0f;
|
||||
@@ -112,11 +82,9 @@ public class VRDoorController : MonoBehaviour
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// 完全に閉じた座標で固定
|
||||
if (leftDoor != null) leftDoor.localPosition = leftDoorClosedPos;
|
||||
if (rightDoor != null) rightDoor.localPosition = rightDoorClosedPos;
|
||||
|
||||
isMoving = false;
|
||||
Debug.Log("🔒 ドアが自動で閉じました。");
|
||||
}
|
||||
}
|
||||
@@ -2,79 +2,65 @@ using UnityEngine;
|
||||
|
||||
public class VRInputPoseDetector : MonoBehaviour
|
||||
{
|
||||
[Header("必須コンポーネント登録")]
|
||||
[Header("参照")]
|
||||
[SerializeField] private Transform headTransform;
|
||||
[SerializeField] private Transform rightHandTransform;
|
||||
[SerializeField] private Transform leftHandTransform;
|
||||
|
||||
[Header("構えの判定範囲設定")]
|
||||
[Header("判定パラメータ")]
|
||||
[SerializeField] private float minForwardDistance = 0.15f;
|
||||
[SerializeField] private float maxForwardDistance = 0.5f;
|
||||
[SerializeField] private float minHeightOffset = -0.35f;
|
||||
[SerializeField] private float maxHeightOffset = -0.1f;
|
||||
|
||||
[Header("タイマー・バッファ設定")]
|
||||
[Header("タイマーパラメータ")]
|
||||
[SerializeField] private float requiredPoseTime = 0.3f;
|
||||
|
||||
[SerializeField] private float poseExitDelay = 0.15f;
|
||||
|
||||
[Header("視覚フィードバック(ガイドUI設定)")]
|
||||
[Header("視覚フィードバック")]
|
||||
[SerializeField] private Renderer guideRenderer;
|
||||
[SerializeField] private Color defaultColor = new Color(0.5f, 0.5f, 0.5f, 0.05f); // 普段(超薄いグレー)
|
||||
[SerializeField] private Color chargingColor = new Color(1f, 0.8f, 0f, 0.2f); // 溜め中(うっすら黄色)
|
||||
[SerializeField] private Color successColor = new Color(0f, 0.6f, 1f, 0.4f); // 構え完了(綺麗な青シールド)
|
||||
[SerializeField] private Color defaultColor = new Color(0.5f, 0.5f, 0.5f, 0.05f);
|
||||
[SerializeField] private Color chargingColor = new Color(1f, 0.8f, 0f, 0.2f);
|
||||
[SerializeField] private Color successColor = new Color(0f, 0.6f, 1f, 0.4f);
|
||||
|
||||
private float poseTimer = 0f;
|
||||
private float exitTimer = 0f;
|
||||
private bool isPoseMaintained = false;
|
||||
private float poseTimer;
|
||||
private float exitTimer;
|
||||
|
||||
public bool IsPosing => isPoseMaintained;
|
||||
public bool IsPosing { get; private set; }
|
||||
|
||||
void Update()
|
||||
private void Update()
|
||||
{
|
||||
if (headTransform == null || rightHandTransform == null || leftHandTransform == null) return;
|
||||
|
||||
// 両手の世界座標を頭を中心としたローカル座標に変換
|
||||
Vector3 localRightHand = headTransform.InverseTransformPoint(rightHandTransform.position);
|
||||
Vector3 localLeftHand = headTransform.InverseTransformPoint(leftHandTransform.position);
|
||||
|
||||
// 両手が構え範囲に入っているかチェック
|
||||
bool isRightHandGuarding = CheckHandPosition(localRightHand);
|
||||
bool isLeftHandGuarding = CheckHandPosition(localLeftHand);
|
||||
bool isBothHandsGuarding = CheckHandPosition(localRightHand) && CheckHandPosition(localLeftHand);
|
||||
|
||||
bool isBothHandsGuarding = isRightHandGuarding && isLeftHandGuarding;
|
||||
|
||||
// ─── 🛡️ チャタリング防止付き・構え判定ロジック ───
|
||||
if (isBothHandsGuarding)
|
||||
{
|
||||
// 範囲内に手が戻ったので、解除カウントダウンを即座にリセット
|
||||
exitTimer = 0f;
|
||||
|
||||
if (!isPoseMaintained)
|
||||
if (!IsPosing)
|
||||
{
|
||||
poseTimer += Time.deltaTime;
|
||||
if (poseTimer >= requiredPoseTime)
|
||||
{
|
||||
isPoseMaintained = true;
|
||||
Debug.Log("🛡️ 【構え成功】 パリィ受付がアクティブになりました。");
|
||||
IsPosing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 範囲外に出た場合
|
||||
poseTimer = 0f; // まだ構えが完了していない段階(溜め中)なら即リセット
|
||||
poseTimer = 0f;
|
||||
|
||||
if (isPoseMaintained)
|
||||
if (IsPosing)
|
||||
{
|
||||
// すでに構えが成功している状態なら、猶予タイマーを開始(ここで即解除しない!)
|
||||
exitTimer += Time.deltaTime;
|
||||
|
||||
if (exitTimer >= poseExitDelay)
|
||||
{
|
||||
isPoseMaintained = false;
|
||||
IsPosing = false;
|
||||
exitTimer = 0f;
|
||||
Debug.Log("❌ 【構え解除】 猶予時間を超えたためガードが解けました。");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,9 +70,8 @@ public class VRInputPoseDetector : MonoBehaviour
|
||||
|
||||
private bool CheckHandPosition(Vector3 localHandPos)
|
||||
{
|
||||
bool inForwardRange = localHandPos.z >= minForwardDistance && localHandPos.z <= maxForwardDistance; // Z軸:頭より前に手があるか
|
||||
bool inHeightRange = localHandPos.y >= minHeightOffset && localHandPos.y <= maxHeightOffset; // Y軸:手の高さが適切か
|
||||
|
||||
bool inForwardRange = localHandPos.z >= minForwardDistance && localHandPos.z <= maxForwardDistance;
|
||||
bool inHeightRange = localHandPos.y >= minHeightOffset && localHandPos.y <= maxHeightOffset;
|
||||
return inForwardRange && inHeightRange;
|
||||
}
|
||||
|
||||
@@ -94,18 +79,17 @@ public class VRInputPoseDetector : MonoBehaviour
|
||||
{
|
||||
if (guideRenderer == null) return;
|
||||
|
||||
// ボスの攻撃を見やすくするため、現在の状態に応じてガイドシールドのカラーを3段階可変させる
|
||||
if (isPoseMaintained)
|
||||
if (IsPosing)
|
||||
{
|
||||
guideRenderer.material.color = successColor; // 成功:青シールド
|
||||
guideRenderer.material.color = successColor;
|
||||
}
|
||||
else if (isBothHandsGuarding)
|
||||
{
|
||||
guideRenderer.material.color = chargingColor; // 溜め中:黄色
|
||||
guideRenderer.material.color = chargingColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
guideRenderer.material.color = defaultColor; // 構えなし:目立たない透明グレー
|
||||
guideRenderer.material.color = defaultColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6159a60476d605c4494622c0790e9b02
|
||||
@@ -1,6 +1,6 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem; // Input Systemを使うために必要
|
||||
using UnityEngine.XR; // 振動機能のために必要
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.XR;
|
||||
|
||||
[RequireComponent(typeof(Collider))]
|
||||
[RequireComponent(typeof(Rigidbody))]
|
||||
@@ -10,37 +10,30 @@ public class VRInputPunch : MonoBehaviour
|
||||
[SerializeField] private XRNode controllerNode = XRNode.RightHand;
|
||||
[SerializeField] private VRInputPoseDetector poseDetector;
|
||||
|
||||
[Header("攻撃(パンチ)設定")]
|
||||
[SerializeField] private float minPunchSpeed = 1.0f; // パンチとして判定する最低速度 (m/s)
|
||||
[SerializeField] private float damageMultiplier = 10.0f; // ダメージ倍率
|
||||
[SerializeField] private float hapticAmplitude = 0.5f; // 通常ヒットの振動の強さ
|
||||
[SerializeField] private float hapticDuration = 0.1f; // 通常ヒットの振動の長さ
|
||||
[Header("パンチパラメータ")]
|
||||
[SerializeField] private float minPunchSpeed = 1.0f;
|
||||
[SerializeField] private float damageMultiplier = 10.0f;
|
||||
[SerializeField] private float hapticAmplitude = 0.5f;
|
||||
[SerializeField] private float hapticDuration = 0.1f;
|
||||
|
||||
[Header("パリィ設定")]
|
||||
[SerializeField] private float parryStunDuration = 3.0f; // スタン時間(秒)
|
||||
[SerializeField] private float parryHapticAmplitude = 0.9f; // パリィ成功時の振動の強さ
|
||||
[SerializeField] private float parryHapticDuration = 0.25f; // パリィ成功時の振動の長さ
|
||||
// ─── 多段ヒット・連続暴発防止用タイマー ───
|
||||
private float punchHitCooldown = 0f; // 次のパンチが当たるまでのインターバル
|
||||
private float parryTriggerCooldown = 0f; // 次のパリィが成功するまでのインターバル
|
||||
[Header("パリィパラメータ")]
|
||||
[SerializeField] private float parryStunDuration = 3.0f;
|
||||
[SerializeField] private float parryHapticAmplitude = 0.9f;
|
||||
[SerializeField] private float parryHapticDuration = 0.25f;
|
||||
|
||||
private const string EnemyFistTag = "EnemyFist";
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (velocityAction.action != null)
|
||||
{
|
||||
velocityAction.action.Enable();
|
||||
}
|
||||
if (velocityAction.action != null) velocityAction.action.Enable();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (velocityAction.action != null)
|
||||
{
|
||||
velocityAction.action.Disable();
|
||||
}
|
||||
if (velocityAction.action != null) velocityAction.action.Disable();
|
||||
}
|
||||
|
||||
void Start()
|
||||
private void Start()
|
||||
{
|
||||
Rigidbody rb = GetComponent<Rigidbody>();
|
||||
rb.isKinematic = true;
|
||||
@@ -50,104 +43,71 @@ public class VRInputPunch : MonoBehaviour
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
// コントローラーの現在の移動速度を取得
|
||||
Vector3 velocity = velocityAction.action.ReadValue<Vector3>();
|
||||
Vector3 velocity = velocityAction.action != null ? velocityAction.action.ReadValue<Vector3>() : Vector3.zero;
|
||||
float speed = velocity.magnitude;
|
||||
|
||||
// ========================================================================
|
||||
// 【パリィ判定】当たったオブジェクトが敵の拳だった場合
|
||||
// ========================================================================
|
||||
if (other.CompareTag("EnemyFist"))
|
||||
if (other.CompareTag(EnemyFistTag))
|
||||
{
|
||||
Debug.Log($"🛡️ [パリィチェック] 敵の拳と接触。構え状態: {poseDetector.IsPosing}");
|
||||
|
||||
// 自分が「構え状態」のときだけパリィの成立チェックを行う
|
||||
if (poseDetector.IsPosing)
|
||||
if (poseDetector != null && poseDetector.IsPosing)
|
||||
{
|
||||
// 1. ザコ敵(EnemyAI)へのパリィ判定
|
||||
EnemyAI enemyAI = other.GetComponentInParent<EnemyAI>();
|
||||
if (enemyAI != null)
|
||||
{
|
||||
enemyAI.TriggerParryStun(parryStunDuration);
|
||||
TriggerHaptics(parryHapticAmplitude, parryHapticDuration);
|
||||
Debug.Log($"🛡️ 【ジャストパリィ成功!】 敵の体勢を崩しました。");
|
||||
return; // ★本当のパリィ成功時のみ、ここで処理を終了する
|
||||
}
|
||||
|
||||
// 2. ボス(BossController)へのパリィ判定
|
||||
BossController boss = other.GetComponentInParent<BossController>();
|
||||
if (boss != null)
|
||||
{
|
||||
// ★リファクタリング:ボスが本当に攻撃中(Bite/Charge)で、パリィが成功した時だけ終了する
|
||||
if (boss.TriggerParryStun())
|
||||
{
|
||||
TriggerHaptics(parryHapticAmplitude, parryHapticDuration);
|
||||
Debug.Log("🛡️ 【ジャストパリィ成功!】 ボスの体勢を崩しました!");
|
||||
return; // ★本当のパリィ成功時のみ、ここで処理を終了する
|
||||
}
|
||||
}
|
||||
if (TryProcessParry(other)) return;
|
||||
}
|
||||
|
||||
// ─── ★ここが今回の核心(超重要リファクタリング) ───
|
||||
// 構え状態ではなかった場合、またはボスが攻撃中でなくパリィが不成立だった場合は、
|
||||
// 「プレイヤーが隙を狙って殴りに行った」とみなして、returnせずにそのまま下のパンチ処理へ流す!
|
||||
Debug.Log("🥊 パリィ不成立、または攻撃チャンス:通常のパンチダメージ処理へ移行します。");
|
||||
}
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// 【パンチ攻撃の処理】
|
||||
// ========================================================================
|
||||
Debug.Log($"[InputSystem接触] {other.name} に触れました。センサー速度: {speed:F2} m/s");
|
||||
|
||||
if (speed >= minPunchSpeed)
|
||||
{
|
||||
IDamageble damageble = other.GetComponentInParent<IDamageble>();
|
||||
|
||||
if (damageble != null)
|
||||
{
|
||||
DamageInfo info = new DamageInfo();
|
||||
// 構えからパンチするとダメージ2倍、ゲージ取得度2倍
|
||||
if (poseDetector.IsPosing)
|
||||
{
|
||||
info.amount = (speed * damageMultiplier) * 2f;
|
||||
info.isPoseBonus = true;
|
||||
|
||||
if (PlayerStatusManager.Instance != null) PlayerStatusManager.Instance.AddGauge(20f);
|
||||
}
|
||||
else
|
||||
{
|
||||
info.amount = speed * damageMultiplier;
|
||||
info.isPoseBonus = false;
|
||||
|
||||
if (PlayerStatusManager.Instance != null) PlayerStatusManager.Instance.AddGauge(10f);
|
||||
}
|
||||
info.hitPosition = other.transform.position;
|
||||
info.punchDirection = velocity.normalized;
|
||||
|
||||
if (PlayerStatusManager.Instance != null)
|
||||
{
|
||||
info.amount *= PlayerStatusManager.Instance.CurrentAttackMultiplier;
|
||||
}
|
||||
|
||||
damageble.TakeDamage(info);
|
||||
|
||||
Debug.Log($"パンチ成功 ダメージ: {info.amount:F1}");
|
||||
|
||||
// 通常ヒットの振動を発生
|
||||
TriggerHaptics(hapticAmplitude, hapticDuration);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"スピードが足りません。必要: {minPunchSpeed} / 現在: {speed:F2}");
|
||||
ProcessPunchAttack(other, speed, velocity);
|
||||
}
|
||||
}
|
||||
|
||||
/// 引数に応じて指定された強さと長さでコントローラーを振動させる
|
||||
private bool TryProcessParry(Collider other)
|
||||
{
|
||||
EnemyAI enemyAI = other.GetComponentInParent<EnemyAI>();
|
||||
if (enemyAI != null)
|
||||
{
|
||||
enemyAI.TriggerParryStun(parryStunDuration);
|
||||
TriggerHaptics(parryHapticAmplitude, parryHapticDuration);
|
||||
return true;
|
||||
}
|
||||
|
||||
BossController boss = other.GetComponentInParent<BossController>();
|
||||
if (boss != null && boss.TriggerParryStun())
|
||||
{
|
||||
TriggerHaptics(parryHapticAmplitude, parryHapticDuration);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ProcessPunchAttack(Collider other, float speed, Vector3 velocity)
|
||||
{
|
||||
IDamageable damageable = other.GetComponentInParent<IDamageable>();
|
||||
if (damageable == null) return;
|
||||
|
||||
bool isPosing = poseDetector != null && poseDetector.IsPosing;
|
||||
|
||||
DamageInfo info = new DamageInfo
|
||||
{
|
||||
amount = speed * damageMultiplier * (isPosing ? 2f : 1f),
|
||||
isPoseBonus = isPosing,
|
||||
hitPosition = other.transform.position,
|
||||
punchDirection = velocity.normalized
|
||||
};
|
||||
|
||||
if (PlayerStatusManager.Instance != null)
|
||||
{
|
||||
PlayerStatusManager.Instance.AddGauge(isPosing ? 20f : 10f);
|
||||
info.amount *= PlayerStatusManager.Instance.Buffs.CurrentAttackMultiplier;
|
||||
}
|
||||
|
||||
damageable.TakeDamage(info);
|
||||
TriggerHaptics(hapticAmplitude, hapticDuration);
|
||||
}
|
||||
|
||||
private void TriggerHaptics(float amplitude, float duration)
|
||||
{
|
||||
// 明示的に UnityEngine.XR.InputDevice と指定して型衝突を回避
|
||||
UnityEngine.XR.InputDevice device = InputDevices.GetDeviceAtXRNode(controllerNode);
|
||||
if (device.isValid)
|
||||
{
|
||||
|
||||
@@ -1,78 +1,60 @@
|
||||
using UnityEngine;
|
||||
|
||||
// World Space Canvas をプレイヤーの真正面に移動させ、開いている間はプレイヤーの視点に滑らかに追従させるスクリプト
|
||||
public class VRUIDoorFaceFollow : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Transform vrCameraTransform;
|
||||
|
||||
[Header("配置・追従のパラメータ")]
|
||||
[Header("配置・追従パラメータ")]
|
||||
[SerializeField] private float forwardDistance = 0.5f;
|
||||
[SerializeField] private float heightOffset = -0.1f;
|
||||
[SerializeField] private float followSpeed = 5.0f;
|
||||
|
||||
// 画面がアクティブ(表示)になった瞬間を検知するためのフラグ
|
||||
private bool wasActiveLastFrame = false;
|
||||
private bool wasActiveLastFrame;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// カメラが未設定の場合は、シーン内からメインカメラを自動取得
|
||||
if (vrCameraTransform == null && Camera.main != null)
|
||||
{
|
||||
vrCameraTransform = Camera.main.transform;
|
||||
}
|
||||
}
|
||||
|
||||
// 全体の時間が止まっている(Time.timeScale = 0)間でも動作させるため、カメラ移動の後に実行される LateUpdate を使用します
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (vrCameraTransform == null) return;
|
||||
|
||||
// このオブジェクト(UI画面)が有効化された「最初のフレーム」の処理
|
||||
if (!wasActiveLastFrame)
|
||||
{
|
||||
// 開いた瞬間は、プレイヤーの今の向きの「完全な正面」に瞬間移動させる(遅れてついてくるのを防ぐため)
|
||||
SetInitialPosition();
|
||||
wasActiveLastFrame = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 毎フレームの滑らかな追従処理
|
||||
SmoothFollowPlayer();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
// 画面が閉じられたらフラグをリセットする
|
||||
wasActiveLastFrame = false;
|
||||
}
|
||||
|
||||
/// 画面が開いた瞬間に、プレイヤーの正面の正しい位置・向きに強制配置する
|
||||
private void SetInitialPosition()
|
||||
{
|
||||
// カメラの位置から、カメラが向いている正面方向に一定距離進んだ座標を計算
|
||||
Vector3 targetPosition = vrCameraTransform.position + (vrCameraTransform.forward * forwardDistance);
|
||||
targetPosition.y += heightOffset; // 高さを微調整
|
||||
targetPosition.y += heightOffset;
|
||||
|
||||
transform.position = targetPosition;
|
||||
|
||||
// 画面の向きをプレイヤー(カメラ)の方向に向ける(背を向けないようにする)
|
||||
// UIパネルがプレイヤー側を正面にするよう、LookAtの後に180度回転させて裏返りを防ぎます
|
||||
transform.LookAt(vrCameraTransform.position);
|
||||
transform.Rotate(0, 180, 0);
|
||||
}
|
||||
|
||||
/// プレイヤーが首を動かしたり移動したりした際、画面を滑らかについてこさせる
|
||||
private void SmoothFollowPlayer()
|
||||
{
|
||||
// 目的地(目標座標)の計算
|
||||
Vector3 targetPosition = vrCameraTransform.position + (vrCameraTransform.forward * forwardDistance);
|
||||
targetPosition.y += heightOffset;
|
||||
|
||||
// Time.timeScale = 0 でも動かすため、Time.unscaledDeltaTime を使用
|
||||
// 現在位置から目標位置まで滑らかに補間移動(Lerp)させる
|
||||
transform.position = Vector3.Lerp(transform.position, targetPosition, Time.unscaledDeltaTime * followSpeed);
|
||||
|
||||
// 向きも滑らかに補間(Slerp)してプレイヤーの方を向かせ続ける
|
||||
Quaternion targetRotation = Quaternion.LookRotation(transform.position - vrCameraTransform.position);
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.unscaledDeltaTime * followSpeed);
|
||||
}
|
||||
|
||||
47
Assets/_Scripts/WorldPauseService.cs
Normal file
47
Assets/_Scripts/WorldPauseService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Scripts/WorldPauseService.cs.meta
Normal file
2
Assets/_Scripts/WorldPauseService.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f00095a3e63cc440be35394ad49b505
|
||||
Reference in New Issue
Block a user