using System.Collections.Generic;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
namespace UnityEngine.XR.Content.Interaction
{
///
/// Allows for actions to 'lock' the input controls they use so that others actions using the same controls will not receive input at the same time.
/// InputMediator works by injecting input processors on every binding in active action maps.
/// Usage is via and .
///
public static class InputMediator
{
///
/// Substring of the names that all of the input processors that are injected have.
///
///
const string k_ConsumeKey = "Consume";
static bool s_Updating;
// Data associated with each control, storing if an action has locked it,
// and other actions that are allowed to make use of this control at the same time
class ConsumptionState
{
public int m_LockedAction = -1;
public int m_AllowedAction1 = -1;
public int m_AllowedAction2 = -1;
public bool m_Automatic;
}
///
/// Generic Consumption processor - handles all the aspects of looking up actions that have locked a control
/// Implementations merely need to implement the methods to determine if a control has returned to rest (and thus should reset)
/// And the 'identity' value of a control, which is the value it should have when the control is locked
///
///
abstract class ConsumeProcessor : InputProcessor where TValue : struct
{
public int m_ActionIndex = -1;
public override TValue Process(TValue value, InputControl control)
{
// Check the dictionary for this control
// If it does not exist, proceed unhindered
if (control == null || !s_ConsumedControls.TryGetValue(control, out var currentState))
return value;
// If there is no locked action, also proceed unhindered
if (currentState.m_LockedAction == -1)
return value;
// Check for an action match
var actionMatched = (currentState.m_LockedAction == m_ActionIndex) || (currentState.m_AllowedAction1 == m_ActionIndex) || (currentState.m_AllowedAction2 == m_ActionIndex);
// Check if we should automatically release
if (actionMatched)
{
if (currentState.m_Automatic && ValueNearZero(value))
currentState.m_LockedAction = -1;
return value;
}
return IdentityValue();
}
public abstract bool ValueNearZero(TValue value);
public abstract TValue IdentityValue();
}
class ConsumeFloat : ConsumeProcessor
{
public override bool ValueNearZero(float value)
{
return value < float.Epsilon;
}
public override float IdentityValue()
{
return 0.0f;
}
}
class ConsumeVector2 : ConsumeProcessor
{
public override bool ValueNearZero(Vector2 value)
{
return value.sqrMagnitude < float.Epsilon;
}
public override Vector2 IdentityValue()
{
return Vector2.zero;
}
}
class ConsumeVector3 : ConsumeProcessor
{
public override bool ValueNearZero(Vector3 value)
{
return value.sqrMagnitude < float.Epsilon;
}
public override Vector3 IdentityValue()
{
return Vector3.zero;
}
}
class ConsumeQuaternion : ConsumeProcessor
{
public override bool ValueNearZero(Quaternion value)
{
return Quaternion.Angle(value, Quaternion.identity) < float.Epsilon;
}
public override Quaternion IdentityValue()
{
return Quaternion.identity;
}
}
static Dictionary s_ConsumedControls = new Dictionary();
static Dictionary s_ActionIndices = new Dictionary();
static HashSet s_InitializedActions = new HashSet();
[RuntimeInitializeOnLoadMethod]
static void Initialize()
{
InputSystem.InputSystem.RegisterProcessor(nameof(ConsumeFloat));
InputSystem.InputSystem.RegisterProcessor(nameof(ConsumeVector2));
InputSystem.InputSystem.RegisterProcessor(nameof(ConsumeVector3));
InputSystem.InputSystem.RegisterProcessor(nameof(ConsumeQuaternion));
Application.quitting += OnApplicationQuitting;
InputSystem.InputSystem.onActionChange += OnActionChange;
InitializeConsumeProcessors();
}
static void OnApplicationQuitting()
{
InputSystem.InputSystem.onActionChange -= OnActionChange;
}
///
/// Attempts to 'lock' the controls belonging to an action - which means other actions using the same control will only get zero/identity values during this time
///
/// The action that should lock their controls
/// If the control lock should release automatically when the controls go to a resting state
/// If the action should forcefully take a lock from another consuming action
/// An additional action that can access these controls at this time
/// An additional action that can access these controls at this time
/// False if _any_ of the associated controls were unable to be locked
public static bool ConsumeControl(InputAction source, bool automaticRelease, bool force = false, InputAction friendAction1 = null, InputAction friendAction2 = null)
{
if (source == null)
return false;
var actionIndex1 = GetActionIndex(source);
var actionIndex2 = GetActionIndex(friendAction1);
var actionIndex3 = GetActionIndex(friendAction2);
var lockCount = 0;
var sourceControls = source.controls;
foreach (var currentControl in sourceControls)
{
// Check to see if it is in the list already
// If not, make an entry for it
if (!s_ConsumedControls.TryGetValue(currentControl, out var controlState))
{
var parent = currentControl.parent;
if (currentControl is AxisControl && parent is Vector2Control)
{
if (!s_ConsumedControls.TryGetValue(parent, out controlState))
{
controlState = new ConsumptionState { m_Automatic = automaticRelease };
s_ConsumedControls.Add(parent, controlState);
}
}
else
{
controlState = new ConsumptionState { m_Automatic = automaticRelease };
}
s_ConsumedControls.Add(currentControl, controlState);
}
if (force || controlState.m_LockedAction == -1)
{
controlState.m_LockedAction = actionIndex1;
controlState.m_AllowedAction1 = actionIndex2;
controlState.m_AllowedAction2 = actionIndex3;
lockCount++;
}
}
return (lockCount == sourceControls.Count);
}
///
/// Releases an action's lock over its associated controls. Other actions using the same controls will begin receiving input again
///
/// The action that is attempting to release its lock
/// If this input lock should be released regardless of requesting action
/// False if _any_ of the associated controls were unable to be released
public static bool ReleaseControl(InputAction source, bool force = false)
{
if (source == null)
return false;
var actionIndex = GetActionIndex(source);
var lockCount = 0;
var sourceControls = source.controls;
foreach (var currentControl in sourceControls)
{
// Check to see if it is in the list already
// If not, nothing to release
if (!s_ConsumedControls.TryGetValue(currentControl, out var controlState))
{
lockCount++;
continue;
}
if (force || controlState.m_LockedAction == actionIndex)
{
controlState.m_LockedAction = -1;
lockCount++;
}
}
return (lockCount == sourceControls.Count);
}
static void InitializeConsumeProcessors()
{
s_Updating = true;
var actionList = InputSystem.InputSystem.ListEnabledActions();
foreach (var action in actionList)
{
EnsureConsumeProcessorAdded(action);
// Since this list only contains currently enabled actions,
// any actions that are enabled later will need to
// have the consume processor added. Since those actions may not
// trigger a BoundControlsChanged change, the OnActionChange event handler
// will check against this list and append to it as actions are enabled.
// This set is checked against for performance reasons
// to avoid the more costly EnsureConsumeProcessorAdded(InputAction) method.
s_InitializedActions.Add(action);
}
s_Updating = false;
}
static void OnActionChange(object actionSource, InputActionChange change)
{
if (s_Updating)
return;
s_Updating = true;
if (change == InputActionChange.ActionEnabled)
{
var action = (InputAction)actionSource;
if (s_InitializedActions.Add(action))
EnsureConsumeProcessorAdded(action);
}
else if (change == InputActionChange.ActionMapEnabled)
{
var actionMap = (InputActionMap)actionSource;
foreach (var action in actionMap.actions)
{
if (s_InitializedActions.Add(action))
EnsureConsumeProcessorAdded(action);
}
}
else if (change == InputActionChange.BoundControlsChanged)
{
// We skip pure actions here as they can get into an invalid state if bindings were changed
if (actionSource is InputActionMap actionMap)
{
EnsureConsumeProcessorAdded(actionMap);
}
else if (actionSource is InputActionAsset actionAsset)
{
EnsureConsumeProcessorAdded(actionAsset);
}
}
s_Updating = false;
}
static string ControlTypeToConsumeType(string controlType)
{
switch (controlType)
{
case "Single":
case "Button":
case "float":
return nameof(ConsumeFloat);
case "Vector2":
return nameof(ConsumeVector2);
case "Vector3":
return nameof(ConsumeVector3);
case "Quaternion":
return nameof(ConsumeQuaternion);
}
return "";
}
static string ProcessBindingControl(string bindingPath)
{
var control = InputSystem.InputSystem.FindControl(bindingPath);
var consumeType = "";
if (control != null)
consumeType = ControlTypeToConsumeType(control.valueType.Name);
else
{
// Try to fall back based on path keywords
var bindingLower = bindingPath.ToLower();
if (bindingLower.EndsWith("position"))
consumeType = ControlTypeToConsumeType("Vector3");
if (bindingLower.EndsWith("rotation"))
consumeType = ControlTypeToConsumeType("Quaternion");
if (bindingLower.EndsWith("x"))
consumeType = ControlTypeToConsumeType("float");
if (bindingLower.EndsWith("y"))
consumeType = ControlTypeToConsumeType("float");
if (bindingLower.EndsWith("axis"))
consumeType = ControlTypeToConsumeType("Vector2");
}
if (string.IsNullOrEmpty(consumeType))
return "";
return consumeType;
}
static void EnsureConsumeProcessorAdded(InputAction action)
{
var bindingCount = action.bindings.Count;
for (var i = 0; i < bindingCount; i++)
{
var currentBinding = action.bindings[i];
// Ignore composites, but not parts of composites
if (currentBinding.isComposite)
continue;
// Ignore bindings that aren't ready yet
if (currentBinding.effectiveProcessors == null)
continue;
var actionIndex = GetActionIndex(action);
if (!currentBinding.effectiveProcessors.Contains(k_ConsumeKey))
{
// Ignore unused bindings
if (string.IsNullOrEmpty(currentBinding.path))
continue;
// Get the binding's control type and cache it in the control lookup
var bindingType = ProcessBindingControl(currentBinding.path);
// If the composite can't figure out its type, then skip it
if (string.IsNullOrEmpty(bindingType))
{
//Debug.LogWarning($"Could not add consume processor for binding { currentBinding.path }, in {action.name}");
continue;
}
if (currentBinding.processors.Length > 0)
action.ApplyBindingOverride(i, new InputBinding { overrideProcessors = $"{bindingType}(m_ActionIndex={actionIndex}), {currentBinding.processors}" });
else
action.ApplyBindingOverride(i, new InputBinding { overrideProcessors = $"{bindingType}(m_ActionIndex={actionIndex})" });
}
}
}
static void EnsureConsumeProcessorAdded(InputActionMap actionMap)
{
foreach (var action in actionMap.actions)
{
EnsureConsumeProcessorAdded(action);
}
}
static void EnsureConsumeProcessorAdded(InputActionAsset actionAsset)
{
foreach (var map in actionAsset.actionMaps)
{
EnsureConsumeProcessorAdded(map);
}
}
static int GetActionIndex(InputAction source)
{
if (source == null)
return -1;
if (!s_ActionIndices.TryGetValue(source, out var actionIndex))
{
actionIndex = s_ActionIndices.Count;
s_ActionIndices.Add(source, actionIndex);
}
return actionIndex;
}
}
}