// Copyright (c) Craftwork Games. All rights reserved. // Licensed under the MIT license. // See LICENSE file in the project root for full license information. using System; using Microsoft.Xna.Framework; namespace MonoGame.Extended.Animations; /// /// Represents an animation controller with features to play, pause, stop, reset, and set the state of /// animation playback such as looping, reversing, and ping-pong effects. /// public class AnimationController : IAnimationController { private readonly IAnimation _definition; private int _direction; private int _internalFrame; /// /// Gets a value that indicates whether this animation has been disposed of. /// public bool IsDisposed { get; protected set; } /// public bool IsPaused { get; private set; } /// public bool IsAnimating { get; private set; } /// public bool IsLooping { get; set; } /// public bool IsReversed { get => _direction == -1; set => _direction = value ? -1 : 1; } /// public bool IsPingPong { get; set; } /// public double Speed { get; set; } /// public event Action OnAnimationEvent; /// public TimeSpan CurrentFrameTimeRemaining { get; private set; } /// public int CurrentFrame => _definition.Frames[_internalFrame].FrameIndex; /// public int FrameCount => _definition.FrameCount; /// /// Initializes a new instance of the class with the specified definition. /// /// The definition of the animation. public AnimationController(IAnimation definition) { _definition = definition; // Set initial properties but keep original values in the definition cached for Reset() IsLooping = definition.IsLooping; IsReversed = definition.IsReversed; IsPingPong = definition.IsPingPong; Speed = 1.0f; // Start off playing Play(); } /// public bool Pause() => Pause(false); /// public bool Pause(bool resetFrameDuration) { // We can only pause something that is animating and not already paused. This is to prevent situations // that could accidentally reset frame duration if it was set to true. if (!IsAnimating || IsPaused) { return false; } IsPaused = true; if (resetFrameDuration) { CurrentFrameTimeRemaining = _definition.Frames[_internalFrame].Duration; } return true; } /// public bool Play() => Play(0); /// public bool Play(int startingFrame) { if (startingFrame < 0 || startingFrame >= _definition.FrameCount) { throw new ArgumentOutOfRangeException(nameof(startingFrame), $"{nameof(startingFrame)} cannot be less than zero or greater than or equal to the total number of frames in this {nameof(AnimationController)}"); } // Cannot play something that is already playing if (IsAnimating) { return false; } IsAnimating = true; _internalFrame = startingFrame; CurrentFrameTimeRemaining = _definition.Frames[_internalFrame].Duration; return true; } /// public void Reset() { IsReversed = _definition.IsReversed; IsPingPong = _definition.IsPingPong; IsLooping = _definition.IsLooping; IsAnimating = false; IsPaused = true; Speed = 1.0d; _internalFrame = IsReversed ? _definition.FrameCount - 1 : 0; CurrentFrameTimeRemaining = _definition.Frames[_internalFrame].Duration; } /// public void SetFrame(int index) { if (index < 0 || index >= _definition.FrameCount) { throw new ArgumentOutOfRangeException(nameof(index), $"{nameof(index)} cannot be less than zero or greater than or equal to the total number of frames in this {nameof(AnimationController)}"); } _internalFrame = index; CurrentFrameTimeRemaining = _definition.Frames[_internalFrame].Duration; OnAnimationEvent?.Invoke(this, AnimationEventTrigger.FrameBegin); } /// public bool Stop() => Stop(AnimationEventTrigger.AnimationStopped); private bool Stop(AnimationEventTrigger trigger) { // We can't stop something that's not animating. This is to prevent accidentally invoking OnAnimationEnd if (!IsAnimating) { return false; } IsAnimating = false; IsPaused = true; OnAnimationEvent?.Invoke(this, trigger); return true; } /// public bool Unpause() => Unpause(false); /// public bool Unpause(bool advanceToNextFrame) { // We can't unpause something that's not animating and also isn't paused. This is to prevent improper usage // that could accidentally advance to the next frame if it was set to true. if (!IsAnimating || !IsPaused) { return false; } IsPaused = false; if (advanceToNextFrame) { _ = AdvanceFrame(); } return true; } /// public void Update(GameTime gameTime) { TimeSpan elapsedTime = gameTime.ElapsedGameTime; TimeSpan remainingTime = TimeSpan.Zero; if (!IsAnimating || IsPaused) { return; } CurrentFrameTimeRemaining -= elapsedTime * Speed; while (CurrentFrameTimeRemaining <= TimeSpan.Zero) { remainingTime += -CurrentFrameTimeRemaining; // End the current frame OnAnimationEvent?.Invoke(this, AnimationEventTrigger.FrameEnd); if (!AdvanceFrame()) { break; } CurrentFrameTimeRemaining -= remainingTime; remainingTime = TimeSpan.Zero; } } private bool AdvanceFrame() { // Increment the current frame _internalFrame += _direction; // Ensure frame is in bounds if (_internalFrame < 0 || _internalFrame >= _definition.FrameCount) { // Is this a looping animation? if (IsLooping) { // Is this a standard loop or is it a ping pong? if (IsPingPong) { _direction = -_direction; _internalFrame += _direction * 2; } else { _internalFrame = IsReversed ? _definition.FrameCount - 1 : 0; } // We looped OnAnimationEvent?.Invoke(this, AnimationEventTrigger.AnimationLoop); } else { // No looping and we've reached the end, stop the animation _internalFrame -= _direction; Stop(AnimationEventTrigger.AnimationCompleted); return false; } } CurrentFrameTimeRemaining = _definition.Frames[_internalFrame].Duration; OnAnimationEvent?.Invoke(this, AnimationEventTrigger.FrameBegin); return true; } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// /// /// When overriding this method, check if is or /// . Only dispose of other managed resources when it is . /// /// /// /// Indicates whether this was called from or the finalizer. protected virtual void Dispose(bool disposing) { if (IsDisposed) { return; } IsDisposed = true; } }