// 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)
{
Update(gameTime.ElapsedGameTime);
}
///
public void Update(TimeSpan elapsedTime)
{
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;
}
}