2019-08-14 19:07:23 +02:00
using System ;
2021-12-21 20:12:15 +01:00
using System.Collections.Generic ;
using System.Collections.ObjectModel ;
2019-08-14 19:07:23 +02:00
using Microsoft.Xna.Framework ;
using Microsoft.Xna.Framework.Graphics ;
2020-03-21 00:49:43 +01:00
using MLEM.Misc ;
2019-08-14 19:07:23 +02:00
using MLEM.Textures ;
namespace MLEM.Animations {
2020-05-20 23:59:40 +02:00
/// <summary>
/// A sprite animation that allows for any number of frames that each last any number of seconds
/// </summary>
2020-03-21 00:49:43 +01:00
public class SpriteAnimation : GenericDataHolder {
2019-08-14 19:07:23 +02:00
2020-06-18 17:24:35 +02:00
private readonly AnimationFrame [ ] frames ;
2020-05-20 23:59:40 +02:00
/// <summary>
/// Returns the <see cref="AnimationFrame"/> at the given index.
/// Index ordering is based on the order that animation frames were added in.
/// </summary>
/// <param name="index">The index in the list of animation frames</param>
2019-08-14 19:07:23 +02:00
public AnimationFrame this [ int index ] = > this . frames [ index ] ;
2020-05-20 23:59:40 +02:00
/// <summary>
/// The frame that the animation is currently on.
/// </summary>
2019-08-14 19:07:23 +02:00
public AnimationFrame CurrentFrame {
get {
2019-08-21 20:25:32 +02:00
// we might have overshot the end time by a little bit, so just return the last frame
if ( this . TimeIntoAnimation > = this . TotalTime )
return this . frames [ this . frames . Length - 1 ] ;
2019-08-14 19:07:23 +02:00
var accum = 0D ;
foreach ( var frame in this . frames ) {
accum + = frame . Seconds ;
if ( accum > = this . TimeIntoAnimation )
return frame ;
}
2019-08-21 20:25:32 +02:00
// if we're here then the time is negative for some reason, so just return the first frame
return this . frames [ 0 ] ;
2019-08-14 19:07:23 +02:00
}
}
2020-05-20 23:59:40 +02:00
/// <summary>
2021-12-21 20:12:15 +01:00
/// The texture region that this animation's <see cref="CurrentFrame"/> should render.
/// If there are multiple regions, <see cref="CurrentRegions"/> should be used instead.
2020-05-20 23:59:40 +02:00
/// </summary>
2019-08-14 19:07:23 +02:00
public TextureRegion CurrentRegion = > this . CurrentFrame . Region ;
2020-05-20 23:59:40 +02:00
/// <summary>
2021-12-21 20:12:15 +01:00
/// The texture regions that this animation's <see cref="CurrentFrame"/> should render.
/// If there is only one region, <see cref="CurrentRegion"/> can be used for convenience.
/// </summary>
public IList < TextureRegion > CurrentRegions = > this . CurrentFrame . Regions ;
/// <summary>
2020-05-20 23:59:40 +02:00
/// The total amount of time that this animation has.
/// This is auatomatically calculated based on the frame time of each frame.
/// </summary>
2019-08-14 19:07:23 +02:00
public readonly double TotalTime ;
2020-05-20 23:59:40 +02:00
/// <summary>
/// The amount of seconds that the animation has been going on for.
/// If <see cref="TotalTime"/> is reached, this value resets to 0.
/// </summary>
2019-08-14 19:07:23 +02:00
public double TimeIntoAnimation { get ; private set ; }
2020-05-20 23:59:40 +02:00
/// <summary>
/// The finished state of this animation.
/// This is only true for longer than a frame if <see cref="IsLooping"/> is false.
/// </summary>
2019-08-14 19:07:23 +02:00
public bool IsFinished { get ; private set ; }
2020-05-20 23:59:40 +02:00
/// <summary>
/// The name of this animation. This is useful if used in combination with <see cref="SpriteAnimationGroup"/>.
/// </summary>
2019-08-21 20:25:32 +02:00
public string Name ;
2020-05-20 23:59:40 +02:00
/// <summary>
/// The speed multiplier that this animation should run with.
/// Numbers higher than 1 will increase the speed.
/// </summary>
2020-01-09 21:38:04 +01:00
public float SpeedMultiplier = 1 ;
2020-05-20 23:59:40 +02:00
/// <summary>
/// Set to false to stop this animation from looping.
/// To check if the animation has finished playing, see <see cref="IsFinished"/>.
/// </summary>
2019-08-14 19:07:23 +02:00
public bool IsLooping = true ;
2020-05-20 23:59:40 +02:00
/// <summary>
/// A callback that gets fired when the animation completes.
/// </summary>
public event Completed OnCompleted ;
/// <summary>
/// Set this to true to pause the playback of the animation.
/// <see cref="TimeIntoAnimation"/> will not continue and the <see cref="CurrentFrame"/> will not change.
/// </summary>
2019-08-14 19:07:23 +02:00
public bool IsPaused ;
2020-05-20 23:59:40 +02:00
/// <summary>
/// Creates a new sprite animation that contains the given frames.
/// </summary>
/// <param name="frames">The frames this animation should have</param>
2019-08-14 19:07:23 +02:00
public SpriteAnimation ( params AnimationFrame [ ] frames ) {
2021-12-21 20:12:15 +01:00
if ( frames . Length < = 0 )
throw new ArgumentException ( "Cannot create a sprite animation without any frames" ) ;
2019-08-14 19:07:23 +02:00
this . frames = frames ;
foreach ( var frame in frames )
this . TotalTime + = frame . Seconds ;
}
2020-05-20 23:59:40 +02:00
/// <summary>
/// Creates a new sprite animation that contains the given texture regions as frames.
/// </summary>
/// <param name="timePerFrame">The amount of time that each frame should last for</param>
/// <param name="regions">The texture regions that should make up this animation</param>
2021-12-28 14:56:11 +01:00
public SpriteAnimation ( double timePerFrame , params TextureRegion [ ] regions ) : this ( Array . ConvertAll ( regions , r = > new AnimationFrame ( timePerFrame , r ) ) ) { }
2021-12-21 20:12:15 +01:00
/// <summary>
/// Creates a new sprite animation that contains the given texture region arrays as frames, where each frame represents one set of texture regions.
/// </summary>
/// <param name="timePerFrame">The amount of time that each frame should last for</param>
/// <param name="regions">The texture regions that should make up this animation</param>
2021-12-28 14:56:11 +01:00
public SpriteAnimation ( double timePerFrame , params TextureRegion [ ] [ ] regions ) : this ( Array . ConvertAll ( regions , r = > new AnimationFrame ( timePerFrame , r ) ) ) { }
2019-08-14 19:07:23 +02:00
2020-05-20 23:59:40 +02:00
/// <summary>
/// Creates a new sprite animation based on the given texture regions in rectangle-based format.
/// </summary>
/// <param name="timePerFrame">The amount of time that each frame should last for</param>
/// <param name="texture">The texture that the regions should come from</param>
/// <param name="regions">The texture regions that should make up this animation</param>
2021-12-28 14:56:11 +01:00
public SpriteAnimation ( double timePerFrame , Texture2D texture , params Rectangle [ ] regions ) : this ( timePerFrame , Array . ConvertAll ( regions , r = > new TextureRegion ( texture , r ) ) ) { }
2019-08-14 19:07:23 +02:00
2020-05-20 23:59:40 +02:00
/// <summary>
/// Updates this animation, causing <see cref="TimeIntoAnimation"/> to be increased and the <see cref="CurrentFrame"/> to be updated.
/// </summary>
/// <param name="time">The game's time</param>
2019-08-14 19:07:23 +02:00
public void Update ( GameTime time ) {
2020-08-13 19:46:49 +02:00
this . Update ( time . ElapsedGameTime ) ;
2020-02-03 04:37:14 +01:00
}
2020-08-13 19:46:49 +02:00
/// <summary>
/// Updates this animation, causing <see cref="TimeIntoAnimation"/> to be increased and the <see cref="CurrentFrame"/> to be updated.
/// </summary>
/// <param name="elapsed">The amount of time that has passed</param>
public void Update ( TimeSpan elapsed ) {
2019-08-14 19:07:23 +02:00
if ( this . IsFinished | | this . IsPaused )
return ;
2020-08-13 19:46:49 +02:00
this . TimeIntoAnimation + = elapsed . TotalSeconds * this . SpeedMultiplier ;
2019-08-14 19:07:23 +02:00
if ( this . TimeIntoAnimation > = this . TotalTime ) {
if ( ! this . IsLooping ) {
this . IsFinished = true ;
} else {
this . Restart ( ) ;
}
this . OnCompleted ? . Invoke ( this ) ;
}
}
2020-05-20 23:59:40 +02:00
/// <summary>
/// Restarts this animation from the first frame.
/// </summary>
2019-08-14 19:07:23 +02:00
public void Restart ( ) {
this . TimeIntoAnimation = 0 ;
this . IsFinished = false ;
this . IsPaused = false ;
}
2020-05-21 17:21:34 +02:00
/// <summary>
/// A callback for when a sprite animation is completed.
/// </summary>
/// <param name="animation">The animation that has completed</param>
2019-08-14 19:07:23 +02:00
public delegate void Completed ( SpriteAnimation animation ) ;
}
2020-05-20 23:59:40 +02:00
/// <summary>
/// Represents a single frame of a <see cref="SpriteAnimation"/>
/// </summary>
2021-12-21 20:52:05 +01:00
public class AnimationFrame : GenericDataHolder {
2019-08-14 19:07:23 +02:00
2020-05-20 23:59:40 +02:00
/// <summary>
2021-12-21 20:12:15 +01:00
/// The texture regions that this frame should render.
/// If there is only one region, <see cref="Region"/> can be used for convenience.
2020-05-20 23:59:40 +02:00
/// </summary>
2021-12-21 20:12:15 +01:00
public readonly IList < TextureRegion > Regions ;
2020-05-20 23:59:40 +02:00
/// <summary>
2021-12-21 20:12:15 +01:00
/// The total amount of seconds that this frame should last for.
2020-05-20 23:59:40 +02:00
/// </summary>
2020-01-04 00:27:45 +01:00
public readonly double Seconds ;
2021-12-21 20:12:15 +01:00
/// <summary>
/// The texture region that this frame should render.
/// If there are multiple regions, <see cref="Regions"/> should be used instead.
/// </summary>
2021-12-21 20:17:02 +01:00
/// <exception cref="InvalidOperationException">Thrown if this animation frame has more than one region, in which case <see cref="Regions"/> should be used instead.</exception>
public TextureRegion Region = > this . Regions . Count = = 1 ? this . Regions [ 0 ] : throw new InvalidOperationException ( "Cannot return single region for an animation frame with multiple regions" ) ;
2019-08-14 19:07:23 +02:00
2020-05-20 23:59:40 +02:00
/// <summary>
2021-12-21 20:12:15 +01:00
/// Creates a new animation frame based on a set of texture regions and a time.
2020-05-20 23:59:40 +02:00
/// </summary>
2021-12-21 20:12:15 +01:00
/// <param name="regions">The texture regions that this frame should render</param>
2020-05-20 23:59:40 +02:00
/// <param name="seconds">The total amount of seconds that this frame should last for</param>
2021-12-21 20:12:15 +01:00
public AnimationFrame ( double seconds , params TextureRegion [ ] regions ) {
if ( regions . Length < = 0 )
throw new ArgumentException ( "Cannot create an animation frame without any regions" ) ;
this . Regions = new ReadOnlyCollection < TextureRegion > ( regions ) ;
2019-08-14 19:07:23 +02:00
this . Seconds = seconds ;
}
}
}