using System; using System.Collections; using System.Collections.Generic; using System.Linq; using D2D; using D2D.Utilities; using DG.Tweening; using UnityEngine; namespace D2D.Animations { public enum AnimationPlayMode { PlayOnEnable, // PlayOnVisible, PlayByScript } public enum AnimationLooping { None, Pong, ToTo, } public abstract class DAnimation : MonoBehaviour { #region Variables [Tooltip("Random end point of the animation (from x to y)")] [SerializeField] private Vector2 _to = new Vector2(1, 1); [SerializeField] private float to = 1; [Tooltip("Random (from x to y) duration of animation")] [SerializeField] private Vector2 _duration = new Vector2(.5f, .5f); [SerializeField] private float duration = .5f; [SerializeField] private DurationData _durationData; [SerializeField] private Vector2 _startDelay; [SerializeField] private float startDelay; [SerializeField] private Vector2 _delayBetweenYoyo; [SerializeField] private float delayBetweenYoyo; [Tooltip("Delay between animation cycles")] [SerializeField] private Vector2 _delayBetweenCycles; [SerializeField] private float delayBetweenCycles; [Space(10)] [Tooltip("You can set it to -1 for infinite yoyo animation")] [SerializeField] public int _loops = 1; [SerializeField] public bool _isRelative; [Tooltip("Need to kill this GO after animation (not looped) completed?")] [SerializeField] public bool _destroyOnFinish; [SerializeField] private Ease _ease = Ease.InOutSine; [SerializeField] public AnimationPlayMode _playMode = AnimationPlayMode.PlayOnEnable; [SerializeField] private DAnimation[] _onComplete; [SerializeField] public AnimationCurve curve = null; public event Action Tick; public event Action Completed; public bool _isFrom; public bool _isLocal; public bool isRandomnessSupported; public bool isAdvancedInfoVisible = true; public bool isOnCompleteVisible; public bool isDurationDataMode; public bool _isPlayingInEditor; public bool isTargetVisibleInEditor; [ContextMenu("Toggle target visibility")] private void ToggleTargetVisibility() => isTargetVisibleInEditor = !isTargetVisibleInEditor; public Vector3 beforePlayPosition; public Vector3 beforePlayRotation; public Vector3 beforePlayScale; public Ease Ease => _ease; public bool IsPlaying => CurrentTween != null && CurrentTween.IsPlaying(); public bool IsJustBorn => _plays == 0 || CurrentTween == null; /// /// Simple recursive algorithm which calculates whole chain duration /// (with all _onCompletes). /// public float ChainDuration { get { return _onComplete.IsNullOrEmpty() ? CalculatedDuration : CalculatedDuration + _onComplete.Max(t => t.ChainDuration); } } /// /// For now very stupid recursive algorithm. /// For more complex solution it is recommended to find last tween in chain by hand. /// public DAnimation LastTweenInChain { get { return _onComplete.IsNullOrEmpty() ? this : _onComplete.OrderBy(t => t.CalculatedDuration).First(); } } public bool IsLooped => _loops > 1 || _loops == -1; public bool HasStartDelay => StartDelay != Vector2.zero; public Tween CurrentTween { get; private set; } [HideInInspector] public bool isIncremental; public float CalculatedDuration { get { if (_calculatedDuration < 0) _calculatedDuration = Duration.RandomFloat(); return _calculatedDuration; } set => _calculatedDuration = value; } private float _calculatedDuration = -1; private bool _isCustomDuration; protected float CalculatedTo { get { if (_calculatedTo < 0) _calculatedTo = To.RandomFloat(); return _calculatedTo; } set => _calculatedTo = value; } private float _calculatedTo = -1; public Transform Target { get { if (_target == null) Target = transform; return _target; } private set => _target = value; } public Transform _target; private Vector2 To => isRandomnessSupported ? _to : new Vector2(to, to); private Vector2 Duration { get { if (isDurationDataMode) { if (_durationData == null) throw new Exception("No duration data provided!"); return _durationData.Value; } return isRandomnessSupported ? _duration : new Vector2(duration, duration); } } private Vector2 StartDelay => isRandomnessSupported ? _startDelay : new Vector2(startDelay, startDelay); private Vector2 DelayBetweenCycles => isRandomnessSupported ? _delayBetweenCycles : new Vector2(delayBetweenCycles, delayBetweenCycles); private Vector2 DelayBetweenYoyo => isRandomnessSupported ? _delayBetweenYoyo : new Vector2(delayBetweenYoyo, delayBetweenYoyo); // For looped animations private Coroutine _loopCoroutine; private Coroutine _basePlayAndReplayCoroutine; private int _animationLoops; private int _plays; #endregion private void OnValidate() { _plays = 0; SetStaticRecursively(gameObject, false); if (!_onComplete.IsNullOrEmpty()) _onComplete.ForEach(a => a._playMode = AnimationPlayMode.PlayByScript); } private static void SetStaticRecursively(GameObject go, bool isStatic) { foreach (Transform trans in go.GetComponentsInChildren(true)) { trans.gameObject.isStatic = isStatic; } } private void OnEnable() { if (_playMode != AnimationPlayMode.PlayOnEnable) return; // Set immediately tween back to initial state (0%) if (CurrentTween != null) { CurrentTween.Rewind(false); // Reinitialize everything ReInit(true); CalculatedDuration = -1; CalculatedTo = -1; } Play(); } #region Initializing /// /// Full animation initialize. /// public void InitAnimation() { if (!_isCustomDuration) _calculatedDuration = -1; if (CurrentTween != null) CurrentTween.onUpdate -= BroadcastTick; CurrentTween = CreateTween(); CurrentTween.onUpdate += BroadcastTick; CurrentTween.SetRelative(_isRelative).SetAutoKill(false); if (Ease == Ease.INTERNAL_Custom) CurrentTween.SetEase(curve); else CurrentTween.SetEase(_ease); if (_destroyOnFinish && _loops == 1) DestroySelfAfterDelay(); if (isIncremental) { CurrentTween.SetLoops(-1, LoopType.Incremental); } } private void BroadcastTick() { Tick?.Invoke(); } private void DestroySelfAfterDelay() { Target.gameObject.KillSelf(CalculatedDuration + TweenSettings.Instance.killDelay); } /// /// Actual children class tween specific implementation. /// protected abstract Tween CreateTween(); /// /// Destroys the old animation and creates a new tween with updates parameters. /// Mostly resets random parameters. /// public DAnimation ReInit(bool killPrevious = false) { _plays = 0; OnReset(); if (killPrevious) CurrentTween?.Kill(); InitAnimation(); return this; } #endregion #region Setters public DAnimation SetTarget(Transform newTarget) { Target = newTarget; OnSetTarget(); return this; } public DAnimation SetUpdateType(UpdateType type) { if (CurrentTween == null) { throw new Exception("Cant set update type for tween if CurrentTween = null"); } CurrentTween.SetUpdate(type); return this; } public DAnimation SetDuration(float d) { _isCustomDuration = true; _calculatedDuration = d; return this; } /// /// Yeah, we can: https://stackoverflow.com/questions/20078025/can-somehow-a-method-from-base-class-return-child-class /// But we want to easily serialize DAnimation in inspector => avoid generics /// public virtual DAnimation SetEndPoint(Vector3 endPoint) { return this; } #endregion #region Play methods public DAnimation Play() { if (_basePlayAndReplayCoroutine != null) StopCoroutine(_basePlayAndReplayCoroutine); _basePlayAndReplayCoroutine = StartCoroutine(BasePlayAndReplay(PlayCurrentTween)); return this; } private void PlayCurrentTween() => CurrentTween.Play(); public DAnimation Play(Vector3 endPoint, Action onComplete = null) { var a = SetEndPoint(endPoint).ReInit().Play(); if (onComplete != null) a.Completed += onComplete; return a; } public DAnimation Restart() { if (_basePlayAndReplayCoroutine != null) StopCoroutine(_basePlayAndReplayCoroutine); _basePlayAndReplayCoroutine = StartCoroutine(BasePlayAndReplay(() => CurrentTween.Restart())); return this; } public void KillTo0() { if (CurrentTween != null) { CurrentTween.Goto(0, true); CurrentTween.Kill(); CurrentTween = null; } } public void Kill() { if (CurrentTween != null) { CurrentTween.Kill(); CurrentTween = null; } } protected virtual void OnLoop() { } private IEnumerator PlayLooped() { _animationLoops = 0; while (_loops == -1 || _animationLoops < _loops) { CurrentTween.Restart(); yield return new WaitForSeconds(CalculatedDuration); yield return new WaitForSeconds(DelayBetweenYoyo.RandomFloat()); OnLoop(); CurrentTween.PlayBackwards(); yield return new WaitForSeconds(CalculatedDuration); _animationLoops += 2; if (_animationLoops == _loops && _destroyOnFinish) { RestartChainedIfHave(); InvokeCompletion(); DestroySelfAfterDelay(); } yield return new WaitForSeconds(DelayBetweenCycles.RandomFloat()); } } public DAnimation Stop() { if (CurrentTween == null) return null; if (DelayBetweenCycles.RandomFloat() > 0) { if (_loopCoroutine != null) StopCoroutine(_loopCoroutine); } else { CurrentTween.Pause(); } return this; } private IEnumerator BasePlayAndReplay(Action callback) { bool needDelay = false; if (IsJustBorn) { OnBeforeFirstPlayAndInit(); needDelay = true; } OnBeforePlayAndInit(); if (!Target.gameObject.activeSelf) yield break; if (HasStartDelay && needDelay) yield return new WaitForSeconds(StartDelay.RandomFloat()); if (CurrentTween == null || _isRelative) InitAnimation(); if (IsJustBorn) { if (_loops == 1) { CurrentTween.onComplete += RestartChainedIfHave; CurrentTween.onComplete += InvokeCompletion; } } _plays++; if (IsLooped) { if (_loopCoroutine != null) StopCoroutine(_loopCoroutine); _loopCoroutine = StartCoroutine(PlayLooped()); } else { callback?.Invoke(); } } private void InvokeCompletion() { Completed?.Invoke(); } private void RestartChainedIfHave() { if (!_onComplete.IsNullOrEmpty()) _onComplete.ForEach(c => c.Restart()); } #endregion protected virtual void OnReset() { } protected virtual void OnBeforePlayAndInit() { } protected virtual void OnBeforeFirstPlayAndInit() { } protected virtual void OnSetTarget() { } } }