feat: debug scene navigation system
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de5e8b6dd52514441b1115e22ce12f85
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using OctoCore.Navigation.Data;
|
||||
using OctoRef = OctoCore.OctoCore;
|
||||
|
||||
namespace Unheimlich.Navigation.Debuging
|
||||
{
|
||||
/// <summary>
|
||||
/// ScriptableObject holding the ordered list of debug scenes.
|
||||
/// Drag OctoSceneData assets in the desired order in the inspector.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "DebugNavigation", menuName = "Unheimlich/Debug/Debug Navigation")]
|
||||
public class DebugNavigation : ScriptableObject
|
||||
{
|
||||
[Tooltip("Ordered list of scenes available in debug mode.")]
|
||||
[SerializeField] private List<OctoSceneData> _scenes = new List<OctoSceneData>();
|
||||
|
||||
public IReadOnlyList<OctoSceneData> Scenes => _scenes;
|
||||
|
||||
/// <summary>Invoked when a debug scene change is requested.</summary>
|
||||
public Action<OctoSceneData> OnDebugSceneRequested;
|
||||
|
||||
private int _currentIndex = -1;
|
||||
public int CurrentIndex => _currentIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Sync the current index with the active scene.
|
||||
/// If the active scene is not in the list, _currentIndex stays at -1
|
||||
/// so Next() will correctly start at index 0.
|
||||
/// </summary>
|
||||
public void SyncWithCurrentScene(OctoSceneData currentScene)
|
||||
{
|
||||
_currentIndex = currentScene != null ? _scenes.IndexOf(currentScene) : -1;
|
||||
|
||||
if (_currentIndex == -1)
|
||||
UnityEngine.Debug.Log("[DebugNavigation] Scene not found in debug list — Next() will start at index 0.");
|
||||
}
|
||||
|
||||
/// <summary>Load the previous scene in the list (wraps).</summary>
|
||||
public void Previous()
|
||||
{
|
||||
if (_scenes == null || _scenes.Count == 0) return;
|
||||
|
||||
// If not in list, go to last
|
||||
_currentIndex = (_currentIndex <= 0) ? _scenes.Count - 1 : _currentIndex - 1;
|
||||
RequestScene(_currentIndex);
|
||||
}
|
||||
|
||||
/// <summary>Load the next scene in the list (wraps).</summary>
|
||||
public void Next()
|
||||
{
|
||||
if (_scenes == null || _scenes.Count == 0) return;
|
||||
|
||||
// If not in list (-1), go to 0. Otherwise increment.
|
||||
_currentIndex = (_currentIndex >= _scenes.Count - 1) ? 0 : _currentIndex + 1;
|
||||
RequestScene(_currentIndex);
|
||||
}
|
||||
|
||||
/// <summary>Load a scene by index directly (called by Debug Menu UI buttons).</summary>
|
||||
public void LoadByIndex(int index)
|
||||
{
|
||||
if (index < 0 || index >= _scenes.Count)
|
||||
{
|
||||
UnityEngine.Debug.LogWarning($"[DebugNavigation] Index {index} out of range.");
|
||||
return;
|
||||
}
|
||||
|
||||
_currentIndex = index;
|
||||
RequestScene(_currentIndex);
|
||||
}
|
||||
|
||||
private void RequestScene(int index)
|
||||
{
|
||||
OctoSceneData scene = _scenes[index];
|
||||
|
||||
if (scene == null)
|
||||
{
|
||||
UnityEngine.Debug.LogWarning($"[DebugNavigation] Scene at index {index} is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't reload the scene that's already active
|
||||
if (OctoRef.Instance != null &&
|
||||
OctoRef.Instance.Navigation.CurrentScene == scene)
|
||||
{
|
||||
UnityEngine.Debug.Log($"[DebugNavigation] Scene '{scene.SceneName}' is already loaded, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
OnDebugSceneRequested?.Invoke(scene);
|
||||
|
||||
if (OctoRef.Instance)
|
||||
OctoRef.Instance.Navigation.LoadScene(scene);
|
||||
else
|
||||
UnityEngine.Debug.LogError("[DebugNavigation] OctoCore instance not found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9089d8b7e41057e4cac3aad004af4b93
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 767e525b8e268ce40b879a01f27987aa
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,320 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.InputSystem;
|
||||
using OctoRef = OctoCore.OctoCore;
|
||||
|
||||
namespace Unheimlich.Navigation.Debuging
|
||||
{
|
||||
/// <summary>
|
||||
/// Listens for debug input combos and drives DebugNavigation.
|
||||
///
|
||||
/// — Gamepad : L2+R2 held 0.5s → unlock/lock. L2/R2 short press → prev/next (menu must be open).
|
||||
/// — Keyboard : backquote (² AZERTY / ~ QWERTY) → unlock/lock. ←/→ → prev/next.
|
||||
///
|
||||
/// Device display: switches based on last active input, not physical connection.
|
||||
/// </summary>
|
||||
public class DebugMenuUnlocker : MonoBehaviour
|
||||
{
|
||||
[Header("Data")]
|
||||
[SerializeField] private DebugNavigation _debugNavigation;
|
||||
|
||||
[Header("Timing")]
|
||||
[Tooltip("Duration in seconds L2+R2 must be held to toggle the debug menu.")]
|
||||
[SerializeField] private float _holdDuration = 0.5f;
|
||||
|
||||
[Header("Unlock Events")]
|
||||
public UnityEvent OnDebugMenuUnlockedGamepad;
|
||||
public UnityEvent OnDebugMenuUnlockedKeyboard;
|
||||
public UnityEvent OnDebugMenuLocked;
|
||||
|
||||
[Header("Navigation Events")]
|
||||
public UnityEvent OnPreviousScene;
|
||||
public UnityEvent OnNextScene;
|
||||
|
||||
[Header("Device Switch Events")]
|
||||
[Tooltip("Fired when gamepad input is detected and last input was keyboard/mouse.")]
|
||||
public UnityEvent OnSwitchedToGamepad;
|
||||
|
||||
[Tooltip("Fired when keyboard/mouse input is detected and last input was gamepad.")]
|
||||
public UnityEvent OnSwitchedToKeyboard;
|
||||
|
||||
// ── State ────────────────────────────────────────────────────────────
|
||||
|
||||
private bool _isUnlocked = false;
|
||||
private bool _isL2Held = false;
|
||||
private bool _isR2Held = false;
|
||||
private float _holdTimer = 0f;
|
||||
private bool _holdFired = false;
|
||||
private bool _lastWasGamepad = false;
|
||||
|
||||
// Input Actions
|
||||
private InputAction _keyboardUnlockAction;
|
||||
private InputAction _gamepadL2Action;
|
||||
private InputAction _gamepadR2Action;
|
||||
private InputAction _keyboardPreviousAction;
|
||||
private InputAction _keyboardNextAction;
|
||||
private InputAction _anyGamepadAction;
|
||||
private InputAction _anyKeyboardAction;
|
||||
|
||||
// ── Unity lifecycle ──────────────────────────────────────────────────
|
||||
|
||||
private void Awake() => BuildInputActions();
|
||||
private void OnEnable() => EnableActions();
|
||||
private void Start()
|
||||
{
|
||||
StartCoroutine(InitWhenReady());
|
||||
}
|
||||
|
||||
private IEnumerator InitWhenReady()
|
||||
{
|
||||
yield return new WaitUntil(() => OctoRef.Instance != null);
|
||||
yield return new WaitUntil(() => OctoRef.Instance.Navigation.CurrentScene != null);
|
||||
|
||||
SyncDebugNavigation();
|
||||
SubscribeToSceneEvents();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
DisableActions();
|
||||
UnsubscribeFromSceneEvents();
|
||||
}
|
||||
private void OnDestroy() => DisposeActions();
|
||||
private void Update() => TickHoldTimer();
|
||||
|
||||
// ── Build ────────────────────────────────────────────────────────────
|
||||
|
||||
private void BuildInputActions()
|
||||
{
|
||||
// Keyboard unlock toggle
|
||||
_keyboardUnlockAction = new InputAction("DebugUnlockKeyboard", InputActionType.Button);
|
||||
_keyboardUnlockAction.AddBinding("<Keyboard>/backquote");
|
||||
_keyboardUnlockAction.performed += _ => ToggleDebugMenu(false);
|
||||
|
||||
// Gamepad L2 / R2
|
||||
_gamepadL2Action = new InputAction("DebugL2", InputActionType.Button);
|
||||
_gamepadL2Action.AddBinding("<Gamepad>/leftTrigger")
|
||||
.WithProcessor("AxisDeadzone(min=0.5,max=1)");
|
||||
_gamepadL2Action.performed += _ => OnL2Down();
|
||||
_gamepadL2Action.canceled += _ => OnL2Up();
|
||||
|
||||
_gamepadR2Action = new InputAction("DebugR2", InputActionType.Button);
|
||||
_gamepadR2Action.AddBinding("<Gamepad>/rightTrigger")
|
||||
.WithProcessor("AxisDeadzone(min=0.5,max=1)");
|
||||
_gamepadR2Action.performed += _ => OnR2Down();
|
||||
_gamepadR2Action.canceled += _ => OnR2Up();
|
||||
|
||||
// Keyboard navigation
|
||||
_keyboardPreviousAction = new InputAction("DebugPrevKeyboard", InputActionType.Button);
|
||||
_keyboardPreviousAction.AddBinding("<Keyboard>/leftArrow");
|
||||
_keyboardPreviousAction.performed += _ => TryPrevious();
|
||||
|
||||
_keyboardNextAction = new InputAction("DebugNextKeyboard", InputActionType.Button);
|
||||
_keyboardNextAction.AddBinding("<Keyboard>/rightArrow");
|
||||
_keyboardNextAction.performed += _ => TryNext();
|
||||
|
||||
// Device detection — wildcard bindings
|
||||
// Note: on exclut les triggers du anyGamepad pour éviter les faux positifs
|
||||
// lors du relâchement après un hold unlock
|
||||
_anyGamepadAction = new InputAction("AnyGamepad", InputActionType.PassThrough);
|
||||
_anyGamepadAction.AddBinding("<Gamepad>/leftStick");
|
||||
_anyGamepadAction.AddBinding("<Gamepad>/rightStick");
|
||||
_anyGamepadAction.AddBinding("<Gamepad>/buttonSouth");
|
||||
_anyGamepadAction.AddBinding("<Gamepad>/buttonNorth");
|
||||
_anyGamepadAction.AddBinding("<Gamepad>/buttonEast");
|
||||
_anyGamepadAction.AddBinding("<Gamepad>/buttonWest");
|
||||
_anyGamepadAction.AddBinding("<Gamepad>/leftShoulder");
|
||||
_anyGamepadAction.AddBinding("<Gamepad>/rightShoulder");
|
||||
_anyGamepadAction.AddBinding("<Gamepad>/start");
|
||||
_anyGamepadAction.AddBinding("<Gamepad>/select");
|
||||
_anyGamepadAction.AddBinding("<Gamepad>/dpad");
|
||||
_anyGamepadAction.performed += _ => OnAnyGamepadInput();
|
||||
|
||||
_anyKeyboardAction = new InputAction("AnyKeyboard", InputActionType.PassThrough);
|
||||
_anyKeyboardAction.AddBinding("<Keyboard>/anyKey");
|
||||
_anyKeyboardAction.AddBinding("<Mouse>/delta");
|
||||
_anyKeyboardAction.AddBinding("<Mouse>/leftButton");
|
||||
_anyKeyboardAction.AddBinding("<Mouse>/rightButton");
|
||||
_anyKeyboardAction.performed += _ => OnAnyKeyboardInput();
|
||||
}
|
||||
|
||||
private void EnableActions()
|
||||
{
|
||||
_keyboardUnlockAction?.Enable();
|
||||
_gamepadL2Action?.Enable();
|
||||
_gamepadR2Action?.Enable();
|
||||
_keyboardPreviousAction?.Enable();
|
||||
_keyboardNextAction?.Enable();
|
||||
_anyGamepadAction?.Enable();
|
||||
_anyKeyboardAction?.Enable();
|
||||
}
|
||||
|
||||
private void DisableActions()
|
||||
{
|
||||
_keyboardUnlockAction?.Disable();
|
||||
_gamepadL2Action?.Disable();
|
||||
_gamepadR2Action?.Disable();
|
||||
_keyboardPreviousAction?.Disable();
|
||||
_keyboardNextAction?.Disable();
|
||||
_anyGamepadAction?.Disable();
|
||||
_anyKeyboardAction?.Disable();
|
||||
}
|
||||
|
||||
private void DisposeActions()
|
||||
{
|
||||
_keyboardUnlockAction?.Dispose();
|
||||
_gamepadL2Action?.Dispose();
|
||||
_gamepadR2Action?.Dispose();
|
||||
_keyboardPreviousAction?.Dispose();
|
||||
_keyboardNextAction?.Dispose();
|
||||
_anyGamepadAction?.Dispose();
|
||||
_anyKeyboardAction?.Dispose();
|
||||
}
|
||||
|
||||
// ── L2 / R2 hold logic ───────────────────────────────────────────────
|
||||
|
||||
private void OnL2Down()
|
||||
{
|
||||
_isL2Held = true;
|
||||
if (_isR2Held) StartHold();
|
||||
}
|
||||
|
||||
private void OnL2Up()
|
||||
{
|
||||
// Short press navigation — only if hold didn't fire
|
||||
if (_isUnlocked && !_holdFired && !_isR2Held)
|
||||
TryPrevious();
|
||||
|
||||
_isL2Held = false;
|
||||
|
||||
// Reset hold only when both triggers are released
|
||||
if (!_isR2Held) ResetHold();
|
||||
}
|
||||
|
||||
private void OnR2Down()
|
||||
{
|
||||
_isR2Held = true;
|
||||
if (_isL2Held) StartHold();
|
||||
}
|
||||
|
||||
private void OnR2Up()
|
||||
{
|
||||
// Short press navigation — only if hold didn't fire
|
||||
if (_isUnlocked && !_holdFired && !_isL2Held)
|
||||
TryNext();
|
||||
|
||||
_isR2Held = false;
|
||||
|
||||
// Reset hold only when both triggers are released
|
||||
if (!_isL2Held) ResetHold();
|
||||
}
|
||||
|
||||
private void StartHold()
|
||||
{
|
||||
_holdTimer = 0f;
|
||||
_holdFired = false;
|
||||
}
|
||||
|
||||
private void ResetHold()
|
||||
{
|
||||
_holdTimer = 0f;
|
||||
_holdFired = false;
|
||||
}
|
||||
|
||||
private void TickHoldTimer()
|
||||
{
|
||||
if (!_isL2Held || !_isR2Held || _holdFired) return;
|
||||
|
||||
_holdTimer += Time.unscaledDeltaTime;
|
||||
|
||||
if (_holdTimer >= _holdDuration)
|
||||
{
|
||||
_holdFired = true; // marque comme fired AVANT le toggle pour bloquer OnR2Up/OnL2Up
|
||||
ToggleDebugMenu(true);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Device switch ────────────────────────────────────────────────────
|
||||
|
||||
private void OnAnyGamepadInput()
|
||||
{
|
||||
if (_lastWasGamepad) return;
|
||||
_lastWasGamepad = true;
|
||||
OnSwitchedToGamepad?.Invoke();
|
||||
}
|
||||
|
||||
private void OnAnyKeyboardInput()
|
||||
{
|
||||
if (!_lastWasGamepad) return;
|
||||
_lastWasGamepad = false;
|
||||
OnSwitchedToKeyboard?.Invoke();
|
||||
}
|
||||
|
||||
// ── Logic ────────────────────────────────────────────────────────────
|
||||
|
||||
private void SubscribeToSceneEvents()
|
||||
{
|
||||
if (OctoRef.Instance == null) return;
|
||||
OctoRef.Instance.Navigation.OnSceneLoadCompleted += OnSceneLoadCompleted;
|
||||
}
|
||||
|
||||
private void UnsubscribeFromSceneEvents()
|
||||
{
|
||||
if (OctoRef.Instance == null) return;
|
||||
OctoRef.Instance.Navigation.OnSceneLoadCompleted -= OnSceneLoadCompleted;
|
||||
}
|
||||
|
||||
private void OnSceneLoadCompleted(OctoCore.Navigation.Data.OctoSceneData sceneData)
|
||||
{
|
||||
_debugNavigation?.SyncWithCurrentScene(sceneData);
|
||||
}
|
||||
|
||||
private void SyncDebugNavigation()
|
||||
{
|
||||
if (_debugNavigation == null || OctoRef.Instance == null) return;
|
||||
_debugNavigation.SyncWithCurrentScene(OctoRef.Instance.Navigation.CurrentScene);
|
||||
}
|
||||
|
||||
private void ToggleDebugMenu(bool isGamepad)
|
||||
{
|
||||
_isUnlocked = !_isUnlocked;
|
||||
|
||||
if (_isUnlocked)
|
||||
{
|
||||
SyncDebugNavigation();
|
||||
if (isGamepad) OnDebugMenuUnlockedGamepad?.Invoke();
|
||||
else OnDebugMenuUnlockedKeyboard?.Invoke();
|
||||
UnityEngine.Debug.Log($"[DebugMenuUnlocker] Unlocked ({(isGamepad ? "Gamepad" : "Keyboard")}).");
|
||||
}
|
||||
else
|
||||
{
|
||||
OnDebugMenuLocked?.Invoke();
|
||||
UnityEngine.Debug.Log("[DebugMenuUnlocker] Locked.");
|
||||
}
|
||||
}
|
||||
|
||||
private void TryPrevious()
|
||||
{
|
||||
if (!_isUnlocked || _debugNavigation == null) return;
|
||||
OnPreviousScene?.Invoke();
|
||||
_debugNavigation.Previous();
|
||||
}
|
||||
|
||||
private void TryNext()
|
||||
{
|
||||
if (!_isUnlocked || _debugNavigation == null) return;
|
||||
OnNextScene?.Invoke();
|
||||
_debugNavigation.Next();
|
||||
}
|
||||
|
||||
/// <summary>Called directly by Debug Menu UI buttons (0-based index).</summary>
|
||||
public void LoadSceneByIndex(int index)
|
||||
{
|
||||
if (_debugNavigation == null) return;
|
||||
_debugNavigation.LoadByIndex(index);
|
||||
}
|
||||
|
||||
public bool IsUnlocked => _isUnlocked;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e578ffb82b0e1c74bbb3a7e914b5b305
|
||||
Reference in New Issue
Block a user