407 lines
17 KiB
C#
407 lines
17 KiB
C#
/*
|
|
* 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
|
|
{
|
|
/// <summary>
|
|
/// Chains together a number of <see cref="IActiveState"/>s into a sequence.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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: <see cref="ActivationStep.MinActiveTime"/> <= "Time step
|
|
/// N active for" <= <see cref="ActivationStep.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:
|
|
/// <see cref="ActivationStep.ActiveState"/>: The IActiveState that is used to determine if the conditions of this step are fulfilled
|
|
/// <see cref="ActivationStep.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.
|
|
/// <see cref="ActivationStep.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.
|
|
/// </remarks>
|
|
public class Sequence : MonoBehaviour,
|
|
IActiveState, ITimeConsumer
|
|
{
|
|
/// <summary>
|
|
/// Wrapper for <see cref="IActiveState"/>s which are to be incorporated into a <see cref="Sequence"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This type is exclusively intended to be used as part of a <see cref="Sequence"/>, and details of its
|
|
/// intended usage and expected behavior are documented in the remarks for that type.
|
|
/// </remarks>
|
|
[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;
|
|
|
|
/// <summary>
|
|
/// The <see cref="IActiveState"/> whose activation is the condition for completing an ActivationStep of
|
|
/// the containing <see cref="Sequence"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// For a detailed overview of the function of ActivationSteps and their role in Sequences, please see the
|
|
/// documentation for <see cref="Sequence"/>.
|
|
/// </remarks>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// The minimum amount of time for which <see cref="ActiveState"/> must be active in order for this
|
|
/// ActivationStep to be considered completed.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// For a detailed overview of the function of ActivationSteps and their role in Sequences, please see the
|
|
/// documentation for <see cref="Sequence"/>.
|
|
/// </remarks>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// The maximum amount of time a <see cref="Sequence"/> can remain in this ActivationStep (without proceeding
|
|
/// to the next one) before the Sequence "fails out" and must start again from the beginning.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// For a detailed overview of the function of ActivationSteps and their role in Sequences, please see the
|
|
/// documentation for <see cref="Sequence"/>.
|
|
/// </remarks>
|
|
public float MaxStepTime => _maxStepTime;
|
|
|
|
/// <summary>
|
|
/// Basic constructor for ActivationStep. Should not be called directly.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This empty constructor is typically invoked by Unity logic when deserializing a serialized-and-saved
|
|
/// ActivationStep.
|
|
/// </remarks>
|
|
public ActivationStep()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor for constructing new ActivationSteps programmatically.
|
|
/// </summary>
|
|
/// <param name="activeState">The <see cref="IActiveState"/> to be assigned to <see cref="ActiveState"/></param>
|
|
/// <param name="minActiveTime">The float value to be assigned to <see cref="MinActiveTime"/></param>
|
|
/// <param name="maxStepTime">The float value to be assigned to <see cref="MaxStepTime"/></param>
|
|
public ActivationStep(IActiveState activeState, float minActiveTime, float maxStepTime)
|
|
{
|
|
ActiveState = activeState;
|
|
_minActiveTime = minActiveTime;
|
|
_maxStepTime = maxStepTime;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Method invoked when a <see cref="Sequence"/> "moves into" this ActivationStep. This method should only ever
|
|
/// be invoked by the internal logic of its containing Sequence.
|
|
/// </summary>
|
|
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<float> _timeProvider = () => Time.time;
|
|
|
|
/// <summary>
|
|
/// Implements <see cref="ITimeConsumer.SetTimeProvider(Func{float})"/>.
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="timeProvider">The new time provider to be used by this instance.</param>
|
|
public void SetTimeProvider(Func<float> timeProvider)
|
|
{
|
|
_timeProvider = timeProvider;
|
|
}
|
|
|
|
private IActiveState RemainActiveWhile { get; set; }
|
|
|
|
/// <summary>
|
|
/// Returns the index of the step in <see cref="_stepsToActivate"/> whose conditions are
|
|
/// waiting to be activated.
|
|
/// If <see cref="Active"/> is true, this value will be set to the
|
|
/// size of <see cref="_stepsToActivate"/>.
|
|
/// If <see cref="_stepsToActivate"/> has no steps, this property will be 0.
|
|
/// </summary>
|
|
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<ActivationStep>();
|
|
}
|
|
|
|
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
|
|
|
|
/// <summary>
|
|
/// Implements <see cref="IActiveState.Active"/>, in this case indicating whether the described sequence
|
|
/// of IActiveStates has been "recognized" as having occurred with acceptable order and timing.
|
|
/// </summary>
|
|
public bool Active { get; private set; }
|
|
|
|
static Sequence()
|
|
{
|
|
ActiveStateDebugTree.RegisterModel<Sequence>(new DebugModel());
|
|
}
|
|
|
|
private class DebugModel : ActiveStateModel<Sequence>
|
|
{
|
|
private IEnumerator GetChildrenCoroutine(Sequence sequence,
|
|
TaskCompletionSource<IEnumerable<IActiveState>> tcs)
|
|
{
|
|
while (sequence._stepsToActivate.Any(s => s.ActiveState == null))
|
|
{
|
|
// Wait for all child ActiveStates to be initialized.
|
|
yield return null;
|
|
}
|
|
List<IActiveState> children = new List<IActiveState>();
|
|
children.AddRange(sequence._stepsToActivate.Select(step => step.ActiveState));
|
|
children.Add(sequence.RemainActiveWhile);
|
|
tcs.SetResult(children.Where(c => c != null));
|
|
}
|
|
|
|
protected override Task<IEnumerable<IActiveState>> GetChildrenAsync(Sequence activeState)
|
|
{
|
|
TaskCompletionSource<IEnumerable<IActiveState>> tcs = new();
|
|
if (activeState.isActiveAndEnabled)
|
|
{
|
|
activeState.StartCoroutine(GetChildrenCoroutine(activeState, tcs));
|
|
}
|
|
else
|
|
{
|
|
tcs.SetResult(Enumerable.Empty<IActiveState>());
|
|
}
|
|
return tcs.Task;
|
|
}
|
|
}
|
|
|
|
#region Inject
|
|
|
|
/// <summary>
|
|
/// Sets the <see cref="ActivationStep"/>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.
|
|
/// </summary>
|
|
public void InjectOptionalStepsToActivate(ActivationStep[] stepsToActivate)
|
|
{
|
|
_stepsToActivate = stepsToActivate;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the <see cref="IActiveState"/> 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.
|
|
/// </summary>
|
|
public void InjectOptionalRemainActiveWhile(IActiveState activeState)
|
|
{
|
|
_remainActiveWhile = activeState as UnityEngine.Object;
|
|
RemainActiveWhile = activeState;
|
|
}
|
|
|
|
[Obsolete("Use SetTimeProvider()")]
|
|
public void InjectOptionalTimeProvider(Func<float> timeProvider)
|
|
{
|
|
_timeProvider = timeProvider;
|
|
}
|
|
|
|
#endregion
|
|
|
|
}
|
|
}
|