/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * Licensed under the Oculus SDK License Agreement (the "License"); * you may not use the Oculus SDK except in compliance with the License, * which is provided at the time of installation or download, or which * otherwise accompanies this software in either electronic or hard copy form. * * You may obtain a copy of the License at * * https://developer.oculus.com/licenses/oculussdk/ * * Unless required by applicable law or agreed to in writing, the Oculus SDK * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using Oculus.Interaction.PoseDetection.Debug; using System.Linq; using UnityEngine; using UnityEngine.Assertions; using System.Threading.Tasks; using System.Collections; namespace Oculus.Interaction.PoseDetection { /// /// Chains together a number of s into a sequence. /// /// /// The Sequence._stepsToActivate field contains an optional list of IActiveState's which must be 'activated' in /// order. /// The sequence can progress from Step N to N + 1 when: <= "Time step /// N active for" <= , and: /// /// Step N just became inactive OR /// Step N is the last step OR /// Step N+1 is active /// /// Note that once the sequence has moved on to the next step, the previous step does not need to remain active. /// Each step has three fields: /// : The IActiveState that is used to determine if the conditions of this step are fulfilled /// : How long (in seconds) the IActiveState of this step must be contiguously active before moving /// on to the next step. If the ActiveState drops out of being active for even a single frame /// the countdown is reset. /// : If the elapsed time that the sequence is spent waiting for this step to reach its MinActiveTime /// exceeds this value then the whole sequence is reset back to the beginning. /// /// Once all steps are complete the Sequence.Active becomes true. It will remain true as long as RemainActiveWhile /// is true. If _remainActiveCooldown > 0, Sequence.Active will remain active even after RemainActiveWhile becomes /// false until the cooldown timer is met. The timer is reset if RemainActiveWhile becomes true again. /// public class Sequence : MonoBehaviour, IActiveState, ITimeConsumer { /// /// Wrapper for s which are to be incorporated into a . /// /// /// This type is exclusively intended to be used as part of a , and details of its /// intended usage and expected behavior are documented in the remarks for that type. /// [Serializable] public class ActivationStep { [Tooltip("The IActiveState that is used to determine if the conditions of this step are fulfilled.")] [SerializeField, Interface(typeof(IActiveState))] private UnityEngine.Object _activeState; /// /// The whose activation is the condition for completing an ActivationStep of /// the containing . /// /// /// For a detailed overview of the function of ActivationSteps and their role in Sequences, please see the /// documentation for . /// public IActiveState ActiveState { get; private set; } [SerializeField] [Tooltip("This step must be consistently active for this amount of time before continuing to the next step.")] private float _minActiveTime; /// /// The minimum amount of time for which must be active in order for this /// ActivationStep to be considered completed. /// /// /// For a detailed overview of the function of ActivationSteps and their role in Sequences, please see the /// documentation for . /// public float MinActiveTime => _minActiveTime; [SerializeField] [Tooltip( "Maximum time that can be spent waiting for this step to complete, before the whole sequence is abandoned. " + "This value must be greater than minActiveTime, or zero. This value is ignored if zero, and for the first step in the list.")] private float _maxStepTime; /// /// The maximum amount of time a can remain in this ActivationStep (without proceeding /// to the next one) before the Sequence "fails out" and must start again from the beginning. /// /// /// For a detailed overview of the function of ActivationSteps and their role in Sequences, please see the /// documentation for . /// public float MaxStepTime => _maxStepTime; /// /// Basic constructor for ActivationStep. Should not be called directly. /// /// /// This empty constructor is typically invoked by Unity logic when deserializing a serialized-and-saved /// ActivationStep. /// public ActivationStep() { } /// /// Constructor for constructing new ActivationSteps programmatically. /// /// The to be assigned to /// The float value to be assigned to /// The float value to be assigned to public ActivationStep(IActiveState activeState, float minActiveTime, float maxStepTime) { ActiveState = activeState; _minActiveTime = minActiveTime; _maxStepTime = maxStepTime; } /// /// Method invoked when a "moves into" this ActivationStep. This method should only ever /// be invoked by the internal logic of its containing Sequence. /// public void Start() { if (ActiveState == null) { ActiveState = _activeState as IActiveState; } Assert.IsNotNull(ActiveState); } } [Tooltip("The sequence will step through these ActivationSteps one at a " + "time, advancing when each step becomes Active. Once all steps are active, " + "the sequence itself will become Active.")] [SerializeField, Optional] private ActivationStep[] _stepsToActivate; [Tooltip("Once the sequence is active, it will remain active as long as " + "this IActiveState is Active.")] [SerializeField, Optional, Interface(typeof(IActiveState))] private UnityEngine.Object _remainActiveWhile; [Tooltip("Sequence will not become inactive until RemainActiveWhile has " + "been inactive for at least this many seconds.")] [SerializeField, Optional] private float _remainActiveCooldown; private Func _timeProvider = () => Time.time; /// /// Implements . /// Sets the time provider for this sequence, allowing for the default time provider (Unity's built-in Time.time) /// to be overridden with custom behavior to enable pausing, time dilation, etc. /// /// The new time provider to be used by this instance. public void SetTimeProvider(Func timeProvider) { _timeProvider = timeProvider; } private IActiveState RemainActiveWhile { get; set; } /// /// Returns the index of the step in whose conditions are /// waiting to be activated. /// If is true, this value will be set to the /// size of . /// If has no steps, this property will be 0. /// public int CurrentActivationStep { get; private set; } private float _currentStepActivatedTime; private float _stepFailedTime; private bool _currentStepWasActive; private float _cooldownExceededTime; private bool _wasRemainActive; #region Unity Lifecycle protected virtual void Awake() { RemainActiveWhile = _remainActiveWhile as IActiveState; ResetState(); } protected virtual void Start() { if (_stepsToActivate == null) { _stepsToActivate = Array.Empty(); } foreach (var step in _stepsToActivate) { step.Start(); } } protected virtual void Update() { var time = _timeProvider(); if (Active) { // Test for active, if RemainActiveWhile is set. bool shouldBeActive = RemainActiveWhile != null && RemainActiveWhile.Active; if (!shouldBeActive) { if (_wasRemainActive) { _cooldownExceededTime = time + _remainActiveCooldown; } if (_cooldownExceededTime <= time) { Active = false; } } _wasRemainActive = shouldBeActive; // No longer active; start activation condition at the beginning if (!Active) { ResetState(); } return; } if (CurrentActivationStep < _stepsToActivate.Length) { var currentStep = _stepsToActivate[CurrentActivationStep]; if (time > _stepFailedTime && CurrentActivationStep > 0 && currentStep.MaxStepTime > 0.0f) { // Failed to activate before max time limit reached. Start from the beginning. ResetState(); } bool currentStepIsActive = currentStep.ActiveState.Active; if (currentStepIsActive) { if (!_currentStepWasActive) { // Step wasn't active, but now it is! Start the timer until next step can // be entered. _currentStepActivatedTime = time + currentStep.MinActiveTime; } } if (time >= _currentStepActivatedTime && _currentStepWasActive) { // Time constraint met. Go to next step if either: // - this step just became inactive OR // - this is the last step OR // - the next step is active var nextStepIndex = CurrentActivationStep + 1; bool thisStepCondition = !currentStepIsActive; bool nextStepCondition = (nextStepIndex == _stepsToActivate.Length) || _stepsToActivate[nextStepIndex].ActiveState.Active; if (thisStepCondition || nextStepCondition) { EnterNextStep(time); } } _currentStepWasActive = currentStepIsActive; } else if (RemainActiveWhile != null) { Active = RemainActiveWhile.Active; } } private void EnterNextStep(float time) { CurrentActivationStep++; _currentStepWasActive = false; if (CurrentActivationStep < _stepsToActivate.Length) { var currentStep = _stepsToActivate[CurrentActivationStep]; _stepFailedTime = time + currentStep.MaxStepTime; return; } // This was the last step. Activate. Active = true; // In case there is no RemainActiveWhile condition, start the cooldown // timer _cooldownExceededTime = time + _remainActiveCooldown; // Activate native component NativeMethods.isdk_NativeComponent_Activate(0x5365717565446574); } private void ResetState() { CurrentActivationStep = 0; _currentStepWasActive = false; _currentStepActivatedTime = 0.0f; } #endregion /// /// Implements , in this case indicating whether the described sequence /// of IActiveStates has been "recognized" as having occurred with acceptable order and timing. /// public bool Active { get; private set; } static Sequence() { ActiveStateDebugTree.RegisterModel(new DebugModel()); } private class DebugModel : ActiveStateModel { private IEnumerator GetChildrenCoroutine(Sequence sequence, TaskCompletionSource> tcs) { while (sequence._stepsToActivate.Any(s => s.ActiveState == null)) { // Wait for all child ActiveStates to be initialized. yield return null; } List children = new List(); children.AddRange(sequence._stepsToActivate.Select(step => step.ActiveState)); children.Add(sequence.RemainActiveWhile); tcs.SetResult(children.Where(c => c != null)); } protected override Task> GetChildrenAsync(Sequence activeState) { TaskCompletionSource> tcs = new(); if (activeState.isActiveAndEnabled) { activeState.StartCoroutine(GetChildrenCoroutine(activeState, tcs)); } else { tcs.SetResult(Enumerable.Empty()); } return tcs.Task; } } #region Inject /// /// Sets the s for a dynamically instantiated Sequence. This method /// exists to support Interaction SDK's dependency injection pattern and is not needed for typical Unity Editor-based /// usage. /// public void InjectOptionalStepsToActivate(ActivationStep[] stepsToActivate) { _stepsToActivate = stepsToActivate; } /// /// Sets the for the optional "remain active while" capability (which allows a Sequence to /// remain active pending the inactivity of the given IActiveState rather than resetting after activation) for a dynamically /// instantiated Sequence. This method exists to support Interaction SDK's dependency injection pattern and is not needed /// for typical Unity Editor-based usage. /// public void InjectOptionalRemainActiveWhile(IActiveState activeState) { _remainActiveWhile = activeState as UnityEngine.Object; RemainActiveWhile = activeState; } [Obsolete("Use SetTimeProvider()")] public void InjectOptionalTimeProvider(Func timeProvider) { _timeProvider = timeProvider; } #endregion } }