/* * 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; namespace Oculus.Interaction.PoseDetection { /// /// Chains together a number of IActiveStates 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: MinActiveTime <= "Time step N active for" <= MaxStepTime, 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: /// ActiveState: The IActiveState that is used to determine if the conditions of this step are fulfilled /// MinActiveTime: 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. /// MaxStepTime: 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 { [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; 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; 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; public float MaxStepTime => _maxStepTime; public ActivationStep() { } public ActivationStep(IActiveState activeState, float minActiveTime, float maxStepTime) { ActiveState = activeState; _minActiveTime = minActiveTime; _maxStepTime = maxStepTime; } 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 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; Func _timeProvider; private float _cooldownExceededTime; private bool _wasRemainActive; #region Unity Lifecycle protected virtual void Awake() { RemainActiveWhile = _remainActiveWhile as IActiveState; ResetState(); } protected virtual void Start() { if (_timeProvider == null) { _timeProvider = () => Time.time; } 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 public bool Active { get; private set; } static Sequence() { ActiveStateDebugTree.RegisterModel(new DebugModel()); } private class DebugModel : ActiveStateModel { protected override IEnumerable GetChildren(Sequence activeState) { List children = new List(); children.AddRange(activeState._stepsToActivate.Select(step => step.ActiveState)); children.Add(activeState.RemainActiveWhile); return children.Where(c => c != null); } } #region Inject public void InjectOptionalStepsToActivate(ActivationStep[] stepsToActivate) { _stepsToActivate = stepsToActivate; } public void InjectOptionalRemainActiveWhile(IActiveState activeState) { _remainActiveWhile = activeState as UnityEngine.Object; RemainActiveWhile = activeState; } public void InjectOptionalTimeProvider(Func timeProvider) { _timeProvider = timeProvider; } #endregion } }