948 lines
36 KiB
C#
948 lines
36 KiB
C#
// Copyright (c) 2015 - 2023 Doozy Entertainment. All Rights Reserved.
|
|
// This code can only be used under the standard Unity Asset Store End User License Agreement
|
|
// A Copy of the EULA APPENDIX 1 is available at http://unity3d.com/company/legal/as_terms
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Doozy.Runtime.Common.Attributes;
|
|
using Doozy.Runtime.Common.Extensions;
|
|
using Doozy.Runtime.Reactor.Easings;
|
|
using Doozy.Runtime.Reactor.Ticker;
|
|
using UnityEngine;
|
|
using static UnityEngine.Mathf;
|
|
|
|
namespace Doozy.Runtime.Reactor.Internal
|
|
{
|
|
[Serializable]
|
|
public abstract class Reaction
|
|
{
|
|
#region Reaction State
|
|
|
|
/// <summary> Reaction current state </summary>
|
|
public ReactionState state { get; internal set; }
|
|
|
|
/// <summary> Reaction state before it was paused (internal use only) </summary>
|
|
public ReactionState stateBeforePause { get; internal set; }
|
|
|
|
/// <summary> Reaction is in the Pool </summary>
|
|
public bool isPooled => state == ReactionState.Pooled;
|
|
|
|
/// <summary> Reaction is ready to run </summary>
|
|
public bool isIdle => state == ReactionState.Idle;
|
|
|
|
/// <summary> Reaction is running (is not in the pool and is not idle) </summary>
|
|
public bool isActive => !isPooled & !isIdle;
|
|
|
|
/// <summary> Reaction is running, but it is paused </summary>
|
|
public bool isPaused => state == ReactionState.Paused;
|
|
|
|
/// <summary> Reaction is running and is playing the animation </summary>
|
|
public bool isPlaying => state == ReactionState.Playing;
|
|
|
|
/// <summary> Reaction is running and is waiting to start playing the animation </summary>
|
|
public bool inStartDelay => state == ReactionState.StartDelay;
|
|
|
|
/// <summary> Reaction is running and is waiting the start playing the next loop </summary>
|
|
public bool inLoopDelay => state == ReactionState.LoopDelay;
|
|
|
|
#endregion
|
|
|
|
/// <summary> Reaction settings </summary>
|
|
[SerializeField] private ReactionSettings Settings;
|
|
|
|
/// <summary> Reaction settings </summary>
|
|
public ReactionSettings settings
|
|
{
|
|
get => Settings;
|
|
internal set => Settings = value;
|
|
}
|
|
|
|
private float m_LastProgress;
|
|
/// <summary> Reaction calculated progress = elapsedDuration / duration </summary>
|
|
public float progress => m_LastProgress = Clamp01((float)(elapsedDuration / duration));
|
|
|
|
/// <summary> Reaction calculated eased progress. Progress value with the ease modifier applied </summary>
|
|
public float easedProgress => Settings.CalculateEasedProgress(progress);
|
|
|
|
/// <summary>
|
|
/// Reaction current play direction
|
|
/// <para/> Forward - progress goes from 0 to 1
|
|
/// <para/> Reverse - progress goes from 1 to 0
|
|
/// </summary>
|
|
public PlayDirection direction { get; internal set; }
|
|
|
|
/// <summary> Reaction heartbeat (ticker) </summary>
|
|
public Heartbeat heartbeat { get; private set; }
|
|
|
|
/// <summary> Current start delay </summary>
|
|
public float startDelay { get; internal set; }
|
|
/// <summary> Current elapsed start delay </summary>
|
|
public double elapsedStartDelay { get; private set; }
|
|
/// <summary>
|
|
/// Special constant used to simulate zero duration. It is needed because we calculate progress = elapsedDuration / duration.
|
|
/// If duration becomes zero, we get NaN (Not a Number) error. Miau!
|
|
/// </summary>
|
|
private const float MIN_DURATION = 0.001f;
|
|
/// <summary> Current duration </summary>
|
|
public float duration { get; internal set; }
|
|
/// <summary> Current elapsed duration </summary>
|
|
public double elapsedDuration { get; protected set; }
|
|
/// <summary> Current start duration (needed to reverse the reaction flow) </summary>
|
|
protected float startDuration { get; set; }
|
|
/// <summary> Current target duration (needed to reverse the reaction flow) </summary>
|
|
protected float targetDuration { get; set; }
|
|
/// <summary> Flag used to mark that the reaction is not playing from start to finish, but from a FROM progress value to a TO progress value </summary>
|
|
protected bool customStartDuration { get; set; }
|
|
/// <summary> Current loops count </summary>
|
|
public int loops { get; internal set; }
|
|
/// <summary> Current elapsed loops count </summary>
|
|
public int elapsedLoops { get; private set; }
|
|
/// <summary> Current loop delay (delay between loops) </summary>
|
|
public float loopDelay { get; internal set; }
|
|
/// <summary> Current elapsed loop delay (elapsed delay between loops) </summary>
|
|
public double elapsedLoopDelay { get; private set; }
|
|
|
|
/// <summary> Callback invoked when the Reaction starts playing </summary>
|
|
public ReactionCallback OnPlayCallback;
|
|
/// <summary> Callback invoked when the Reaction stops playing </summary>
|
|
public ReactionCallback OnStopCallback;
|
|
/// <summary> Callback invoked when the Reaction finished playing </summary>
|
|
public ReactionCallback OnFinishCallback;
|
|
/// <summary> Callback invoked when the Reaction starts playing a loop (invoked every loop) </summary>
|
|
public ReactionCallback OnLoopCallback;
|
|
/// <summary> Callback invoked when the Reaction was paused (the Reaction is still running) </summary>
|
|
public ReactionCallback OnPauseCallback;
|
|
/// <summary> Callback invoked when the Reaction resumed playing </summary>
|
|
public ReactionCallback OnResumeCallback;
|
|
/// <summary> Callback invoked when the Reaction updates </summary>
|
|
public ReactionCallback OnUpdateCallback;
|
|
|
|
#region Cycle Variables
|
|
|
|
protected float currentCycleEasedProgress => Settings.CalculateEasedProgress(currentCycleProgress);
|
|
protected List<float> cycleDurations { get; set; }
|
|
protected int numberOfCycles { get; set; }
|
|
protected int previousCycleIndex { get; set; }
|
|
protected int currentCycleIndex { get; set; }
|
|
protected float currentCycleDuration
|
|
{
|
|
get
|
|
{
|
|
if (cycleDurations == null || currentCycleIndex != cycleDurations.Count)
|
|
ComputePlayMode();
|
|
return cycleDurations[currentCycleIndex];
|
|
}
|
|
}
|
|
protected float currentCycleElapsedDuration
|
|
{
|
|
get
|
|
{
|
|
if (currentCycleIndex == 0) return (float)elapsedDuration;
|
|
float cycleStartDuration = cycleDurations.TakeWhile((t, i) => currentCycleIndex != i).Sum();
|
|
return Clamp((float)(elapsedDuration - cycleStartDuration), 0, targetDuration);
|
|
}
|
|
}
|
|
|
|
protected float currentCycleProgress
|
|
{
|
|
get
|
|
{
|
|
float cycleProgress = Clamp01(currentCycleElapsedDuration / currentCycleDuration);
|
|
return Approximately(0, cycleProgress) ? 0f : Approximately(cycleProgress, 1f) ? 1f : cycleProgress;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary> Reset all callbacks </summary>
|
|
public void ResetCallbacks()
|
|
{
|
|
OnUpdateCallback = null;
|
|
OnPlayCallback = null;
|
|
OnStopCallback = null;
|
|
OnFinishCallback = null;
|
|
OnLoopCallback = null;
|
|
OnPauseCallback = null;
|
|
OnResumeCallback = null;
|
|
}
|
|
|
|
protected Reaction()
|
|
{
|
|
// ReSharper disable once IdentifierTypo
|
|
const int preallocatedCapacity = 100;
|
|
cycleDurations = new List<float>(preallocatedCapacity);
|
|
|
|
Settings = new ReactionSettings();
|
|
this.SetRuntimeHeartbeat();
|
|
}
|
|
|
|
/// <summary> Reset the reaction </summary>
|
|
public virtual void Reset()
|
|
{
|
|
if (isActive) Stop(true);
|
|
ClearIds();
|
|
this.ClearCallbacks();
|
|
Settings ??= new ReactionSettings();
|
|
Settings.Reset();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clear the reaction's ids
|
|
/// </summary>
|
|
private void ClearIds()
|
|
{
|
|
objectId = null;
|
|
stringId = null;
|
|
intId = k_DefaultIntId;
|
|
targetObject = null;
|
|
}
|
|
|
|
/// <summary> Reverse the reaction's play direction (works if the reaction is running) </summary>
|
|
public void Reverse()
|
|
{
|
|
if (!isActive) return;
|
|
if (inStartDelay)
|
|
{
|
|
Stop();
|
|
return;
|
|
}
|
|
direction = (PlayDirection)((int)direction * -1f);
|
|
}
|
|
|
|
/// <summary> Rewind the reaction to the start (works in both play directions) </summary>
|
|
public void Rewind()
|
|
{
|
|
elapsedDuration = direction == PlayDirection.Forward ? 0f : targetDuration;
|
|
}
|
|
|
|
/// <summary> Pause the reaction (if it's active) </summary>
|
|
/// <param name="silent"> If TRUE, callbacks will not be invoked </param>
|
|
public void Pause(bool silent = false)
|
|
{
|
|
if (!isActive) return;
|
|
stateBeforePause = state;
|
|
state = ReactionState.Paused;
|
|
if (!silent) OnPauseCallback?.Invoke();
|
|
}
|
|
|
|
/// <summary> Resume the reaction (if it's paused) </summary>
|
|
/// <param name="silent"> If TRUE, callbacks will not be invoked </param>
|
|
public void Resume(bool silent = false)
|
|
{
|
|
if (!isPaused) return;
|
|
state = stateBeforePause;
|
|
if (isActive & !heartbeat.isActive)
|
|
heartbeat.RegisterToTickService();
|
|
if (!silent) OnResumeCallback?.Invoke();
|
|
}
|
|
|
|
/// <summary> Play the reaction, in the given direction </summary>
|
|
/// <param name="playDirection"> Play direction </param>
|
|
public void Play(PlayDirection playDirection) =>
|
|
Play(playDirection == PlayDirection.Reverse);
|
|
|
|
/// <summary> Play the reaction </summary>
|
|
/// <param name="inReverse"> If TRUE, the reaction will play in reverse </param>
|
|
public virtual void Play(bool inReverse = false)
|
|
{
|
|
if (isActive)
|
|
{
|
|
switch (direction)
|
|
{
|
|
case PlayDirection.Forward:
|
|
if (inReverse)
|
|
{
|
|
if (inStartDelay)
|
|
{
|
|
Stop();
|
|
return;
|
|
}
|
|
Reverse();
|
|
return;
|
|
}
|
|
break;
|
|
case PlayDirection.Reverse:
|
|
if (!inReverse)
|
|
{
|
|
if (inStartDelay)
|
|
{
|
|
Stop();
|
|
return;
|
|
}
|
|
Reverse();
|
|
return;
|
|
}
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
}
|
|
|
|
if (isActive) Stop(true);
|
|
ResetElapsedValues();
|
|
RefreshSettings();
|
|
|
|
direction = inReverse ? PlayDirection.Reverse : PlayDirection.Forward;
|
|
|
|
customStartDuration = false;
|
|
startDuration = 0f;
|
|
targetDuration = duration;
|
|
|
|
elapsedDuration = direction == PlayDirection.Forward ? startDuration : targetDuration;
|
|
m_LastProgress = progress;
|
|
|
|
ComputePlayMode();
|
|
OnPlayCallback?.Invoke();
|
|
|
|
if (startDelay <= 0 & duration <= MIN_DURATION)
|
|
{
|
|
// No delay, no duration, just invoke the finish callback
|
|
switch (direction)
|
|
{
|
|
|
|
case PlayDirection.Forward:
|
|
SetProgressAtOne();
|
|
break;
|
|
case PlayDirection.Reverse:
|
|
SetProgressAtZero();
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
|
|
OnStopCallback?.Invoke(); // invoke the stop callback
|
|
OnFinishCallback?.Invoke(); // invoke the finish callback
|
|
|
|
// the implementation below is not working for zero duration
|
|
// ------------------------------------------
|
|
// elapsedDuration = direction == PlayDirection.Forward ? targetDuration : startDuration;
|
|
// m_LastProgress = progress;
|
|
// UpdateReaction();
|
|
// return;
|
|
// ------------------------------------------
|
|
}
|
|
|
|
state = startDelay > 0 & direction == PlayDirection.Forward ? ReactionState.StartDelay : ReactionState.Playing;
|
|
heartbeat.RegisterToTickService();
|
|
}
|
|
|
|
/// <summary> Play the reaction from the given start progress (from) to the given end progress (to) </summary>
|
|
/// <param name="fromProgress"> From (start) progress </param>
|
|
/// <param name="toProgress"> To (end) progress </param>
|
|
public virtual void PlayFromToProgress(float fromProgress, float toProgress)
|
|
{
|
|
fromProgress = GetAdjustedProgress(fromProgress, settings.playMode);
|
|
toProgress = GetAdjustedProgress(toProgress, settings.playMode);
|
|
|
|
if (isActive) Stop(true);
|
|
ResetElapsedValues();
|
|
RefreshSettings();
|
|
|
|
direction = fromProgress <= toProgress ? PlayDirection.Forward : PlayDirection.Reverse;
|
|
|
|
customStartDuration = true;
|
|
|
|
float fromDuration = GetDurationAtProgress(fromProgress, duration);
|
|
float toDuration = GetDurationAtProgress(toProgress, duration);
|
|
|
|
startDuration = direction == PlayDirection.Forward ? fromDuration : toDuration;
|
|
targetDuration = direction == PlayDirection.Forward ? toDuration : fromDuration;
|
|
|
|
elapsedDuration = direction == PlayDirection.Forward ? startDuration : targetDuration;
|
|
m_LastProgress = progress;
|
|
|
|
ComputePlayMode();
|
|
OnPlayCallback?.Invoke();
|
|
|
|
if (duration <= MIN_DURATION)
|
|
{
|
|
// No delay, no duration, just set the proper progress value and invoke the finish callback
|
|
switch (direction)
|
|
{
|
|
|
|
case PlayDirection.Forward:
|
|
SetProgressAtOne();
|
|
break;
|
|
case PlayDirection.Reverse:
|
|
SetProgressAtZero();
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
|
|
OnStopCallback?.Invoke(); // invoke the stop callback
|
|
OnFinishCallback?.Invoke(); // invoke the finish callback
|
|
|
|
// the implementation below is not working properly for zero duration
|
|
// ------------------------------------------------------------------
|
|
// elapsedDuration = direction == PlayDirection.Forward ? targetDuration : startDuration;
|
|
// m_LastProgress = progress;
|
|
// UpdateReaction();
|
|
// return;
|
|
// ------------------------------------------------------------------
|
|
}
|
|
|
|
state = ReactionState.Playing;
|
|
heartbeat.RegisterToTickService();
|
|
}
|
|
|
|
/// <summary> Play the reaction from the current progress to the given end progress (to) </summary>
|
|
/// <param name="toProgress"> To (end) progress </param>
|
|
public virtual void PlayToProgress(float toProgress) =>
|
|
PlayFromToProgress(m_LastProgress, toProgress);
|
|
|
|
/// <summary> Play the reaction from the given start progress (from) to the current progress </summary>
|
|
/// <param name="fromProgress"> From (start) progress </param>
|
|
public virtual void PlayFromProgress(float fromProgress) =>
|
|
PlayFromToProgress(fromProgress, m_LastProgress);
|
|
|
|
/// <summary> Get the elapsedDuration value at the given target progress </summary>
|
|
/// <param name="targetProgress"> Target progress </param>
|
|
/// <param name="totalDuration"> Duration </param>
|
|
protected float GetDurationAtProgress(float targetProgress, float totalDuration)
|
|
{
|
|
targetProgress = Clamp01(targetProgress);
|
|
totalDuration = Max(0, totalDuration);
|
|
totalDuration = totalDuration == 0 ? 1 : totalDuration;
|
|
return Clamp(totalDuration * targetProgress, 0, totalDuration).Round(4);
|
|
}
|
|
|
|
/// <summary> Returns the progress value adjusted to the given play mode </summary>
|
|
/// <param name="progress"> Progress </param>
|
|
/// <param name="playMode"> Play mode </param>
|
|
private static float GetAdjustedProgress(float progress, PlayMode playMode)
|
|
{
|
|
progress = progress.Clamp01();
|
|
|
|
switch (playMode)
|
|
{
|
|
case PlayMode.Normal:
|
|
{
|
|
return progress;
|
|
}
|
|
case PlayMode.PingPong:
|
|
{
|
|
if (progress == 0f) return 0f;
|
|
if (progress.Approximately(0.5f)) return 1f;
|
|
if (progress.Approximately(1f)) return 0f;
|
|
if (progress < 0.5f) return progress * 2f;
|
|
if (progress > 0.5f) return (1f - progress) * 2f;
|
|
|
|
return progress;
|
|
}
|
|
case PlayMode.Spring:
|
|
{
|
|
return progress;
|
|
}
|
|
case PlayMode.Shake:
|
|
{
|
|
return progress;
|
|
}
|
|
default:
|
|
throw new ArgumentOutOfRangeException(nameof(playMode), playMode, null);
|
|
}
|
|
}
|
|
|
|
/// <summary> Set the reaction's progress at the given target progress </summary>
|
|
/// <param name="targetProgress"> Target progress </param>
|
|
public virtual void SetProgressAt(float targetProgress)
|
|
{
|
|
targetProgress = GetAdjustedProgress(targetProgress, settings.playMode);
|
|
|
|
if (isActive) Stop(true);
|
|
ResetElapsedValues();
|
|
RefreshSettings();
|
|
|
|
direction = PlayDirection.Forward;
|
|
// startDuration = 0f;
|
|
// targetDuration = 1f;
|
|
|
|
//save current settings
|
|
EaseMode easeMode = settings.easeMode;
|
|
Ease ease = settings.ease;
|
|
AnimationCurve curve = settings.curve;
|
|
|
|
//apply linear lease
|
|
settings.easeMode = EaseMode.Ease;
|
|
settings.ease = Ease.Linear;
|
|
|
|
//update the progress
|
|
// elapsedDuration = GetDurationAtProgress(targetProgress, targetDuration);
|
|
elapsedDuration = Clamp01(targetProgress) * duration;
|
|
// elapsedDuration = Clamp01((float)elapsedDuration);
|
|
m_LastProgress = progress;
|
|
|
|
if (heartbeat.isActive) heartbeat.UnregisterFromTickService();
|
|
|
|
if (settings.playMode != PlayMode.Normal)
|
|
ComputePlayMode(); //This operation needed here because if we set the progress before the reaction ran, we have no values and can get NaN (Not a Number) values
|
|
UpdateCurrentCycleIndex();
|
|
UpdateCurrentValue();
|
|
OnUpdateCallback?.Invoke();
|
|
|
|
//restore previous settings
|
|
settings.ease = ease;
|
|
settings.curve = curve;
|
|
settings.easeMode = easeMode;
|
|
}
|
|
|
|
/// <summary> Set the reaction's progress at 1 (end) </summary>
|
|
public void SetProgressAtOne() =>
|
|
SetProgressAt(1f);
|
|
|
|
/// <summary> Set the reaction's progress at 0 (start) </summary>
|
|
public void SetProgressAtZero() =>
|
|
SetProgressAt(0f);
|
|
|
|
/// <summary>
|
|
/// Update the reaction (called by the reaction's Heartbeat to update all the values)
|
|
/// </summary>
|
|
internal void UpdateReaction()
|
|
{
|
|
if (isPooled)
|
|
{
|
|
if (heartbeat.isActive)
|
|
{
|
|
heartbeat.UnregisterFromTickService();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (isIdle & heartbeat.isActive)
|
|
{
|
|
heartbeat.UnregisterFromTickService();
|
|
}
|
|
|
|
if (IsPaused()) return;
|
|
if (InStartDelay()) return;
|
|
if (InLoopDelay()) return;
|
|
|
|
elapsedDuration = elapsedDuration < 0f ? 0f : elapsedDuration;
|
|
elapsedDuration = Clamp((float)elapsedDuration, startDuration, targetDuration);
|
|
elapsedDuration = elapsedDuration > duration ? duration : elapsedDuration;
|
|
m_LastProgress = progress;
|
|
|
|
UpdateCurrentCycleIndex();
|
|
UpdateCurrentValue();
|
|
OnUpdateCallback?.Invoke();
|
|
|
|
switch (direction)
|
|
{
|
|
case PlayDirection.Forward:
|
|
if (elapsedDuration < targetDuration)
|
|
{
|
|
elapsedDuration += heartbeat.deltaTime * (int)direction;
|
|
return;
|
|
}
|
|
break;
|
|
case PlayDirection.Reverse:
|
|
if (elapsedDuration > startDuration)
|
|
{
|
|
elapsedDuration += heartbeat.deltaTime * (int)direction;
|
|
return;
|
|
}
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
|
|
elapsedLoops++;
|
|
|
|
if (loops < 0 || loops != 0 && elapsedLoops <= loops)
|
|
{
|
|
if (!customStartDuration)
|
|
{
|
|
duration = Max(MIN_DURATION, Settings.GetDuration());
|
|
startDuration = 0f;
|
|
targetDuration = duration;
|
|
ComputePlayMode();
|
|
}
|
|
elapsedDuration = direction == PlayDirection.Forward ? startDuration : targetDuration;
|
|
m_LastProgress = progress;
|
|
|
|
loopDelay = Settings.GetLoopDelay();
|
|
|
|
if (loopDelay > 0)
|
|
{
|
|
state = ReactionState.LoopDelay;
|
|
return;
|
|
}
|
|
|
|
OnLoopCallback?.Invoke();
|
|
state = ReactionState.Playing;
|
|
return;
|
|
}
|
|
|
|
elapsedDuration = direction == PlayDirection.Forward ? targetDuration : startDuration;
|
|
elapsedDuration = elapsedDuration.Round(4);
|
|
m_LastProgress = progress;
|
|
UpdateCurrentCycleIndex();
|
|
UpdateCurrentValue();
|
|
OnUpdateCallback?.Invoke();
|
|
Finish();
|
|
}
|
|
|
|
/// <summary> Returns TRUE if the reaction is paused and updates the lastUpdateTime for the heartbeat </summary>
|
|
private bool IsPaused()
|
|
{
|
|
if (!isPaused) return false;
|
|
heartbeat.lastUpdateTime = heartbeat.timeSinceStartup;
|
|
return true;
|
|
}
|
|
|
|
/// <summary> Returns TRUE if the reaction is in start delay and updates the start delay related variables </summary>
|
|
private bool InStartDelay()
|
|
{
|
|
if (!inStartDelay) return false;
|
|
elapsedStartDelay += heartbeat.deltaTime;
|
|
elapsedStartDelay = Clamp((float)elapsedStartDelay, 0, startDelay);
|
|
if (startDelay - elapsedStartDelay > 0) return true;
|
|
state = ReactionState.Playing;
|
|
elapsedStartDelay = 0f;
|
|
return false;
|
|
}
|
|
|
|
/// <summary> Returns TRUE if the reaction is in loop delay and updates the loop delay related variables </summary>
|
|
private bool InLoopDelay()
|
|
{
|
|
if (!inLoopDelay) return false;
|
|
elapsedLoopDelay += heartbeat.deltaTime;
|
|
elapsedLoopDelay = Clamp((float)elapsedLoopDelay, 0, loopDelay);
|
|
if (loopDelay - elapsedLoopDelay > 0f) return true;
|
|
OnLoopCallback?.Invoke();
|
|
state = ReactionState.Playing;
|
|
elapsedLoopDelay = 0f;
|
|
return false;
|
|
}
|
|
|
|
/// <summary> Update the reaction's current value </summary>
|
|
public abstract void UpdateCurrentValue();
|
|
|
|
/// <summary> Stop the reaction from playing (does not call finish) </summary>
|
|
/// <param name="silent"> If TRUE, callbacks will not be invoked </param>
|
|
/// <param name="recycle"> If TRUE, it will try recycle this reaction, by returning it to the pool </param>
|
|
public virtual void Stop(bool silent = false, bool recycle = false)
|
|
{
|
|
if (heartbeat.isActive) heartbeat.UnregisterFromTickService();
|
|
if (isPooled) return;
|
|
if (!silent) OnStopCallback?.Invoke();
|
|
state = ReactionState.Idle;
|
|
if (recycle) Recycle();
|
|
}
|
|
|
|
/// <summary> Finish the reaction by stopping it, calling callbacks (stop and then finish) and then (if reusable) returns it to the pool </summary>
|
|
/// <param name="silent"> If TRUE, callbacks will not be invoked </param>
|
|
/// <param name="endAnimation"> If TRUE, the animation ends in the To value (set the progress to 1 (one)) </param>
|
|
/// <param name="recycle"> If TRUE, it will try recycle this reaction, by returning it to the pool </param>
|
|
public virtual void Finish(bool silent = false, bool endAnimation = false, bool recycle = false)
|
|
{
|
|
if (!isActive) return;
|
|
// ReSharper disable once RedundantArgumentDefaultValue
|
|
Stop(silent, false);
|
|
if (!silent) OnFinishCallback?.Invoke();
|
|
if (endAnimation) SetProgressAtOne();
|
|
if (recycle) Recycle();
|
|
}
|
|
|
|
/// <summary> Set the heartbeat for this reaction and connect the UpdateReaction to it </summary>
|
|
/// <param name="h"> New heartbeat </param>
|
|
public void SetHeartbeat(Heartbeat h)
|
|
{
|
|
heartbeat = h ?? new RuntimeHeartbeat();
|
|
heartbeat.AddOnTickCallback(UpdateReaction);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reset all relevant elapsed values
|
|
/// <para/> Used mostly for reset purposes
|
|
/// </summary>
|
|
private void ResetElapsedValues()
|
|
{
|
|
elapsedStartDelay = 0;
|
|
elapsedDuration = 0;
|
|
elapsedLoops = 0;
|
|
elapsedLoopDelay = 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refresh the reaction's current play settings.
|
|
/// <para/> Refreshes the play settings (gets new random values if they are used)
|
|
/// <para/> Calls the appropriate Compute method for the current play mode
|
|
/// </summary>
|
|
public void RefreshSettings()
|
|
{
|
|
settings.Validate();
|
|
|
|
startDelay = Settings.GetStartDelay();
|
|
duration = Settings.GetDuration();
|
|
duration = float.IsNaN(duration) || float.IsInfinity(duration) ? 0 : duration;
|
|
duration = Max(MIN_DURATION, duration); //avoid zero duration as it creates NaN values
|
|
loops = Settings.GetLoops();
|
|
loopDelay = Settings.GetLoopDelay();
|
|
|
|
ComputePlayMode();
|
|
}
|
|
|
|
/// <summary> Call the appropriate Compute method depending on the current play mode </summary>
|
|
public void ComputePlayMode()
|
|
{
|
|
switch (Settings.playMode)
|
|
{
|
|
case PlayMode.Normal:
|
|
ComputeNormal();
|
|
break;
|
|
case PlayMode.PingPong:
|
|
ComputePingPong();
|
|
break;
|
|
case PlayMode.Spring:
|
|
ComputeSpring();
|
|
break;
|
|
case PlayMode.Shake:
|
|
ComputeShake();
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
}
|
|
|
|
/// <summary> Update the current cycle index </summary>
|
|
private void UpdateCurrentCycleIndex()
|
|
{
|
|
previousCycleIndex = currentCycleIndex;
|
|
|
|
switch (direction)
|
|
{
|
|
case PlayDirection.Forward:
|
|
{
|
|
float compoundDuration = 0f;
|
|
for (int i = 0; i < cycleDurations.Count; i++)
|
|
{
|
|
currentCycleIndex = i;
|
|
compoundDuration += cycleDurations[i];
|
|
if (elapsedDuration <= compoundDuration) return;
|
|
}
|
|
}
|
|
break;
|
|
case PlayDirection.Reverse:
|
|
{
|
|
// float compoundDuration = targetDuration;
|
|
float compoundDuration = duration;
|
|
for (int i = cycleDurations.Count - 1; i >= 0; i--)
|
|
{
|
|
currentCycleIndex = i;
|
|
compoundDuration -= cycleDurations[i];
|
|
if (elapsedDuration > compoundDuration) return;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void EnsureCycleDurationsListCapacity(int requiredCapacity)
|
|
{
|
|
if (cycleDurations == null)
|
|
{
|
|
cycleDurations = new List<float>(requiredCapacity);
|
|
return;
|
|
}
|
|
|
|
if(requiredCapacity <= cycleDurations.Capacity)
|
|
return;
|
|
|
|
cycleDurations.Capacity = requiredCapacity;
|
|
}
|
|
|
|
/// <summary> Compute normal play mode cycle </summary>
|
|
protected virtual void ComputeNormal()
|
|
{
|
|
currentCycleIndex = 0;
|
|
numberOfCycles = 1;
|
|
|
|
EnsureCycleDurationsListCapacity(numberOfCycles);
|
|
|
|
if (cycleDurations.Count != numberOfCycles)
|
|
{
|
|
cycleDurations.Clear();
|
|
cycleDurations.Add(duration);
|
|
}
|
|
else
|
|
{
|
|
cycleDurations[0] = duration;
|
|
}
|
|
}
|
|
|
|
/// <summary> Compute ping-pong play mode cycles </summary>
|
|
protected virtual void ComputePingPong()
|
|
{
|
|
currentCycleIndex = 0;
|
|
numberOfCycles = 2;
|
|
float halfDuration = duration / 2f;
|
|
|
|
EnsureCycleDurationsListCapacity(numberOfCycles);
|
|
|
|
if (cycleDurations.Count != numberOfCycles)
|
|
{
|
|
cycleDurations.Clear();
|
|
cycleDurations.Add(halfDuration);
|
|
cycleDurations.Add(halfDuration);
|
|
}
|
|
else
|
|
{
|
|
cycleDurations[0] = halfDuration;
|
|
cycleDurations[1] = halfDuration;
|
|
}
|
|
}
|
|
|
|
/// <summary> Compute spring play mode cycles </summary>
|
|
protected virtual void ComputeSpring()
|
|
{
|
|
currentCycleIndex = 0;
|
|
numberOfCycles = Max(1, settings.vibration + (int)(settings.vibration * duration));
|
|
if (numberOfCycles % 2 != 0) numberOfCycles++;
|
|
|
|
EnsureCycleDurationsListCapacity(numberOfCycles);
|
|
|
|
if (cycleDurations.Count != numberOfCycles)
|
|
{
|
|
cycleDurations.Clear();
|
|
for (int i = 0; i < numberOfCycles; i++)
|
|
{
|
|
cycleDurations.Add(0f);
|
|
}
|
|
}
|
|
|
|
float compoundDuration = 0f;
|
|
for (int i = 0; i < numberOfCycles; i++)
|
|
{
|
|
cycleDurations[i] = duration * ((float)(i + 1) / numberOfCycles);
|
|
cycleDurations[i] = cycleDurations[i].Round(4);
|
|
compoundDuration += cycleDurations[i];
|
|
}
|
|
|
|
float durationRatio = duration / compoundDuration;
|
|
for (int i = 0; i < numberOfCycles; i++)
|
|
cycleDurations[i] *= durationRatio;
|
|
}
|
|
|
|
/// <summary> Compute shake play mode cycles </summary>
|
|
protected virtual void ComputeShake()
|
|
{
|
|
currentCycleIndex = 0;
|
|
numberOfCycles = Max(1, settings.vibration + (int)(settings.vibration * duration));
|
|
if (numberOfCycles % 2 == 0) numberOfCycles++;
|
|
|
|
EnsureCycleDurationsListCapacity(numberOfCycles);
|
|
|
|
if (cycleDurations.Count != numberOfCycles)
|
|
{
|
|
cycleDurations.Clear();
|
|
for (int i = 0; i < numberOfCycles; i++)
|
|
{
|
|
cycleDurations.Add(0f);
|
|
}
|
|
}
|
|
|
|
float compoundDuration = 0f;
|
|
for (int i = 0; i < numberOfCycles; i++)
|
|
{
|
|
if (settings.fadeOutShake)
|
|
{
|
|
float ofCycles = ((float)(i + 1) / numberOfCycles);
|
|
cycleDurations[i] = EaseFactory.GetEase(Ease.OutExpo).Evaluate(ofCycles) * duration;
|
|
}
|
|
else
|
|
{
|
|
cycleDurations[i] = duration / numberOfCycles;
|
|
}
|
|
|
|
compoundDuration += cycleDurations[i];
|
|
}
|
|
|
|
float durationRatio = duration / compoundDuration;
|
|
for (int i = 0; i < numberOfCycles; i++)
|
|
cycleDurations[i] *= durationRatio;
|
|
|
|
float tempDuration = 0f;
|
|
for (int i = 0; i < numberOfCycles - 1; i++)
|
|
tempDuration += cycleDurations[i];
|
|
|
|
cycleDurations[numberOfCycles - 1] = duration - tempDuration;
|
|
}
|
|
|
|
public void Recycle()
|
|
{
|
|
this.AddToPool();
|
|
}
|
|
|
|
#region Static Methods
|
|
|
|
/// <summary> Get a reaction from the given reaction type, either from the pool or a new one </summary>
|
|
/// <typeparam name="T"> Reaction Type </typeparam>
|
|
public static T Get<T>() where T : Reaction => ReactionPool.Get<T>();
|
|
|
|
#endregion
|
|
|
|
#region IDs
|
|
|
|
public const int k_DefaultIntId = -1234;
|
|
|
|
[ClearOnReload(true)]
|
|
internal static readonly ReactionDictionary<object> ReactionByObjectId = new ReactionDictionary<object>();
|
|
[ClearOnReload(true)]
|
|
internal static readonly ReactionDictionary<string> ReactionByStringId = new ReactionDictionary<string>();
|
|
[ClearOnReload(true)]
|
|
internal static readonly ReactionDictionary<int> ReactionByIntId = new ReactionDictionary<int>();
|
|
[ClearOnReload(true)]
|
|
internal static readonly ReactionDictionary<object> ReactionByTargetObject = new ReactionDictionary<object>();
|
|
|
|
public bool hasObjectId { get; internal set; }
|
|
public object objectId { get; internal set; }
|
|
|
|
public string stringId { get; internal set; }
|
|
public bool hasStringId { get; internal set; }
|
|
|
|
public int intId { get; internal set; }
|
|
public bool hasIntId { get; internal set; }
|
|
|
|
/// <summary> The object this reaction is attached to </summary>
|
|
public object targetObject { get; internal set; }
|
|
|
|
public bool hasTargetObject { get; internal set; }
|
|
|
|
public static void StopAllReactionsByObjectId(object id, bool silent = false)
|
|
{
|
|
foreach (Reaction reaction in ReactionByObjectId.GetReactions(id))
|
|
reaction.Stop(silent);
|
|
}
|
|
|
|
public static void StopAllReactionsByStringId(string id, bool silent = false)
|
|
{
|
|
foreach (Reaction reaction in ReactionByStringId.GetReactions(id))
|
|
reaction.Stop(silent);
|
|
}
|
|
|
|
public static void StopAllReactionsByIntId(int id, bool silent = false)
|
|
{
|
|
foreach (Reaction reaction in ReactionByIntId.GetReactions(id))
|
|
reaction.Stop(silent);
|
|
}
|
|
|
|
public static void StopAllReactionsByTargetObject(object target, bool silent = false)
|
|
{
|
|
foreach (Reaction reaction in ReactionByTargetObject.GetReactions(target))
|
|
reaction.Stop(silent);
|
|
}
|
|
|
|
#endregion
|
|
|
|
public override string ToString() =>
|
|
$"[{(heartbeat != null ? heartbeat.GetType().Name : "No Heartbeat")}] " +
|
|
$"[{GetType().Name}] " +
|
|
$"[{state}] > " +
|
|
$"[{direction}] > " +
|
|
$"[{elapsedDuration.Round(3):0.000} / {duration} seconds] " +
|
|
$"[{nameof(progress)}: {progress.Round(2):0.00} {(progress.Round(2) * 100f).Round(0):000}%]";
|
|
}
|
|
}
|