From 8e2be47e4ca7bb521f1bc984dd33a456f46ae874 Mon Sep 17 00:00:00 2001 From: David Fidge Date: Fri, 31 Mar 2023 14:08:14 +1030 Subject: [PATCH] Fix looping animations incorrectly completing when an update contains a large elapsed time. Ping pong waits until frame delay is complete on re-entry of the first frame before firing Complete rather than firing Complete immediately upon entering the frame. This makes it consistent with a non ping pong animation which waits for the frame delay of the last frame before it fires Complete. Ping Pong now finishes on the first frame rather than potentially finishing on a different frame if the elapsed time since last update was larger than the time remaining for the animation to become complete. Add unit tests for SpriteSheetAnimation. --- .../Sprites/SpriteSheetAnimation.cs | 19 +- .../Sprites/SpriteSheetAnimationTests.cs | 910 ++++++++++++++++++ 2 files changed, 922 insertions(+), 7 deletions(-) create mode 100644 src/cs/Tests/MonoGame.Extended.Tests/Sprites/SpriteSheetAnimationTests.cs diff --git a/src/cs/MonoGame.Extended/Sprites/SpriteSheetAnimation.cs b/src/cs/MonoGame.Extended/Sprites/SpriteSheetAnimation.cs index 3ff3a563..3b60e5e4 100644 --- a/src/cs/MonoGame.Extended/Sprites/SpriteSheetAnimation.cs +++ b/src/cs/MonoGame.Extended/Sprites/SpriteSheetAnimation.cs @@ -41,7 +41,7 @@ namespace MonoGame.Extended.Sprites public new bool IsComplete => CurrentTime >= AnimationDuration; public float AnimationDuration => IsPingPong - ? (KeyFrames.Length*2 - 2)*FrameDuration + ? (KeyFrames.Length*2 - (IsLooping ? 2 : 1))*FrameDuration : KeyFrames.Length*FrameDuration; public TextureRegion2D CurrentFrame => KeyFrames[CurrentFrameIndex]; @@ -59,10 +59,10 @@ namespace MonoGame.Extended.Sprites { if (IsComplete) { - OnCompleted?.Invoke(); - if (IsLooping) - CurrentTime -= AnimationDuration; + CurrentTime %= AnimationDuration; + else + OnCompleted?.Invoke(); } if (KeyFrames.Length == 1) @@ -76,10 +76,15 @@ namespace MonoGame.Extended.Sprites if (IsPingPong) { - frameIndex = frameIndex%(length*2 - 2); + if (IsComplete) + frameIndex = 0; + else + { + frameIndex = frameIndex % (length * 2 - 2); - if (frameIndex >= length) - frameIndex = length - 2 - (frameIndex - length); + if (frameIndex >= length) + frameIndex = length - 2 - (frameIndex - length); + } } if (IsLooping) diff --git a/src/cs/Tests/MonoGame.Extended.Tests/Sprites/SpriteSheetAnimationTests.cs b/src/cs/Tests/MonoGame.Extended.Tests/Sprites/SpriteSheetAnimationTests.cs new file mode 100644 index 00000000..35ad3e3d --- /dev/null +++ b/src/cs/Tests/MonoGame.Extended.Tests/Sprites/SpriteSheetAnimationTests.cs @@ -0,0 +1,910 @@ +using System; +using Microsoft.Xna.Framework; +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 TextureRegion2D("Region 1", null, new Rectangle()); + var textureRegion2D2 = new TextureRegion2D("Region 2", null, 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 TextureRegion2D("Region 1", null, new Rectangle()); + var textureRegion2D2 = new TextureRegion2D("Region 2", null, 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 TextureRegion2D("Region 1", null, new Rectangle()); + var textureRegion2D2 = new TextureRegion2D("Region 2", null, 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 TextureRegion2D("Region 1", null, new Rectangle()); + var textureRegion2D2 = new TextureRegion2D("Region 2", null, 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 TextureRegion2D("Region 1", null, new Rectangle()); + var textureRegion2D2 = new TextureRegion2D("Region 2", null, 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 TextureRegion2D("Region 1", null, new Rectangle()); + var textureRegion2D2 = new TextureRegion2D("Region 2", null, 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 TextureRegion2D("Region 1", null, new Rectangle()); + var textureRegion2D2 = new TextureRegion2D("Region 2", null, 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 TextureRegion2D("Region 1", null, new Rectangle()); + var textureRegion2D2 = new TextureRegion2D("Region 2", null, 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 TextureRegion2D("Region 1", null, new Rectangle()); + var textureRegion2D2 = new TextureRegion2D("Region 2", null, 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 TextureRegion2D("Region 1", null, new Rectangle()); + var textureRegion2D2 = new TextureRegion2D("Region 2", null, 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 TextureRegion2D("Region 1", null, new Rectangle()); + var textureRegion2D2 = new TextureRegion2D("Region 2", null, 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 TextureRegion2D("Region 1", null, new Rectangle()); + var textureRegion2D2 = new TextureRegion2D("Region 2", null, 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 TextureRegion2D("Region 1", null, new Rectangle()); + var textureRegion2D2 = new TextureRegion2D("Region 2", null, 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 TextureRegion2D("Region 1", null, new Rectangle()); + var textureRegion2D2 = new TextureRegion2D("Region 2", null, 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 TextureRegion2D("Region 1", null, new Rectangle()); + var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle()); + var textureRegion2D3 = new TextureRegion2D("Region 3", null, 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 TextureRegion2D("Region 1", null, new Rectangle()); + var textureRegion2D2 = new TextureRegion2D("Region 2", null, 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 TextureRegion2D("Region 1", null, new Rectangle()); + var textureRegion2D2 = new TextureRegion2D("Region 2", null, 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 TextureRegion2D("Region 1", null, new Rectangle()); + var textureRegion2D2 = new TextureRegion2D("Region 2", null, 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 TextureRegion2D("Region 1", null, new Rectangle()); + var textureRegion2D2 = new TextureRegion2D("Region 2", null, 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 TextureRegion2D("Region 1", null, new Rectangle()); + var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle()); + var textureRegion2D3 = new TextureRegion2D("Region 3", null, 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 + } + } +}