using System;
using System.Collections.Generic;
using System.ComponentModel;
using UnityEngine.InputSystem;
namespace UnityEngine.XR.Interaction.Toolkit.Examples
{
///
/// Use this class as a central manager to configure locomotion control schemes and configuration preferences.
///
///
/// Input bindings will often overlap between different locomotion methods, and this class can be used to
/// set binding masks which are used to determine which bindings of an action to enable and which to ignore.
///
///
/// Teleport (Input Action)
/// (1) Binding <XRController>{LeftHand}/PrimaryButton (Use in control scheme "Generic XR")
/// (2) Binding <XRController>{LeftHand}/Primary2DAxis (Use in control scheme "Noncontinuous Move")
/// Move (Input Action)
/// (3) Binding <XRController>{LeftHand}/Primary2DAxis (Use in control scheme "Continuous Move")
///
/// Set ="Generic XR"
/// Set ="Noncontinuous Move"
/// Set ="Continuous Move"
/// Set to be both input actions (Teleport and Move).
///
/// When =,
/// bindings (1) and (2) will be enabled, but binding (3) will be disabled.
///
/// When =,
/// bindings (1) and (3) will be enabled, but binding (2) will be disabled.
///
///
public class LocomotionSchemeManager : MonoBehaviour
{
///
/// Sets which movement control scheme to use.
///
///
public enum MoveScheme
{
///
/// Use noncontinuous movement control scheme.
///
Noncontinuous,
///
/// Use continuous movement control scheme.
///
Continuous,
}
///
/// Sets which turn style of locomotion to use.
///
///
public enum TurnStyle
{
///
/// Use snap turning to rotate the direction you are facing by snapping by a specified angle.
///
Snap,
///
/// Use continuous turning to smoothly rotate the direction you are facing by a specified speed.
///
Continuous,
}
///
/// Sets which orientation the forward direction of continuous movement is relative to.
///
///
///
public enum MoveForwardSource
{
///
/// Use to continuously move in a direction based on the head orientation.
///
Head,
///
/// Use to continuously move in a direction based on the left hand orientation.
///
LeftHand,
///
/// Use to continuously move in a direction based on the right hand orientation.
///
RightHand,
}
[SerializeField]
[Tooltip("Controls which movement control scheme to use.")]
MoveScheme m_MoveScheme;
///
/// Controls which movement control scheme to use.
///
///
public MoveScheme moveScheme
{
get => m_MoveScheme;
set
{
SetMoveScheme(value);
m_MoveScheme = value;
}
}
[SerializeField]
[Tooltip("Controls which turn style of locomotion to use.")]
TurnStyle m_TurnStyle;
///
/// Controls which turn style of locomotion to use.
///
///
public TurnStyle turnStyle
{
get => m_TurnStyle;
set
{
SetTurnStyle(value);
m_TurnStyle = value;
}
}
[SerializeField]
[Tooltip("Controls which orientation the forward direction of continuous movement is relative to.")]
MoveForwardSource m_MoveForwardSource;
///
/// Controls which orientation the forward direction of continuous movement is relative to.
///
///
public MoveForwardSource moveForwardSource
{
get => m_MoveForwardSource;
set
{
SetMoveForwardSource(value);
m_MoveForwardSource = value;
}
}
[SerializeField]
[Tooltip("Input action assets associated with locomotion to affect when the active movement control scheme is set." +
" Can use this list by itself or together with the Action Maps list to set control scheme masks by Asset or Map.")]
List m_ActionAssets;
///
/// Input action assets associated with locomotion to affect when the active movement control scheme is set.
/// Can use this list by itself or together with the Action Maps list to set control scheme masks by Asset or Map.
///
///
public List actionAssets
{
get => m_ActionAssets;
set => m_ActionAssets = value;
}
[SerializeField]
[Tooltip("Input action maps associated with locomotion to affect when the active movement control scheme is set." +
" Can use this list together with the Action Assets list to set control scheme masks by Map instead of the whole Asset.")]
List m_ActionMaps;
///
/// Input action maps associated with locomotion to affect when the active movement control scheme is set.
/// Can use this list together with the Action Assets list to set control scheme masks by Map instead of the whole Asset.
///
///
public List actionMaps
{
get => m_ActionMaps;
set => m_ActionMaps = value;
}
[SerializeField]
[Tooltip("Input actions associated with locomotion to affect when the active movement control scheme is set." +
" Can use this list to select exactly the actions to affect instead of setting control scheme masks by Asset or Map.")]
List m_Actions;
///
/// Input actions associated with locomotion that are affected by the active movement control scheme.
/// Can use this list to select exactly the actions to affect instead of setting control scheme masks by Asset or Map.
///
///
///
public List actions
{
get => m_Actions;
set => m_Actions = value;
}
[SerializeField]
[Tooltip("Name of an input control scheme that defines the grouping of bindings that should remain enabled when applying any movement control scheme." +
" Control schemes are created and named in the Input Actions window. The other movement control schemes are applied additively to this scheme." +
" Can be an empty string, which means only bindings that match the specified movement control scheme will be enabled.")]
string m_BaseControlScheme;
///
/// Name of an input control scheme that defines the grouping of bindings that should remain enabled when applying any movement control scheme.
/// Control schemes are created and named in the Input Actions window. The other movement control schemes are applied additively to this scheme.
/// Can be an empty string, which means only bindings that match the specified movement control scheme will be enabled.
///
public string baseControlScheme
{
get => m_BaseControlScheme;
set => m_BaseControlScheme = value;
}
[SerializeField]
[Tooltip("Name of an input control scheme that defines the grouping of bindings that should remain enabled when applying the noncontinuous movement control scheme." +
" Control schemes are created and named in the Input Actions window. Can be an empty string, which means only bindings that match the" +
" base control scheme will be enabled.")]
string m_NoncontinuousControlScheme;
///
/// Name of an input control scheme that defines the grouping of bindings that should remain enabled when applying the noncontinuous movement control scheme.
/// Control schemes are created and named in the Input Actions window. Can be an empty string, which means only bindings that match the
/// base control scheme will be enabled.
///
public string noncontinuousControlScheme
{
get => m_NoncontinuousControlScheme;
set => m_NoncontinuousControlScheme = value;
}
[SerializeField]
[Tooltip("Name of an input control scheme that defines the grouping of bindings that should remain enabled when applying the continuous movement control scheme." +
" Control schemes are created and named in the Input Actions window. Can be an empty string, which means only bindings that match the" +
" base control scheme will be enabled.")]
string m_ContinuousControlScheme;
///
/// Name of an input control scheme that defines the grouping of bindings that should remain enabled when applying the continuous movement control scheme.
/// Control schemes are created and named in the Input Actions window. Can be an empty string, which means only bindings that match the
/// base control scheme will be enabled.
///
public string continuousControlScheme
{
get => m_ContinuousControlScheme;
set => m_ContinuousControlScheme = value;
}
[SerializeField]
[Tooltip("Stores the locomotion provider for continuous movement.")]
ContinuousMoveProviderBase m_ContinuousMoveProvider;
///
/// Stores the locomotion provider for continuous movement.
///
///
public ContinuousMoveProviderBase continuousMoveProvider
{
get => m_ContinuousMoveProvider;
set => m_ContinuousMoveProvider = value;
}
[SerializeField]
[Tooltip("Stores the locomotion provider for continuous turning.")]
ContinuousTurnProviderBase m_ContinuousTurnProvider;
///
/// Stores the locomotion provider for continuous turning.
///
///
public ContinuousTurnProviderBase continuousTurnProvider
{
get => m_ContinuousTurnProvider;
set => m_ContinuousTurnProvider = value;
}
[SerializeField]
[Tooltip("Stores the locomotion provider for snap turning.")]
SnapTurnProviderBase m_SnapTurnProvider;
///
/// Stores the locomotion provider for snap turning.
///
///
public SnapTurnProviderBase snapTurnProvider
{
get => m_SnapTurnProvider;
set => m_SnapTurnProvider = value;
}
[SerializeField]
[Tooltip("Stores the \"Head\" Transform used with continuous movement when inputs should be relative to head orientation (usually the main camera).")]
Transform m_HeadForwardSource;
///
/// Stores the "Head" used with continuous movement when inputs should be relative to head orientation (usually the main camera).
///
public Transform headForwardSource
{
get => m_HeadForwardSource;
set => m_HeadForwardSource = value;
}
[SerializeField]
[Tooltip("Stores the \"Left Hand\" Transform used with continuous movement when inputs should be relative to the left hand's orientation.")]
Transform m_LeftHandForwardSource;
///
/// Stores the "Left Hand" used with continuous movement when inputs should be relative to the left hand's orientation.
///
public Transform leftHandForwardSource
{
get => m_LeftHandForwardSource;
set => m_LeftHandForwardSource = value;
}
[SerializeField]
[Tooltip("Stores the \"Right Hand\" Transform used with continuous movement when inputs should be relative to the right hand's orientation.")]
Transform m_RightHandForwardSource;
///
/// Stores the "Right Hand" used with continuous movement when inputs should be relative to the right hand's orientation.
///
public Transform rightHandForwardSource
{
get => m_RightHandForwardSource;
set => m_RightHandForwardSource = value;
}
void OnEnable()
{
SetMoveScheme(m_MoveScheme);
SetTurnStyle(m_TurnStyle);
SetMoveForwardSource(m_MoveForwardSource);
}
void OnDisable()
{
ClearBindingMasks();
}
void SetMoveScheme(MoveScheme scheme)
{
switch (scheme)
{
case MoveScheme.Noncontinuous:
SetBindingMasks(m_NoncontinuousControlScheme);
if (m_ContinuousMoveProvider != null)
{
m_ContinuousMoveProvider.enabled = false;
}
break;
case MoveScheme.Continuous:
SetBindingMasks(m_ContinuousControlScheme);
if (m_ContinuousMoveProvider != null)
{
m_ContinuousMoveProvider.enabled = true;
}
break;
default:
throw new InvalidEnumArgumentException(nameof(scheme), (int)scheme, typeof(MoveScheme));
}
}
void SetTurnStyle(TurnStyle style)
{
switch (style)
{
case TurnStyle.Snap:
if (m_ContinuousTurnProvider != null)
{
m_ContinuousTurnProvider.enabled = false;
}
if (m_SnapTurnProvider != null)
{
// TODO: If the Continuous Turn and Snap Turn providers both use the same
// action, then disabling the first provider will cause the action to be
// disabled, so the action needs to be enabled, which is done by forcing
// the OnEnable() of the second provider to be called.
// ReSharper disable Unity.InefficientPropertyAccess
m_SnapTurnProvider.enabled = false;
m_SnapTurnProvider.enabled = true;
// ReSharper restore Unity.InefficientPropertyAccess
m_SnapTurnProvider.enableTurnLeftRight = true;
}
break;
case TurnStyle.Continuous:
if (m_SnapTurnProvider != null)
{
m_SnapTurnProvider.enableTurnLeftRight = false;
}
if (m_ContinuousTurnProvider != null)
{
m_ContinuousTurnProvider.enabled = true;
}
break;
default:
throw new InvalidEnumArgumentException(nameof(style), (int)style, typeof(TurnStyle));
}
}
void SetMoveForwardSource(MoveForwardSource forwardSource)
{
if (m_ContinuousMoveProvider == null)
{
Debug.LogError($"Cannot set forward source to {forwardSource}," +
$" the reference to the {nameof(ContinuousMoveProviderBase)} is missing or the object has been destroyed.", this);
return;
}
switch (forwardSource)
{
case MoveForwardSource.Head:
m_ContinuousMoveProvider.forwardSource = m_HeadForwardSource;
break;
case MoveForwardSource.LeftHand:
m_ContinuousMoveProvider.forwardSource = m_LeftHandForwardSource;
break;
case MoveForwardSource.RightHand:
m_ContinuousMoveProvider.forwardSource = m_RightHandForwardSource;
break;
default:
throw new InvalidEnumArgumentException(nameof(forwardSource), (int)forwardSource, typeof(MoveForwardSource));
}
}
void SetBindingMasks(string controlSchemeName)
{
foreach (var actionReference in m_Actions)
{
if (actionReference == null)
continue;
var action = actionReference.action;
if (action == null)
{
Debug.LogError($"Cannot set binding mask on {actionReference} since the action could not be found.", this);
continue;
}
// Get the (optional) base control scheme and the control scheme to apply on top of base
var baseInputControlScheme = FindControlScheme(m_BaseControlScheme, actionReference);
var inputControlScheme = FindControlScheme(controlSchemeName, actionReference);
action.bindingMask = GetBindingMask(baseInputControlScheme, inputControlScheme);
}
if (m_ActionMaps.Count > 0 && m_ActionAssets.Count == 0)
{
Debug.LogError($"Cannot set binding mask on action maps since no input action asset references have been set.", this);
}
foreach (var actionAsset in m_ActionAssets)
{
if (actionAsset == null)
continue;
// Get the (optional) base control scheme and the control scheme to apply on top of base
var baseInputControlScheme = FindControlScheme(m_BaseControlScheme, actionAsset);
var inputControlScheme = FindControlScheme(controlSchemeName, actionAsset);
if (m_ActionMaps.Count == 0)
{
actionAsset.bindingMask = GetBindingMask(baseInputControlScheme, inputControlScheme);
continue;
}
foreach (var mapName in m_ActionMaps)
{
var actionMap = actionAsset.FindActionMap(mapName);
if (actionMap == null)
{
Debug.LogError($"Cannot set binding mask on \"{mapName}\" since the action map not be found in '{actionAsset}'.", this);
continue;
}
actionMap.bindingMask = GetBindingMask(baseInputControlScheme, inputControlScheme);
}
}
}
void ClearBindingMasks()
{
SetBindingMasks(string.Empty);
}
InputControlScheme? FindControlScheme(string controlSchemeName, InputActionReference action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
if (string.IsNullOrEmpty(controlSchemeName))
return null;
var asset = action.asset;
if (asset == null)
{
Debug.LogError($"Cannot find control scheme \"{controlSchemeName}\" for '{action}' since it does not belong to an {nameof(InputActionAsset)}.", this);
return null;
}
return FindControlScheme(controlSchemeName, asset);
}
InputControlScheme? FindControlScheme(string controlSchemeName, InputActionAsset asset)
{
if (asset == null)
throw new ArgumentNullException(nameof(asset));
if (string.IsNullOrEmpty(controlSchemeName))
return null;
var scheme = asset.FindControlScheme(controlSchemeName);
if (scheme == null)
{
Debug.LogError($"Cannot find control scheme \"{controlSchemeName}\" in '{asset}'.", this);
return null;
}
return scheme;
}
static InputBinding? GetBindingMask(InputControlScheme? baseInputControlScheme, InputControlScheme? inputControlScheme)
{
if (inputControlScheme.HasValue)
{
return baseInputControlScheme.HasValue
? InputBinding.MaskByGroups(baseInputControlScheme.Value.bindingGroup, inputControlScheme.Value.bindingGroup)
: InputBinding.MaskByGroup(inputControlScheme.Value.bindingGroup);
}
return baseInputControlScheme.HasValue
? InputBinding.MaskByGroup(baseInputControlScheme.Value.bindingGroup)
: (InputBinding?)null;
}
}
}