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