From a501f76162478bfc4f8b2d6e4faac15b67145dac Mon Sep 17 00:00:00 2001 From: Christopher Whitley <103014489+AristurtleDev@users.noreply.github.com> Date: Sun, 23 Jun 2024 23:10:16 -0400 Subject: [PATCH] Sprite Rework (#897) * Add file header * Refator to file-scoped namespace * Least complex to most complext method ordering * Refactor: Group properties at top before constructor * Use the new UV properties from Texture2DRegion. This removes the dependency on the texture size in the calculations if the TextureRegion property is changed. * No longer need to cache normalized origin to preserve it * Use ArgumentNullException instead * Use ArgumentNullException.ThrowIfNull * Refactor: Use brackets for readability * Add xml documentation * Add check for disposed texture * Moved `Sprite` to `MonoGame.Extended.Graphics` * Moved extension methods for rendering sprite into `SpriteBatch.Extensions` * `ISpriteBatchDrawable` is not used by anything It was originally part of the ScreneGraph implementation in MonoGame.Extended which was replaced by the ECS system. It appears that this interface was forgotten to be removed and is not used for anything anymore. * Add `CreateSprite` methods * Add file header * Moved `SpriteSheetAnimationFrame` to `MonoGame.Extended.Graphics` * Move properties before constructor. * Changed `Duration` to `TimeSpan` type * Renamed `Index` to `FrameIndex` * Add `TextureRegion` property * Make property get only * Cleanup whitespace * Remove `SpriteSheetAnimationFrameJsonConverter` * Cleanup unused namespaces * Make file-scoped namespace * Added xml documentation * Added file header * marked constructor as internal. This will be created from a spritesheet itself * Move `SpriteSheetAnimationCycle` to `MonoGame.Extended.Graphcis` * File scoped namespace * Add file header * Reorganize: Move properties above constructor * Make `Frames` property a read-only span * Remove duration, this is being moved to the frame itself * Add `Name` property * Set looping, reversed, and ping pong properties in ctor * Make ctor internal This will be an object created by a spritesheet instane itself. * Cleanup: Whitespace * Add `FrameCount` property * Add `GetFrame` method and accompanying `this[int]` method. * Documentation: Added missing documentation. * WIP: Temporary commit for workstation change * WIP: Temporary commit for workstation change * WIP: Temporary commit for workstation change * WIP: Temporary commit for workstation change * Refactor: Animation Refactor Completed * Refactor: Sprite stuff finished * Remove tests that can't be run on GitHub CI Need to find alternative * Make optional name param the last param in Texture2DRegion ctors * Resolve merge conflicts --- .editorconfig | 2 +- .../TextureAtlases/TexturePackerWriter.cs | 2 +- .../InputListeners/InputListenerComponent.cs | 4 +- .../MonoGame.Extended/AnimationComponent.cs | 3 +- .../MonoGame.Extended/Animations/Animation.cs | 279 ++++++ .../Animations/AnimationEvent.cs | 25 + .../Animations/AnimationEventTrigger.cs | 42 + .../Animations/IAnimation.cs | 149 +++ .../Animations/IAnimationDefinition.cs | 44 + .../Animations/IAnimationFrame.cs | 23 + .../Texture2DAtlasJsonContentTypeReader.cs | 4 +- .../ContentReaders/Texture2DAtlasReader.cs | 4 +- .../FramesPerSecondCounter.cs | 44 +- .../Graphics/AnimatedSprite.cs | 41 + source/MonoGame.Extended/Graphics/Sprite.cs | 229 +++++ .../Graphics/SpriteBatch.Extensions.cs | 111 ++- .../MonoGame.Extended/Graphics/SpriteSheet.cs | 92 ++ .../SpriteSheetAnimationDefinition.cs | 29 + .../SpriteSheetAnimationDefinitionBuilder.cs | 88 ++ .../Graphics/SpriteSheetAnimationFrame.cs | 34 + .../Graphics/Texture2DAtlas.cs | 65 +- .../Graphics/Texture2DRegion.cs | 18 +- .../Graphics/TextureRegion.Extensions.cs | 14 +- source/MonoGame.Extended/IUpdate.cs | 9 - .../Sprites/AnimatedSprite.cs | 50 - source/MonoGame.Extended/Sprites/Animation.cs | 86 -- .../Sprites/ISpriteBatchDrawable.cs | 18 - source/MonoGame.Extended/Sprites/Sprite.cs | 109 --- .../Sprites/SpriteExtensions.cs | 36 - .../MonoGame.Extended/Sprites/SpriteSheet.cs | 28 - .../Sprites/SpriteSheetAnimation.cs | 107 -- .../Sprites/SpriteSheetAnimationCycle.cs | 18 - .../Sprites/SpriteSheetAnimationData.cs | 21 - .../Sprites/SpriteSheetAnimationFrame.cs | 52 - source/MonoGame.Extended/Timers/GameTimer.cs | 44 +- .../AspectBuilderTests.cs | 2 +- .../AspectTests.cs | 2 +- .../ComponentManagerTests.cs | 2 +- .../ComponentTypeTests.cs | 7 +- .../WorldManagerTests.cs | 6 +- .../Animations/AnimationTests.cs | 220 +++++ .../MonoGame.Extended.Tests.csproj | 6 + .../Sprites/SpriteSheetAnimationTests.cs | 911 ------------------ .../Sprites/SpriteTests.cs | 91 -- .../TestGraphicsDevice.cs | 12 - tests/MonoGame.Extended.Tests/TestHelper.cs | 27 - .../TextureAtlases/TextureAtlasTests.cs | 236 ----- .../TextureAtlases/TextureRegion2DTests.cs | 40 - 48 files changed, 1579 insertions(+), 1907 deletions(-) create mode 100644 source/MonoGame.Extended/Animations/Animation.cs create mode 100644 source/MonoGame.Extended/Animations/AnimationEvent.cs create mode 100644 source/MonoGame.Extended/Animations/AnimationEventTrigger.cs create mode 100644 source/MonoGame.Extended/Animations/IAnimation.cs create mode 100644 source/MonoGame.Extended/Animations/IAnimationDefinition.cs create mode 100644 source/MonoGame.Extended/Animations/IAnimationFrame.cs create mode 100644 source/MonoGame.Extended/Graphics/AnimatedSprite.cs create mode 100644 source/MonoGame.Extended/Graphics/Sprite.cs create mode 100644 source/MonoGame.Extended/Graphics/SpriteSheet.cs create mode 100644 source/MonoGame.Extended/Graphics/SpriteSheetAnimationDefinition.cs create mode 100644 source/MonoGame.Extended/Graphics/SpriteSheetAnimationDefinitionBuilder.cs create mode 100644 source/MonoGame.Extended/Graphics/SpriteSheetAnimationFrame.cs delete mode 100644 source/MonoGame.Extended/IUpdate.cs delete mode 100644 source/MonoGame.Extended/Sprites/AnimatedSprite.cs delete mode 100644 source/MonoGame.Extended/Sprites/Animation.cs delete mode 100644 source/MonoGame.Extended/Sprites/ISpriteBatchDrawable.cs delete mode 100644 source/MonoGame.Extended/Sprites/Sprite.cs delete mode 100644 source/MonoGame.Extended/Sprites/SpriteExtensions.cs delete mode 100644 source/MonoGame.Extended/Sprites/SpriteSheet.cs delete mode 100644 source/MonoGame.Extended/Sprites/SpriteSheetAnimation.cs delete mode 100644 source/MonoGame.Extended/Sprites/SpriteSheetAnimationCycle.cs delete mode 100644 source/MonoGame.Extended/Sprites/SpriteSheetAnimationData.cs delete mode 100644 source/MonoGame.Extended/Sprites/SpriteSheetAnimationFrame.cs create mode 100644 tests/MonoGame.Extended.Tests/Animations/AnimationTests.cs delete mode 100644 tests/MonoGame.Extended.Tests/Sprites/SpriteSheetAnimationTests.cs delete mode 100644 tests/MonoGame.Extended.Tests/Sprites/SpriteTests.cs delete mode 100644 tests/MonoGame.Extended.Tests/TestGraphicsDevice.cs delete mode 100644 tests/MonoGame.Extended.Tests/TestHelper.cs delete mode 100644 tests/MonoGame.Extended.Tests/TextureAtlases/TextureAtlasTests.cs delete mode 100644 tests/MonoGame.Extended.Tests/TextureAtlases/TextureRegion2DTests.cs diff --git a/.editorconfig b/.editorconfig index 9f90c20d..73797762 100644 --- a/.editorconfig +++ b/.editorconfig @@ -43,7 +43,7 @@ tab_width = 4 ################################################################################ ### New line preferences ################################################################################ -end_of_line = crlf +end_of_line = lf ###############################################################################3 ### diff --git a/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerWriter.cs b/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerWriter.cs index 735d817b..35833d8f 100644 --- a/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerWriter.cs +++ b/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerWriter.cs @@ -25,11 +25,11 @@ namespace MonoGame.Extended.Content.Pipeline.TextureAtlases var regionName = Path.ChangeExtension(region.Filename, null); Debug.Assert(regionName != null, "regionName != null"); - writer.Write(regionName); writer.Write(region.Frame.X); writer.Write(region.Frame.Y); writer.Write(region.Frame.Width); writer.Write(region.Frame.Height); + writer.Write(regionName); } } diff --git a/source/MonoGame.Extended.Input/InputListeners/InputListenerComponent.cs b/source/MonoGame.Extended.Input/InputListeners/InputListenerComponent.cs index 302f6b5d..7a76b6a2 100644 --- a/source/MonoGame.Extended.Input/InputListeners/InputListenerComponent.cs +++ b/source/MonoGame.Extended.Input/InputListeners/InputListenerComponent.cs @@ -3,7 +3,7 @@ using Microsoft.Xna.Framework; namespace MonoGame.Extended.Input.InputListeners { - public class InputListenerComponent : GameComponent, IUpdate + public class InputListenerComponent : GameComponent, IUpdateable { private readonly List _listeners; @@ -34,4 +34,4 @@ namespace MonoGame.Extended.Input.InputListeners GamePadListener.CheckConnections(); } } -} \ No newline at end of file +} diff --git a/source/MonoGame.Extended/AnimationComponent.cs b/source/MonoGame.Extended/AnimationComponent.cs index 28a0e576..0573334c 100644 --- a/source/MonoGame.Extended/AnimationComponent.cs +++ b/source/MonoGame.Extended/AnimationComponent.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using Microsoft.Xna.Framework; -using MonoGame.Extended.Sprites; namespace MonoGame.Extended.Animations { @@ -27,4 +26,4 @@ namespace MonoGame.Extended.Animations Animations.RemoveAll(a => a.IsDisposed); } } -} \ No newline at end of file +} diff --git a/source/MonoGame.Extended/Animations/Animation.cs b/source/MonoGame.Extended/Animations/Animation.cs new file mode 100644 index 00000000..5d647391 --- /dev/null +++ b/source/MonoGame.Extended/Animations/Animation.cs @@ -0,0 +1,279 @@ +// 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 with various control features such as play, pause, stop, looping, reversing, and +/// ping-pong effects. +/// +public class Animation : IAnimation +{ + private readonly IAnimationDefinition _definition; + private int _direction; + + /// + /// 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 { get; private set; } + + /// + public int FrameCount => _definition.FrameCount; + + /// + /// Initializes a new instance of the class with the specified definition. + /// + /// The definition of the animation. + public Animation(IAnimationDefinition 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; + } + + /// + 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[CurrentFrame].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(Animation)}"); + } + + // Cannot play something that is already playing + if (IsAnimating) + { + return false; + } + + IsAnimating = true; + CurrentFrame = startingFrame; + CurrentFrameTimeRemaining = _definition.Frames[CurrentFrame].Duration; + return true; + } + + /// + public void Reset() + { + IsReversed = _definition.IsReversed; + IsPingPong = _definition.IsPingPong; + IsLooping = _definition.IsLooping; + IsAnimating = false; + IsPaused = true; + Speed = 1.0d; + CurrentFrame = IsReversed ? _definition.FrameCount - 1 : 0; + CurrentFrameTimeRemaining = _definition.Frames[CurrentFrame].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(Animation)}"); + } + + CurrentFrame = index; + CurrentFrameTimeRemaining = _definition.Frames[CurrentFrame].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 + CurrentFrame += _direction; + + // Ensure frame is in bounds + if (CurrentFrame < 0 || CurrentFrame >= _definition.FrameCount) + { + // Is this a looping animation? + if (IsLooping) + { + // Is this a standard loop or is it a ping pong? + if (IsPingPong) + { + _direction = -_direction; + CurrentFrame += _direction * 2; + } + else + { + CurrentFrame = IsReversed ? _definition.FrameCount - 1 : 0; + } + + // We looped + OnAnimationEvent?.Invoke(this, AnimationEventTrigger.AnimationLoop); + } + else + { + // No looping and we've reached the end, stop the animation + CurrentFrame -= _direction; + Stop(AnimationEventTrigger.AnimationCompleted); + return false; + } + } + + CurrentFrameTimeRemaining = _definition.Frames[CurrentFrame].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; + } +} diff --git a/source/MonoGame.Extended/Animations/AnimationEvent.cs b/source/MonoGame.Extended/Animations/AnimationEvent.cs new file mode 100644 index 00000000..1c932ed8 --- /dev/null +++ b/source/MonoGame.Extended/Animations/AnimationEvent.cs @@ -0,0 +1,25 @@ +// 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; + +namespace MonoGame.Extended.Animations; + +/// +/// Represents an event that occurs during an animation. +/// +public class AnimationEvent : EventArgs +{ + /// + /// Gets the animation associated with the event. + /// + public IAnimation Animation { get; } + + /// + /// Gets the trigger that caused the event. + /// + public AnimationEventTrigger Trigger { get; } + + internal AnimationEvent(IAnimation animation, AnimationEventTrigger trigger) => (Animation, Trigger) = (animation, trigger); +} diff --git a/source/MonoGame.Extended/Animations/AnimationEventTrigger.cs b/source/MonoGame.Extended/Animations/AnimationEventTrigger.cs new file mode 100644 index 00000000..7e50c383 --- /dev/null +++ b/source/MonoGame.Extended/Animations/AnimationEventTrigger.cs @@ -0,0 +1,42 @@ +// Copyright (c) Craftwork Games. All rights reserved. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. + +namespace MonoGame.Extended.Animations; + +/// +/// Specifies the trigger for an animation event. +/// +public enum AnimationEventTrigger +{ + /// + /// Triggered at the beginning of a frame. + /// + FrameBegin, + + /// + /// Triggered at the end of a frame. + /// + FrameEnd, + + /// + /// Triggered when a frame is skipped. + /// + FrameSkipped, + + /// + /// Triggered when the animation loops. + /// + AnimationLoop, + + /// + /// Triggered when the animation completes. + /// + AnimationCompleted, + + /// + /// Triggered when the animation stops. + /// + AnimationStopped, +} + diff --git a/source/MonoGame.Extended/Animations/IAnimation.cs b/source/MonoGame.Extended/Animations/IAnimation.cs new file mode 100644 index 00000000..0224914c --- /dev/null +++ b/source/MonoGame.Extended/Animations/IAnimation.cs @@ -0,0 +1,149 @@ +// 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; + +/// +/// Defines the interface for an animation with various control features such as play, pause, stop, looping, reversing, and ping-pong effects. +/// +public interface IAnimation : IDisposable +{ + /// + /// Gets a value indicating whether the animation is paused. + /// + bool IsPaused { get; } + + /// + /// Gets a value indicating whether the animation is currently animating. + /// + bool IsAnimating { get; } + + /// + /// Gets or sets a value indicating whether the animation should loop. + /// + bool IsLooping { get; set; } + + /// + /// Gets or sets a value indicating whether the animation is reversed. + /// + bool IsReversed { get; set; } + + /// + /// Gets or sets a value indicating whether the animation should ping-pong (reverse direction at the ends). + /// + bool IsPingPong { get; set; } + + /// + /// Gets or sets the speed of the animation. + /// + /// The speed cannot be less than zero. + double Speed { get; set; } + + /// + /// Gets or sets the action to perform when an animation event is triggered. + /// + event Action OnAnimationEvent; + + /// + /// Gets the time remaining for the current frame. + /// + TimeSpan CurrentFrameTimeRemaining { get; } + + /// + /// Gets the index of the current frame of the animation. + /// + int CurrentFrame { get; } + + /// + /// Gets the total number of frames in the animation. + /// + int FrameCount { get; } + + /// + /// Sets the animation to a specified frame. + /// + /// The index of the frame to set. + /// + /// Thrown when the parameter is less than zero or greater than or equal to the total + /// number of frames. + /// + void SetFrame(int index); + + /// + /// Plays the animation from the beginning. + /// + /// + /// if the animation was successfully started; otherwise, . + /// + bool Play(); + + /// + /// Plays the animation from a specified starting frame. + /// + /// The frame to start the animation from. + /// + /// if the animation was successfully started; otherwise, . + /// + /// + /// Thrown when the parameter is less than zero or greater than or equal to the + /// total number of frames. + /// + bool Play(int startingFrame); + + /// + /// Pauses the animation. + /// + /// + /// if the animation was successfully paused; otherwise, . + /// + bool Pause(); + + /// + /// Pauses the animation. + /// + /// If set to , resets the frame duration. + /// + /// if the animation was successfully paused; otherwise, . + /// + bool Pause(bool resetFrameDuration); + + /// + /// Unpauses the animation. + /// + /// + /// if the animation was successfully unpaused; otherwise, . + /// + bool Unpause(); + + /// + /// Unpauses the animation. + /// + /// If set to , advances to the next frame. + /// + /// if the animation was successfully unpaused; otherwise, . + /// + bool Unpause(bool advanceToNextFrame); + + /// + /// Updates the animation. + /// + /// A snapshot of the timing values for the current update cycle. + void Update(GameTime gameTime); + + /// + /// Stops the animation. + /// + /// + /// if the animation was successfully stopped; otherwise, . + /// + bool Stop(); + + /// + /// Resets the animation to its initial state. + /// + void Reset(); +} diff --git a/source/MonoGame.Extended/Animations/IAnimationDefinition.cs b/source/MonoGame.Extended/Animations/IAnimationDefinition.cs new file mode 100644 index 00000000..fa48ae5b --- /dev/null +++ b/source/MonoGame.Extended/Animations/IAnimationDefinition.cs @@ -0,0 +1,44 @@ +// 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; + +namespace MonoGame.Extended.Animations; + +/// +/// Defines the interface for an animation definition, specifying properties of the animation such as frames, looping, +/// reversing, and ping-pong effects. +/// +public interface IAnimationDefinition +{ + /// + /// Gets the name of the animation. + /// + string Name { get; } + + /// + /// Gets the read-only collection of frames in the animation. + /// + ReadOnlySpan Frames { get; } + + /// + /// Gets the total number of frames in the animation. + /// + int FrameCount { get; } + + /// + /// Gets a value indicating whether the animation should loop. + /// + bool IsLooping { get; } + + /// + /// Gets a value indicating whether the animation is reversed. + /// + bool IsReversed { get; } + + /// + /// Gets a value indicating whether the animation should ping-pong (reverse direction at the ends). + /// + bool IsPingPong { get; } +} diff --git a/source/MonoGame.Extended/Animations/IAnimationFrame.cs b/source/MonoGame.Extended/Animations/IAnimationFrame.cs new file mode 100644 index 00000000..4bab2805 --- /dev/null +++ b/source/MonoGame.Extended/Animations/IAnimationFrame.cs @@ -0,0 +1,23 @@ +// 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; + +namespace MonoGame.Extended.Animations; + +/// +/// Defines the interface for an animation frame, specifying the frame index and its duration. +/// +public interface IAnimationFrame +{ + /// + /// Gets the frame index represented by this animation frame. + /// + int FrameIndex { get; } + + /// + /// Gets the total duration this frame should be displayed during an animation. + /// + TimeSpan Duration { get; } +} diff --git a/source/MonoGame.Extended/Content/ContentReaders/Texture2DAtlasJsonContentTypeReader.cs b/source/MonoGame.Extended/Content/ContentReaders/Texture2DAtlasJsonContentTypeReader.cs index 50ea6d60..e7058964 100644 --- a/source/MonoGame.Extended/Content/ContentReaders/Texture2DAtlasJsonContentTypeReader.cs +++ b/source/MonoGame.Extended/Content/ContentReaders/Texture2DAtlasJsonContentTypeReader.cs @@ -26,11 +26,11 @@ namespace MonoGame.Extended.Content.ContentReaders for (var i = 0; i < regionCount; i++) { atlas.CreateRegion( - ContentReaderExtensions.RemoveExtension(texturePackerFile.Regions[i].Filename), texturePackerFile.Regions[i].Frame.X, texturePackerFile.Regions[i].Frame.Y, texturePackerFile.Regions[i].Frame.Width, - texturePackerFile.Regions[i].Frame.Height); + texturePackerFile.Regions[i].Frame.Height, + ContentReaderExtensions.RemoveExtension(texturePackerFile.Regions[i].Filename)); } return atlas; diff --git a/source/MonoGame.Extended/Content/ContentReaders/Texture2DAtlasReader.cs b/source/MonoGame.Extended/Content/ContentReaders/Texture2DAtlasReader.cs index b7603d90..6ba935c2 100644 --- a/source/MonoGame.Extended/Content/ContentReaders/Texture2DAtlasReader.cs +++ b/source/MonoGame.Extended/Content/ContentReaders/Texture2DAtlasReader.cs @@ -17,11 +17,11 @@ namespace MonoGame.Extended.Content.ContentReaders for (var i = 0; i < regionCount; i++) { atlas.CreateRegion( - reader.ReadString(), reader.ReadInt32(), reader.ReadInt32(), reader.ReadInt32(), - reader.ReadInt32()); + reader.ReadInt32(), + reader.ReadString()); } return atlas; diff --git a/source/MonoGame.Extended/FramesPerSecondCounter.cs b/source/MonoGame.Extended/FramesPerSecondCounter.cs index 432f1ac6..8ae77569 100644 --- a/source/MonoGame.Extended/FramesPerSecondCounter.cs +++ b/source/MonoGame.Extended/FramesPerSecondCounter.cs @@ -3,18 +3,58 @@ using Microsoft.Xna.Framework; namespace MonoGame.Extended { - public class FramesPerSecondCounter : IUpdate + public class FramesPerSecondCounter : IUpdateable { + private bool _enabled; + private int _updateOrder; + private static readonly TimeSpan _oneSecondTimeSpan = new TimeSpan(0, 0, 1); private int _framesCounter; private TimeSpan _timer = _oneSecondTimeSpan; + /// + public bool Enabled + { + get => _enabled; + set + { + if (_enabled == value) + { + return; + } + _enabled = value; + EnabledChanged?.Invoke(this, EventArgs.Empty); + } + } + + /// + public int UpdateOrder + { + get => _updateOrder; + set + { + if (_updateOrder == value) + { + return; + } + _updateOrder= value; + EnabledChanged?.Invoke(this, EventArgs.Empty); + } + } + + /// + public event EventHandler EnabledChanged; + + /// + public event EventHandler UpdateOrderChanged; + public FramesPerSecondCounter() { } public int FramesPerSecond { get; private set; } + public void Update(GameTime gameTime) { _timer += gameTime.ElapsedGameTime; @@ -31,4 +71,4 @@ namespace MonoGame.Extended _framesCounter++; } } -} \ No newline at end of file +} diff --git a/source/MonoGame.Extended/Graphics/AnimatedSprite.cs b/source/MonoGame.Extended/Graphics/AnimatedSprite.cs new file mode 100644 index 00000000..15096f1f --- /dev/null +++ b/source/MonoGame.Extended/Graphics/AnimatedSprite.cs @@ -0,0 +1,41 @@ +// Copyright (c) Craftwork Games. All rights reserved. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. + +using Microsoft.Xna.Framework; +using MonoGame.Extended.Animations; + +namespace MonoGame.Extended.Graphics; + +/// +/// Represents an animated sprite that can play, pause, and update animations. +/// +public class AnimatedSprite : Sprite +{ + private readonly Texture2DRegion[] _regions; + + /// + /// Gets the animation used by this animated sprite. + /// + public IAnimation Animation { get; } + + internal AnimatedSprite(SpriteSheetAnimationDefinition definition, Texture2DRegion[] regions) + : base(regions[0]) + { + Animation = new Animation(definition); + _regions = regions; + } + + /// + public void Update(GameTime gameTime) + { + int index = Animation.CurrentFrame; + Animation.Update(gameTime); + + // If the current frame changed during the update, change the texture region + if (index != Animation.CurrentFrame) + { + TextureRegion = _regions[Animation.CurrentFrame]; + } + } +} diff --git a/source/MonoGame.Extended/Graphics/Sprite.cs b/source/MonoGame.Extended/Graphics/Sprite.cs new file mode 100644 index 00000000..096f8951 --- /dev/null +++ b/source/MonoGame.Extended/Graphics/Sprite.cs @@ -0,0 +1,229 @@ +// 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 System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Graphics; + +/// +/// Represents a drawable 2D texture region with additional properties for rendering, such as position, scale, +/// rotation, and color. +/// +public class Sprite : IColorable +{ + private Texture2DRegion _textureRegion; + + /// + /// Gets or sets a value indicating whether this sprite is visible. + /// + public bool IsVisible { get; set; } + + /// + /// Gets or sets the color mask used when rendering this sprite. + /// + /// + /// The color mask is applied to the sprite's texture by multiplying each pixel's color value with the specified + /// color. For example, setting the color to `Color.Red` will make the sprite appear as a red tint over its + /// texture. + /// + public Color Color { get; set; } + + /// + /// Gets or sets the alpha transparency value used when rendering this sprite. + /// + /// + /// The alpha value should be between 0 (fully transparent) and 1 (fully opaque). + /// + public float Alpha { get; set; } + + /// + /// Gets or sets the layer depth used when rendering this sprite. + /// + /// + /// Sprites with higher depth values are rendered on top of those with lower depth values. + /// + public float Depth { get; set; } + + /// + /// Gets or sets the sprite effects to apply when rendering this sprite. + /// + /// + /// This specifies the desired horizontal and/or vertical flip effect of the sprite. + /// + public SpriteEffects Effect { get; set; } + + /// + /// Gets or Sets an object that contains user defined data about this sprite. + /// + public object Tag { get; set; } + + /// + /// Gets or sets the origin of this sprite. + /// + /// + /// The origin is relative to the bounds of the texture region and represents the point around which the sprite is + /// rotated and scaled. + /// + public Vector2 Origin { get; set; } + + /// + /// Gets or sets the normalized origin of this sprite relative to its texture region. + /// + /// + /// The normalized origin represents the origin as a fraction of the texture region's UV coordinates, + /// where (0, 0) is the top-left corner and (1, 1) is the bottom-right corner. + /// + public Vector2 OriginNormalized + { + get + { + float normalizedX = (Origin.X - TextureRegion.LeftUV) / (TextureRegion.RightUV - TextureRegion.LeftUV); + float normalizedY = (Origin.Y - TextureRegion.TopUV) / (TextureRegion.BottomUV - TextureRegion.TopUV); + return new Vector2(normalizedX, normalizedY); + } + set + { + float actualX = value.X * (TextureRegion.RightUV - TextureRegion.LeftUV) + TextureRegion.LeftUV; + float actualY = value.Y * (TextureRegion.BottomUV - TextureRegion.TopUV) + TextureRegion.TopUV; + Origin = new Vector2(actualX, actualY); + } + } + + /// + /// Gets or sets the source texture region of this sprite. + /// + /// Thrown when setting to a null texture region. + /// + /// Thrown if the source texture of the assigned has been disposed of when setting. + /// + public Texture2DRegion TextureRegion + { + get => _textureRegion; + set + { + ArgumentNullException.ThrowIfNull(value); + if (value.Texture.IsDisposed) + { + throw new ObjectDisposedException(nameof(value), $"The source {nameof(Texture2D)} of the {nameof(TextureRegion)} was disposed prior to setting this property."); + } + _textureRegion = value; + } + } + + /// + /// Initializes a new instance of the class with the specified texture. + /// + /// The source texture of the sprite. + /// + /// Thrown if the parameter is . + /// + /// + /// Thrown if the parameter was disposed of prior. + /// + public Sprite(Texture2D texture) + : this(new Texture2DRegion(texture)) + { + } + + + /// + /// Initializes a new instance of the class with the specified texture region. + /// The sprite represents a renderable 2D image defined by the given texture region. + /// + /// The source texture region of the sprite. + /// + /// Thrown if the parameter is . + /// + /// + /// Thrown if the source texture of the parameter has been disposed of. + /// + public Sprite(Texture2DRegion textureRegion) + { + ArgumentNullException.ThrowIfNull(textureRegion); + if (textureRegion.Texture.IsDisposed) + { + throw new ObjectDisposedException(nameof(textureRegion), $"The source {nameof(Texture2D)} of the {nameof(textureRegion)} was disposed prior."); + } + + _textureRegion = textureRegion; + + Alpha = 1.0f; + Color = Color.White; + IsVisible = true; + Effect = SpriteEffects.None; + OriginNormalized = new Vector2(0.5f, 0.5f); + Depth = 0.0f; + } + + /// + /// Gets the bounding rectangle of the sprite in world/screen coordinates. + /// + /// The transformation of the sprite. + /// The bounding rectangle of the sprite in world/screen coordinates. + public RectangleF GetBoundingRectangle(Transform2 transform) + { + return GetBoundingRectangle(transform.Position, transform.Rotation, transform.Scale); + } + + /// + /// Gets the bounding rectangle of the sprite in world/screen coordinates. + /// + /// The xy-coordinate position of the sprite in world/screen coordinates. + /// The rotation, in radians, of the sprite. + /// The scale of the sprite. + /// The bounding rectangle of the sprite in world/screen coordinates. + public RectangleF GetBoundingRectangle(Vector2 position, float rotation, Vector2 scale) + { + var corners = GetCorners(position, rotation, scale); + var min = new Vector2(corners.Min(i => i.X), corners.Min(i => i.Y)); + var max = new Vector2(corners.Max(i => i.X), corners.Max(i => i.Y)); + return new RectangleF(min.X, min.Y, max.X - min.X, max.Y - min.Y); + } + + /// + /// Gets the corner points of the sprite in world/screen coordinates. + /// + /// The xy-coordinate position of the sprite in world/screen coordinates. + /// The rotation, in radians, of the sprite. + /// The scale of the sprite. + /// The corner points of the sprite in world/screen coordinates. + public Vector2[] GetCorners(Vector2 position, float rotation, Vector2 scale) + { + var min = -Origin; + var max = min + new Vector2(TextureRegion.Width, TextureRegion.Height); + var offset = position; + + if (scale != Vector2.One) + { + min *= scale; + max = max * scale; + } + + var corners = new Vector2[4]; + corners[0] = min; + corners[1] = new Vector2(max.X, min.Y); + corners[2] = max; + corners[3] = new Vector2(min.X, max.Y); + + if (rotation != 0) + { + var matrix = Matrix.CreateRotationZ(rotation); + + for (var i = 0; i < 4; i++) + { + corners[i] = Vector2.Transform(corners[i], matrix); + } + } + + for (var i = 0; i < 4; i++) + { + corners[i] += offset; + } + + return corners; + } +} diff --git a/source/MonoGame.Extended/Graphics/SpriteBatch.Extensions.cs b/source/MonoGame.Extended/Graphics/SpriteBatch.Extensions.cs index 487a2924..63b7e225 100644 --- a/source/MonoGame.Extended/Graphics/SpriteBatch.Extensions.cs +++ b/source/MonoGame.Extended/Graphics/SpriteBatch.Extensions.cs @@ -2,20 +2,29 @@ // Licensed under the MIT license. // See LICENSE file in the project root for full license information. - using System; -using System.Reflection; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace MonoGame.Extended.Graphics; +/// +/// Provides extension methods for the class. +/// public static class SpriteBatchExtensions { #region ----------------------------NinePatch----------------------------- private static readonly Rectangle[] _patchCache = new Rectangle[9]; private static Rectangle _rect = default; + /// + /// Draws a nine-patch region to the sprite batch. + /// + /// The sprite batch. + /// The nine-patch region. + /// The destination rectangle. + /// The color to tint the nine-patch region. + /// An optional clipping rectangle. public static void Draw(this SpriteBatch spriteBatch, NinePatch ninePatchRegion, Rectangle destinationRectangle, Color color, Rectangle? clippingRectangle = null) { CreateDestinationPatches(ninePatchRegion, destinationRectangle); @@ -44,8 +53,75 @@ public static class SpriteBatchExtensions #endregion -------------------------NinePatch----------------------------- + #region ----------------------------Sprite----------------------------- + /// + /// Draws a sprite to the sprite batch. + /// + /// The sprite to draw. + /// The sprite batch. + /// The position to draw the sprite. + /// The rotation of the sprite. + /// The scale of the sprite. + public static void Draw(this Sprite sprite, SpriteBatch spriteBatch, Vector2 position, float rotation, Vector2 scale) + { + Draw(spriteBatch, sprite, position, rotation, scale); + } + + /// + /// Draws a sprite to the sprite batch with a transform. + /// + /// The sprite batch. + /// The sprite to draw. + /// The transform to apply to the sprite. + public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Transform2 transform) + { + Draw(spriteBatch, sprite, transform.Position, transform.Rotation, transform.Scale); + } + + /// + /// Draws a sprite to the sprite batch. + /// + /// The sprite batch. + /// The sprite to draw. + /// The position to draw the sprite. + /// The rotation of the sprite. + public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Vector2 position, float rotation = 0) + { + Draw(spriteBatch, sprite, position, rotation, Vector2.One); + } + + /// + /// Draws a sprite to the sprite batch. + /// + /// The sprite batch. + /// The sprite to draw. + /// The position to draw the sprite. + /// The rotation of the sprite. + /// The scale of the sprite. + public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Vector2 position, float rotation, Vector2 scale) + { + if (sprite == null) throw new ArgumentNullException(nameof(sprite)); + + if (sprite.IsVisible) + { + var texture = sprite.TextureRegion.Texture; + var sourceRectangle = sprite.TextureRegion.Bounds; + spriteBatch.Draw(texture, position, sourceRectangle, sprite.Color * sprite.Alpha, rotation, sprite.Origin, scale, sprite.Effect, sprite.Depth); + } + } + #endregion -------------------------Sprite----------------------------- + #region ----------------------------Texture2D----------------------------- + /// + /// Draws a texture to the sprite batch with optional clipping. + /// + /// The sprite batch. + /// The texture to draw. + /// The source rectangle. + /// The destination rectangle. + /// The color to tint the texture. + /// An optional clipping rectangle. public static void Draw(this SpriteBatch spriteBatch, Texture2D texture, Rectangle sourceRectangle, Rectangle destinationRectangle, Color color, Rectangle? clippingRectangle) { if (!ClipRectangles(ref sourceRectangle, ref destinationRectangle, clippingRectangle)) @@ -61,11 +137,32 @@ public static class SpriteBatchExtensions #region ----------------------------TextureRegion----------------------------- + /// + /// Draws a texture region to the sprite batch. + /// + /// The sprite batch. + /// The texture region to draw. + /// The position to draw the texture region. + /// The color to tint the texture region. + /// An optional clipping rectangle. public static void Draw(this SpriteBatch spriteBatch, Texture2DRegion textureRegion, Vector2 position, Color color, Rectangle? clippingRectangle = null) { Draw(spriteBatch, textureRegion, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0, clippingRectangle); } + /// + /// Draws a texture region to the sprite batch with specified parameters. + /// + /// The sprite batch. + /// The texture region to draw. + /// The position to draw the texture region. + /// The color to tint the texture region. + /// The rotation of the texture region. + /// The origin of the texture region. + /// The scale of the texture region. + /// The sprite effects to apply. + /// The layer depth. + /// An optional clipping rectangle. public static void Draw(this SpriteBatch spriteBatch, Texture2DRegion textureRegion, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth, Rectangle? clippingRectangle = null) { @@ -92,13 +189,19 @@ public static class SpriteBatchExtensions spriteBatch.Draw(textureRegion.Texture, position, sourceRectangle, color, rotation, origin, scale, effects, layerDepth); } + /// + /// Draws a texture region to the sprite batch. + /// + /// The sprite batch. + /// The texture region to draw. + /// The destination rectangle. + /// The color to tint the texture region. + /// An optional clipping rectangle. public static void Draw(this SpriteBatch spriteBatch, Texture2DRegion textureRegion, Rectangle destinationRectangle, Color color, Rectangle? clippingRectangle = null) { Draw(spriteBatch, textureRegion.Texture, textureRegion.Bounds, destinationRectangle, color, clippingRectangle); } - - #endregion -------------------------TextureRegion----------------------------- #region ----------------------------Utilities----------------------------- diff --git a/source/MonoGame.Extended/Graphics/SpriteSheet.cs b/source/MonoGame.Extended/Graphics/SpriteSheet.cs new file mode 100644 index 00000000..1d65a292 --- /dev/null +++ b/source/MonoGame.Extended/Graphics/SpriteSheet.cs @@ -0,0 +1,92 @@ +// 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 System.Collections.Generic; + +namespace MonoGame.Extended.Graphics; + +/// +/// Represents a sprite sheet containing textures and animations. +/// +public class SpriteSheet +{ + private readonly Dictionary _animations = new Dictionary(); + + /// + /// Gets the number of animations defined in the sprite sheet. + /// + public int AnimationCount => _animations.Count; + + /// + /// Gets the name of the sprite sheet. + /// + public string Name { get; } + + /// + /// Gets the texture atlas associated with the sprite sheet. + /// + public Texture2DAtlas TextureAtlas { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the sprite sheet. + /// The texture atlas to use for the sprite sheet. + /// Thrown when the is null. + public SpriteSheet(string name, Texture2DAtlas textureAtlas) + { + ArgumentNullException.ThrowIfNull(textureAtlas); + + TextureAtlas = textureAtlas; + Name = name; + } + + /// + /// Creates a sprite from the specified region index. + /// + /// The index of the region in the texture atlas. + /// A new instance. + public Sprite CreateSprite(int regionIndex) => TextureAtlas.CreateSprite(regionIndex); + + /// + /// Creates a sprite from the specified region name. + /// + /// The name of the region in the texture atlas. + /// A new instance. + public Sprite CreateSprite(string regionName) => TextureAtlas.CreateSprite(regionName); + + /// + /// Creates an animated sprite from the specified animation name. + /// + /// The name of the animation. + /// A new instance. + public AnimatedSprite CreateAnimatedSprite(string animationName) + { + SpriteSheetAnimationDefinition animationDefinition = _animations[animationName]; + Texture2DRegion[] regions = TextureAtlas.GetRegions(animationDefinition.Frames); + return new AnimatedSprite(animationDefinition, regions); + } + + /// + /// Defines a new animation for the sprite sheet. + /// + /// The name of the animation. + /// The action to build the animation definition. + public void DefineAnimation(string name, Action buildAction) + { + SpriteSheetAnimationDefinitionBuilder builder = new SpriteSheetAnimationDefinitionBuilder(name, this); + buildAction(builder); + SpriteSheetAnimationDefinition definition = builder.Build(); + _animations.Add(name, definition); + } + + /// + /// Removes the animation definition with the specified name. + /// + /// The name of the animation to remove. + /// true if the animation was successfully removed; otherwise, false. + public bool RemoveAnimationDefinition(string name) => _animations.Remove(name); +} + diff --git a/source/MonoGame.Extended/Graphics/SpriteSheetAnimationDefinition.cs b/source/MonoGame.Extended/Graphics/SpriteSheetAnimationDefinition.cs new file mode 100644 index 00000000..2375401b --- /dev/null +++ b/source/MonoGame.Extended/Graphics/SpriteSheetAnimationDefinition.cs @@ -0,0 +1,29 @@ +// 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 MonoGame.Extended.Animations; + +namespace MonoGame.Extended.Graphics; + +internal class SpriteSheetAnimationDefinition : IAnimationDefinition +{ + private readonly SpriteSheetAnimationFrame[] _frames; + + public string Name { get; } + public ReadOnlySpan Frames => _frames; + public int FrameCount => _frames.Length; + public bool IsLooping { get; } + public bool IsReversed { get; } + public bool IsPingPong { get; } + + internal SpriteSheetAnimationDefinition(string name, SpriteSheetAnimationFrame[] frames, bool isLooping, bool isReversed, bool isPingPong) + { + Name = name; + IsLooping = isLooping; + IsReversed = isReversed; + IsPingPong = isPingPong; + _frames = frames; + } +} diff --git a/source/MonoGame.Extended/Graphics/SpriteSheetAnimationDefinitionBuilder.cs b/source/MonoGame.Extended/Graphics/SpriteSheetAnimationDefinitionBuilder.cs new file mode 100644 index 00000000..53ce1273 --- /dev/null +++ b/source/MonoGame.Extended/Graphics/SpriteSheetAnimationDefinitionBuilder.cs @@ -0,0 +1,88 @@ +// 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 System.Collections.Generic; + +namespace MonoGame.Extended.Graphics; + +/// +/// A builder class for creating instances. +/// +public sealed class SpriteSheetAnimationDefinitionBuilder +{ + private readonly string _name; + private readonly SpriteSheet _spriteSheet; + private readonly List _frames = new List(); + private bool _isLooping; + private bool _isReversed; + private bool _isPingPong; + + internal SpriteSheetAnimationDefinitionBuilder(string name, SpriteSheet spriteSheet) + { + _name = name; + _spriteSheet = spriteSheet; + } + + /// + /// Adds a frame to the animation using the region index and duration. + /// + /// The index of the region in the sprite sheet. + /// The duration of the frame. + /// The instance for chaining. + public SpriteSheetAnimationDefinitionBuilder AddFrame(int regionIndex, TimeSpan duration) + { + SpriteSheetAnimationFrame frame = new SpriteSheetAnimationFrame(regionIndex, duration); + _frames.Add(frame); + return this; + } + + /// + /// Adds a frame to the animation using the region name and duration. + /// + /// The name of the region in the sprite sheet. + /// The duration of the frame. + /// The instance for chaining. + public SpriteSheetAnimationDefinitionBuilder AddFrame(string regionName, TimeSpan duration) + { + int index = _spriteSheet.TextureAtlas.GetIndexOfRegion(regionName); + return AddFrame(index, duration); + } + + /// + /// Sets whether the animation should loop. + /// + /// If set to true, the animation will loop. + /// The instance for chaining. + public SpriteSheetAnimationDefinitionBuilder IsLooping(bool isLooping) + { + _isLooping = isLooping; + return this; + } + + /// + /// Sets whether the animation should be reversed. + /// + /// If set to true, the animation will play in reverse. + /// The instance for chaining. + public SpriteSheetAnimationDefinitionBuilder IsReversed(bool isReversed) + { + _isReversed = isReversed; + return this; + } + + /// + /// Sets whether the animation should ping-pong (reverse direction at the ends). + /// + /// If set to true, the animation will ping-pong. + /// The instance for chaining. + public SpriteSheetAnimationDefinitionBuilder IsPingPong(bool isPingPong) + { + _isPingPong = isPingPong; + return this; + } + + internal SpriteSheetAnimationDefinition Build() => + new SpriteSheetAnimationDefinition(_name, _frames.ToArray(), _isLooping, _isReversed, _isPingPong); +} diff --git a/source/MonoGame.Extended/Graphics/SpriteSheetAnimationFrame.cs b/source/MonoGame.Extended/Graphics/SpriteSheetAnimationFrame.cs new file mode 100644 index 00000000..db569504 --- /dev/null +++ b/source/MonoGame.Extended/Graphics/SpriteSheetAnimationFrame.cs @@ -0,0 +1,34 @@ +// 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 System.Diagnostics; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.Animations; + +namespace MonoGame.Extended.Graphics; + +/// +/// Represents a single frame within a sprite sheet animation, including its index, display duration, and texture +/// region. +/// +[DebuggerDisplay("{Index} {Duration}")] +internal class SpriteSheetAnimationFrame : IAnimationFrame +{ + /// + /// Gets the index of the frame in the overall sprite sheet. + /// + public int FrameIndex { get; } + + /// + /// Gets the total duration this frame should be displayed during an animation. + /// + public TimeSpan Duration { get; } + + internal SpriteSheetAnimationFrame(int index, TimeSpan duration) + { + FrameIndex = index; + Duration = duration; + } +} diff --git a/source/MonoGame.Extended/Graphics/Texture2DAtlas.cs b/source/MonoGame.Extended/Graphics/Texture2DAtlas.cs index da57a972..daf766ba 100644 --- a/source/MonoGame.Extended/Graphics/Texture2DAtlas.cs +++ b/source/MonoGame.Extended/Graphics/Texture2DAtlas.cs @@ -1,8 +1,13 @@ +// 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 System.Collections; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.Animations; namespace MonoGame.Extended.Graphics; @@ -110,21 +115,21 @@ public class Texture2DAtlas : IEnumerable /// The width, in pixels, of the region. /// The height, in pixels, of the region. /// The created texture region. - public Texture2DRegion CreateRegion(int x, int y, int width, int height) => CreateRegion(null, new Rectangle(x, y, width, height)); + public Texture2DRegion CreateRegion(int x, int y, int width, int height) => CreateRegion(new Rectangle(x, y, width, height), null); /// /// Creates a new texture region with the specified name and adds it to this atlas. /// - /// The name of the texture region. /// The x-coordinate of the region. /// The y-coordinate of the region. /// The width, in pixels, of the region. /// The height, in pixels, of the region. + /// The name of the texture region. /// The created texture region. /// /// Thrown if a region with the same name as the parameter already exists in this atlas. /// - public Texture2DRegion CreateRegion(string name, int x, int y, int width, int height) => CreateRegion(name, new Rectangle(x, y, width, height)); + public Texture2DRegion CreateRegion(int x, int y, int width, int height, string name) => CreateRegion(new Rectangle(x, y, width, height), name); /// /// Creates a new texture region and adds it to this atlas. @@ -132,39 +137,39 @@ public class Texture2DAtlas : IEnumerable /// The location of the region. /// The size, in pixels, of the region. /// The created texture region. - public Texture2DRegion CreateRegion(Point location, Size size) => CreateRegion(null, new Rectangle(location.X, location.Y, size.Width, size.Height)); + public Texture2DRegion CreateRegion(Point location, Size size) => CreateRegion(new Rectangle(location.X, location.Y, size.Width, size.Height), null); /// /// Creates a new texture region with the specified name and adds it to this atlas. /// - /// The name of the texture region. /// The location of the region. /// The size, in pixels, of the region. + /// The name of the texture region. /// The created texture region. /// /// Thrown if a region with the same name as the parameter already exists in this atlas. /// - public Texture2DRegion CreateRegion(string name, Point location, Size size) => CreateRegion(name, new Rectangle(location.X, location.Y, size.Width, size.Height)); + public Texture2DRegion CreateRegion(string name, Point location, Size size) => CreateRegion(new Rectangle(location.X, location.Y, size.Width, size.Height), name); /// /// Creates a new texture region and adds it to this atlas. /// /// The bounds of the region. /// The created texture region. - public Texture2DRegion CreateRegion(Rectangle bounds) => CreateRegion(null, bounds); + public Texture2DRegion CreateRegion(Rectangle bounds) => CreateRegion(bounds, null); /// /// Creates a new texture region with the specified name and adds it to this atlas. /// - /// The name of the texture region. /// The bounds of the region. + /// The name of the texture region. /// The created texture region. /// /// Thrown if a region with the same name as the parameter already exists in this atlas. /// - public Texture2DRegion CreateRegion(string name, Rectangle bounds) + public Texture2DRegion CreateRegion(Rectangle bounds, string name) { - Texture2DRegion region = new Texture2DRegion(Texture, name, bounds); + Texture2DRegion region = new Texture2DRegion(Texture, bounds, name); AddRegion(region); return region; } @@ -274,6 +279,17 @@ public class Texture2DAtlas : IEnumerable return regions; } + + internal Texture2DRegion[] GetRegions(ReadOnlySpan frames) + { + Texture2DRegion[] regions = new Texture2DRegion[frames.Length]; + for (int i = 0; i < frames.Length; i++) + { + regions[i] = GetRegion(frames[i].FrameIndex); + } + + return regions; + } /// /// Gets the regions with the specified names. @@ -354,6 +370,35 @@ public class Texture2DAtlas : IEnumerable _regionsByName.Add(region.Name, region); } + /// + /// Creates a new using the region from this atlas at the specified index. + /// + /// The index of the region to use. + /// The created using the region at the specified index. + /// + /// Throw if the value of the is less than zero or is greater than or equal to the total + /// number of regions in this atlas. + /// + public Sprite CreateSprite(int regionIndex) + { + Texture2DRegion region = GetRegion(regionIndex); + return new Sprite(region); + } + + /// + /// Creates a new using the region from this atlas with the specified name. + /// + /// The name of the region to use. + /// The created using the region with the specified name. + /// + /// Thrown if this atlas does not contain a region with a name that matches the parameter. + /// + public Sprite CreateSprite(string regionName) + { + Texture2DRegion region = GetRegion(regionName); + return new Sprite(region); + } + private bool RemoveRegion(Texture2DRegion region) => _regionsByIndex.Remove(region) && _regionsByName.Remove(region.Name); /// diff --git a/source/MonoGame.Extended/Graphics/Texture2DRegion.cs b/source/MonoGame.Extended/Graphics/Texture2DRegion.cs index 8d3e4939..a1212ae7 100644 --- a/source/MonoGame.Extended/Graphics/Texture2DRegion.cs +++ b/source/MonoGame.Extended/Graphics/Texture2DRegion.cs @@ -89,7 +89,7 @@ public class Texture2DRegion /// Thrown if has been disposed prior. /// public Texture2DRegion(Texture2D texture) - : this(texture, null, 0, 0, texture.Width, texture.Height) { } + : this(texture, 0, 0, texture.Width, texture.Height, null) { } /// /// Initializes a new instance of the class representing the entire texture with the @@ -104,7 +104,7 @@ public class Texture2DRegion /// Thrown if has been disposed prior. /// public Texture2DRegion(Texture2D texture, string name) - : this(texture, name, texture.Bounds.X, texture.Bounds.Y, texture.Bounds.Width, texture.Bounds.Height) { } + : this(texture, 0, 0, texture.Bounds.Width, texture.Bounds.Height, null) { } /// /// Initializes a new instance of the class with the specified region of the texture. @@ -118,7 +118,7 @@ public class Texture2DRegion /// Thrown if has been disposed prior. /// public Texture2DRegion(Texture2D texture, Rectangle region) - : this(texture, null, region.X, region.Y, region.Width, region.Height) { } + : this(texture, region.X, region.Y, region.Width, region.Height, null) { } /// /// Initializes a new instance of the class with the specified region of the texture. @@ -135,41 +135,41 @@ public class Texture2DRegion /// Thrown if has been disposed prior. /// public Texture2DRegion(Texture2D texture, int x, int y, int width, int height) - : this(texture, null, x, y, width, height) { } + : this(texture, x, y, width, height, null) { } /// /// Initializes a new instance of the class with the specified region of the texture and /// name. /// /// The texture to create the region from. - /// The name of the texture region. /// The region of the texture to use. + /// The name of the texture region. /// /// Thrown if is . /// /// /// Thrown if has been disposed prior. /// - public Texture2DRegion(Texture2D texture, string name, Rectangle region) - : this(texture, name, region.X, region.Y, region.Width, region.Height) { } + public Texture2DRegion(Texture2D texture, Rectangle region, string name) + : this(texture, region.X, region.Y, region.Width, region.Height, name) { } /// /// Initializes a new instance of the class with the specified region of the texture and /// name. /// /// The texture to create the region from. - /// The name of the texture region. /// The top-left x-coordinate of the region within the texture. /// The top-left y-coordinate of the region within the texture. /// The width, in pixels, of the region. /// The height, in pixels, of the region. + /// The name of the texture region. /// /// Thrown if is . /// /// /// Thrown if has been disposed prior. /// - public Texture2DRegion(Texture2D texture, string name, int x, int y, int width, int height) + public Texture2DRegion(Texture2D texture,int x, int y, int width, int height, string name) { ArgumentNullException.ThrowIfNull(texture); if (texture.IsDisposed) diff --git a/source/MonoGame.Extended/Graphics/TextureRegion.Extensions.cs b/source/MonoGame.Extended/Graphics/TextureRegion.Extensions.cs index ca4d9aa9..4b9a7752 100644 --- a/source/MonoGame.Extended/Graphics/TextureRegion.Extensions.cs +++ b/source/MonoGame.Extended/Graphics/TextureRegion.Extensions.cs @@ -25,14 +25,14 @@ public static class TextureRegionExtensions /// Thrown if source texture of the has been disposed prior. /// public static Texture2DRegion GetSubregion(this Texture2DRegion textureRegion, Rectangle region) => - textureRegion.GetSubregion(null, region.X, region.Y, region.Width, region.Height); + textureRegion.GetSubregion(region.X, region.Y, region.Width, region.Height, null); /// /// Gets a subregion of the specified texture region using the provided rectangle and name. /// /// The texture region to get the subregion from. - /// The name of the new subregion. /// The rectangle defining the subregion. + /// The name of the new subregion. /// A new representing the specified subregion. /// /// Thrown if is . @@ -41,7 +41,7 @@ public static class TextureRegionExtensions /// Thrown if source texture of the has been disposed prior. /// public static Texture2DRegion GetSubregion(this Texture2DRegion textureRegion, string name, Rectangle region) => - textureRegion.GetSubregion(name, region.X, region.Y, region.Width, region.Height); + textureRegion.GetSubregion(region.X, region.Y, region.Width, region.Height, name); /// /// Gets a subregion of the specified texture region using the provided coordinates and dimensions. @@ -59,17 +59,17 @@ public static class TextureRegionExtensions /// Thrown if source texture of the has been disposed prior. /// public static Texture2DRegion GetSubregion(this Texture2DRegion textureRegion, int x, int y, int width, int height) => - textureRegion.GetSubregion(null, x, y, width, height); + textureRegion.GetSubregion(x, y, width, height, null); /// /// Gets a subregion of the specified texture region using the provided name, coordinates, and dimensions. /// /// The texture region to get the subregion from. - /// The name of the new subregion. /// The top-left x-coordinate of the subregion within the texture region. /// The top-left y-coordinate of the subregion within the texture region. /// The width, in pixels, of the subregion. /// The height, in pixels, of the subregion. + /// The name of the new subregion. /// A new representing the specified subregion. /// /// Thrown if is . @@ -77,7 +77,7 @@ public static class TextureRegionExtensions /// /// Thrown if source texture of the has been disposed prior. /// - public static Texture2DRegion GetSubregion(this Texture2DRegion textureRegion, string name, int x, int y, int width, int height) + public static Texture2DRegion GetSubregion(this Texture2DRegion textureRegion, int x, int y, int width, int height, string name) { ArgumentNullException.ThrowIfNull(textureRegion); @@ -87,7 +87,7 @@ public static class TextureRegionExtensions } Rectangle region = textureRegion.Bounds.GetRelativeRectangle(x, y, width, height); - return new Texture2DRegion(textureRegion.Texture, name, region); + return new Texture2DRegion(textureRegion.Texture, region, name); } /// diff --git a/source/MonoGame.Extended/IUpdate.cs b/source/MonoGame.Extended/IUpdate.cs deleted file mode 100644 index 6240e047..00000000 --- a/source/MonoGame.Extended/IUpdate.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.Xna.Framework; - -namespace MonoGame.Extended -{ - public interface IUpdate - { - void Update(GameTime gameTime); - } -} \ No newline at end of file diff --git a/source/MonoGame.Extended/Sprites/AnimatedSprite.cs b/source/MonoGame.Extended/Sprites/AnimatedSprite.cs deleted file mode 100644 index a6bf3935..00000000 --- a/source/MonoGame.Extended/Sprites/AnimatedSprite.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Linq; -using Microsoft.Xna.Framework; - -namespace MonoGame.Extended.Sprites -{ - public class AnimatedSprite : Sprite - { - private readonly SpriteSheet _spriteSheet; - private SpriteSheetAnimation _currentAnimation; - - public AnimatedSprite(SpriteSheet spriteSheet, string playAnimation = null) - : base(spriteSheet.TextureAtlas[0]) - { - _spriteSheet = spriteSheet; - - if (playAnimation != null) - Play(playAnimation); - } - - public SpriteSheetAnimation Play(string name, Action onCompleted = null) - { - if (_currentAnimation == null || _currentAnimation.IsComplete || _currentAnimation.Name != name) - { - var cycle = _spriteSheet.Cycles[name]; - var keyFrames = cycle.Frames.Select(f => _spriteSheet.TextureAtlas[f.Index]).ToArray(); - _currentAnimation = new SpriteSheetAnimation(name, keyFrames, cycle.FrameDuration, cycle.IsLooping, cycle.IsReversed, cycle.IsPingPong); - - if(_currentAnimation != null) - _currentAnimation.OnCompleted = onCompleted; - } - - return _currentAnimation; - } - - public void Update(float deltaTime) - { - if (_currentAnimation != null && !_currentAnimation.IsComplete) - { - _currentAnimation.Update(deltaTime); - TextureRegion = _currentAnimation.CurrentFrame; - } - } - - public void Update(GameTime gameTime) - { - Update(gameTime.GetElapsedSeconds()); - } - } -} \ No newline at end of file diff --git a/source/MonoGame.Extended/Sprites/Animation.cs b/source/MonoGame.Extended/Sprites/Animation.cs deleted file mode 100644 index cd6e092a..00000000 --- a/source/MonoGame.Extended/Sprites/Animation.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using Microsoft.Xna.Framework; - -namespace MonoGame.Extended.Sprites -{ - public abstract class Animation : IUpdate, IDisposable - { - private readonly bool _disposeOnComplete; - private readonly Action _onCompleteAction; - private bool _isComplete; - - protected Animation(Action onCompleteAction, bool disposeOnComplete) - { - _onCompleteAction = onCompleteAction; - _disposeOnComplete = disposeOnComplete; - IsPaused = false; - } - - public bool IsComplete - { - get { return _isComplete; } - protected set - { - if (_isComplete != value) - { - _isComplete = value; - - if (_isComplete) - { - _onCompleteAction?.Invoke(); - - if (_disposeOnComplete) - Dispose(); - } - } - } - } - - public bool IsDisposed { get; private set; } - public bool IsPlaying => !IsPaused && !IsComplete; - public bool IsPaused { get; private set; } - public float CurrentTime { get; protected set; } - - public virtual void Dispose() - { - IsDisposed = true; - } - - public void Update(GameTime gameTime) - { - Update(gameTime.GetElapsedSeconds()); - } - - public void Play() - { - IsPaused = false; - } - - public void Pause() - { - IsPaused = true; - } - - public void Stop() - { - Pause(); - Rewind(); - } - - public void Rewind() - { - CurrentTime = 0; - } - - protected abstract bool OnUpdate(float deltaTime); - - public void Update(float deltaTime) - { - if (!IsPlaying) - return; - - CurrentTime += deltaTime; - IsComplete = OnUpdate(deltaTime); - } - } -} \ No newline at end of file diff --git a/source/MonoGame.Extended/Sprites/ISpriteBatchDrawable.cs b/source/MonoGame.Extended/Sprites/ISpriteBatchDrawable.cs deleted file mode 100644 index eedcf6fd..00000000 --- a/source/MonoGame.Extended/Sprites/ISpriteBatchDrawable.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using MonoGame.Extended.Graphics; - -namespace MonoGame.Extended.Sprites -{ - public interface ISpriteBatchDrawable - { - bool IsVisible { get; } - Texture2DRegion TextureRegion { get; } - Vector2 Position { get; } - float Rotation { get; } - Vector2 Scale { get; } - Color Color { get; } - Vector2 Origin { get; } - SpriteEffects Effect { get; } - } -} \ No newline at end of file diff --git a/source/MonoGame.Extended/Sprites/Sprite.cs b/source/MonoGame.Extended/Sprites/Sprite.cs deleted file mode 100644 index 84247a77..00000000 --- a/source/MonoGame.Extended/Sprites/Sprite.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Linq; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using MonoGame.Extended.Graphics; - -namespace MonoGame.Extended.Sprites -{ - public class Sprite : IColorable - { - private Texture2DRegion _textureRegion; - - public Sprite(Texture2DRegion textureRegion) - { - if (textureRegion == null) throw new ArgumentNullException(nameof(textureRegion)); - - _textureRegion = textureRegion; - - Alpha = 1.0f; - Color = Color.White; - IsVisible = true; - Effect = SpriteEffects.None; - OriginNormalized = new Vector2(0.5f, 0.5f); - Depth = 0.0f; - } - - public Sprite(Texture2D texture) - : this(new Texture2DRegion(texture)) - { - } - - public float Alpha { get; set; } - public float Depth { get; set; } - public object Tag { get; set; } - - public Vector2 OriginNormalized - { - get => new Vector2(Origin.X/TextureRegion.Width, Origin.Y/TextureRegion.Height); - set => Origin = new Vector2(value.X*TextureRegion.Width, value.Y*TextureRegion.Height); - } - - public Color Color { get; set; } - - public RectangleF GetBoundingRectangle(Transform2 transform) - { - return GetBoundingRectangle(transform.Position, transform.Rotation, transform.Scale); - } - - public RectangleF GetBoundingRectangle(Vector2 position, float rotation, Vector2 scale) - { - var corners = GetCorners(position, rotation, scale); - var min = new Vector2(corners.Min(i => i.X), corners.Min(i => i.Y)); - var max = new Vector2(corners.Max(i => i.X), corners.Max(i => i.Y)); - return new RectangleF(min.X, min.Y, max.X - min.X, max.Y - min.Y); - } - - public bool IsVisible { get; set; } - public Vector2 Origin { get; set; } - public SpriteEffects Effect { get; set; } - - public Texture2DRegion TextureRegion - { - get => _textureRegion; - set - { - if (value == null) - throw new InvalidOperationException("TextureRegion cannot be null"); - - // preserve the origin if the texture size changes - var originNormalized = OriginNormalized; - _textureRegion = value; - OriginNormalized = originNormalized; - } - } - - public Vector2[] GetCorners(Vector2 position, float rotation, Vector2 scale) - { - var min = -Origin; - var max = min + new Vector2(TextureRegion.Width, TextureRegion.Height); - var offset = position; - - if (scale != Vector2.One) - { - min *= scale; - max = max * scale; - } - - var corners = new Vector2[4]; - corners[0] = min; - corners[1] = new Vector2(max.X, min.Y); - corners[2] = max; - corners[3] = new Vector2(min.X, max.Y); - - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (rotation != 0) - { - var matrix = Matrix.CreateRotationZ(rotation); - - for (var i = 0; i < 4; i++) - corners[i] = Vector2.Transform(corners[i], matrix); - } - - for (var i = 0; i < 4; i++) - corners[i] += offset; - - return corners; - } - } -} diff --git a/source/MonoGame.Extended/Sprites/SpriteExtensions.cs b/source/MonoGame.Extended/Sprites/SpriteExtensions.cs deleted file mode 100644 index 983065b4..00000000 --- a/source/MonoGame.Extended/Sprites/SpriteExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; - -namespace MonoGame.Extended.Sprites -{ - public static class SpriteExtensions - { - public static void Draw(this Sprite sprite, SpriteBatch spriteBatch, Vector2 position, float rotation, Vector2 scale) - { - Draw(spriteBatch, sprite, position, rotation, scale); - } - - public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Transform2 transform) - { - Draw(spriteBatch, sprite, transform.Position, transform.Rotation, transform.Scale); - } - - public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Vector2 position, float rotation = 0) - { - Draw(spriteBatch, sprite, position, rotation, Vector2.One); - } - - public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Vector2 position, float rotation, Vector2 scale) - { - if (sprite == null) throw new ArgumentNullException(nameof(sprite)); - - if (sprite.IsVisible) - { - var texture = sprite.TextureRegion.Texture; - var sourceRectangle = sprite.TextureRegion.Bounds; - spriteBatch.Draw(texture, position, sourceRectangle, sprite.Color*sprite.Alpha, rotation, sprite.Origin, scale, sprite.Effect, sprite.Depth); - } - } - } -} diff --git a/source/MonoGame.Extended/Sprites/SpriteSheet.cs b/source/MonoGame.Extended/Sprites/SpriteSheet.cs deleted file mode 100644 index b3f22e95..00000000 --- a/source/MonoGame.Extended/Sprites/SpriteSheet.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using MonoGame.Extended.Graphics; -using MonoGame.Extended.TextureAtlases; - -namespace MonoGame.Extended.Sprites -{ - public class SpriteSheet - { - public SpriteSheet() - { - Cycles = new Dictionary(); - } - - public Texture2DAtlas TextureAtlas { get; set; } - public Dictionary Cycles { get; set; } - - public SpriteSheetAnimation CreateAnimation(string name) - { - var cycle = Cycles[name]; - var keyFrames = cycle.Frames - .Select(f => TextureAtlas[f.Index]) - .ToArray(); - - return new SpriteSheetAnimation(name, keyFrames, cycle.FrameDuration, cycle.IsLooping, cycle.IsReversed, cycle.IsPingPong); - } - } -} diff --git a/source/MonoGame.Extended/Sprites/SpriteSheetAnimation.cs b/source/MonoGame.Extended/Sprites/SpriteSheetAnimation.cs deleted file mode 100644 index 4d39db60..00000000 --- a/source/MonoGame.Extended/Sprites/SpriteSheetAnimation.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Linq; -using MonoGame.Extended.Graphics; - -namespace MonoGame.Extended.Sprites -{ - public class SpriteSheetAnimation : Animation - { - public const float DefaultFrameDuration = 0.2f; - - public SpriteSheetAnimation(string name, Texture2DAtlas textureAtlas, float frameDuration = DefaultFrameDuration, - bool isLooping = true, bool isReversed = false, bool isPingPong = false) - : this(name, textureAtlas.ToArray(), frameDuration, isLooping, isReversed, isPingPong) - { - } - - public SpriteSheetAnimation(string name, Texture2DRegion[] keyFrames, float frameDuration = DefaultFrameDuration, - bool isLooping = true, bool isReversed = false, bool isPingPong = false) - : base(null, false) - { - Name = name; - KeyFrames = keyFrames; - FrameDuration = frameDuration; - IsLooping = isLooping; - IsReversed = isReversed; - IsPingPong = isPingPong; - CurrentFrameIndex = IsReversed ? KeyFrames.Length - 1 : 0; - } - - public SpriteSheetAnimation(string name, Texture2DRegion[] keyFrames, SpriteSheetAnimationData data) - : this(name, keyFrames, data.FrameDuration, data.IsLooping, data.IsReversed, data.IsPingPong) - { - } - - public string Name { get; } - public Texture2DRegion[] KeyFrames { get; } - public float FrameDuration { get; set; } - public bool IsLooping { get; set; } - public bool IsReversed { get; set; } - public bool IsPingPong { get; set; } - public new bool IsComplete => CurrentTime >= AnimationDuration; - - public float AnimationDuration => IsPingPong - ? (KeyFrames.Length*2 - (IsLooping ? 2 : 1))*FrameDuration - : KeyFrames.Length*FrameDuration; - - public Texture2DRegion CurrentFrame => KeyFrames[CurrentFrameIndex]; - public int CurrentFrameIndex { get; private set; } - - public float FramesPerSecond - { - get => 1.0f/FrameDuration; - set => FrameDuration = value/1.0f; - } - - public Action OnCompleted { get; set; } - - protected override bool OnUpdate(float deltaTime) - { - if (IsComplete) - { - if (IsLooping) - CurrentTime %= AnimationDuration; - else - OnCompleted?.Invoke(); - } - - if (KeyFrames.Length == 1) - { - CurrentFrameIndex = 0; - return IsComplete; - } - - var frameIndex = (int) (CurrentTime/FrameDuration); - var length = KeyFrames.Length; - - if (IsPingPong) - { - if (IsComplete) - frameIndex = 0; - else - { - frameIndex = frameIndex % (length * 2 - 2); - - if (frameIndex >= length) - frameIndex = length - 2 - (frameIndex - length); - } - } - - if (IsLooping) - { - if (IsReversed) - { - frameIndex = frameIndex%length; - frameIndex = length - frameIndex - 1; - } - else - frameIndex = frameIndex%length; - } - else - frameIndex = IsReversed ? Math.Max(length - frameIndex - 1, 0) : Math.Min(length - 1, frameIndex); - - CurrentFrameIndex = frameIndex; - return IsComplete; - } - } -} diff --git a/source/MonoGame.Extended/Sprites/SpriteSheetAnimationCycle.cs b/source/MonoGame.Extended/Sprites/SpriteSheetAnimationCycle.cs deleted file mode 100644 index d9e86740..00000000 --- a/source/MonoGame.Extended/Sprites/SpriteSheetAnimationCycle.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; - -namespace MonoGame.Extended.Sprites -{ - public class SpriteSheetAnimationCycle - { - public SpriteSheetAnimationCycle() - { - Frames = new List(); - } - - public float FrameDuration { get; set; } = 0.2f; - public List Frames { get; set; } - public bool IsLooping { get; set; } - public bool IsReversed { get; set; } - public bool IsPingPong { get; set; } - } -} \ No newline at end of file diff --git a/source/MonoGame.Extended/Sprites/SpriteSheetAnimationData.cs b/source/MonoGame.Extended/Sprites/SpriteSheetAnimationData.cs deleted file mode 100644 index 0bdf89d9..00000000 --- a/source/MonoGame.Extended/Sprites/SpriteSheetAnimationData.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace MonoGame.Extended.Sprites -{ - public class SpriteSheetAnimationData - { - public SpriteSheetAnimationData(int[] frameIndicies, float frameDuration = 0.2f, bool isLooping = true, - bool isReversed = false, bool isPingPong = false) - { - FrameIndicies = frameIndicies; - FrameDuration = frameDuration; - IsLooping = isLooping; - IsReversed = isReversed; - IsPingPong = isPingPong; - } - - public int[] FrameIndicies { get; } - public float FrameDuration { get; } - public bool IsLooping { get; } - public bool IsReversed { get; } - public bool IsPingPong { get; } - } -} \ No newline at end of file diff --git a/source/MonoGame.Extended/Sprites/SpriteSheetAnimationFrame.cs b/source/MonoGame.Extended/Sprites/SpriteSheetAnimationFrame.cs deleted file mode 100644 index 2de8e39b..00000000 --- a/source/MonoGame.Extended/Sprites/SpriteSheetAnimationFrame.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Diagnostics; -using System.Text.Json; -using System.Text.Json.Serialization; - - -namespace MonoGame.Extended.Sprites -{ - [JsonConverter(typeof(SpriteSheetAnimationFrameJsonConverter))] - [DebuggerDisplay("{Index} {Duration}")] - public class SpriteSheetAnimationFrame - { - public SpriteSheetAnimationFrame(int index, float duration = 0.2f) - { - Index = index; - Duration = duration; - } - - public int Index { get; set; } - public float Duration { get; set; } - } - - public class SpriteSheetAnimationFrameJsonConverter : JsonConverter - { - /// - public override SpriteSheetAnimationFrame Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - switch (reader.TokenType) - { - case JsonTokenType.Number: - var index = reader.GetInt32(); - return new SpriteSheetAnimationFrame(index); - - case JsonTokenType.StartObject: - var frame = JsonSerializer.Deserialize(ref reader, options); - return frame; - - case JsonTokenType.Null: - return null; - - default: - throw new JsonException(); - } - } - - public override void Write(Utf8JsonWriter writer, SpriteSheetAnimationFrame value, JsonSerializerOptions options) - { - ArgumentNullException.ThrowIfNull(writer); - JsonSerializer.Serialize(writer, value, options); - } - } -} diff --git a/source/MonoGame.Extended/Timers/GameTimer.cs b/source/MonoGame.Extended/Timers/GameTimer.cs index 6a3acfe0..9fc6bcc2 100644 --- a/source/MonoGame.Extended/Timers/GameTimer.cs +++ b/source/MonoGame.Extended/Timers/GameTimer.cs @@ -3,8 +3,46 @@ using Microsoft.Xna.Framework; namespace MonoGame.Extended.Timers { - public abstract class GameTimer : IUpdate + public abstract class GameTimer : IUpdateable { + private bool _enabled; + private int _updateOrder; + /// + public bool Enabled + { + get => _enabled; + set + { + if (_enabled == value) + { + return; + } + _enabled = value; + EnabledChanged?.Invoke(this, EventArgs.Empty); + } + } + + /// + public int UpdateOrder + { + get => _updateOrder; + set + { + if (_updateOrder == value) + { + return; + } + _updateOrder = value; + EnabledChanged?.Invoke(this, EventArgs.Empty); + } + } + + /// + public event EventHandler EnabledChanged; + + /// + public event EventHandler UpdateOrderChanged; + protected GameTimer(double intervalSeconds) : this(TimeSpan.FromSeconds(intervalSeconds)) { @@ -20,6 +58,7 @@ namespace MonoGame.Extended.Timers public TimeSpan CurrentTime { get; protected set; } public TimerState State { get; protected set; } + public void Update(GameTime gameTime) { if (State != TimerState.Started) @@ -33,6 +72,7 @@ namespace MonoGame.Extended.Timers public event EventHandler Stopped; public event EventHandler Paused; + public void Start() { State = TimerState.Started; @@ -62,4 +102,4 @@ namespace MonoGame.Extended.Timers protected abstract void OnStopped(); protected abstract void OnUpdate(GameTime gameTime); } -} \ No newline at end of file +} diff --git a/tests/MonoGame.Extended.Entities.Tests/AspectBuilderTests.cs b/tests/MonoGame.Extended.Entities.Tests/AspectBuilderTests.cs index 1b3e9710..c24d1d80 100644 --- a/tests/MonoGame.Extended.Entities.Tests/AspectBuilderTests.cs +++ b/tests/MonoGame.Extended.Entities.Tests/AspectBuilderTests.cs @@ -1,6 +1,6 @@ using System; using Microsoft.Xna.Framework.Graphics; -using MonoGame.Extended.Sprites; +using MonoGame.Extended.Graphics; using Xunit; namespace MonoGame.Extended.Entities.Tests diff --git a/tests/MonoGame.Extended.Entities.Tests/AspectTests.cs b/tests/MonoGame.Extended.Entities.Tests/AspectTests.cs index b06780ba..805da9d2 100644 --- a/tests/MonoGame.Extended.Entities.Tests/AspectTests.cs +++ b/tests/MonoGame.Extended.Entities.Tests/AspectTests.cs @@ -1,5 +1,5 @@ using System.Collections.Specialized; -using MonoGame.Extended.Sprites; +using MonoGame.Extended.Graphics; using Xunit; namespace MonoGame.Extended.Entities.Tests diff --git a/tests/MonoGame.Extended.Entities.Tests/ComponentManagerTests.cs b/tests/MonoGame.Extended.Entities.Tests/ComponentManagerTests.cs index abf1a16d..4343cfa4 100644 --- a/tests/MonoGame.Extended.Entities.Tests/ComponentManagerTests.cs +++ b/tests/MonoGame.Extended.Entities.Tests/ComponentManagerTests.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using MonoGame.Extended.Sprites; +using MonoGame.Extended.Graphics; using Xunit; namespace MonoGame.Extended.Entities.Tests diff --git a/tests/MonoGame.Extended.Entities.Tests/ComponentTypeTests.cs b/tests/MonoGame.Extended.Entities.Tests/ComponentTypeTests.cs index 89538041..9b3fef94 100644 --- a/tests/MonoGame.Extended.Entities.Tests/ComponentTypeTests.cs +++ b/tests/MonoGame.Extended.Entities.Tests/ComponentTypeTests.cs @@ -1,7 +1,4 @@ -using MonoGame.Extended.Sprites; -using Xunit; - -namespace MonoGame.Extended.Entities.Tests +namespace MonoGame.Extended.Entities.Tests { //public class ComponentTypeTests //{ @@ -17,4 +14,4 @@ namespace MonoGame.Extended.Entities.Tests // Assert.Equal(componentType.Id, componentType.GetHashCode()); // } //} -} \ No newline at end of file +} diff --git a/tests/MonoGame.Extended.Entities.Tests/WorldManagerTests.cs b/tests/MonoGame.Extended.Entities.Tests/WorldManagerTests.cs index 7620f1e8..7e53b5e6 100644 --- a/tests/MonoGame.Extended.Entities.Tests/WorldManagerTests.cs +++ b/tests/MonoGame.Extended.Entities.Tests/WorldManagerTests.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using Microsoft.Xna.Framework; using MonoGame.Extended.Entities.Systems; -using MonoGame.Extended.Sprites; -using Xunit; namespace MonoGame.Extended.Entities.Tests; @@ -42,8 +40,8 @@ public class WorldManagerTests private class DummySystem : EntitySystem, IUpdateSystem, IDrawSystem { - public List AddedEntitiesId { get; } = new (); - public List RemovedEntitiesId { get; } = new (); + public List AddedEntitiesId { get; } = new(); + public List RemovedEntitiesId { get; } = new(); public DummySystem() : base(Aspect.All(typeof(DummyComponent))) { } diff --git a/tests/MonoGame.Extended.Tests/Animations/AnimationTests.cs b/tests/MonoGame.Extended.Tests/Animations/AnimationTests.cs new file mode 100644 index 00000000..8dfcb905 --- /dev/null +++ b/tests/MonoGame.Extended.Tests/Animations/AnimationTests.cs @@ -0,0 +1,220 @@ +// 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; +using Xunit; +using MonoGame.Extended.Animations; + +namespace MonoGame.Extended.Tests.Animations; + + +public class AnimationTests +{ + private class TestAnimationFrame : IAnimationFrame + { + public int FrameIndex { get; set; } + public TimeSpan Duration { get; set; } + } + + private class TestAnimationDefinition : IAnimationDefinition + { + private readonly IAnimationFrame[] _frames; + public string Name { get; set; } + public ReadOnlySpan Frames => _frames; + public int FrameCount { get; set; } + public bool IsLooping { get; set; } + public bool IsReversed { get; set; } + public bool IsPingPong { get; set; } + + public TestAnimationDefinition(IAnimationFrame[] frames) => _frames = frames; + } + + private readonly IAnimationDefinition _definition; + private readonly Animation _animation; + + public AnimationTests() + { + var frames = new IAnimationFrame[] + { + new TestAnimationFrame { FrameIndex = 0, Duration = TimeSpan.FromSeconds(1) }, + new TestAnimationFrame { FrameIndex = 1, Duration = TimeSpan.FromSeconds(1) } + }; + _definition = new TestAnimationDefinition(frames) + { + FrameCount = frames.Length, + IsLooping = false, + IsReversed = false, + IsPingPong = false + }; + _animation = new Animation(_definition); + } + + [Fact] + public void Play_ShouldStartAnimation() + { + var result = _animation.Play(); + + Assert.True(result); + Assert.True(_animation.IsAnimating); + Assert.Equal(0, _animation.CurrentFrame); + } + + [Fact] + public void Play_ShouldThrowException_ForInvalidFrame() + { + Assert.Throws(() => _animation.Play(-1)); + Assert.Throws(() => _animation.Play(10)); + } + + [Fact] + public void Pause_ShouldPauseAnimation() + { + _animation.Play(); + var result = _animation.Pause(); + + Assert.True(result); + Assert.True(_animation.IsPaused); + } + + [Fact] + public void Pause_ShouldResetFrameDuration_WhenSpecified() + { + _animation.Play(); + _animation.Update(new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(0.5))); + + var result = _animation.Pause(true); + + Assert.True(result); + Assert.Equal(_definition.Frames[0].Duration, _animation.CurrentFrameTimeRemaining); + } + + [Fact] + public void UnPause_ShouldResumeAnimation() + { + _animation.Play(); + _animation.Pause(); + var result = _animation.Unpause(); + + Assert.True(result); + Assert.False(_animation.IsPaused); + } + + [Fact] + public void UnPause_ShouldAdvanceFrame_WhenSpecified() + { + _animation.Play(); + _animation.Pause(); + var result = _animation.Unpause(true); + + Assert.True(result); + Assert.Equal(1, _animation.CurrentFrame); + } + + [Fact] + public void Stop_ShouldStopAnimation() + { + _animation.Play(); + var result = _animation.Stop(); + + Assert.True(result); + Assert.False(_animation.IsAnimating); + } + + [Fact] + public void Update_ShouldAdvanceFrame() + { + _animation.Play(); + _animation.Update(new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1.1))); + + Assert.Equal(1, _animation.CurrentFrame); + } + + [Fact] + public void Update_ShouldTriggerAnimationEvents() + { + bool frameBeginTriggered = false; + bool frameEndTriggered = false; + bool animationLoopTriggered = false; + bool animationCompletedTriggered = false; + + _animation.OnAnimationEvent += (anim, trigger) => + { + switch (trigger) + { + case AnimationEventTrigger.FrameBegin: + frameBeginTriggered = true; + break; + case AnimationEventTrigger.FrameEnd: + frameEndTriggered = true; + break; + case AnimationEventTrigger.AnimationLoop: + animationLoopTriggered = true; + break; + case AnimationEventTrigger.AnimationCompleted: + animationCompletedTriggered = true; + break; + } + }; + + _animation.Play(); + _animation.Update(new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1.1))); + + Assert.True(frameBeginTriggered); + Assert.True(frameEndTriggered); + + _animation.IsLooping = true; + _animation.Update(new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1.1))); + Assert.True(animationLoopTriggered); + + _animation.IsLooping = false; + _animation.Update(new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(2))); + Assert.True(animationCompletedTriggered); + } + + [Fact] + public void Update_ShouldLoopAnimation() + { + _animation.IsLooping = true; + _animation.Play(); + _animation.Update(new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(2.1))); + + Assert.Equal(0, _animation.CurrentFrame); + } + + [Fact] + public void Reset_ShouldResetAnimation() + { + _animation.Play(); + _animation.Reset(); + + Assert.False(_animation.IsAnimating); + Assert.True(_animation.IsPaused); + Assert.Equal(0, _animation.CurrentFrame); + } + + [Fact] + public void SetFrame_ShouldChangeCurrentFrame() + { + _animation.SetFrame(1); + + Assert.Equal(1, _animation.CurrentFrame); + Assert.Equal(_definition.Frames[1].Duration, _animation.CurrentFrameTimeRemaining); + } + + [Fact] + public void SetFrame_ShouldThrowException_ForInvalidFrame() + { + Assert.Throws(() => _animation.SetFrame(-1)); + Assert.Throws(() => _animation.SetFrame(10)); + } + + [Fact] + public void Dispose_ShouldDisposeAnimation() + { + _animation.Dispose(); + + Assert.True(_animation.IsDisposed); + } +} diff --git a/tests/MonoGame.Extended.Tests/MonoGame.Extended.Tests.csproj b/tests/MonoGame.Extended.Tests/MonoGame.Extended.Tests.csproj index c12f9d2c..c82747f7 100644 --- a/tests/MonoGame.Extended.Tests/MonoGame.Extended.Tests.csproj +++ b/tests/MonoGame.Extended.Tests/MonoGame.Extended.Tests.csproj @@ -1,5 +1,11 @@  + + + + + + diff --git a/tests/MonoGame.Extended.Tests/Sprites/SpriteSheetAnimationTests.cs b/tests/MonoGame.Extended.Tests/Sprites/SpriteSheetAnimationTests.cs deleted file mode 100644 index 7a9bc372..00000000 --- a/tests/MonoGame.Extended.Tests/Sprites/SpriteSheetAnimationTests.cs +++ /dev/null @@ -1,911 +0,0 @@ -using System; -using Microsoft.Xna.Framework; -using MonoGame.Extended.Graphics; -using MonoGame.Extended.Sprites; -using MonoGame.Extended.TextureAtlases; -using Xunit; - -namespace MonoGame.Extended.Tests.Sprites -{ - public class SpriteSheetAnimationTests - { - [Theory] - [InlineData(0, 0)] - [InlineData(0, 0.9f)] - [InlineData(1, 1f)] - [InlineData(1, 1.9f)] - [InlineData(0, 2f)] - [InlineData(0, 2.9f)] - [InlineData(1, 3f)] - [InlineData(0, 4f)] - [InlineData(1, 5f)] - public void Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete(int expectedTextureRegionIndex, float time) - { - var textureRegion2D1 = new Texture2DRegion("Region 1", new Rectangle()); - var textureRegion2D2 = new Texture2DRegion("Region 2", new Rectangle()); - - var textureRegions = new[] { textureRegion2D1, textureRegion2D2 }; - - var spriteSheetAnimationData = new SpriteSheetAnimationData( - new[] { 0, 1 }, - 1, - true - ); - - var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData); - var isCompleteFired = false; - spriteSheetAnimation.OnCompleted += () => isCompleteFired = true; - - spriteSheetAnimation.Play(); - var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time)); - - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - } - - [Fact] - public void Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete_Over_Multiple_Updates() - { - var textureRegion2D1 = new Texture2DRegion("Region 1", new Rectangle()); - var textureRegion2D2 = new Texture2DRegion("Region 2", new Rectangle()); - - var textureRegions = new[] { textureRegion2D1, textureRegion2D2 }; - - var spriteSheetAnimationData = new SpriteSheetAnimationData( - new[] { 0, 1 }, - 1, - true - ); - - var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData); - var isCompleteFired = false; - spriteSheetAnimation.OnCompleted += () => isCompleteFired = true; - - spriteSheetAnimation.Play(); - var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(0)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - } - - [Theory] - [InlineData(0, 0.9f)] - [InlineData(1, 1f)] - [InlineData(1, 1.1f)] - [InlineData(1, 1.9f)] - public void Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete(int expectedTextureRegionIndex, float time) - { - var textureRegion2D1 = new Texture2DRegion("Region 1", new Rectangle()); - var textureRegion2D2 = new Texture2DRegion("Region 2", new Rectangle()); - - var textureRegions = new[] { textureRegion2D1, textureRegion2D2 }; - - var spriteSheetAnimationData = new SpriteSheetAnimationData( - new[] { 0, 1 }, - 1, - false - ); - - var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData); - var isCompleteFired = false; - spriteSheetAnimation.OnCompleted += () => isCompleteFired = true; - - spriteSheetAnimation.Play(); - var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time)); - - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - } - - [Theory] - [InlineData(1, 2f)] - [InlineData(1, 3f)] - [InlineData(1, 4f)] - public void Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Complete_When_AnimationDuration_Is_Reached(int expectedTextureRegionIndex, float time) - { - var textureRegion2D1 = new Texture2DRegion("Region 1", new Rectangle()); - var textureRegion2D2 = new Texture2DRegion("Region 2", new Rectangle()); - - var textureRegions = new[] { textureRegion2D1, textureRegion2D2 }; - - var spriteSheetAnimationData = new SpriteSheetAnimationData( - new[] { 0, 1 }, - 1, - false - ); - - var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData); - var isCompleteFired = false; - spriteSheetAnimation.OnCompleted += () => isCompleteFired = true; - - spriteSheetAnimation.Play(); - var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time)); - - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame); - Assert.True(spriteSheetAnimation.IsComplete); - Assert.True(isCompleteFired); - } - - [Fact] - public void Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Complete_When_AnimationDuration_Is_Reached_Over_Multiple_Updates() - { - var textureRegion2D1 = new Texture2DRegion("Region 1", new Rectangle()); - var textureRegion2D2 = new Texture2DRegion("Region 2", new Rectangle()); - - var textureRegions = new[] { textureRegion2D1, textureRegion2D2 }; - - var spriteSheetAnimationData = new SpriteSheetAnimationData( - new[] { 0, 1 }, - 1, - false - ); - - var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData); - var isCompleteFired = false; - spriteSheetAnimation.OnCompleted += () => isCompleteFired = true; - - spriteSheetAnimation.Play(); - - var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(0)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame); - Assert.True(spriteSheetAnimation.IsComplete); - Assert.True(isCompleteFired); - - isCompleteFired = false; // Reset isCompleteFired for next execution - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame); - Assert.True(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); // Event is not fired again as animation was already completed - } - - [Theory] - [InlineData(1, 0)] - [InlineData(1, 0.9f)] - [InlineData(0, 1f)] - [InlineData(0, 1.9f)] - [InlineData(1, 2f)] - [InlineData(1, 2.9f)] - [InlineData(0, 3f)] - [InlineData(1, 4f)] - [InlineData(0, 5f)] - public void Reversed_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete(int expectedTextureRegionIndex, float time) - { - var textureRegion2D1 = new Texture2DRegion("Region 1", new Rectangle()); - var textureRegion2D2 = new Texture2DRegion("Region 2", new Rectangle()); - - var textureRegions = new[] { textureRegion2D1, textureRegion2D2 }; - - var spriteSheetAnimationData = new SpriteSheetAnimationData( - new[] { 0, 1 }, - 1, - true, - true - ); - - var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData); - var isCompleteFired = false; - spriteSheetAnimation.OnCompleted += () => isCompleteFired = true; - - spriteSheetAnimation.Play(); - var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time)); - - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - } - - [Fact] - public void Reversed_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete_Over_Multiple_Updates() - { - var textureRegion2D1 = new Texture2DRegion("Region 1", new Rectangle()); - var textureRegion2D2 = new Texture2DRegion("Region 2", new Rectangle()); - - var textureRegions = new[] { textureRegion2D1, textureRegion2D2 }; - - var spriteSheetAnimationData = new SpriteSheetAnimationData( - new[] { 0, 1 }, - 1, - true, - true - ); - - var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData); - var isCompleteFired = false; - spriteSheetAnimation.OnCompleted += () => isCompleteFired = true; - - spriteSheetAnimation.Play(); - var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(0)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - } - - [Theory] - [InlineData(1, 0.9f)] - [InlineData(0, 1f)] - [InlineData(0, 1.1f)] - [InlineData(0, 1.9f)] - public void Reversed_Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete(int expectedTextureRegionIndex, float time) - { - var textureRegion2D1 = new Texture2DRegion("Region 1", new Rectangle()); - var textureRegion2D2 = new Texture2DRegion("Region 2", new Rectangle()); - - var textureRegions = new[] { textureRegion2D1, textureRegion2D2 }; - - var spriteSheetAnimationData = new SpriteSheetAnimationData( - new[] { 0, 1 }, - 1, - false, - true - ); - - var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData); - var isCompleteFired = false; - spriteSheetAnimation.OnCompleted += () => isCompleteFired = true; - - spriteSheetAnimation.Play(); - var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time)); - - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - } - - [Theory] - [InlineData(0, 2f)] - [InlineData(0, 3f)] - [InlineData(0, 4f)] - public void Reversed_Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Complete_When_AnimationDuration_Is_Reached(int expectedTextureRegionIndex, float time) - { - var textureRegion2D1 = new Texture2DRegion("Region 1", new Rectangle()); - var textureRegion2D2 = new Texture2DRegion("Region 2", new Rectangle()); - - var textureRegions = new[] { textureRegion2D1, textureRegion2D2 }; - - var spriteSheetAnimationData = new SpriteSheetAnimationData( - new[] { 0, 1 }, - 1, - false, - true - ); - - var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData); - var isCompleteFired = false; - spriteSheetAnimation.OnCompleted += () => isCompleteFired = true; - - spriteSheetAnimation.Play(); - var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time)); - - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame); - Assert.True(spriteSheetAnimation.IsComplete); - Assert.True(isCompleteFired); - } - - [Fact] - public void Reversed_Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Complete_When_AnimationDuration_Is_Reached_Over_Multiple_Updates() - { - var textureRegion2D1 = new Texture2DRegion("Region 1", new Rectangle()); - var textureRegion2D2 = new Texture2DRegion("Region 2", new Rectangle()); - - var textureRegions = new[] { textureRegion2D1, textureRegion2D2 }; - - var spriteSheetAnimationData = new SpriteSheetAnimationData( - new[] { 0, 1 }, - 1, - false, - true - ); - - var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData); - var isCompleteFired = false; - spriteSheetAnimation.OnCompleted += () => isCompleteFired = true; - - spriteSheetAnimation.Play(); - - var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(0)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame); - Assert.True(spriteSheetAnimation.IsComplete); - Assert.True(isCompleteFired); - - isCompleteFired = false; // Reset isCompleteFired for next execution - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame); - Assert.True(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); // Event is not fired again as animation was already completed; - } - - [Theory] - [InlineData(0, 0)] - [InlineData(0, 0.9f)] - [InlineData(1, 1f)] - [InlineData(1, 1.9f)] - [InlineData(0, 2f)] - [InlineData(0, 2.9f)] - [InlineData(1, 3f)] - [InlineData(0, 4f)] - [InlineData(1, 5f)] - [InlineData(0, 6f)] - [InlineData(1, 7f)] - [InlineData(0, 8f)] - public void PingPong_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete(int expectedTextureRegionIndex, float time) - { - var textureRegion2D1 = new Texture2DRegion("Region 1", new Rectangle()); - var textureRegion2D2 = new Texture2DRegion("Region 2", new Rectangle()); - - var textureRegions = new[] { textureRegion2D1, textureRegion2D2 }; - - var spriteSheetAnimationData = new SpriteSheetAnimationData( - new[] { 0, 1 }, - 1, - true, - false, - true - ); - - var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData); - var isCompleteFired = false; - spriteSheetAnimation.OnCompleted += () => isCompleteFired = true; - - spriteSheetAnimation.Play(); - var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time)); - - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - } - - [Fact] - public void PingPong_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete_Over_Multiple_Updates() - { - var textureRegion2D1 = new Texture2DRegion("Region 1", new Rectangle()); - var textureRegion2D2 = new Texture2DRegion("Region 2", new Rectangle()); - - var textureRegions = new[] { textureRegion2D1, textureRegion2D2 }; - - var spriteSheetAnimationData = new SpriteSheetAnimationData( - new[] { 0, 1 }, - 1, - true, - false, - true - ); - - var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData); - var isCompleteFired = false; - spriteSheetAnimation.OnCompleted += () => isCompleteFired = true; - - spriteSheetAnimation.Play(); - var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(0)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - } - - [Theory] - [InlineData(0, 0.9f)] - [InlineData(1, 1f)] - [InlineData(1, 1.9f)] - [InlineData(0, 2f)] - [InlineData(0, 2.9f)] - public void PingPong_Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete(int expectedTextureRegionIndex, float time) - { - var textureRegion2D1 = new Texture2DRegion("Region 1", new Rectangle()); - var textureRegion2D2 = new Texture2DRegion("Region 2", new Rectangle()); - - var textureRegions = new[] { textureRegion2D1, textureRegion2D2 }; - - var spriteSheetAnimationData = new SpriteSheetAnimationData( - new[] { 0, 1 }, - 1, - false, - false, - true - ); - - var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData); - var isCompleteFired = false; - spriteSheetAnimation.OnCompleted += () => isCompleteFired = true; - - spriteSheetAnimation.Play(); - var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time)); - - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - } - - [Theory] - [InlineData(0, 3f)] - [InlineData(0, 4f)] - [InlineData(0, 5f)] - public void PingPong_Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Complete_When_AnimationDuration_Is_Reached(int expectedTextureRegionIndex, float time) - { - var textureRegion2D1 = new Texture2DRegion("Region 1", new Rectangle()); - var textureRegion2D2 = new Texture2DRegion("Region 2", new Rectangle()); - - var textureRegions = new[] { textureRegion2D1, textureRegion2D2 }; - - var spriteSheetAnimationData = new SpriteSheetAnimationData( - new[] { 0, 1 }, - 1, - false, - false, - true - ); - - var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData); - var isCompleteFired = false; - spriteSheetAnimation.OnCompleted += () => isCompleteFired = true; - - spriteSheetAnimation.Play(); - var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time)); - - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame); - Assert.True(spriteSheetAnimation.IsComplete); - Assert.True(isCompleteFired); - } - - [Fact] - public void PingPong_Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Complete_When_AnimationDuration_Is_Reached_Over_Multiple_Updates() - { - var textureRegion2D1 = new Texture2DRegion("Region 1", new Rectangle()); - var textureRegion2D2 = new Texture2DRegion("Region 2", new Rectangle()); - var textureRegion2D3 = new Texture2DRegion("Region 3", new Rectangle()); - - var textureRegions = new[] { textureRegion2D1, textureRegion2D2, textureRegion2D3 }; - - var spriteSheetAnimationData = new SpriteSheetAnimationData( - new[] { 0, 1, 2 }, - 1, - false, - false, - true - ); - - var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData); - - var isCompleteFired = false; - spriteSheetAnimation.OnCompleted += () => isCompleteFired = true; - - spriteSheetAnimation.Play(); - - var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(0)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[2], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame); - Assert.True(spriteSheetAnimation.IsComplete); - Assert.True(isCompleteFired); - - isCompleteFired = false; // Reset isCompleteFired for next execution - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame); - Assert.True(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); // Event is not fired again as animation was already completed - } - - [Theory] - [InlineData(1, 0)] - [InlineData(1, 0.9f)] - [InlineData(0, 1f)] - [InlineData(0, 1.9f)] - [InlineData(1, 2f)] - [InlineData(1, 2.9f)] - [InlineData(0, 3f)] - [InlineData(1, 4f)] - [InlineData(0, 5f)] - [InlineData(1, 6f)] - [InlineData(0, 7f)] - [InlineData(1, 8f)] - public void Reversed_PingPong_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete(int expectedTextureRegionIndex, float time) - { - var textureRegion2D1 = new Texture2DRegion("Region 1", new Rectangle()); - var textureRegion2D2 = new Texture2DRegion("Region 2", new Rectangle()); - - var textureRegions = new[] { textureRegion2D1, textureRegion2D2 }; - - var spriteSheetAnimationData = new SpriteSheetAnimationData( - new[] { 0, 1 }, - 1, - true, - true, - true - ); - - var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData); - var isCompleteFired = false; - spriteSheetAnimation.OnCompleted += () => isCompleteFired = true; - - spriteSheetAnimation.Play(); - var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time)); - - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - } - - [Fact] - public void Reversed_PingPong_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete_Over_Multiple_Updates() - { - var textureRegion2D1 = new Texture2DRegion("Region 1", new Rectangle()); - var textureRegion2D2 = new Texture2DRegion("Region 2", new Rectangle()); - - var textureRegions = new[] { textureRegion2D1, textureRegion2D2 }; - - var spriteSheetAnimationData = new SpriteSheetAnimationData( - new[] { 0, 1 }, - 1, - true, - true, - true - ); - - var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData); - var isCompleteFired = false; - spriteSheetAnimation.OnCompleted += () => isCompleteFired = true; - - spriteSheetAnimation.Play(); - var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(0)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - } - - [Theory] - [InlineData(1, 0.9f)] - [InlineData(0, 1f)] - [InlineData(0, 1.9f)] - [InlineData(1, 2f)] - [InlineData(1, 2.9f)] - public void Reversed_PingPong_Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete(int expectedTextureRegionIndex, float time) - { - var textureRegion2D1 = new Texture2DRegion("Region 1", new Rectangle()); - var textureRegion2D2 = new Texture2DRegion("Region 2", new Rectangle()); - - var textureRegions = new[] { textureRegion2D1, textureRegion2D2 }; - - var spriteSheetAnimationData = new SpriteSheetAnimationData( - new[] { 0, 1 }, - 1, - false, - true, - true - ); - - var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData); - var isCompleteFired = false; - spriteSheetAnimation.OnCompleted += () => isCompleteFired = true; - - spriteSheetAnimation.Play(); - var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time)); - - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - } - - [Theory] - [InlineData(1, 3f)] - [InlineData(1, 4f)] - [InlineData(1, 5f)] - public void Reversed_PingPong_Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Complete_When_AnimationDuration_Is_Reached(int expectedTextureRegionIndex, float time) - { - var textureRegion2D1 = new Texture2DRegion("Region 1", new Rectangle()); - var textureRegion2D2 = new Texture2DRegion("Region 2", new Rectangle()); - - var textureRegions = new[] { textureRegion2D1, textureRegion2D2 }; - - var spriteSheetAnimationData = new SpriteSheetAnimationData( - new[] { 0, 1 }, - 1, - false, - true, - true - ); - - var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData); - var isCompleteFired = false; - spriteSheetAnimation.OnCompleted += () => isCompleteFired = true; - - spriteSheetAnimation.Play(); - var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time)); - - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame); - Assert.True(spriteSheetAnimation.IsComplete); - Assert.True(isCompleteFired); - } - - [Fact] - public void Reversed_PingPong_Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Complete_When_AnimationDuration_Is_Reached_Over_Multiple_Updates() - { - var textureRegion2D1 = new Texture2DRegion("Region 1", new Rectangle()); - var textureRegion2D2 = new Texture2DRegion("Region 2", new Rectangle()); - var textureRegion2D3 = new Texture2DRegion("Region 3", new Rectangle()); - - var textureRegions = new[] { textureRegion2D1, textureRegion2D2, textureRegion2D3 }; - - var spriteSheetAnimationData = new SpriteSheetAnimationData( - new[] { 0, 1, 2 }, - 1, - false, - true, - true - ); - - var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData); - - var isCompleteFired = false; - spriteSheetAnimation.OnCompleted += () => isCompleteFired = true; - - spriteSheetAnimation.Play(); - - var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(0)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[2], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[2], spriteSheetAnimation.CurrentFrame); - Assert.False(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); - - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[2], spriteSheetAnimation.CurrentFrame); - Assert.True(spriteSheetAnimation.IsComplete); - Assert.True(isCompleteFired); - - isCompleteFired = false; // Reset isCompleteFired for next execution - gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1)); - spriteSheetAnimation.Update(gameTime); - - Assert.Equal(textureRegions[2], spriteSheetAnimation.CurrentFrame); - Assert.True(spriteSheetAnimation.IsComplete); - Assert.False(isCompleteFired); // Event is not fired again as animation was already completed - } - } -} diff --git a/tests/MonoGame.Extended.Tests/Sprites/SpriteTests.cs b/tests/MonoGame.Extended.Tests/Sprites/SpriteTests.cs deleted file mode 100644 index 307e2727..00000000 --- a/tests/MonoGame.Extended.Tests/Sprites/SpriteTests.cs +++ /dev/null @@ -1,91 +0,0 @@ -//using Microsoft.Xna.Framework; -//using Microsoft.Xna.Framework.Graphics; -//using MonoGame.Extended.Sprites; -//using MonoGame.Extended.TextureAtlases; -//using NSubstitute; -//using Xunit; - -//namespace MonoGame.Extended.Tests.Sprites -//{ -// -// public class SpriteTests -// { -// [Fact] -// public void Sprite_BoundingRectangleAfterPosition_Test() -// { -// var graphicsDevice = TestHelper.CreateGraphicsDevice(); -// var texture = Substitute.For(graphicsDevice, 50, 200); -// var sprite = new Sprite(texture); - -// Assert.Equal(new RectangleF(375, 140, 50, 200), sprite.GetBoundingRectangle(new Vector2(400, 240), 0, Vector2.One)); -// } - -// [Fact] -// public void Sprite_BoundingRectangleAfterOrigin_Test() -// { -// var graphicsDevice = TestHelper.CreateGraphicsDevice(); -// var texture = Substitute.For(graphicsDevice, 50, 200); -// var sprite = new Sprite(texture) { OriginNormalized = new Vector2(1.0f, 1.0f) }; - -// Assert.Equal(new RectangleF(-50, -200, 50, 200), sprite.GetBoundingRectangle(Vector2.Zero, 0, Vector2.One)); -// } - -// [Fact] -// public void Sprite_BoundingRectangleAfterScale_Test() -// { -// var graphicsDevice = TestHelper.CreateGraphicsDevice(); -// var texture = Substitute.For(graphicsDevice, 50, 200); -// var sprite = new Sprite(texture); - -// Assert.Equal(new RectangleF(-50, -200, 100, 400), sprite.GetBoundingRectangle(Vector2.Zero, 0, Vector2.One * 2.0f)); -// } - -// [Fact] -// public void Sprite_BoundingRectangleAfterRotation_Test() -// { -// var graphicsDevice = TestHelper.CreateGraphicsDevice(); -// var texture = Substitute.For(graphicsDevice, 50, 200); -// var sprite = new Sprite(texture); - -// AssertExtensions.AreApproximatelyEqual(new RectangleF(-100, -25, 200, 50), sprite.GetBoundingRectangle(Vector2.Zero, MathHelper.ToRadians(90), Vector2.One * 2.0f)); -// } - -// [Fact] -// public void Sprite_TextureRegionIsFullTextureWhenTextureConstructorIsUsed_Test() -// { -// var graphicsDevice = TestHelper.CreateGraphicsDevice(); -// var texture = Substitute.For(graphicsDevice, 100, 200); -// var sprite = new Sprite(texture); - -// Assert.Equal(new Rectangle(0, 0, 100, 200), sprite.TextureRegion.Bounds); -// } - -// [Fact] -// public void Sprite_DefaultOriginIsCentre_Test() -// { -// var graphicsDevice = TestHelper.CreateGraphicsDevice(); -// var texture = Substitute.For(graphicsDevice, 100, 200); -// var sprite = new Sprite(texture); - -// Assert.Equal(new Vector2(0.5f, 0.5f), sprite.OriginNormalized); -// Assert.Equal(new Vector2(50, 100), sprite.Origin); -// } - -// [Fact] -// public void Sprite_PreserveNormalizedOriginWhenTextureRegionChanges_Test() -// { -// var graphicsDevice = TestHelper.CreateGraphicsDevice(); -// var texture = Substitute.For(graphicsDevice, 100, 100); -// var textureRegion = new TextureRegion2D(texture, 10, 20, 30, 40); -// var sprite = new Sprite(textureRegion); - -// Assert.Equal(new Vector2(0.5f, 0.5f), sprite.OriginNormalized); -// Assert.Equal(new Vector2(15, 20), sprite.Origin); - -// sprite.TextureRegion = new TextureRegion2D(texture, 30, 40, 50, 60); - -// Assert.Equal(new Vector2(0.5f, 0.5f), sprite.OriginNormalized); -// Assert.Equal(new Vector2(25, 30), sprite.Origin); -// } -// } -//} diff --git a/tests/MonoGame.Extended.Tests/TestGraphicsDevice.cs b/tests/MonoGame.Extended.Tests/TestGraphicsDevice.cs deleted file mode 100644 index 8a8741b1..00000000 --- a/tests/MonoGame.Extended.Tests/TestGraphicsDevice.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.Xna.Framework.Graphics; - -namespace MonoGame.Extended.Tests -{ - public class TestGraphicsDevice : GraphicsDevice - { - public TestGraphicsDevice() - : base(GraphicsAdapter.DefaultAdapter, GraphicsProfile.Reach, new PresentationParameters()) - { - } - } -} \ No newline at end of file diff --git a/tests/MonoGame.Extended.Tests/TestHelper.cs b/tests/MonoGame.Extended.Tests/TestHelper.cs deleted file mode 100644 index b6a02bb5..00000000 --- a/tests/MonoGame.Extended.Tests/TestHelper.cs +++ /dev/null @@ -1,27 +0,0 @@ -//using Microsoft.Xna.Framework; -//using Microsoft.Xna.Framework.Graphics; -//using Xunit; - -//namespace MonoGame.Extended.Tests -//{ -// public static class TestHelper -// { -// public static void AreEqual(Vector3 a, Vector3 b, double delta) -// { -// Assert.Equal(a.X, b.X, delta); -// Assert.Equal(a.Y, b.Y, delta); -// Assert.Equal(a.Z, b.Z, delta); -// } - -// public static GraphicsDevice CreateGraphicsDevice() -// { -// return new GraphicsDevice( -// GraphicsAdapter.DefaultAdapter, -// GraphicsProfile.HiDef, -// new PresentationParameters()) -// { -// Viewport = new Viewport(0, 0, 800, 480) -// }; -// } -// } -//} \ No newline at end of file diff --git a/tests/MonoGame.Extended.Tests/TextureAtlases/TextureAtlasTests.cs b/tests/MonoGame.Extended.Tests/TextureAtlases/TextureAtlasTests.cs deleted file mode 100644 index 13d9dfc0..00000000 --- a/tests/MonoGame.Extended.Tests/TextureAtlases/TextureAtlasTests.cs +++ /dev/null @@ -1,236 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Xna.Framework.Graphics; -using MonoGame.Extended.TextureAtlases; -using Xunit; - -namespace MonoGame.Extended.Tests.TextureAtlases -{ - //public class TextureAtlasTests : IDisposable - //{ - // private readonly TestGame _game; - - // public TextureAtlasTests() - // { - // _game = new TestGame(); - // } - - // public void Dispose() - // { - // _game.Dispose(); - // } - - // [Fact] - // public void TextureAtlas_CreateRegion_Test() - // { - // using (var texture = new Texture2D(_game.GraphicsDevice, 100, 200)) - // { - // var atlas = new TextureAtlas(null, texture); - - // var region = atlas.CreateRegion("region0", 10, 20, 30, 40); - - // Assert.Same(texture, region.Texture); - // Assert.Equal(10, region.X); - // Assert.Equal(20, region.Y); - // Assert.Equal(30, region.Width); - // Assert.Equal(40, region.Height); - // } - // } - - // [Fact] - // public void TextureAtlas_GetRegionsByIndex_Test() - // { - // using (var texture = new Texture2D(_game.GraphicsDevice, 100, 200)) - // { - // var atlas = new TextureAtlas(null, texture); - - // var region0 = atlas.CreateRegion("region0", 10, 20, 30, 40); - // var region1 = atlas.CreateRegion("region1", 50, 60, 35, 45); - - // Assert.Same(region0, atlas[0]); - // Assert.Same(region1, atlas[1]); - // Assert.Same(region0, atlas.GetRegion(0)); - // Assert.Same(region1, atlas.GetRegion(1)); - // } - // } - - - // [Fact] - // public void TextureAtlas_GetRegionsByName_Test() - // { - // using (var texture = new Texture2D(_game.GraphicsDevice, 100, 200)) - // { - // var atlas = new TextureAtlas(null, texture); - - // var region0 = atlas.CreateRegion("region0", 10, 20, 30, 40); - // var region1 = atlas.CreateRegion("region1", 50, 60, 35, 45); - - // Assert.Same(region0, atlas["region0"]); - // Assert.Same(region1, atlas["region1"]); - // Assert.Same(region0, atlas.GetRegion("region0")); - // Assert.Same(region1, atlas.GetRegion("region1")); - // } - // } - - // [Fact] - // public void TextureAtlas_RemoveRegions_Test() - // { - // using (var texture = new Texture2D(_game.GraphicsDevice, 100, 200)) - // { - // var atlas = new TextureAtlas(null, texture); - - // var region0 = atlas.CreateRegion("region0", 10, 20, 30, 40); - // var region1 = atlas.CreateRegion("region1", 50, 60, 35, 45); - // var region2 = atlas.CreateRegion("region2", 32, 33, 34, 35); - - // Assert.Same(texture, atlas.Texture); - // Assert.Equal(3, atlas.RegionCount); - // Assert.Equal(atlas.RegionCount, atlas.Regions.Count()); - // Assert.Same(region1, atlas[1]); - - // atlas.RemoveRegion(1); - - // Assert.Equal(2, atlas.Regions.Count()); - // Assert.Same(region0, atlas[0]); - // Assert.Same(region2, atlas[1]); - - // atlas.RemoveRegion("region0"); - - // Assert.Single(atlas.Regions); - // Assert.Same(region2, atlas[0]); - // } - // } - - // [Fact] - // public void TextureAtlas_CreateRegionThatAlreadyExistsThrowsException_Test() - // { - // using (var texture = new Texture2D(_game.GraphicsDevice, 100, 200)) - // { - // var atlas = new TextureAtlas(null, texture); - - // atlas.CreateRegion("region0", 10, 20, 30, 40); - // Assert.Throws(() => atlas.CreateRegion("region0", 50, 60, 35, 45)); - // } - // } - - // [Fact] - // public void TextureAtlas_GetRegion_InvalidIndexThrowsException_Test() - // { - // using (var texture = new Texture2D(_game.GraphicsDevice, 100, 200)) - // { - // var atlas = new TextureAtlas(null, texture); - - // atlas.CreateRegion("region0", 10, 20, 30, 40); - // Assert.Throws(() => atlas.GetRegion(-1)); - // } - // } - - // [Fact] - // public void TextureAtlas_GetRegion_InvalidNameThrowsException_Test() - // { - // using (var texture = new Texture2D(_game.GraphicsDevice, 100, 200)) - // { - // var atlas = new TextureAtlas(null, texture); - - // atlas.CreateRegion("region0", 10, 20, 30, 40); - // Assert.Throws(() => atlas.GetRegion("region1")); - // } - // } - - // [Fact] - // public void TextureAtlas_EnumerateRegions_Test() - // { - // using (var texture = new Texture2D(_game.GraphicsDevice, 100, 200)) - // { - // var atlas = new TextureAtlas(null, texture); - - // var regions = new TextureRegion2D[3]; - // regions[0] = atlas.CreateRegion("region0", 10, 20, 30, 40); - // regions[1] = atlas.CreateRegion("region1", 50, 60, 35, 45); - // regions[2] = atlas.CreateRegion("region2", 32, 33, 34, 35); - // var index = 0; - - // foreach (var region in atlas) - // { - // Assert.Same(region, regions[index]); - // index++; - // } - // } - // } - - // [Fact] - // public void TextureAtlas_Create_WithDefaultParameters_Test() - // { - // using (var texture = new Texture2D(_game.GraphicsDevice, 50, 100) {Name = "testTexture"}) - // { - // var atlas = TextureAtlas.Create(null, texture, 25, 50); - - // Assert.Equal(4, atlas.RegionCount); - // Assert.True(atlas.Regions.All(i => i.Width == 25)); - // Assert.True(atlas.Regions.All(i => i.Height == 50)); - // Assert.True(atlas.Regions.All(i => ReferenceEquals(i.Texture, texture))); - // Assert.True(atlas.Regions.All(i => i.Name.StartsWith(texture.Name))); - // } - // } - - // [Fact] - // public void TextureAtlas_Create_WithMaxRegionCount_Test() - // { - // using (var texture = new Texture2D(_game.GraphicsDevice, 64, 64)) - // { - // var atlas = TextureAtlas.Create(null, texture, 32, 32, maxRegionCount: 3); - - // Assert.Equal(3, atlas.RegionCount); - // } - // } - - // [Fact] - // public void TextureAtlas_Create_WithMargin_Test() - // { - // using (var texture = new Texture2D(_game.GraphicsDevice, 24, 24)) - // { - // var atlas = TextureAtlas.Create(null, texture, 10, 10, margin: 2); - - // Assert.Equal(4, atlas.RegionCount); - // Assert.True(atlas.Regions.All(i => i.Width == 10 && i.Height == 10)); - // Assert.Equal(2, atlas[0].X); - // Assert.Equal(2, atlas[0].Y); - // Assert.Equal(12, atlas[3].X); - // Assert.Equal(12, atlas[3].Y); - // } - // } - - // [Fact] - // public void TextureAtlas_Create_WithSpacing_Test() - // { - // using (var texture = new Texture2D(_game.GraphicsDevice, 24, 24)) - // { - // var atlas = TextureAtlas.Create(null, texture, 10, 10, spacing: 2); - - // Assert.Equal(4, atlas.RegionCount); - // Assert.True(atlas.Regions.All(i => i.Width == 10 && i.Height == 10)); - // Assert.Equal(0, atlas[0].X); - // Assert.Equal(0, atlas[0].Y); - // Assert.Equal(12, atlas[3].X); - // Assert.Equal(12, atlas[3].Y); - // } - // } - - // [Fact] - // public void TextureAtlas_Create_WithMarginAndSpacing_Test() - // { - // using (var texture = new Texture2D(_game.GraphicsDevice, 28, 28)) - // { - // var atlas = TextureAtlas.Create(null, texture, 10, 10, margin: 3, spacing: 2); - - // Assert.Equal(4, atlas.RegionCount); - // Assert.True(atlas.Regions.All(i => i.Width == 10 && i.Height == 10)); - // Assert.Equal(3, atlas[0].X); - // Assert.Equal(3, atlas[0].Y); - // Assert.Equal(15, atlas[3].X); - // Assert.Equal(15, atlas[3].Y); - // } - // } - //} -} diff --git a/tests/MonoGame.Extended.Tests/TextureAtlases/TextureRegion2DTests.cs b/tests/MonoGame.Extended.Tests/TextureAtlases/TextureRegion2DTests.cs deleted file mode 100644 index 1274b100..00000000 --- a/tests/MonoGame.Extended.Tests/TextureAtlases/TextureRegion2DTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -//using Microsoft.Xna.Framework.Graphics; -//using MonoGame.Extended.TextureAtlases; -//using Xunit; - -//namespace MonoGame.Extended.Tests.TextureAtlases -//{ -// -// public class TextureRegion2DTests -// { -// [Fact] -// public void TextureRegion2D_FromTexture_Test() -// { -// var graphicsDevice = TestHelper.CreateGraphicsDevice(); -// var texture = new Texture2D(graphicsDevice, 100, 200); -// var textureRegion = new TextureRegion2D(texture); - -// Assert.AreSame(texture, textureRegion.Texture); -// Assert.Equal(0, textureRegion.X); -// Assert.Equal(0, textureRegion.Y); -// Assert.Equal(100, textureRegion.Width); -// Assert.Equal(200, textureRegion.Height); -// Assert.IsNull(textureRegion.Tag); -// } - -// [Fact] -// public void TextureRegion2D_Specified_Test() -// { -// var graphicsDevice = TestHelper.CreateGraphicsDevice(); -// var texture = new Texture2D(graphicsDevice, 100, 200); -// var textureRegion = new TextureRegion2D(texture, 10, 20, 30, 40); - -// Assert.AreSame(texture, textureRegion.Texture); -// Assert.Equal(10, textureRegion.X); -// Assert.Equal(20, textureRegion.Y); -// Assert.Equal(30, textureRegion.Width); -// Assert.Equal(40, textureRegion.Height); -// Assert.IsNull(textureRegion.Tag); -// } -// } -//} \ No newline at end of file