using UnityEngine;
using System.Collections.Generic;
using Lean.Common;
namespace Lean.Transition
{
/// This component updates all active transition methods, both in game, and in the editor.
[ExecuteInEditMode]
[HelpURL(HelpUrlPrefix + "LeanTransition")]
[AddComponentMenu(ComponentMenuPrefix + "Lean Transition")]
public class LeanTransition : MonoBehaviour
{
public const string ComponentMenuPrefix = "Lean/Transition/";
public const string MethodsMenuPrefix = "Lean/Transition/Methods/";
public const string MethodsMenuSuffix = " Transition ";
public const string HelpUrlPrefix = "https://carloswilkes.com/Documentation/LeanTransition#";
/// This allows you to set where in the game loop animations are updated when timing = LeanTime.Default.
public LeanTiming DefaultTiming { set { defaultTiming = value; } get { return defaultTiming; } } [SerializeField] [UnityEngine.Serialization.FormerlySerializedAs("Timing")] private LeanTiming defaultTiming = LeanTiming.UnscaledUpdate;
/// This stores a list of all active and enabled LeanTransition instances in the scene.
public static List Instances = new List();
public static event System.Action OnRegistered;
public static event System.Action OnFinished;
private static List unscaledUpdateStates = new List();
private static List unscaledLateUpdateStates = new List();
private static List unscaledFixedUpdateStates = new List();
private static List updateStates = new List();
private static List lateUpdateStates = new List();
private static List fixedUpdateStates = new List();
private static List tempBaseMethods = new List();
private static List baseMethodStack = new List();
private static Dictionary aliasTypePairs = new Dictionary();
private static LeanState previousState;
private static LeanState currentQueue;
private static LeanState defaultQueue;
private static LeanTiming currentTiming;
private static float currentSpeed = 1.0f;
private static bool started;
private static Dictionary currentAliases = new Dictionary();
/// This property gives you the first DefaultTiming instance value.
public static LeanTiming CurrentDefaultTiming
{
get
{
if (Instances.Count > 0)
{
return Instances[0].defaultTiming;
}
return default(LeanTiming);
}
}
/// This tells you how many transitions are currently running.
public static int Count
{
get
{
return unscaledUpdateStates.Count + unscaledLateUpdateStates.Count + unscaledFixedUpdateStates.Count + updateStates.Count + lateUpdateStates.Count + fixedUpdateStates.Count;
}
}
/// After a transition state is registered, it will be stored here. This allows you to copy it out for later use.
public static LeanState PreviousState
{
get
{
return previousState;
}
}
/// If you want the next registered transition state to automatically begin after an existing transition state, then specify it here.
public static LeanState CurrentQueue
{
set
{
currentQueue = value;
}
}
/// This allows you to change where in the game loop all future transitions in the current animation will be updated.
public static LeanTiming CurrentTiming
{
set
{
currentTiming = value;
}
}
/// This allows you to change the transition speed multiplier of all future transitions in the current animation.
public static float CurrentSpeed
{
set
{
currentSpeed = value;
}
get
{
return currentSpeed;
}
}
/// This allows you to change the alias name to UnityEngine.Object association of all future transitions in the current animation.
public static Dictionary CurrentAliases
{
get
{
return currentAliases;
}
}
public static void AddAlias(string key, Object obj)
{
currentAliases.Remove(key);
currentAliases.Add(key, obj);
}
/// This method will return the specified timing, unless it's set to Default, then it will return UnscaledTime.
public static LeanTiming GetTiming(LeanTiming current = LeanTiming.Default)
{
if (current == LeanTiming.Default)
{
current = LeanTiming.UnscaledUpdate;
}
return current;
}
/// This method works like GetTiming, but it won't return any unscaled times.
public static LeanTiming GetTimingAbs(LeanTiming current)
{
return (LeanTiming)System.Math.Abs((int)current);
}
/// If you failed to submit a previous transition then this will throw an error, and then submit them.
public static void RequireSubmitted()
{
if (currentQueue != null)
{
Debug.LogError("You forgot to submit the last transition! " + currentQueue.GetType() + " - " + currentQueue.GetTarget());
Submit();
}
if (baseMethodStack.Count > 0)
{
Debug.LogError("Failed to submit all methods.");
Submit();
}
}
/// This will reset any previously called CurrentTiming calls.
public static void ResetTiming()
{
currentTiming = CurrentDefaultTiming;
}
/// This will reset any previously called CurrentQueue calls.
public static void ResetQueue()
{
currentQueue = null;
}
/// This will reset any previously called CurrentSpeed calls.
public static void ResetSpeed()
{
currentSpeed = 1.0f;
}
/// This will reset the CurrentTiming, CurrentQueue, and CurrentSpeed values.
public static void ResetState()
{
defaultQueue = null;
ResetTiming();
ResetQueue();
ResetSpeed();
}
/// This will submit any previously registered transitions, and reset the timing.
public static void Submit()
{
ResetState();
baseMethodStack.Clear();
}
/// This will begin all transitions on the specified GameObject, all its children, and then submit them.
/// If you failed to submit a previous transition then this will also throw an error.
public static void BeginAllTransitions(Transform root, float speed = 1.0f)
{
ResetState();
if (root != null)
{
RequireSubmitted();
InsertTransitions(root, speed);
Submit();
}
}
/// This will begin all transitions on the specified GameObject, and all its children.
public static void InsertTransitions(GameObject root, float speed = 1.0f, LeanState parentHead = null)
{
if (root != null)
{
InsertTransitions(root.transform, speed);
}
}
/// This will begin all transitions on the specified Transform, and all its children.
public static void InsertTransitions(Transform root, float speed = 1.0f, LeanState parentHead = null)
{
if (root != null)
{
var spd = currentSpeed;
var min = baseMethodStack.Count; root.GetComponents(tempBaseMethods); baseMethodStack.AddRange(tempBaseMethods); tempBaseMethods.Clear();
var max = baseMethodStack.Count;
currentSpeed *= speed;
if (parentHead != null)
{
previousState = parentHead;
currentQueue = parentHead;
}
defaultQueue = parentHead;
for (var i = min; i < max; i++)
{
baseMethodStack[i].Register();
}
baseMethodStack.RemoveRange(min, max - min);
var childParentHead = previousState;
for (var i = 0; i < root.childCount; i++)
{
InsertTransitions(root.GetChild(i), 1.0f, childParentHead);
}
currentSpeed = spd;
}
}
/// This method returns all TargetAliases on all transitions on the specified Transform.
public static Dictionary FindAllAliasTypePairs(Transform root)
{
aliasTypePairs.Clear();
AddAliasTypePairs(root);
return aliasTypePairs;
}
private static void AddAliasTypePairs(Transform root)
{
if (root != null)
{
root.GetComponents(tempBaseMethods);
for (var i = 0; i < tempBaseMethods.Count; i++)
{
var baseMethod = tempBaseMethods[i] as LeanMethodWithStateAndTarget;
if (baseMethod != null)
{
var targetType = baseMethod.GetTargetType();
var alias = baseMethod.Alias;
if (string.IsNullOrEmpty(alias) == false)
{
var existingType = default(System.Type);
// Exists?
if (aliasTypePairs.TryGetValue(alias, out existingType) == true)
{
// Clashing types?
if (existingType != targetType)
{
// If both are components then the clash can be resolved by using GameObject
if (targetType.IsSubclassOf(typeof(Component)) == true)
{
// If it's already a GameObject, skip
if (existingType == typeof(GameObject))
{
continue;
}
// Change existing type to GameObject?
else if (existingType.IsSubclassOf(typeof(Component)) == true)
{
aliasTypePairs[alias] = typeof(GameObject);
continue;
}
}
// If the clash cannot be resolved, throw an error
Debug.LogError("The (" + root.name + ") GameObject contains multiple transitions that define a target alias of (" + alias + "), but these transitions use different types (" + existingType + ") + (" + targetType + "). You must give them different aliases.", root);
}
}
// Add new?
else
{
aliasTypePairs.Add(alias, targetType);
}
}
}
}
}
}
public static T SpawnWithTarget(Stack pool, U target)
where T : LeanStateWithTarget, new()
where U : Object
{
var data = Spawn(pool);
data.Target = target;
return data;
}
public static T Spawn(Stack pool)
where T : LeanState, new()
{
// Make sure the transition manager exists
if (Instances.Count == 0)
{
new GameObject("LeanTransition").AddComponent();
}
// Setup initial data
var state = pool.Count > 0 ? pool.Pop() : new T();
state.Age = -1.0f;
state.Ignore = false;
state.Prev.Clear();
state.Next.Clear();
// Join to previous transition?
if (currentQueue != null)
{
state.BeginAfter(currentQueue);
currentQueue = defaultQueue;
}
// Make this the new head
previousState = state;
return state;
}
public static LeanState Register(LeanState state, float duration)
{
state.Duration = duration;
// Execute immediately?
if (duration == 0.0f && state.Prev.Count == 0)
{
FinishState(state);
if (previousState == state)
{
previousState = null;
}
return null;
}
// Register for later execution?
else
{
if (currentSpeed > 0.0f)
{
state.Duration /= currentSpeed;
}
// Convert currentTiming if it's set to default, then register the state in the correct list
var finalUpdate = GetTiming(currentTiming);
switch (finalUpdate)
{
case LeanTiming.UnscaledFixedUpdate: unscaledFixedUpdateStates.Add(state); break;
case LeanTiming.UnscaledLateUpdate: unscaledLateUpdateStates.Add(state); break;
case LeanTiming.UnscaledUpdate: unscaledUpdateStates.Add(state); break;
case LeanTiming.Update: updateStates.Add(state); break;
case LeanTiming.LateUpdate: lateUpdateStates.Add(state); break;
case LeanTiming.FixedUpdate: fixedUpdateStates.Add(state); break;
}
}
if (OnRegistered != null)
{
OnRegistered(state);
}
return state;
}
protected virtual void OnEnable()
{
Instances.Add(this);
ResetState();
#if UNITY_EDITOR
UnityEditor.EditorApplication.update -= HandleUpdateInEditor;
UnityEditor.EditorApplication.update += HandleUpdateInEditor;
#endif
}
protected virtual void OnDisable()
{
Instances.Remove(this);
if (Instances.Count == 0)
{
unscaledFixedUpdateStates.Clear();
unscaledLateUpdateStates.Clear();
unscaledUpdateStates.Clear();
updateStates.Clear();
lateUpdateStates.Clear();
fixedUpdateStates.Clear();
}
}
#if UNITY_EDITOR
private void HandleUpdateInEditor()
{
var delta = Time.deltaTime;
if (Application.isPlaying == false)
{
UpdateAll(unscaledFixedUpdateStates, delta);
UpdateAll( unscaledLateUpdateStates, delta);
UpdateAll( unscaledUpdateStates, delta);
UpdateAll( updateStates, delta);
UpdateAll( lateUpdateStates, delta);
UpdateAll( fixedUpdateStates, delta);
}
}
#endif
protected virtual void Update()
{
if (this == Instances[0] && Application.isPlaying == true && started == true)
{
UpdateAll(unscaledUpdateStates, Time.unscaledDeltaTime);
UpdateAll( updateStates, Time.deltaTime );
}
}
protected virtual void LateUpdate()
{
if (this == Instances[0] && Application.isPlaying == true)
{
if (started == true)
{
UpdateAll(unscaledLateUpdateStates, Time.unscaledDeltaTime);
UpdateAll( lateUpdateStates, Time.deltaTime );
}
else
{
started = true;
}
}
}
protected virtual void FixedUpdate()
{
if (this == Instances[0] && Application.isPlaying == true && started == true)
{
UpdateAll(unscaledFixedUpdateStates, Time.fixedUnscaledDeltaTime);
UpdateAll( fixedUpdateStates, Time.fixedDeltaTime );
}
}
/// This method will mark all transitions as Skip = true if they match the transition type and target object of the specified transition.
private void RemoveConflictsBefore(List states, LeanState currentState, int currentIndex)
{
var currentConflict = currentState.Conflict;
if (currentConflict != LeanState.ConflictType.None)
{
var currentType = currentState.GetType();
var currentTarget = currentState.GetTarget();
for (var i = 0; i < currentIndex; i++)
{
var transition = states[i];
if (transition.Ignore == false && transition.GetType() == currentType && transition.GetTarget() == currentTarget)
{
transition.Ignore = true;
if (currentConflict == LeanState.ConflictType.Complete)
{
transition.Update(1.0f);
}
}
}
}
}
private void UpdateAll(List states, float delta)
{
ResetState();
for (var i = states.Count - 1; i >= 0; i--)
{
var state = states[i];
// If we have a negative duration, skip ahead of time?
if (state.Prev.Count > 0 && state.Duration < 0.0f)
{
var skip = -state.Duration;
for (var j = state.Prev.Count - 1; j >= 0; j--)
{
var prev = state.Prev[j];
if (prev.Remaining <= skip)
{
prev.Next.Remove(state);
state.Prev.RemoveAt(j);
}
}
}
// Only update if the previous transitions have finished
if (state.Prev.Count == 0)
{
// If the transition age is negative, it hasn't started yet
if (state.Age < 0.0f)
{
state.Age = 0.0f;
// If this newly beginning transition is identical to an already registered one, mark the existing one as conflicting so it doesn't get updated
RemoveConflictsBefore(states, state, i);
// Begin the transition (this will often copy the current state of the variable that is being transitioned)
state.Begin();
}
// Age
state.Age += delta;
// Finished?
if (state.Age >= state.Duration)
{
FinishState(state);
states.RemoveAt(i);
}
// Update
else
{
if (state.Ignore == false)
{
state.Update(state.Age / state.Duration);
}
#if UNITY_EDITOR
DirtyTarget(state);
#endif
}
}
}
}
private static void FinishState(LeanState state)
{
// Activate all chained states and clear them
for (var j = state.Next.Count - 1; j >= 0; j--)
{
state.Next[j].Prev.Remove(state);
}
state.Next.Clear();
// Make sure we call update one final time with a progress value of exactly 1.0
if (state.Ignore == false)
{
state.Update(1.0f);
}
if (OnFinished != null)
{
OnFinished(state);
}
#if UNITY_EDITOR
DirtyTarget(state);
#endif
state.Despawn();
}
#if UNITY_EDITOR
/// If a transition is being animated in the editor, then the target object may not update, so this method will automatically dirty it so that it will.
private static void DirtyTarget(LeanState transition)
{
if (Application.isPlaying == false)
{
var targetField = transition.GetType().GetField("Target");
if (targetField != null)
{
var target = targetField.GetValue(transition) as Object;
if (target != null)
{
UnityEditor.EditorUtility.SetDirty(target);
}
}
}
}
#endif
}
}
#if UNITY_EDITOR
namespace Lean.Transition.Editor
{
using TARGET = LeanTransition;
[UnityEditor.CanEditMultipleObjects]
[UnityEditor.CustomEditor(typeof(TARGET))]
public class LeanTransition_Editor : LeanEditor
{
protected override void OnInspector()
{
TARGET tgt; TARGET[] tgts; GetTargets(out tgt, out tgts);
Draw("defaultTiming", "This allows you to set where in the game loop animations are updated when timing = LeanTime.Default.");
Separator();
BeginDisabled(true);
UnityEditor.EditorGUILayout.IntField("Transition Count", LeanTransition.Count);
EndDisabled();
}
[UnityEditor.MenuItem("GameObject/Lean/Transition", false, 1)]
private static void CreateLocalization()
{
var gameObject = new GameObject(typeof(LeanTransition).Name);
UnityEditor.Undo.RegisterCreatedObjectUndo(gameObject, "Create LeanTransition");
gameObject.AddComponent();
UnityEditor.Selection.activeGameObject = gameObject;
}
}
}
#endif