Created
November 4, 2019 21:03
-
-
Save julenka/8aa4d9f85cd8eb02709b237453f3e360 to your computer and use it in GitHub Desktop.
Interactable Event Receivever Fix
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright (c) Microsoft Corporation. All rights reserved. | |
// Licensed under the MIT License. See LICENSE in the project root for license information. | |
using Microsoft.MixedReality.Toolkit.Input; | |
using Microsoft.MixedReality.Toolkit.Utilities; | |
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEngine; | |
using UnityEngine.Events; | |
using UnityEngine.EventSystems; | |
using UnityEngine.Serialization; | |
namespace Microsoft.MixedReality.Toolkit.UI | |
{ | |
/// <summary> | |
/// Uses input and action data to declare a set of states | |
/// Maintains a collection of themes that react to state changes and provide sensory feedback | |
/// Passes state information and input data on to receivers that detect patterns and does stuff. | |
/// </summary> | |
[System.Serializable] | |
[HelpURL("https://microsoft.github.io/MixedRealityToolkit-Unity/Documentation/README_Interactable.html")] | |
public class Interactable : | |
MonoBehaviour, | |
IMixedRealityFocusChangedHandler, | |
IMixedRealityFocusHandler, | |
IMixedRealityInputHandler, | |
IMixedRealitySpeechHandler, | |
IMixedRealityTouchHandler, | |
IMixedRealityInputHandler<Vector2>, | |
IMixedRealityInputHandler<Vector3>, | |
IMixedRealityInputHandler<MixedRealityPose> | |
{ | |
/// <summary> | |
/// Pointers that are focusing the interactable | |
/// </summary> | |
public List<IMixedRealityPointer> FocusingPointers => focusingPointers; | |
protected readonly List<IMixedRealityPointer> focusingPointers = new List<IMixedRealityPointer>(); | |
/// <summary> | |
/// Input sources that are pressing the interactable | |
/// </summary> | |
public HashSet<IMixedRealityInputSource> PressingInputSources => pressingInputSources; | |
protected readonly HashSet<IMixedRealityInputSource> pressingInputSources = new HashSet<IMixedRealityInputSource>(); | |
[FormerlySerializedAs("States")] | |
[SerializeField] | |
private States states; | |
/// <summary> | |
/// A collection of states and basic state logic | |
/// </summary> | |
public States States | |
{ | |
get { return states; } | |
set | |
{ | |
states = value; | |
SetupStates(); | |
} | |
} | |
/// <summary> | |
/// The state logic for comparing state | |
/// </summary> | |
public InteractableStates StateManager { get; protected set; } | |
/// <summary> | |
/// Which action is this interactable listening for | |
/// </summary> | |
public MixedRealityInputAction InputAction { get; set; } | |
/// <summary> | |
/// The id of the selected inputAction, for serialization | |
/// </summary> | |
[HideInInspector] | |
[SerializeField] | |
private int InputActionId = -1; | |
[FormerlySerializedAs("IsGlobal")] | |
[SerializeField] | |
protected bool isGlobal = false; | |
/// <summary> | |
/// Is the interactable listening to global events (input only) | |
/// </summary> | |
public bool IsGlobal | |
{ | |
get { return isGlobal; } | |
set | |
{ | |
if (isGlobal != value) | |
{ | |
isGlobal = value; | |
// If we are active, then register or unregister our the global input handler with the InputSystem | |
// If we are disabled, then we will re-register OnEnable() | |
if (gameObject.activeInHierarchy) | |
{ | |
RegisterHandler<IMixedRealityInputHandler>(isGlobal); | |
} | |
} | |
} | |
} | |
/// <summary> | |
/// A way of adding more layers of states for controls like toggles. | |
/// This is capitalized and doesn't match conventions for backwards compatability | |
/// (to not break people using Interactable). We tried using FormerlySerializedAs("Dimensions) | |
/// and renaming to "dimensions", however Unity did not properly pick up the former serialization, | |
/// so we maintained the old value. See https://github.com/microsoft/MixedRealityToolkit-Unity/issues/6169 | |
/// </summary> | |
[SerializeField] | |
protected int Dimensions = 1; | |
/// <summary> | |
/// A way of adding more layers of states for controls like toggles | |
/// </summary> | |
public int NumOfDimensions | |
{ | |
get { return Dimensions; } | |
set | |
{ | |
if (Dimensions != value) | |
{ | |
// Value cannot be negative or zero | |
if (value > 0) | |
{ | |
// If we are currently in Toggle mode, we are about to not be | |
// Auto-turn off state | |
if (ButtonMode == SelectionModes.Toggle) | |
{ | |
IsToggled = false; | |
} | |
Dimensions = value; | |
CurrentDimension = Mathf.Clamp(CurrentDimension, 0, Dimensions - 1); | |
} | |
else | |
{ | |
Debug.LogWarning($"Value {value} for Dimensions property setter cannot be negative or zero."); | |
} | |
} | |
} | |
} | |
// cache of current dimension | |
[SerializeField] | |
protected int dimensionIndex = 0; | |
/// <summary> | |
/// Current Dimension index based zero and must be less than Dimensions | |
/// </summary> | |
public int CurrentDimension | |
{ | |
get { return dimensionIndex; } | |
set | |
{ | |
if (dimensionIndex != value) | |
{ | |
// If valid value and not our current value, then update | |
if (value >= 0 && value < NumOfDimensions) | |
{ | |
dimensionIndex = value; | |
// If we are in toggle mode, update IsToggled state based on current dimension | |
// This needs to happen after updating dimensionIndex, since IsToggled.set will call CurrentDimension.set again | |
if (ButtonMode == SelectionModes.Toggle) | |
{ | |
IsToggled = dimensionIndex > 0; | |
} | |
UpdateActiveThemes(); | |
forceUpdate = true; | |
} | |
else | |
{ | |
Debug.LogWarning($"Value {value} for property setter CurrentDimension cannot be less than 0 and cannot be greater than or equal to Dimensions={NumOfDimensions}"); | |
} | |
} | |
} | |
} | |
/// <summary> | |
/// Returns the current selection mode of the Interactable based on the number of Dimensions available | |
/// </summary> | |
/// <remarks> | |
/// Returns the following under the associated conditions: | |
/// SelectionModes.Invalid => Dimensions less than or equal to 0 | |
/// SelectionModes.Button => Dimensions == 1 | |
/// SelectionModes.Toggle => Dimensions == 2 | |
/// SelectionModes.MultiDimension => Dimensions > 2 | |
/// </remarks> | |
public SelectionModes ButtonMode | |
{ | |
get | |
{ | |
return ConvertToSelectionMode(NumOfDimensions); | |
} | |
} | |
/// <summary> | |
/// The Dimension value to set on start | |
/// </summary> | |
[FormerlySerializedAs("StartDimensionIndex")] | |
[SerializeField] | |
private int startDimensionIndex = 0; | |
/// <summary> | |
/// Is the interactive selectable? | |
/// When a multi-dimension button, can the user initiate switching dimensions? | |
/// </summary> | |
public bool CanSelect = true; | |
/// <summary> | |
/// Can the user deselect a toggle? | |
/// A radial button or tab should set this to false | |
/// </summary> | |
public bool CanDeselect = true; | |
/// <summary> | |
/// A voice command to fire a click event | |
/// </summary> | |
public string VoiceCommand = ""; | |
[FormerlySerializedAs("RequiresFocus")] | |
[SerializeField] | |
public bool voiceRequiresFocus = true; | |
/// <summary> | |
/// Does the voice command require this to have focus? | |
/// Registers as a global listener for speech commands, ignores input events | |
/// </summary> | |
public bool VoiceRequiresFocus | |
{ | |
get { return voiceRequiresFocus; } | |
set | |
{ | |
if (voiceRequiresFocus != value) | |
{ | |
voiceRequiresFocus = value; | |
// If we are active, then change global speech registeration. | |
// Register handle if we do not require focus, unregister otherwise | |
if (gameObject.activeInHierarchy) | |
{ | |
RegisterHandler<IMixedRealitySpeechHandler>(!voiceRequiresFocus); | |
} | |
} | |
} | |
} | |
[FormerlySerializedAs("Profiles")] | |
[SerializeField] | |
private List<InteractableProfileItem> profiles = new List<InteractableProfileItem>(); | |
/// <summary> | |
/// List of profile configurations that match Visual Themes with GameObjects targets | |
/// Setting at runtime will re-create the runtime Theme Engines (i.e ActiveThemes property) being used by this class | |
/// </summary> | |
public List<InteractableProfileItem> Profiles | |
{ | |
get { return profiles; } | |
set | |
{ | |
profiles = value; | |
SetupThemes(); | |
} | |
} | |
/// <summary> | |
/// Base onclick event | |
/// </summary> | |
public UnityEvent OnClick = new UnityEvent(); | |
[SerializeField] | |
private List<InteractableEvent> Events = new List<InteractableEvent>(); | |
/// <summary> | |
/// List of events added to this interactable | |
/// </summary> | |
public List<InteractableEvent> InteractableEvents | |
{ | |
get { return Events; } | |
set | |
{ | |
Events = value; | |
SetupEvents(); | |
} | |
} | |
private List<InteractableThemeBase> activeThemes = new List<InteractableThemeBase>(); | |
/// <summary> | |
/// The list of running theme instances to receive state changes | |
/// When the dimension index changes, activeThemes updates to those assigned to that dimension. | |
/// </summary> | |
public IReadOnlyList<InteractableThemeBase> ActiveThemes => activeThemes.AsReadOnly(); | |
/// <summary> | |
/// List of (dimension index, InteractableThemeBase) pairs that describe all possible themes the | |
/// interactable can have. First element in the tuple represents dimension index for the theme. | |
/// This list gets initialized on startup, or whenever the profiles for the interactable changes. | |
/// The list of active themes inspects this list to determine which themes to use based on current dimension. | |
/// </summary> | |
private List<System.Tuple<int, InteractableThemeBase>> allThemeDimensionPairs = new List<System.Tuple<int, InteractableThemeBase>>(); | |
/// <summary> | |
/// How many times this interactable was clicked | |
/// </summary> | |
/// <remarks> | |
/// Useful for checking when a click event occurs. | |
/// </remarks> | |
public int ClickCount { get; private set; } | |
#region States | |
// Field just used for serialization to save if the Interactable should start enabled or disabled | |
[FormerlySerializedAs("Enabled")] | |
[SerializeField] | |
private bool enabledOnStart = true; | |
/// <summary> | |
/// Defines whether the Interactable is enabled or not internally | |
/// This is different than the Enabled property at the GameObject/Component level | |
/// When false, Interactable will continue to run in Unity but not respond to Input. | |
/// </summary> | |
/// <remarks> | |
/// Property is useful for disabling UX, such as greying out a button, until a user completes some pre-mandatory step such as fill out their name, etc | |
/// </remarks> | |
public virtual bool IsEnabled | |
{ | |
// Note the inverse setting since targeting "Disable" state but property is concerning "Enabled" | |
get { return !(GetStateValue(InteractableStates.InteractableStateEnum.Disabled) > 0); } | |
set | |
{ | |
if (IsEnabled != value) | |
{ | |
// If we are disabling input, we should reset our base input tracking states since we will not be responding to input while disabled | |
if (!value) | |
{ | |
ResetInputTrackingStates(); | |
} | |
SetState(InteractableStates.InteractableStateEnum.Disabled, !value); | |
} | |
} | |
} | |
/// <summary> | |
/// Has focus | |
/// </summary> | |
public virtual bool HasFocus | |
{ | |
get { return GetStateValue(InteractableStates.InteractableStateEnum.Focus) > 0; } | |
set | |
{ | |
if (HasFocus != value) | |
{ | |
if (!value && HasPress) | |
{ | |
rollOffTimer = 0; | |
} | |
else | |
{ | |
rollOffTimer = rollOffTime; | |
} | |
SetState(InteractableStates.InteractableStateEnum.Focus, value); | |
} | |
} | |
} | |
/// <summary> | |
/// Currently being pressed | |
/// </summary> | |
public virtual bool HasPress | |
{ | |
get { return GetStateValue(InteractableStates.InteractableStateEnum.Pressed) > 0; } | |
set { SetState(InteractableStates.InteractableStateEnum.Pressed, value); } | |
} | |
/// <summary> | |
/// Targeted means the item has focus and finger is up | |
/// Currently not controlled by Interactable directly | |
/// </summary> | |
public virtual bool IsTargeted | |
{ | |
get { return GetStateValue(InteractableStates.InteractableStateEnum.Targeted) > 0; } | |
set { SetState(InteractableStates.InteractableStateEnum.Targeted, value); } | |
} | |
/// <summary> | |
/// State that corresponds to no focus,and finger is up. | |
/// Currently not controlled by Interactable directly | |
/// </summary> | |
public virtual bool IsInteractive | |
{ | |
get { return GetStateValue(InteractableStates.InteractableStateEnum.Interactive) > 0; } | |
set { SetState(InteractableStates.InteractableStateEnum.Interactive, value); } | |
} | |
/// <summary> | |
/// State that corresponds to has focus,and finger down. | |
/// Currently not controlled by Interactable directly | |
/// </summary> | |
public virtual bool HasObservationTargeted | |
{ | |
get { return GetStateValue(InteractableStates.InteractableStateEnum.ObservationTargeted) > 0; } | |
set { SetState(InteractableStates.InteractableStateEnum.ObservationTargeted, value); } | |
} | |
/// <summary> | |
/// State that corresponds to no focus,and finger is down. | |
/// Currently not controlled by Interactable directly | |
/// </summary> | |
public virtual bool HasObservation | |
{ | |
get { return GetStateValue(InteractableStates.InteractableStateEnum.Observation) > 0; } | |
set { SetState(InteractableStates.InteractableStateEnum.Observation, value); } | |
} | |
/// <summary> | |
/// The Interactable has been clicked | |
/// </summary> | |
public virtual bool IsVisited | |
{ | |
get { return GetStateValue(InteractableStates.InteractableStateEnum.Visited) > 0; } | |
set { SetState(InteractableStates.InteractableStateEnum.Visited, value); } | |
} | |
/// <summary> | |
/// Determines whether Interactable is toggled or not. If true, CurrentDimension should be 1 and if false, CurrentDimension should be 0 | |
/// </summary> | |
/// <remarks> | |
/// Only valid when ButtonMode == SelectionMode.Toggle (i.e Dimensions == 2) | |
/// </remarks> | |
public virtual bool IsToggled | |
{ | |
get | |
{ | |
return GetStateValue(InteractableStates.InteractableStateEnum.Toggled) > 0; | |
} | |
set | |
{ | |
if (IsToggled != value) | |
{ | |
// We can only change Toggle state if we are in Toggle mode | |
if (ButtonMode == SelectionModes.Toggle) | |
{ | |
SetState(InteractableStates.InteractableStateEnum.Toggled, value); | |
CurrentDimension = value ? 1 : 0; | |
} | |
else | |
{ | |
Debug.LogWarning($"SetToggled(bool) called, but SelectionMode is set to {ButtonMode}, so Current Dimension was unchanged."); | |
} | |
} | |
} | |
} | |
/// <summary> | |
/// Currently pressed and some movement has occurred | |
/// </summary> | |
public virtual bool HasGesture | |
{ | |
get { return GetStateValue(InteractableStates.InteractableStateEnum.Gesture) > 0; } | |
set { SetState(InteractableStates.InteractableStateEnum.Gesture, value); } | |
} | |
/// <summary> | |
/// State that corresponds to Gesture reaching max threshold or limits | |
/// Currently not controlled by Interactable directly | |
/// </summary> | |
public virtual bool HasGestureMax | |
{ | |
get { return GetStateValue(InteractableStates.InteractableStateEnum.GestureMax) > 0; } | |
set { SetState(InteractableStates.InteractableStateEnum.GestureMax, value); } | |
} | |
/// <summary> | |
/// State that corresponds to Interactable is touching another object | |
/// Currently not controlled by Interactable directly | |
/// </summary> | |
public virtual bool HasCollision | |
{ | |
get { return GetStateValue(InteractableStates.InteractableStateEnum.Collision) > 0; } | |
set { SetState(InteractableStates.InteractableStateEnum.Collision, value); } | |
} | |
/// <summary> | |
/// A voice command has just occurred | |
/// </summary> | |
public virtual bool HasVoiceCommand | |
{ | |
get { return GetStateValue(InteractableStates.InteractableStateEnum.VoiceCommand) > 0; } | |
set { SetState(InteractableStates.InteractableStateEnum.VoiceCommand, value); } | |
} | |
/// <summary> | |
/// A near interaction touchable is actively being touched | |
/// </summary> | |
public virtual bool HasPhysicalTouch | |
{ | |
get { return GetStateValue(InteractableStates.InteractableStateEnum.PhysicalTouch) > 0; } | |
set { SetState(InteractableStates.InteractableStateEnum.PhysicalTouch, value); } | |
} | |
/// <summary> | |
/// State that corresponds to miscellaneous/custom use by consumers | |
/// Currently not controlled by Interactable directly | |
/// </summary> | |
public virtual bool HasCustom | |
{ | |
get { return GetStateValue(InteractableStates.InteractableStateEnum.Custom) > 0; } | |
set { SetState(InteractableStates.InteractableStateEnum.Custom, value); } | |
} | |
/// <summary> | |
/// A near interaction grabbable is actively being grabbed | |
/// </summary> | |
public virtual bool HasGrab | |
{ | |
get { return GetStateValue(InteractableStates.InteractableStateEnum.Grab) > 0; } | |
set { SetState(InteractableStates.InteractableStateEnum.Grab, value); } | |
} | |
#endregion | |
protected State lastState; | |
// directly manipulate a theme value, skip blending | |
protected bool forceUpdate = false; | |
// allows for switching colliders without firing a lose focus immediately | |
// for advanced controls like drop-downs | |
protected float rollOffTime = 0.25f; | |
protected float rollOffTimer = 0.25f; | |
protected List<IInteractableHandler> handlers = new List<IInteractableHandler>(); | |
/// <summary> | |
/// A click must occur within this many seconds after an input down | |
/// </summary> | |
protected float clickTime = 1.5f; | |
protected Coroutine clickValidTimer; | |
/// <summary> | |
/// Amount of time to "simulate" press states for interactions that do not utilize input up/down such as voice command | |
/// This allows for visual feedbacks and other typical UX responsiveness and behavior to occur | |
/// </summary> | |
protected const float globalFeedbackClickTime = 0.3f; | |
protected Coroutine globalTimer; | |
#region Gesture State Variables | |
/// <summary> | |
/// The position of the controller when input down occurs. | |
/// Used to determine when controller has moved far enough to trigger gesture | |
/// </summary> | |
protected Vector3? dragStartPosition = null; | |
// Input must move at least this distance before a gesture is considered started, for 2D input like thumbstick | |
static readonly float gestureStartThresholdVector2 = 0.1f; | |
// Input must move at least this distance before a gesture is considered started, for 3D input | |
static readonly float gestureStartThresholdVector3 = 0.05f; | |
// Input must move at least this distance before a gesture is considered started, for | |
// mixed reality pose input. This is the distance and hand or controller needs to move | |
static readonly float gestureStartThresholdMixedRealityPose = 0.1f; | |
#endregion | |
#region MonoBehaviorImplementation | |
protected virtual void Awake() | |
{ | |
if (States == null) | |
{ | |
States = GetDefaultInteractableStates(); | |
} | |
IsEnabled = enabledOnStart; | |
InputAction = ResolveInputAction(InputActionId); | |
CurrentDimension = startDimensionIndex; | |
RefreshSetup(); | |
} | |
protected virtual void OnEnable() | |
{ | |
if (!VoiceRequiresFocus) | |
{ | |
RegisterHandler<IMixedRealitySpeechHandler>(true); | |
} | |
if (IsGlobal) | |
{ | |
RegisterHandler<IMixedRealityInputHandler>(true); | |
} | |
focusingPointers.RemoveAll((focusingPointer) => (focusingPointer.FocusTarget as Interactable) != this); | |
if (focusingPointers.Count == 0) | |
{ | |
ResetInputTrackingStates(); | |
} | |
} | |
protected virtual void OnDisable() | |
{ | |
// If we registered to receive global events, remove ourselves when disabled | |
if (!VoiceRequiresFocus) | |
{ | |
RegisterHandler<IMixedRealitySpeechHandler>(false); | |
} | |
if (IsGlobal) | |
{ | |
RegisterHandler<IMixedRealityInputHandler>(false); | |
} | |
ResetInputTrackingStates(); | |
} | |
protected virtual void Start() | |
{ | |
InternalUpdate(); | |
} | |
protected virtual void Update() | |
{ | |
InternalUpdate(); | |
} | |
private void InternalUpdate() | |
{ | |
if (rollOffTimer < rollOffTime && HasPress) | |
{ | |
rollOffTimer += Time.deltaTime; | |
if (rollOffTimer >= rollOffTime) | |
{ | |
HasPress = false; | |
} | |
} | |
for (int i = 0; i < InteractableEvents.Count; i++) | |
{ | |
if (InteractableEvents[i].Receiver != null) | |
{ | |
InteractableEvents[i].Receiver.OnUpdate(StateManager, this); | |
} | |
} | |
for (int i = 0; i < activeThemes.Count; i++) | |
{ | |
if (activeThemes[i].Loaded) | |
{ | |
activeThemes[i].OnUpdate(StateManager.CurrentState().ActiveIndex, forceUpdate); | |
} | |
} | |
if (lastState != StateManager.CurrentState()) | |
{ | |
for (int i = 0; i < handlers.Count; i++) | |
{ | |
if (handlers[i] != null) | |
{ | |
handlers[i].OnStateChange(StateManager, this); | |
} | |
} | |
} | |
if (forceUpdate) | |
{ | |
forceUpdate = false; | |
} | |
lastState = StateManager.CurrentState(); | |
} | |
#endregion MonoBehavior Implimentation | |
#region Interactable Initiation | |
/// <summary> | |
/// Force re-initialization of Interactable from events, themes and state references | |
/// </summary> | |
public void RefreshSetup() | |
{ | |
SetupEvents(); | |
SetupThemes(); | |
SetupStates(); | |
} | |
/// <summary> | |
/// starts the StateManager | |
/// </summary> | |
protected virtual void SetupStates() | |
{ | |
// Note that statemanager will clear states by allocating a new object | |
// But resetting states directly will call setters which may perform necessary steps to enter appropriate state | |
ResetAllStates(); | |
Debug.Assert(typeof(InteractableStates).IsAssignableFrom(States.StateModelType), $"Invalid state model of type {States.StateModelType}. State model must extend from {typeof(InteractableStates)}"); | |
StateManager = (InteractableStates)States.CreateStateModel(); | |
} | |
/// <summary> | |
/// Creates the event receiver instances from the Events list | |
/// </summary> | |
protected virtual void SetupEvents() | |
{ | |
for (int i = 0; i < InteractableEvents.Count; i++) | |
{ | |
var receiver = InteractableEvent.CreateReceiver(InteractableEvents[i]); | |
if (receiver != null) | |
{ | |
InteractableEvents[i].Receiver = receiver; | |
InteractableEvents[i].Receiver.Host = this; | |
} | |
else | |
{ | |
Debug.Log("Invalid receiver found on " + gameObject.name); | |
} | |
} | |
} | |
/// <summary> | |
/// Updates the list of active themes based the current dimensions index | |
/// </summary> | |
protected virtual void UpdateActiveThemes() | |
{ | |
activeThemes.Clear(); | |
for (int i = 0; i < allThemeDimensionPairs.Count; i++) | |
{ | |
if (allThemeDimensionPairs[i].Item1 == CurrentDimension) | |
{ | |
activeThemes.Add(allThemeDimensionPairs[i].Item2); | |
} | |
} | |
} | |
/// <summary> | |
/// At startup or whenever a profile changes, creates all | |
/// possible themes that interactable can be in. We then update | |
/// the set of active themes by inspecting this list, looking for | |
/// only themes whose index matched CurrentDimensionIndex. | |
/// </summary> | |
private void SetupThemes() | |
{ | |
allThemeDimensionPairs.Clear(); | |
// Profiles are one per GameObject/ThemeContainer | |
// ThemeContainers are one per dimension | |
// ThemeDefinitions are one per desired effect (i.e theme) | |
foreach (var profile in Profiles) | |
{ | |
if (profile.Target != null && profile.Themes != null) | |
{ | |
for (int i = 0; i < profile.Themes.Count; i++) | |
{ | |
var themeContainer = profile.Themes[i]; | |
if (themeContainer.States.Equals(States)) | |
{ | |
foreach (var themeDefinition in themeContainer.Definitions) | |
{ | |
allThemeDimensionPairs.Add(new System.Tuple<int, InteractableThemeBase>( | |
i, | |
InteractableThemeBase.CreateAndInitTheme(themeDefinition, profile.Target))); | |
} | |
} | |
else | |
{ | |
Debug.LogWarning($"Could not use {themeContainer.name} in Interactable on {gameObject.name} because Theme's States does not match {States.name}"); | |
} | |
} | |
} | |
} | |
UpdateActiveThemes(); | |
} | |
#endregion Interactable Initiation | |
#region State Utilities | |
/// <summary> | |
/// Grabs the state value index, returns -1 if no StateManager available | |
/// </summary> | |
public int GetStateValue(InteractableStates.InteractableStateEnum state) | |
{ | |
if (StateManager != null) | |
{ | |
return StateManager.GetStateValue((int)state); | |
} | |
return -1; | |
} | |
/// <summary> | |
/// a public way to set state directly | |
/// </summary> | |
public void SetState(InteractableStates.InteractableStateEnum state, bool value) | |
{ | |
if (StateManager != null) | |
{ | |
StateManager.SetStateValue(state, value ? 1 : 0); | |
UpdateState(); | |
} | |
} | |
/// <summary> | |
/// runs the state logic and sets state based on the current state values | |
/// </summary> | |
protected virtual void UpdateState() | |
{ | |
StateManager.CompareStates(); | |
} | |
/// <summary> | |
/// Reset the input tracking states directly managed by Interactable such as whether the component has focus or is being grabbed | |
/// Useful for when needing to reset input interactions | |
/// </summary> | |
public void ResetInputTrackingStates() | |
{ | |
HasFocus = false; | |
HasPress = false; | |
HasPhysicalTouch = false; | |
HasGrab = false; | |
HasGesture = false; | |
HasGestureMax = false; | |
HasVoiceCommand = false; | |
if (globalTimer != null) | |
{ | |
StopCoroutine(globalTimer); | |
globalTimer = null; | |
} | |
dragStartPosition = null; | |
} | |
/// <summary> | |
/// Reset all states in the Interactable and pointer information | |
/// </summary> | |
public void ResetAllStates() | |
{ | |
focusingPointers.Clear(); | |
pressingInputSources.Clear(); | |
ResetInputTrackingStates(); | |
IsEnabled = true; | |
HasObservation = false; | |
HasObservationTargeted = false; | |
IsInteractive = false; | |
IsTargeted = false; | |
IsToggled = false; | |
IsVisited = false; | |
HasCollision = false; | |
HasCustom = false; | |
} | |
#endregion State Utilities | |
#region Dimensions Utilities | |
/// <summary> | |
/// Increases the Current Dimension by 1. If at end (i.e Dimensions - 1), then loop around to beginning (i.e 0) | |
/// </summary> | |
public void IncreaseDimension() | |
{ | |
if (CurrentDimension == NumOfDimensions - 1) | |
{ | |
CurrentDimension = 0; | |
} | |
else | |
{ | |
CurrentDimension++; | |
} | |
} | |
/// <summary> | |
/// Decreases the Current Dimension by 1. If at zero, then loop around to end (i.e Dimensions - 1) | |
/// </summary> | |
public void DecreaseDimension() | |
{ | |
if (CurrentDimension == 0) | |
{ | |
CurrentDimension = NumOfDimensions - 1; | |
} | |
else | |
{ | |
CurrentDimension--; | |
} | |
} | |
/// <summary> | |
/// Helper method to convert number of dimensions to the appropriate SelectionModes | |
/// </summary> | |
/// <param name="dimensions">number of dimensions</param> | |
/// <returns>SelectionModes for corresponding number of dimensions</returns> | |
public static SelectionModes ConvertToSelectionMode(int dimensions) | |
{ | |
if (dimensions <= 0) | |
{ | |
return SelectionModes.Invalid; | |
} | |
else if (dimensions == 1) | |
{ | |
return SelectionModes.Button; | |
} | |
else if (dimensions == 2) | |
{ | |
return SelectionModes.Toggle; | |
} | |
else | |
{ | |
return SelectionModes.MultiDimension; | |
} | |
} | |
#endregion Dimensions Utilities | |
#region Events | |
/// <summary> | |
/// Register OnClick extra handlers | |
/// </summary> | |
public void AddHandler(IInteractableHandler handler) | |
{ | |
if (!handlers.Contains(handler)) | |
{ | |
handlers.Add(handler); | |
} | |
} | |
/// <summary> | |
/// Remove onClick handlers | |
/// </summary> | |
public void RemoveHandler(IInteractableHandler handler) | |
{ | |
if (handlers.Contains(handler)) | |
{ | |
handlers.Remove(handler); | |
} | |
} | |
/// <summary> | |
/// Event receivers can be used to listen for different | |
/// events at runtime. This method allows receivers to be dynamically added at runtime. | |
/// </summary> | |
/// <returns>The new event receiver</returns> | |
public T AddReceiver<T>() where T : ReceiverBase, new() | |
{ | |
var interactableEvent = new InteractableEvent(); | |
var result = new T(); | |
result.Event = interactableEvent.Event; | |
interactableEvent.Receiver = result; | |
InteractableEvents.Add(interactableEvent); | |
return result; | |
} | |
/// <summary> | |
/// Returns the first receiver of type T on the interactable, | |
/// or null if nothing is found. | |
/// </summary> | |
public T GetReceiver<T>() where T : ReceiverBase | |
{ | |
for (int i = 0; i < InteractableEvents.Count; i++) | |
{ | |
if (InteractableEvents[i] != null && InteractableEvents[i].Receiver is T) | |
{ | |
return (T)InteractableEvents[i].Receiver; | |
} | |
} | |
return null; | |
} | |
/// <summary> | |
/// Returns all receivers of type T on the interactable. | |
/// If nothing is found, returns empty list. | |
/// </summary> | |
public List<T> GetReceivers<T>() where T : ReceiverBase | |
{ | |
List<T> result = new List<T>(); | |
for (int i = 0; i < InteractableEvents.Count; i++) | |
{ | |
if (InteractableEvents[i] != null && InteractableEvents[i].Receiver is T) | |
{ | |
result.Add((T)InteractableEvents[i].Receiver); | |
} | |
} | |
return result; | |
} | |
#endregion | |
#region Input Timers | |
/// <summary> | |
/// Starts a timer to check if input is in progress | |
/// - Make sure global pointer events are not double firing | |
/// - Make sure Global Input events are not double firing | |
/// - Make sure pointer events are not duplicating an input event | |
/// </summary> | |
protected void StartClickTimer(bool isFromInputDown = false) | |
{ | |
if (IsGlobal || isFromInputDown) | |
{ | |
if (clickValidTimer != null) | |
{ | |
StopClickTimer(); | |
} | |
clickValidTimer = StartCoroutine(InputDownTimer(clickTime)); | |
} | |
} | |
protected void StopClickTimer() | |
{ | |
Debug.Assert(clickValidTimer != null, "StopClickTimer called but no click timer is running"); | |
StopCoroutine(clickValidTimer); | |
clickValidTimer = null; | |
} | |
/// <summary> | |
/// A timer for the MixedRealityInputHandlers, clicks should occur within a certain time. | |
/// </summary> | |
protected IEnumerator InputDownTimer(float time) | |
{ | |
yield return new WaitForSeconds(time); | |
clickValidTimer = null; | |
} | |
/// <summary> | |
/// Return true if the interactable can fire a click event. | |
/// Clicks can only occur within a short duration of an input down firing. | |
/// </summary> | |
private bool CanFireClick() | |
{ | |
return clickValidTimer != null; | |
} | |
#endregion | |
#region Interactable Utilities | |
private void RegisterHandler<T>(bool enable) where T : IEventSystemHandler | |
{ | |
if (enable) | |
{ | |
CoreServices.InputSystem?.RegisterHandler<T>(this); | |
} | |
else | |
{ | |
CoreServices.InputSystem?.UnregisterHandler<T>(this); | |
} | |
} | |
/// <summary> | |
/// Assigns the InputAction based on the InputActionId | |
/// </summary> | |
public static MixedRealityInputAction ResolveInputAction(int index) | |
{ | |
MixedRealityInputAction[] actions = CoreServices.InputSystem.InputSystemProfile.InputActionsProfile.InputActions; | |
index = Mathf.Clamp(index, 0, actions.Length - 1); | |
return actions[index]; | |
} | |
/// <summary> | |
/// Based on inputAction and state, should interactable listen to this up/down event. | |
/// </summary> | |
protected virtual bool ShouldListenToUpDownEvent(InputEventData data) | |
{ | |
if (!(HasFocus || IsGlobal)) | |
{ | |
return false; | |
} | |
if (data.MixedRealityInputAction != InputAction) | |
{ | |
return false; | |
} | |
// Special case: Make sure that we are not being focused only by a PokePointer, since PokePointer | |
// dispatches touch events and should not be dispatching button presses like select, grip, menu, etc. | |
int focusingPointerCount = 0; | |
int focusingPokePointerCount = 0; | |
for (int i = 0; i < focusingPointers.Count; i++) | |
{ | |
if (focusingPointers[i].InputSourceParent.SourceId == data.SourceId) | |
{ | |
focusingPointerCount++; | |
if (focusingPointers[i] is PokePointer) | |
{ | |
focusingPokePointerCount++; | |
} | |
} | |
} | |
if (focusingPointerCount > 0 && focusingPointerCount == focusingPokePointerCount) | |
{ | |
return false; | |
} | |
return true; | |
} | |
/// <summary> | |
/// Returns true if the inputeventdata is being dispatched from a near pointer | |
/// </summary> | |
private bool IsInputFromNearInteraction(InputEventData eventData) | |
{ | |
bool isAnyNearpointerFocusing = false; | |
for (int i = 0; i < focusingPointers.Count; i++) | |
{ | |
if (focusingPointers[i].InputSourceParent.SourceId == eventData.InputSource.SourceId && focusingPointers[i] is IMixedRealityNearPointer) | |
{ | |
isAnyNearpointerFocusing = true; | |
break; | |
} | |
} | |
return isAnyNearpointerFocusing; | |
} | |
/// <summary> | |
/// Based on button settings and state, should this button listen to input? | |
/// </summary> | |
protected virtual bool CanInteract() | |
{ | |
// Interactable can interact if we are enabled and we are not a toggle button | |
// If we are a toggle button, then we can only toggle if CanSelect (to turn on) or CanDeslect (to turn off) | |
return IsEnabled && | |
(ButtonMode != SelectionModes.Toggle | |
|| (CurrentDimension == 0 && CanSelect) | |
|| (CurrentDimension == 1 && CanDeselect)); | |
} | |
/// <summary> | |
/// A public way to trigger or route an onClick event from an external source, like PressableButton | |
/// </summary> | |
public void TriggerOnClick() | |
{ | |
IncreaseDimension(); | |
SendOnClick(null); | |
IsVisited = true; | |
} | |
/// <summary> | |
/// Call onClick methods on receivers or IInteractableHandlers | |
/// </summary> | |
protected void SendOnClick(IMixedRealityPointer pointer) | |
{ | |
OnClick.Invoke(); | |
ClickCount++; | |
for (int i = 0; i < InteractableEvents.Count; i++) | |
{ | |
if (InteractableEvents[i].Receiver != null) | |
{ | |
InteractableEvents[i].Receiver.OnClick(StateManager, this, pointer); | |
} | |
} | |
for (int i = 0; i < handlers.Count; i++) | |
{ | |
if (handlers[i] != null) | |
{ | |
handlers[i].OnClick(StateManager, this, pointer); | |
} | |
} | |
} | |
/// <summary> | |
/// For input "clicks" that do not have corresponding input up/down tracking such as voice commands | |
/// Simulate pressed and start timer to reset states after some click time | |
/// </summary> | |
protected void StartGlobalVisual(bool voiceCommand = false) | |
{ | |
if (voiceCommand) | |
{ | |
HasVoiceCommand = true; | |
} | |
IsVisited = true; | |
HasFocus = true; | |
HasPress = true; | |
if (globalTimer != null) | |
{ | |
StopCoroutine(globalTimer); | |
} | |
globalTimer = StartCoroutine(GlobalVisualReset(globalFeedbackClickTime)); | |
} | |
/// <summary> | |
/// Clears up any automated visual states | |
/// </summary> | |
protected IEnumerator GlobalVisualReset(float time) | |
{ | |
yield return new WaitForSeconds(time); | |
HasVoiceCommand = false; | |
if (!HasFocus) | |
{ | |
HasFocus = false; | |
} | |
if (!HasPress) | |
{ | |
HasPress = false; | |
} | |
globalTimer = null; | |
} | |
/// <summary> | |
/// Public method that can be used to set state of interactable | |
/// corresponding to an input going down (select button, menu button, touch) | |
/// </summary> | |
public void SetInputDown() | |
{ | |
if (!CanInteract()) | |
{ | |
return; | |
} | |
dragStartPosition = null; | |
HasPress = true; | |
StartClickTimer(true); | |
} | |
/// <summary> | |
/// Public method that can be used to set state of interactable | |
/// corresponding to an input going up. | |
/// </summary> | |
public void SetInputUp() | |
{ | |
if (!CanInteract()) | |
{ | |
return; | |
} | |
HasPress = false; | |
HasGesture = false; | |
if (CanFireClick()) | |
{ | |
StopClickTimer(); | |
TriggerOnClick(); | |
IsVisited = true; | |
} | |
} | |
private void OnInputChangedHelper<T>(InputEventData<T> eventData, Vector3 inputPosition, float gestureDeadzoneThreshold) | |
{ | |
if (!CanInteract()) | |
{ | |
return; | |
} | |
if (ShouldListenToMoveEvent(eventData)) | |
{ | |
if (dragStartPosition == null) | |
{ | |
dragStartPosition = inputPosition; | |
} | |
else if (!HasGesture) | |
{ | |
if (Vector3.Distance(dragStartPosition.Value, inputPosition) > gestureStartThresholdVector2) | |
{ | |
HasGesture = true; | |
} | |
} | |
} | |
} | |
private bool ShouldListenToMoveEvent<T>(InputEventData<T> eventData) | |
{ | |
if (!(HasFocus || IsGlobal)) | |
{ | |
return false; | |
} | |
if (!HasPress) | |
{ | |
return false; | |
} | |
// Ensure that this move event is from a pointer that is pressing the interactable | |
int matchingPointerCount = 0; | |
foreach (var pressingInputSource in pressingInputSources) | |
{ | |
if (pressingInputSource == eventData.InputSource) | |
{ | |
matchingPointerCount++; | |
} | |
} | |
return matchingPointerCount > 0; | |
} | |
/// <summary> | |
/// Creates the default States ScriptableObject configured for Interactable | |
/// </summary> | |
/// <returns>Default Interactable States asset</returns> | |
public static States GetDefaultInteractableStates() | |
{ | |
States result = ScriptableObject.CreateInstance<States>(); | |
InteractableStates allInteractableStates = new InteractableStates(); | |
result.StateModelType = typeof(InteractableStates); | |
result.StateList = allInteractableStates.GetDefaultStates(); | |
result.DefaultIndex = 0; | |
return result; | |
} | |
/// <summary> | |
/// Helper function to create a new Theme asset using Default Interactable States and provided theme definitions | |
/// </summary> | |
/// <param name="themeDefintions">List of Theme Definitions to associate with Theme asset</param> | |
/// <returns>Theme ScriptableObject instance</returns> | |
public static Theme GetDefaultThemeAsset(List<ThemeDefinition> themeDefintions) | |
{ | |
// Create the Theme configuration asset | |
Theme newTheme = ScriptableObject.CreateInstance<Theme>(); | |
newTheme.States = GetDefaultInteractableStates(); | |
newTheme.Definitions = themeDefintions; | |
return newTheme; | |
} | |
#endregion | |
#region MixedRealityFocusChangedHandlers | |
/// <inheritdoc/> | |
public void OnBeforeFocusChange(FocusEventData eventData) | |
{ | |
if (!CanInteract()) | |
{ | |
return; | |
} | |
if (eventData.NewFocusedObject == null) | |
{ | |
focusingPointers.Remove(eventData.Pointer); | |
} | |
else if (eventData.NewFocusedObject.transform.IsChildOf(gameObject.transform)) | |
{ | |
if (!focusingPointers.Contains(eventData.Pointer)) | |
{ | |
focusingPointers.Add(eventData.Pointer); | |
} | |
} | |
else if (eventData.OldFocusedObject != null | |
&& eventData.OldFocusedObject.transform.IsChildOf(gameObject.transform)) | |
{ | |
focusingPointers.Remove(eventData.Pointer); | |
} | |
} | |
/// <inheritdoc/> | |
public void OnFocusChanged(FocusEventData eventData) { } | |
#endregion MixedRealityFocusChangedHandlers | |
#region MixedRealityFocusHandlers | |
/// <inheritdoc/> | |
public void OnFocusEnter(FocusEventData eventData) | |
{ | |
if (CanInteract()) | |
{ | |
Debug.Assert(focusingPointers.Count > 0, | |
"OnFocusEnter called but focusingPointers == 0. Most likely caused by the presence of a child object " + | |
"that is handling IMixedRealityFocusChangedHandler"); | |
HasFocus = true; | |
} | |
} | |
/// <inheritdoc/> | |
public void OnFocusExit(FocusEventData eventData) | |
{ | |
if (!CanInteract() && !HasFocus) | |
{ | |
return; | |
} | |
HasFocus = focusingPointers.Count > 0; | |
} | |
#endregion MixedRealityFocusHandlers | |
#region MixedRealityInputHandlers | |
/// <inheritdoc/> | |
public void OnPositionInputChanged(InputEventData<Vector2> eventData) { } | |
#endregion MixedRealityInputHandlers | |
#region MixedRealityVoiceCommands | |
/// <summary> | |
/// Voice commands from MixedRealitySpeechCommandProfile, keyword recognized | |
/// </summary> | |
public void OnSpeechKeywordRecognized(SpeechEventData eventData) | |
{ | |
if (eventData.Command.Keyword == VoiceCommand && (!VoiceRequiresFocus || HasFocus) && IsEnabled) | |
{ | |
StartGlobalVisual(true); | |
HasVoiceCommand = true; | |
SendVoiceCommands(VoiceCommand, 0, 1); | |
TriggerOnClick(); | |
eventData.Use(); | |
} | |
} | |
/// <summary> | |
/// call OnVoinceCommand methods on receivers or IInteractableHandlers | |
/// </summary> | |
protected void SendVoiceCommands(string command, int index, int length) | |
{ | |
for (int i = 0; i < InteractableEvents.Count; i++) | |
{ | |
if (InteractableEvents[i].Receiver != null) | |
{ | |
InteractableEvents[i].Receiver.OnVoiceCommand(StateManager, this, command, index, length); | |
} | |
} | |
for (int i = 0; i < handlers.Count; i++) | |
{ | |
if (handlers[i] != null) | |
{ | |
handlers[i].OnVoiceCommand(StateManager, this, command, index, length); | |
} | |
} | |
} | |
#endregion VoiceCommands | |
#region MixedRealityTouchHandlers | |
public void OnTouchStarted(HandTrackingInputEventData eventData) | |
{ | |
HasPress = true; | |
HasPhysicalTouch = true; | |
eventData.Use(); | |
} | |
public void OnTouchCompleted(HandTrackingInputEventData eventData) | |
{ | |
HasPress = false; | |
HasPhysicalTouch = false; | |
eventData.Use(); | |
} | |
public void OnTouchUpdated(HandTrackingInputEventData eventData) { } | |
#endregion TouchHandlers | |
#region MixedRealityInputHandlers | |
/// <inheritdoc/> | |
public void OnInputUp(InputEventData eventData) | |
{ | |
if (!CanInteract() && !HasPress) | |
{ | |
return; | |
} | |
if (ShouldListenToUpDownEvent(eventData)) | |
{ | |
SetInputUp(); | |
if (IsInputFromNearInteraction(eventData)) | |
{ | |
HasGrab = false; | |
} | |
eventData.Use(); | |
} | |
pressingInputSources.Remove(eventData.InputSource); | |
} | |
/// <inheritdoc/> | |
public void OnInputDown(InputEventData eventData) | |
{ | |
if (!CanInteract()) | |
{ | |
return; | |
} | |
if (ShouldListenToUpDownEvent(eventData)) | |
{ | |
pressingInputSources.Add(eventData.InputSource); | |
SetInputDown(); | |
HasGrab = IsInputFromNearInteraction(eventData); | |
eventData.Use(); | |
} | |
} | |
/// <inheritdoc/> | |
public void OnInputChanged(InputEventData<Vector2> eventData) | |
{ | |
OnInputChangedHelper(eventData, eventData.InputData, gestureStartThresholdVector2); | |
} | |
/// <inheritdoc/> | |
public void OnInputChanged(InputEventData<Vector3> eventData) | |
{ | |
OnInputChangedHelper(eventData, eventData.InputData, gestureStartThresholdVector3); | |
} | |
/// <inheritdoc/> | |
public void OnInputChanged(InputEventData<MixedRealityPose> eventData) | |
{ | |
OnInputChangedHelper(eventData, eventData.InputData.Position, gestureStartThresholdMixedRealityPose); | |
} | |
#endregion InputHandlers | |
#region Deprecated | |
/// <summary> | |
/// Resets input tracking states such as focus or grab that are directly controlled by Interactable | |
/// </summary> | |
[System.Obsolete("Use ResetInputTrackingStates property instead")] | |
public void ResetBaseStates() | |
{ | |
ResetInputTrackingStates(); | |
} | |
/// <summary> | |
/// A public way to access the current dimension | |
/// </summary> | |
[System.Obsolete("Use CurrentDimension property instead")] | |
public int GetDimensionIndex() | |
{ | |
return CurrentDimension; | |
} | |
/// <summary> | |
/// a public way to set the dimension index | |
/// </summary> | |
[System.Obsolete("Use CurrentDimension property instead")] | |
public void SetDimensionIndex(int index) | |
{ | |
CurrentDimension = index; | |
} | |
/// <summary> | |
/// Force re-initialization of Interactable from events, themes and state references | |
/// </summary> | |
[System.Obsolete("Use RefreshSetup() instead")] | |
public void ForceUpdateThemes() | |
{ | |
RefreshSetup(); | |
} | |
/// <summary> | |
/// Does this interactable require focus | |
/// </summary> | |
[System.Obsolete("Use IsGlobal instead")] | |
public bool FocusEnabled { get { return !IsGlobal; } set { IsGlobal = !value; } } | |
/// <summary> | |
/// True if Selection is "Toggle" (Dimensions == 2) | |
/// </summary> | |
[System.Obsolete("Use ButtonMode to test if equal to SelectionModes.Toggle instead")] | |
public bool IsToggleButton { get { return NumOfDimensions == 2; } } | |
/// <summary> | |
/// Is the interactable enabled? | |
/// </summary> | |
[System.Obsolete("Use IsEnabled instead")] | |
public bool Enabled | |
{ | |
get => IsEnabled; | |
set => IsEnabled = value; | |
} | |
/// <summary> | |
/// Do oice commands require focus? | |
/// </summary> | |
[System.Obsolete("Use VoiceRequiresFocus instead")] | |
public bool RequiresFocus | |
{ | |
get => VoiceRequiresFocus; | |
set => VoiceRequiresFocus = value; | |
} | |
/// <summary> | |
/// Is disabled | |
/// </summary> | |
[System.Obsolete("Use IsEnabled instead")] | |
public bool IsDisabled | |
{ | |
get => !IsEnabled; | |
set => IsEnabled = !value; | |
} | |
/// <summary> | |
/// Returns a list of states assigned to the Interactable | |
/// </summary> | |
[System.Obsolete("Use States.StateList instead")] | |
public State[] GetStates() | |
{ | |
if (States != null) | |
{ | |
return States.StateList.ToArray(); | |
} | |
return new State[0]; | |
} | |
/// <summary> | |
/// Handle focus state changes | |
/// </summary> | |
[System.Obsolete("Use Focus property instead")] | |
public virtual void SetFocus(bool focus) | |
{ | |
HasFocus = focus; | |
} | |
/// <summary> | |
/// Change the press state | |
/// </summary> | |
[System.Obsolete("Use Press property instead")] | |
public virtual void SetPress(bool press) | |
{ | |
HasPress = press; | |
} | |
/// <summary> | |
/// Change the disabled state, will override the Enabled property | |
/// </summary> | |
[System.Obsolete("Use IsEnabled property instead")] | |
public virtual void SetDisabled(bool disabled) | |
{ | |
IsEnabled = !disabled; | |
} | |
/// <summary> | |
/// Change the targeted state | |
/// </summary> | |
[System.Obsolete("Use IsTargeted property instead")] | |
public virtual void SetTargeted(bool targeted) | |
{ | |
IsTargeted = targeted; | |
} | |
/// <summary> | |
/// Change the Interactive state | |
/// </summary> | |
[System.Obsolete("Use IsInteractive property instead")] | |
public virtual void SetInteractive(bool interactive) | |
{ | |
IsInteractive = interactive; | |
} | |
/// <summary> | |
/// Change the observation targeted state | |
/// </summary> | |
[System.Obsolete("Use HasObservationTargeted property instead")] | |
public virtual void SetObservationTargeted(bool targeted) | |
{ | |
HasObservationTargeted = targeted; | |
} | |
/// <summary> | |
/// Change the observation state | |
/// </summary> | |
[System.Obsolete("Use HasObservation property instead")] | |
public virtual void SetObservation(bool observation) | |
{ | |
HasObservation = observation; | |
} | |
/// <summary> | |
/// Change the visited state | |
/// </summary> | |
[System.Obsolete("Use IsVisited property instead")] | |
public virtual void SetVisited(bool visited) | |
{ | |
IsVisited = visited; | |
} | |
/// <summary> | |
/// Change the toggled state | |
/// </summary> | |
[System.Obsolete("Use IsToggled property instead")] | |
public virtual void SetToggled(bool toggled) | |
{ | |
IsToggled = toggled; | |
} | |
/// <summary> | |
/// Change the gesture state | |
/// </summary> | |
[System.Obsolete("Use HasGesture property instead")] | |
public virtual void SetGesture(bool gesture) | |
{ | |
HasGesture = gesture; | |
} | |
/// <summary> | |
/// Change the gesture max state | |
/// </summary> | |
[System.Obsolete("Use HasGestureMax property instead")] | |
public virtual void SetGestureMax(bool gesture) | |
{ | |
HasGestureMax = gesture; | |
} | |
/// <summary> | |
/// Change the collision state | |
/// </summary> | |
[System.Obsolete("Use HasCollision property instead")] | |
public virtual void SetCollision(bool collision) | |
{ | |
HasCollision = collision; | |
} | |
/// <summary> | |
/// Change the custom state | |
/// </summary> | |
[System.Obsolete("Use HasCustom property instead")] | |
public virtual void SetCustom(bool custom) | |
{ | |
HasCustom = custom; | |
} | |
/// <summary> | |
/// Change the voice command state | |
/// </summary> | |
[System.Obsolete("Use HasVoiceCommand property instead")] | |
public virtual void SetVoiceCommand(bool voice) | |
{ | |
HasVoiceCommand = voice; | |
} | |
/// <summary> | |
/// Change the physical touch state | |
/// </summary> | |
[System.Obsolete("Use HasPhysicalTouch property instead")] | |
public virtual void SetPhysicalTouch(bool touch) | |
{ | |
HasPhysicalTouch = touch; | |
} | |
/// <summary> | |
/// Change the grab state | |
/// </summary> | |
[System.Obsolete("Use HasGrab property instead")] | |
public virtual void SetGrab(bool grab) | |
{ | |
HasGrab = grab; | |
} | |
#endregion | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright (c) Microsoft Corporation. All rights reserved. | |
// Licensed under the MIT License. See LICENSE in the project root for license information. | |
using Microsoft.MixedReality.Toolkit.Utilities; | |
using Microsoft.MixedReality.Toolkit.Utilities.Editor; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using UnityEngine; | |
using UnityEngine.Events; | |
namespace Microsoft.MixedReality.Toolkit.UI | |
{ | |
/// <summary> | |
/// Event base class for events attached to Interactables. | |
/// </summary> | |
[System.Serializable] | |
public class InteractableEvent | |
{ | |
/// <summary> | |
/// Base Event used to initialize EventReceiver class | |
/// </summary> | |
public UnityEvent Event = new UnityEvent(); | |
/// <summary> | |
/// ReceiverBase instantiation for this InteractableEvent. Used at runtime by Interactable class | |
/// </summary> | |
[NonSerialized] | |
public ReceiverBase Receiver; | |
/// <summary> | |
/// Defines the type of Receiver to associate. Type must be a class that extends ReceiverBase | |
/// </summary> | |
public Type ReceiverType | |
{ | |
get | |
{ | |
if (receiverType == null) | |
{ | |
if (string.IsNullOrEmpty(AssemblyQualifiedName)) | |
{ | |
return null; | |
} | |
receiverType = Type.GetType(AssemblyQualifiedName); | |
} | |
return receiverType; | |
} | |
set | |
{ | |
if (!value.IsSubclassOf(typeof(ReceiverBase))) | |
{ | |
Debug.LogWarning($"Cannot assign type {value} that does not extend {typeof(ReceiverBase)} to ThemeDefinition"); | |
return; | |
} | |
if (receiverType != value) | |
{ | |
receiverType = value; | |
ClassName = receiverType.Name; | |
AssemblyQualifiedName = receiverType.AssemblyQualifiedName; | |
} | |
} | |
} | |
// Unity cannot serialize System.Type, thus must save AssemblyQualifiedName | |
// Field here for Runtime use | |
[NonSerialized] | |
private Type receiverType; | |
[SerializeField] | |
private string ClassName; | |
[SerializeField] | |
private string AssemblyQualifiedName; | |
[SerializeField] | |
private List<InspectorPropertySetting> Settings = new List<InspectorPropertySetting>(); | |
/// <summary> | |
/// Create the event and setup the values from the inspector | |
/// </summary> | |
public static ReceiverBase CreateReceiver(InteractableEvent iEvent) | |
{ | |
if (string.IsNullOrEmpty(iEvent.ClassName)) | |
{ | |
return null; | |
} | |
// Temporary workaround | |
// This is to fix a bug in GA where the AssemblyQualifiedName was never actually saved. Functionality would work in editor...but never on device player | |
if (iEvent.ReceiverType == null) | |
{ | |
var correctType = TypeCacheUtility.GetSubClasses<ReceiverBase>().Where(s => s?.Name == iEvent.ClassName).First(); | |
iEvent.ReceiverType = correctType; | |
} | |
ReceiverBase newEvent = (ReceiverBase)Activator.CreateInstance(iEvent.ReceiverType, iEvent.Event); | |
InspectorGenericFields<ReceiverBase>.LoadSettings(newEvent, iEvent.Settings); | |
return newEvent; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment