vampire-like/Assets/Plugins/InputModule/Runtime/InputModuleComponent.cs

840 lines
27 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityGameFramework.Runtime;
namespace SepCore.InputModule.Runtime
{
[DisallowMultipleComponent]
[AddComponentMenu("Input Module")]
public sealed class InputModuleComponent : GameFrameworkComponent
{
private const string DefaultBindingOverrideSettingKey = "SepCore.InputModule.BindingOverrides";
[SerializeField] private InputActionAsset _inputActionsAsset = null;
[SerializeField] private InputContextId _startupContext = InputContextId.None;
[SerializeField] private bool _loadBindingOverridesOnInit = true;
[SerializeField] private bool _saveBindingOverridesOnDestroy = false;
[SerializeField] private string _bindingOverrideSettingKey = DefaultBindingOverrideSettingKey;
private readonly Dictionary<InputActionId, Action<InputCommand>> _listeners =
new Dictionary<InputActionId, Action<InputCommand>>();
private readonly Dictionary<InputContextId, InputActionMap> _actionMaps =
new Dictionary<InputContextId, InputActionMap>();
private readonly InputContextStack _contextStack = new InputContextStack();
private InputActionAsset _runtimeActions = null;
private SettingComponent _settingComponent = null;
private bool _isInitialized = false;
private bool _onInitCalled = false;
private bool _ownsRuntimeActions = false;
private InputActionRebindingExtensions.RebindingOperation _activeRebindOperation;
private bool _rebindCanceledByUser;
private IInputPromptMap _promptMap;
public event Action<InputCommand> CommandTriggered;
public event Action<InputDeviceKind> DeviceKindChanged;
public event Action<InputContextId> ContextChanged;
public event Action<RebindResult> RebindCompleted;
public InputDeviceKind CurrentDeviceKind { get; private set; } = InputDeviceKind.Unknown;
public InputContextId CurrentContext => _contextStack.Current;
public bool IsInitialized => _isInitialized;
public bool IsRebinding => _activeRebindOperation != null;
public IInputPromptMap PromptMap
{
get
{
if (_promptMap == null)
{
_promptMap = new InputModuleDefaultPromptMap(_runtimeActions);
}
return _promptMap;
}
set => _promptMap = value;
}
public InputActionAsset RuntimeActions
{
get
{
EnsureInitialized();
return _runtimeActions;
}
}
protected override void Awake()
{
base.Awake();
}
private void OnEnable()
{
if (_isInitialized)
{
ApplyContextState();
SubscribeToGlobalDeviceTracking();
}
}
private void OnDisable()
{
UnsubscribeFromGlobalDeviceTracking();
_runtimeActions?.Disable();
}
private void OnDestroy()
{
if (!_isInitialized)
{
return;
}
CancelRebinding();
if (_saveBindingOverridesOnDestroy)
{
SaveBindingOverrides();
}
UnsubscribeFromGlobalDeviceTracking();
UnsubscribeFromActions();
if (_ownsRuntimeActions && _runtimeActions != null)
{
if (Application.isPlaying)
{
Destroy(_runtimeActions);
}
else
{
DestroyImmediate(_runtimeActions);
}
}
_runtimeActions = null;
_actionMaps.Clear();
_listeners.Clear();
_isInitialized = false;
_onInitCalled = false;
}
public void RegisterListener(InputActionId actionId, Action<InputCommand> listener)
{
if (listener == null)
{
return;
}
EnsureInitialized();
_listeners[actionId] = _listeners.TryGetValue(actionId, out Action<InputCommand> existing)
? existing + listener
: listener;
}
public bool TryGetPrompt(InputActionId actionId, out InputPrompt prompt)
{
return PromptMap.TryGetPrompt(actionId, CurrentDeviceKind, out prompt);
}
public void InjectCommand(InputCommand command)
{
if (!_isInitialized)
{
return;
}
CommandTriggered?.Invoke(command);
if (_listeners.TryGetValue(command.ActionId, out Action<InputCommand> listener))
{
listener?.Invoke(command);
}
}
public void ForceDeviceKind(InputDeviceKind deviceKind)
{
if (deviceKind == InputDeviceKind.Unknown || deviceKind == CurrentDeviceKind)
{
return;
}
InputDeviceKind previous = CurrentDeviceKind;
CurrentDeviceKind = deviceKind;
Log.Debug("[InputModule] Device changed (forced): {0} -> {1}", previous, deviceKind);
DeviceKindChanged?.Invoke(CurrentDeviceKind);
}
public void UnregisterListener(InputActionId actionId, Action<InputCommand> listener)
{
if (listener == null || !_listeners.TryGetValue(actionId, out Action<InputCommand> existing))
{
return;
}
existing -= listener;
if (existing == null)
{
_listeners.Remove(actionId);
return;
}
_listeners[actionId] = existing;
}
public void OnInit()
{
if (_isInitialized)
{
Log.Debug("[InputModule] OnInit skipped - already initialized.");
return;
}
_onInitCalled = true;
EnsureInitialized();
ApplyContextState();
}
public void SetContext(InputContextId context)
{
EnsureInitialized();
if (_contextStack.Set(context))
{
ApplyContextState();
NotifyContextChanged();
Log.Debug("[InputModule] SetContext -> {0}", context);
}
}
public void SetUIContext()
{
SetContext(InputContextId.UI);
}
public void SetGameplayExploreContext()
{
SetContext(InputContextId.GameplayExplore);
}
public void PushContext(InputContextId context)
{
EnsureInitialized();
if (_contextStack.Push(context))
{
ApplyContextState();
NotifyContextChanged();
Log.Debug("[InputModule] PushContext -> {0} (stack depth: {1})", context, _contextStack.Count);
}
}
public bool PopContext()
{
EnsureInitialized();
if (!_contextStack.Pop(out _))
{
return false;
}
ApplyContextState();
NotifyContextChanged();
Log.Debug("[InputModule] PopContext -> {0} (stack depth: {1})", _contextStack.Current, _contextStack.Count);
return true;
}
public void ClearContexts()
{
EnsureInitialized();
if (_contextStack.Clear())
{
ApplyContextState();
NotifyContextChanged();
Log.Debug("[InputModule] ClearContexts");
}
}
public bool StartRebinding(
InputContextId contextId,
InputActionId actionId,
int bindingIndex,
float timeoutSeconds = 5f,
InputDeviceKind expectedDeviceKind = InputDeviceKind.Unknown)
{
EnsureInitialized();
if (_activeRebindOperation != null)
{
return false;
}
if (!TryGetAction(contextId, actionId, out InputAction action))
{
return false;
}
if (bindingIndex < 0 || bindingIndex >= action.bindings.Count)
{
return false;
}
InputBinding binding = action.bindings[bindingIndex];
string previousEffectivePath = binding.effectivePath;
string previousOverridePath = binding.overridePath;
_rebindCanceledByUser = false;
PushContext(InputContextId.Rebind);
try
{
DisableActionForRebind(action);
_activeRebindOperation = CreateRebindOperation(
action,
contextId,
actionId,
bindingIndex,
timeoutSeconds,
expectedDeviceKind,
previousEffectivePath,
previousOverridePath);
_activeRebindOperation.Start();
}
catch
{
_activeRebindOperation?.Dispose();
_activeRebindOperation = null;
if (_contextStack.Current == InputContextId.Rebind)
{
PopContext();
}
throw;
}
RebindCompleted?.Invoke(new RebindResult(RebindStatus.Started, contextId, actionId, bindingIndex, previousEffectivePath, previousEffectivePath));
return true;
}
public void CancelRebinding()
{
if (_activeRebindOperation == null)
{
return;
}
_rebindCanceledByUser = true;
_activeRebindOperation.Cancel();
}
public void ResetBindingToDefault(InputContextId contextId, InputActionId actionId, int bindingIndex, bool saveAfterReset = false)
{
EnsureInitialized();
if (!TryGetAction(contextId, actionId, out InputAction action))
{
return;
}
if (bindingIndex < 0 || bindingIndex >= action.bindings.Count)
{
return;
}
action.RemoveBindingOverride(bindingIndex);
if (saveAfterReset)
{
SaveBindingOverrides();
}
}
public void ResetActionToDefaults(InputContextId contextId, InputActionId actionId, bool saveAfterReset = false)
{
EnsureInitialized();
if (!TryGetAction(contextId, actionId, out InputAction action))
{
return;
}
action.RemoveAllBindingOverrides();
if (saveAfterReset)
{
SaveBindingOverrides();
}
}
public void ResetAllBindingsToDefaults(bool saveAfterReset = false)
{
EnsureInitialized();
_runtimeActions.RemoveAllBindingOverrides();
if (saveAfterReset)
{
SaveBindingOverrides();
}
}
public InputBindingSnapshot SaveBindingOverrides()
{
EnsureInitialized();
string overridesJson = _runtimeActions.SaveBindingOverridesAsJson();
string storedData = BindingOverridePersistence.Serialize(overridesJson);
SettingComponent settingComponent = GetSettingComponent();
if (settingComponent != null)
{
settingComponent.SetString(_bindingOverrideSettingKey, storedData);
}
return new InputBindingSnapshot(overridesJson);
}
public void LoadBindingOverrides()
{
EnsureInitialized();
LoadBindingOverridesCore();
}
private void LoadBindingOverridesCore()
{
if (_runtimeActions == null)
{
return;
}
SettingComponent settingComponent = GetSettingComponent();
if (settingComponent == null || !settingComponent.HasSetting(_bindingOverrideSettingKey))
{
return;
}
string storedData = settingComponent.GetString(_bindingOverrideSettingKey);
if (!BindingOverridePersistence.TryDeserialize(storedData, out string overridesJson))
{
return;
}
if (string.IsNullOrEmpty(overridesJson))
{
return;
}
_runtimeActions.LoadBindingOverridesFromJson(overridesJson);
}
public void ApplyBindingSnapshot(InputBindingSnapshot snapshot, bool saveAfterApply = false)
{
EnsureInitialized();
_runtimeActions.LoadBindingOverridesFromJson(snapshot.OverridesJson);
if (saveAfterApply)
{
SaveBindingOverrides();
}
}
private void EnsureInitialized()
{
if (_isInitialized)
{
return;
}
_runtimeActions = _inputActionsAsset != null
? Instantiate(_inputActionsAsset)
: InputModuleDefaultActions.Create();
_ownsRuntimeActions = true;
CacheActionMaps();
SubscribeToActions();
_isInitialized = true;
SubscribeToGlobalDeviceTracking();
if (_promptMap is InputModuleDefaultPromptMap)
{
_promptMap = null;
}
int mapCount = _actionMaps.Count;
if (mapCount == 0)
{
Log.Warning("[InputModule] Initialized but no action maps were cached. Check that action map names match InputContextId enum values.");
}
else
{
Log.Info("[InputModule] Initialized. Source: {0}, Cached maps: {1}, Startup context: {2}",
_inputActionsAsset != null ? "Serialized asset" : "Default actions",
mapCount,
_startupContext);
}
if (_loadBindingOverridesOnInit)
{
LoadBindingOverridesCore();
}
_contextStack.Set(_startupContext);
if (!_onInitCalled)
{
Log.Warning("[InputModule] Auto-initialized before OnInit() was called. Action maps will not be enabled until OnInit() is invoked.");
}
}
private void CacheActionMaps()
{
_actionMaps.Clear();
foreach (InputActionMap map in _runtimeActions.actionMaps)
{
if (TryParseContextId(map.name, out InputContextId contextId))
{
_actionMaps[contextId] = map;
}
else
{
Log.Warning("[InputModule] ActionMap '{0}' does not match any InputContextId and will be ignored.", map.name);
}
}
}
private void SubscribeToActions()
{
foreach (InputAction action in _runtimeActions)
{
action.started += OnActionTriggered;
action.performed += OnActionTriggered;
action.canceled += OnActionTriggered;
}
}
private void UnsubscribeFromActions()
{
if (_runtimeActions == null)
{
return;
}
foreach (InputAction action in _runtimeActions)
{
action.started -= OnActionTriggered;
action.performed -= OnActionTriggered;
action.canceled -= OnActionTriggered;
}
}
private void SubscribeToGlobalDeviceTracking()
{
InputSystem.onActionChange -= OnGlobalActionChange;
InputSystem.onActionChange += OnGlobalActionChange;
}
private void UnsubscribeFromGlobalDeviceTracking()
{
InputSystem.onActionChange -= OnGlobalActionChange;
}
private void OnGlobalActionChange(object obj, InputActionChange change)
{
if (change == InputActionChange.ActionPerformed && obj is InputAction.CallbackContext context)
{
UpdateDeviceKind(context);
}
}
// Disables all action maps, then enables Global + the topmost context in the stack.
// Only called from OnInit() (explicit activation) and OnEnable() (re-enable after disable).
// The stack remembers context history for Pop restoration; only the current (topmost)
// context is active at any time, preventing overlapping bindings from stacked maps.
private void ApplyContextState()
{
if (_runtimeActions == null)
{
return;
}
_runtimeActions.Disable();
if (_actionMaps.TryGetValue(InputContextId.Global, out InputActionMap globalMap))
{
globalMap.Enable();
}
InputContextId current = _contextStack.Current;
if (current != InputContextId.None && _actionMaps.TryGetValue(current, out InputActionMap currentMap))
{
currentMap.Enable();
}
}
private void NotifyContextChanged()
{
ContextChanged?.Invoke(CurrentContext);
}
private void OnActionTriggered(InputAction.CallbackContext context)
{
if (_activeRebindOperation != null)
{
return;
}
InputActionId actionId = ParseActionId(context);
InputContextId contextId = ParseContextId(context);
InputDeviceKind deviceKind = UpdateDeviceKind(context);
InputCommand command = CreateCommand(actionId, contextId, deviceKind, context);
CommandTriggered?.Invoke(command);
if (_listeners.TryGetValue(actionId, out Action<InputCommand> listener))
{
listener?.Invoke(command);
}
}
private InputDeviceKind UpdateDeviceKind(InputAction.CallbackContext context)
{
InputDeviceKind deviceKind = InputDeviceKindUtility.FromDevice(context.control?.device);
if (deviceKind != InputDeviceKind.Unknown && deviceKind != CurrentDeviceKind)
{
InputDeviceKind previous = CurrentDeviceKind;
CurrentDeviceKind = deviceKind;
Log.Debug("[InputModule] Device changed: {0} -> {1}", previous, deviceKind);
DeviceKindChanged?.Invoke(CurrentDeviceKind);
}
return CurrentDeviceKind;
}
private static InputCommand CreateCommand(
InputActionId actionId,
InputContextId contextId,
InputDeviceKind deviceKind,
InputAction.CallbackContext context)
{
Vector2 vector2Value = Vector2.zero;
float scalarValue = 0f;
if (context.action.expectedControlType == "Vector2")
{
vector2Value = context.ReadValue<Vector2>();
scalarValue = vector2Value.magnitude;
}
else
{
scalarValue = context.ReadValue<float>();
}
return new InputCommand(
actionId,
contextId,
ConvertPhase(context.phase),
deviceKind,
vector2Value,
scalarValue,
context.time);
}
private SettingComponent GetSettingComponent()
{
if (_settingComponent == null)
{
_settingComponent = UnityGameFramework.Runtime.GameEntry.GetComponent<SettingComponent>();
}
return _settingComponent;
}
private static InputActionId ParseActionId(InputAction.CallbackContext context)
{
return Enum.TryParse(context.action.name, out InputActionId actionId)
? actionId
: InputActionId.Unknown;
}
private static InputContextId ParseContextId(InputAction.CallbackContext context)
{
return TryParseContextId(context.action.actionMap.name, out InputContextId contextId)
? contextId
: InputContextId.None;
}
private static bool TryParseContextId(string rawContextId, out InputContextId contextId)
{
return Enum.TryParse(rawContextId, out contextId);
}
private static InputCommandPhase ConvertPhase(InputActionPhase phase)
{
switch (phase)
{
case InputActionPhase.Started:
return InputCommandPhase.Started;
case InputActionPhase.Canceled:
return InputCommandPhase.Canceled;
default:
return InputCommandPhase.Performed;
}
}
private bool TryGetAction(InputContextId contextId, InputActionId actionId, out InputAction action)
{
action = null;
if (!_actionMaps.TryGetValue(contextId, out InputActionMap map))
{
return false;
}
action = map.FindAction(actionId.ToString());
return action != null;
}
private bool DetectConflict(InputAction action, int bindingIndex, string newPath)
{
if (string.IsNullOrEmpty(newPath))
{
return false;
}
InputActionMap targetMap = action.actionMap;
InputBinding targetBinding = action.bindings[bindingIndex];
foreach (InputActionMap map in _runtimeActions.actionMaps)
{
if (map != targetMap && map.name != nameof(InputContextId.Global))
{
continue;
}
foreach (InputBinding binding in map.bindings)
{
if (map == targetMap && binding.id == targetBinding.id)
{
continue;
}
if (string.Equals(binding.effectivePath, newPath, System.StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
return false;
}
private InputActionRebindingExtensions.RebindingOperation CreateRebindOperation(
InputAction action,
InputContextId contextId,
InputActionId actionId,
int bindingIndex,
float timeoutSeconds,
InputDeviceKind expectedDeviceKind,
string previousEffectivePath,
string previousOverridePath)
{
InputActionRebindingExtensions.RebindingOperation operation = new InputActionRebindingExtensions.RebindingOperation()
.WithAction(action)
.WithTargetBinding(bindingIndex)
.WithTimeout(timeoutSeconds)
.WithControlsExcluding("<Pointer>/delta")
.WithControlsExcluding("<Pointer>/position")
.WithControlsExcluding("<Touchscreen>/touch*/position")
.WithControlsExcluding("<Touchscreen>/touch*/delta")
.WithControlsExcluding("<Mouse>/clickCount")
.WithMatchingEventsBeingSuppressed()
.OnCancel(_ =>
{
RebindStatus status = _rebindCanceledByUser ? RebindStatus.Canceled : RebindStatus.Timeout;
CompleteRebind(new RebindResult(status, contextId, actionId, bindingIndex, action.bindings[bindingIndex].effectivePath, previousEffectivePath));
})
.OnComplete(rebindOperation =>
{
InputDeviceKind actualDeviceKind = InputDeviceKindUtility.FromDevice(rebindOperation.selectedControl?.device);
if (expectedDeviceKind != InputDeviceKind.Unknown && actualDeviceKind != expectedDeviceKind)
{
RevertBinding(action, bindingIndex, previousOverridePath);
CompleteRebind(new RebindResult(RebindStatus.Canceled, contextId, actionId, bindingIndex, action.bindings[bindingIndex].effectivePath, previousEffectivePath));
return;
}
string newPath = action.bindings[bindingIndex].effectivePath;
if (DetectConflict(action, bindingIndex, newPath))
{
RevertBinding(action, bindingIndex, previousOverridePath);
CompleteRebind(new RebindResult(RebindStatus.ConflictDetected, contextId, actionId, bindingIndex, newPath, previousEffectivePath));
return;
}
CompleteRebind(new RebindResult(RebindStatus.Succeeded, contextId, actionId, bindingIndex, newPath, previousEffectivePath));
});
if (operation.expectedControlType != "Button")
{
operation.WithCancelingThrough("<Keyboard>/escape");
}
return operation;
}
private void CompleteRebind(RebindResult result)
{
_activeRebindOperation?.Dispose();
_activeRebindOperation = null;
if (_contextStack.Current == InputContextId.Rebind)
{
PopContext();
}
RebindCompleted?.Invoke(result);
}
private static void RevertBinding(InputAction action, int bindingIndex, string previousOverridePath)
{
if (string.IsNullOrEmpty(previousOverridePath))
{
action.RemoveBindingOverride(bindingIndex);
}
else
{
action.ApplyBindingOverride(bindingIndex, previousOverridePath);
}
}
private static void DisableActionForRebind(InputAction action)
{
InputActionMap map = action.actionMap;
if (map == null)
return;
if (map.enabled)
{
map.Disable();
return;
}
// Map is already disabled but action.enabled may still report true
// because map.Disable() bails early when m_Enabled is already false.
// Force-sync by briefly enabling and re-disabling the map.
if (action.enabled)
{
map.Enable();
map.Disable();
}
}
}
}