备份,准备做双端
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Manages gameplay input via Unity's Input System, creating InputActions at runtime
|
||||
/// and capturing sub-frame press timestamps (AudioSettings.dspTime) for precise judgement.
|
||||
/// Replaces legacy Input.GetKeyDown polling with event-driven callbacks.
|
||||
/// </summary>
|
||||
public class GameplayInputActions : MonoBehaviour
|
||||
{
|
||||
public static GameplayInputActions Instance { get; private set; }
|
||||
|
||||
// Track press/release events. Args: (trackIndex, dspTime at the input event) for sub-frame precision.
|
||||
public static event Action<int, double> OnTrackPressed;
|
||||
public static event Action<int, double> OnTrackReleased;
|
||||
|
||||
private InputAction[] trackActions = new InputAction[5];
|
||||
private InputAction secondaryTrack3Action;
|
||||
private bool isEnabled;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
CreateActions();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
EnableActions();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
DisableActions();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
DisableActions();
|
||||
DisposeActions();
|
||||
|
||||
if (Instance == this)
|
||||
Instance = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create InputActions at runtime and bind them to the keys from KeyBindingManager.
|
||||
/// Called once at Awake.
|
||||
/// </summary>
|
||||
private void CreateActions()
|
||||
{
|
||||
string[] colors = { "red", "green", "yellow", "purple", "blue" };
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
trackActions[i] = new InputAction($"Track{i}", InputActionType.Button);
|
||||
string bindingPath = KeyBindingManager.GetBindingPathForTrack(i);
|
||||
if (!string.IsNullOrEmpty(bindingPath))
|
||||
{
|
||||
trackActions[i].AddBinding(bindingPath);
|
||||
}
|
||||
|
||||
int trackIndex = i; // capture for closure
|
||||
trackActions[i].started += ctx => HandleTrackPress(trackIndex, ctx);
|
||||
trackActions[i].canceled += ctx => HandleTrackRelease(trackIndex, ctx);
|
||||
}
|
||||
|
||||
// Secondary track 3 key (yellow副键)
|
||||
secondaryTrack3Action = new InputAction("Track3Secondary", InputActionType.Button);
|
||||
string secondaryPath = KeyBindingManager.GetSecondaryTrack3BindingPath();
|
||||
if (!string.IsNullOrEmpty(secondaryPath))
|
||||
{
|
||||
secondaryTrack3Action.AddBinding(secondaryPath);
|
||||
}
|
||||
secondaryTrack3Action.started += ctx => HandleTrackPress(2, ctx); // track 2 = yellow
|
||||
secondaryTrack3Action.canceled += ctx => HandleTrackRelease(2, ctx);
|
||||
}
|
||||
|
||||
private void EnableActions()
|
||||
{
|
||||
if (isEnabled) return;
|
||||
|
||||
foreach (var action in trackActions)
|
||||
{
|
||||
action?.Enable();
|
||||
}
|
||||
secondaryTrack3Action?.Enable();
|
||||
|
||||
isEnabled = true;
|
||||
}
|
||||
|
||||
private void DisableActions()
|
||||
{
|
||||
if (!isEnabled) return;
|
||||
|
||||
foreach (var action in trackActions)
|
||||
{
|
||||
action?.Disable();
|
||||
}
|
||||
secondaryTrack3Action?.Disable();
|
||||
|
||||
isEnabled = false;
|
||||
}
|
||||
|
||||
private void DisposeActions()
|
||||
{
|
||||
foreach (var action in trackActions)
|
||||
{
|
||||
action?.Dispose();
|
||||
}
|
||||
secondaryTrack3Action?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refresh bindings from KeyBindingManager after user rebinds a key.
|
||||
/// Uses ApplyBindingOverride so the action objects (and their event subscriptions) stay intact;
|
||||
/// each action has exactly one binding (index 0) created in CreateActions.
|
||||
/// </summary>
|
||||
public void RefreshBindings()
|
||||
{
|
||||
bool wasEnabled = isEnabled;
|
||||
DisableActions();
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
if (trackActions[i] == null) continue;
|
||||
string path = KeyBindingManager.GetBindingPathForTrack(i);
|
||||
ApplySingleBinding(trackActions[i], path);
|
||||
}
|
||||
|
||||
if (secondaryTrack3Action != null)
|
||||
{
|
||||
string secondaryPath = KeyBindingManager.GetSecondaryTrack3BindingPath();
|
||||
ApplySingleBinding(secondaryTrack3Action, secondaryPath);
|
||||
}
|
||||
|
||||
if (wasEnabled) EnableActions();
|
||||
}
|
||||
|
||||
// Set an action's single binding path. Actions are created with exactly one binding (index 0);
|
||||
// if none exists yet (empty path at creation), add one now.
|
||||
private static void ApplySingleBinding(InputAction action, string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
|
||||
if (action.bindings.Count == 0)
|
||||
{
|
||||
action.AddBinding(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
action.ApplyBindingOverride(0, path);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleTrackPress(int trackIndex, InputAction.CallbackContext context)
|
||||
{
|
||||
// Sub-frame precise timestamp: convert the Input System event time (realtime base)
|
||||
// into the dspTime axis the whole judgement chain already runs on.
|
||||
OnTrackPressed?.Invoke(trackIndex, ResolveEventDspTime(context));
|
||||
}
|
||||
|
||||
private void HandleTrackRelease(int trackIndex, InputAction.CallbackContext context)
|
||||
{
|
||||
OnTrackReleased?.Invoke(trackIndex, ResolveEventDspTime(context));
|
||||
}
|
||||
|
||||
// Upper bound (seconds) on how far back an event timestamp may sit before we distrust it.
|
||||
// Real input latency between the event occurring and this callback is a few ms at most;
|
||||
// anything larger means the timestamp is unusable (e.g. synthetic/queued event) and we
|
||||
// fall back to the previous behaviour for exact equivalence.
|
||||
private const double MaxEventLatencySeconds = 0.05;
|
||||
|
||||
/// <summary>
|
||||
/// Convert an Input System event time (Time.realtimeSinceStartup base) to the
|
||||
/// AudioSettings.dspTime axis used by GameplayClock. The event physically occurred
|
||||
/// `latency` seconds before this callback runs, so the equivalent dsp instant is
|
||||
/// dspTime(now) - latency. Recovers sub-audio-buffer precision without introducing any
|
||||
/// systematic offset. Degrades EXACTLY to AudioSettings.dspTime (the prior behaviour)
|
||||
/// whenever the event timestamp is missing or out of a sane range — business logic equivalent.
|
||||
/// </summary>
|
||||
private static double ResolveEventDspTime(InputAction.CallbackContext context)
|
||||
{
|
||||
double dspNow = AudioSettings.dspTime;
|
||||
|
||||
double eventTime = context.time;
|
||||
if (eventTime <= 0d) return dspNow; // no usable timestamp
|
||||
|
||||
double realtimeNow = Time.realtimeSinceStartupAsDouble;
|
||||
double latency = realtimeNow - eventTime;
|
||||
|
||||
// Negative (clock skew) or implausibly large latency → distrust, keep old behaviour.
|
||||
if (latency < 0d || latency > MaxEventLatencySeconds) return dspNow;
|
||||
|
||||
return dspNow - latency;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user