From c632330e2e9438e3f66691991ca888fe58178c35 Mon Sep 17 00:00:00 2001 From: Natalie Martin Date: Tue, 11 Jun 2024 19:58:57 -0500 Subject: [PATCH] Basic Sprite Animation Create a system for animating sprites by manipulating their SpriteClip components based on frame data. Replace the bespoke rendering systems for the Ship and Wallop in the GracilisGame with the new Sprite and SpriteAnimation systems. Should AnimationFrames be a resource that is managed by the engine itself? --- assets/wallop.png | Bin 0 -> 3384 bytes src/game/components/PlayerShip.h | 3 +- src/game/components/ShipRenderer.h | 12 -- src/game/components/SpriteAnimation.h | 22 +++ src/game/components/WallopRenderer.h | 12 -- src/game/states/GracilisGame.cpp | 34 +++- src/game/states/GracilisGame.h | 3 + src/game/systems/AnimateSprite.cpp | 31 ++++ src/game/systems/AnimateSprite.h | 10 ++ src/game/systems/ShipRendererSystem.cpp | 32 ---- src/game/systems/ShipRendererSystem.h | 11 -- src/game/systems/WallopRendererSystem.cpp | 29 ---- src/game/systems/WallopRendererSystem.h | 11 -- src/game/systems/ship.cpp | 13 +- test/game/EcsTestingHarness.cpp | 5 + test/game/EcsTestingHarness.h | 3 + test/game/systems/AnimateSprite.test.cpp | 191 ++++++++++++++++++++++ 17 files changed, 307 insertions(+), 115 deletions(-) create mode 100644 assets/wallop.png delete mode 100644 src/game/components/ShipRenderer.h create mode 100644 src/game/components/SpriteAnimation.h delete mode 100644 src/game/components/WallopRenderer.h create mode 100644 src/game/systems/AnimateSprite.cpp create mode 100644 src/game/systems/AnimateSprite.h delete mode 100644 src/game/systems/ShipRendererSystem.cpp delete mode 100644 src/game/systems/ShipRendererSystem.h delete mode 100644 src/game/systems/WallopRendererSystem.cpp delete mode 100644 src/game/systems/WallopRendererSystem.h create mode 100644 test/game/systems/AnimateSprite.test.cpp diff --git a/assets/wallop.png b/assets/wallop.png new file mode 100644 index 0000000000000000000000000000000000000000..a8eff48da856767bfd532e79e6791cdcdef3319b GIT binary patch literal 3384 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 literal 0 HcmV?d00001 diff --git a/src/game/components/PlayerShip.h b/src/game/components/PlayerShip.h index cb2ee4c..1ab7e45 100644 --- a/src/game/components/PlayerShip.h +++ b/src/game/components/PlayerShip.h @@ -1,7 +1,7 @@ #ifndef GL_ADAGIO_PLAYERSHIP_H #define GL_ADAGIO_PLAYERSHIP_H -#include "raylib.h" +#include "SpriteAnimation.h" struct PlayerShip { /* @@ -11,6 +11,7 @@ struct PlayerShip { */ Adagio::Vector2d velocity; Adagio::Texture2D wallopTexture; + AnimationFrame* wallopFrames{nullptr}; }; #endif //GL_ADAGIO_PLAYERSHIP_H diff --git a/src/game/components/ShipRenderer.h b/src/game/components/ShipRenderer.h deleted file mode 100644 index 79955c5..0000000 --- a/src/game/components/ShipRenderer.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef GL_ADAGIO_SHIPRENDERER_H -#define GL_ADAGIO_SHIPRENDERER_H - -#include "raylib.h" - -struct ShipRenderer { - Adagio::Texture2D texture; - unsigned char frame; - float lastFrame; -}; - -#endif //GL_ADAGIO_SHIPRENDERER_H diff --git a/src/game/components/SpriteAnimation.h b/src/game/components/SpriteAnimation.h new file mode 100644 index 0000000..98c4066 --- /dev/null +++ b/src/game/components/SpriteAnimation.h @@ -0,0 +1,22 @@ +#ifndef GL_ADAGIO_SPRITEANIMATION_H +#define GL_ADAGIO_SPRITEANIMATION_H + +#include "../../math/Rect.h" + +typedef unsigned char FrameIndex; + +struct AnimationFrame { + double duration{0.0}; + Adagio::RectI clip; +}; + +struct SpriteAnimation { + FrameIndex currentFrame{0}; + FrameIndex frameLength{0}; + bool loop{false}; + bool done{false}; + double timeOnCurrentFrame{0.0}; + AnimationFrame* frames{nullptr}; +}; + +#endif //GL_ADAGIO_SPRITEANIMATION_H diff --git a/src/game/components/WallopRenderer.h b/src/game/components/WallopRenderer.h deleted file mode 100644 index cf996a0..0000000 --- a/src/game/components/WallopRenderer.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef GL_ADAGIO_WALLOPRENDERER_H -#define GL_ADAGIO_WALLOPRENDERER_H - -#include "raylib.h" - -struct WallopRenderer { - Adagio::Texture2D texture; - unsigned char frame; - float lastFrame; -}; - -#endif //GL_ADAGIO_WALLOPRENDERER_H diff --git a/src/game/states/GracilisGame.cpp b/src/game/states/GracilisGame.cpp index 5bcda1c..8c6a8fb 100644 --- a/src/game/states/GracilisGame.cpp +++ b/src/game/states/GracilisGame.cpp @@ -3,17 +3,21 @@ #include "raylib.h" #include "../components/PlayerShip.h" #include "../components/Position.h" -#include "../components/ShipRenderer.h" +#include "../components/Sprite.h" +#include "../components/SpriteAnimation.h" +#include "../components/SpriteClip.h" #include "../systems/RemoveDead.h" #include "../systems/ship.h" -#include "../systems/ShipRendererSystem.h" #include "../systems/Wallop.h" #include "../systems/WallopRendererSystem.h" +#include "../systems/AnimateSprite.h" +#include "../systems/RenderSprite.h" void GracilisGame::init() { std::cout << "GracilisGame init" << std::endl; + registerSystem(AnimateSprite); + registerRenderer(RenderSprite); registerSystem(ShipSystem); - registerRenderer(ShipRendererSystem); registerSystem(WallopSystem); registerRenderer(WallopRendererSystem); registerSystem(RemoveDead); @@ -23,14 +27,34 @@ void GracilisGame::loadContent(Adagio::SpriteBatch &spriteBatch, Adagio::Renderi spriteBatch.setClearColor({0, 0, 0, 255}); shipTex = services.textureManager->load("assets/ship.png"); wallopTex = services.textureManager->load("assets/wallop.png"); + wallopFrames = new AnimationFrame[4]{ + {0.083333, Adagio::RectI{0, 0, 64, 56}}, + {0.083333, Adagio::RectI{64, 0, 64, 56}}, + {0.083333, Adagio::RectI{64 * 2, 0, 64, 56}}, + {0.083333, Adagio::RectI{64 * 3, 0, 64, 56}}, + }; + const auto ship = registry.create(); - registry.emplace(ship, Adagio::Vector2d{0, 0}, wallopTex); + registry.emplace(ship, Adagio::Vector2d{0, 0}, wallopTex, wallopFrames); registry.emplace(ship, Adagio::Vector2d{320, 240}); - registry.emplace(ship, shipTex, 0, 0); + registry.emplace(ship, shipTex, Adagio::Vector2d{0, 0}, 0); + registry.emplace(ship); + SpriteAnimation &animation = registry.emplace(ship); + animation.frameLength = 4; + animation.loop = true; + shipFrames = new AnimationFrame[4]{ + {0.083333, Adagio::RectI{0, 0, 56, 89}}, + {0.083333, Adagio::RectI{56, 0, 56, 89}}, + {0.083333, Adagio::RectI{56 * 2, 0, 56, 89}}, + {0.083333, Adagio::RectI{56 * 3, 0, 56, 89}}, + }; + animation.frames = shipFrames; } void GracilisGame::unloadContent(Adagio::RenderingServices &services) { std::cout << "GracilisGame quit" << std::endl; services.textureManager->unload(shipTex); services.textureManager->unload(wallopTex); + delete shipFrames; + delete wallopFrames; } diff --git a/src/game/states/GracilisGame.h b/src/game/states/GracilisGame.h index eb37e9e..a3b1731 100644 --- a/src/game/states/GracilisGame.h +++ b/src/game/states/GracilisGame.h @@ -1,6 +1,7 @@ #ifndef GL_ADAGIO_GRACILISGAME_H #define GL_ADAGIO_GRACILISGAME_H +#include "../components/SpriteAnimation.h" #include "../../state/EntityGameState.h" class GracilisGame : public Adagio::EntityGameState { @@ -14,6 +15,8 @@ class GracilisGame : public Adagio::EntityGameState { private: Adagio::Texture2D shipTex; Adagio::Texture2D wallopTex; + AnimationFrame* shipFrames{nullptr}; + AnimationFrame* wallopFrames{nullptr}; }; diff --git a/src/game/systems/AnimateSprite.cpp b/src/game/systems/AnimateSprite.cpp new file mode 100644 index 0000000..6a3ea75 --- /dev/null +++ b/src/game/systems/AnimateSprite.cpp @@ -0,0 +1,31 @@ +#include "AnimateSprite.h" +#include "../components/SpriteClip.h" +#include "../components/SpriteAnimation.h" + +void AnimateSprite(entt::registry ®istry, Adagio::GameStats &stats, Adagio::StateMachine *state) { + auto view = registry.view(); + for (auto [entity, clip, animation] : view.each()) { + if (animation.frameLength > 0 && !animation.done) { + animation.timeOnCurrentFrame += stats.getFrameDelta(); + while (animation.timeOnCurrentFrame > animation.frames[animation.currentFrame].duration) { + animation.timeOnCurrentFrame -= animation.frames[animation.currentFrame].duration; + animation.currentFrame++; + if (animation.currentFrame > animation.frameLength - 1) { + if (animation.loop) { + animation.currentFrame = 0; + } else { + animation.currentFrame = animation.frameLength - 1; + animation.done = true; + break; + } + } + } + Adagio::RectI &frameClip = animation.frames[animation.currentFrame].clip; + clip.source.position.x = static_cast(frameClip.position.x); + clip.source.position.y = static_cast(frameClip.position.y); + clip.source.size.x = static_cast(frameClip.size.x); + clip.source.size.y = static_cast(frameClip.size.y); + } + } + +} \ No newline at end of file diff --git a/src/game/systems/AnimateSprite.h b/src/game/systems/AnimateSprite.h new file mode 100644 index 0000000..13926c0 --- /dev/null +++ b/src/game/systems/AnimateSprite.h @@ -0,0 +1,10 @@ +#ifndef GL_ADAGIO_ANIMATESPRITE_H +#define GL_ADAGIO_ANIMATESPRITE_H + +#include "entt/entt.hpp" +#include "../../graphics/SpriteBatch.h" +#include "../../state/GameState.h" + +void AnimateSprite(entt::registry ®istry, Adagio::GameStats &stats, Adagio::StateMachine *state); + +#endif //GL_ADAGIO_ANIMATESPRITE_H diff --git a/src/game/systems/ShipRendererSystem.cpp b/src/game/systems/ShipRendererSystem.cpp deleted file mode 100644 index 63259c6..0000000 --- a/src/game/systems/ShipRendererSystem.cpp +++ /dev/null @@ -1,32 +0,0 @@ -#include "ShipRendererSystem.h" -#include "../components/PlayerShip.h" -#include "../components/Position.h" -#include "../components/ShipRenderer.h" -#include "../../math/Rect.h" - - -void -ShipRendererSystem(entt::registry ®istry, Adagio::SpriteBatch &spriteBatch, Adagio::RenderingServices &services) { - const int FRAME_WIDTH = 56; - const int FRAME_HEIGHT = 89; - const float secondsUntilNextFrame = 0.083333; - const Adagio::GameStats &stats = *(services.gameStats); - auto view = registry.view(); - for (auto [entity, shipSprite, ship, pos]: view.each()) { - Adagio::RectF clippingRect{0, 0, FRAME_WIDTH, FRAME_HEIGHT}; - clippingRect.position.x = FRAME_WIDTH * shipSprite.frame; - auto sprite = spriteBatch.draw(shipSprite.texture, pos.position); - sprite->source = clippingRect; - sprite->destination.position.x = floorf(pos.position.x); - sprite->destination.position.y = floorf(pos.position.y); - sprite->destination.size.x = FRAME_WIDTH; - sprite->destination.size.y = FRAME_HEIGHT; - const float frameDelta = stats.getGameTime() - shipSprite.lastFrame; - if (frameDelta > secondsUntilNextFrame) { - shipSprite.lastFrame = stats.getGameTime(); - if (++shipSprite.frame > 3) { - shipSprite.frame = 0; - } - } - } -} diff --git a/src/game/systems/ShipRendererSystem.h b/src/game/systems/ShipRendererSystem.h deleted file mode 100644 index cf722fe..0000000 --- a/src/game/systems/ShipRendererSystem.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef GL_ADAGIO_SHIPRENDERERSYSTEM_H -#define GL_ADAGIO_SHIPRENDERERSYSTEM_H - -#include "entt/entt.hpp" -#include "../../graphics/SpriteBatch.h" -#include "../../state/GameState.h" - -void -ShipRendererSystem(entt::registry ®istry, Adagio::SpriteBatch &spriteBatch, Adagio::RenderingServices &services); - -#endif //GL_ADAGIO_SHIPRENDERERSYSTEM_H diff --git a/src/game/systems/WallopRendererSystem.cpp b/src/game/systems/WallopRendererSystem.cpp deleted file mode 100644 index 1ea2dfb..0000000 --- a/src/game/systems/WallopRendererSystem.cpp +++ /dev/null @@ -1,29 +0,0 @@ -#include "WallopRendererSystem.h" -#include "../components/Position.h" -#include "../components/WallopRenderer.h" - -void -WallopRendererSystem(entt::registry ®istry, Adagio::SpriteBatch &spriteBatch, Adagio::RenderingServices &services) { - const int FRAME_WIDTH = 64; - const int FRAME_HEIGHT = 56; - const float secondsUntilNextFrame = 0.083333; - const Adagio::GameStats &stats = *(services.gameStats); - auto view = registry.view(); - for (auto [entity, wallopSprite, pos]: view.each()) { - Adagio::RectF clippingRect{0, 0, FRAME_WIDTH, FRAME_HEIGHT}; - clippingRect.position.x = FRAME_WIDTH * wallopSprite.frame; - auto sprite = spriteBatch.draw(wallopSprite.texture, pos.position); - sprite->source = clippingRect; - sprite->destination.position.x = floorf(pos.position.x); - sprite->destination.position.y = floorf(pos.position.y); - sprite->destination.size.x = FRAME_WIDTH * 0.5; - sprite->destination.size.y = FRAME_HEIGHT * 0.5; - const float frameDelta = stats.getGameTime() - wallopSprite.lastFrame; - if (frameDelta > secondsUntilNextFrame) { - wallopSprite.lastFrame = stats.getGameTime(); - if (++wallopSprite.frame > 3) { - wallopSprite.frame = 0; - } - } - } -} diff --git a/src/game/systems/WallopRendererSystem.h b/src/game/systems/WallopRendererSystem.h deleted file mode 100644 index 12fb0a4..0000000 --- a/src/game/systems/WallopRendererSystem.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef GL_ADAGIO_WALLOPERRENDERERSYSTEM_H -#define GL_ADAGIO_WALLOPERRENDERERSYSTEM_H - -#include "entt/entt.hpp" -#include "../../graphics/SpriteBatch.h" -#include "../../state/GameState.h" - -void WallopRendererSystem(entt::registry ®istry, Adagio::SpriteBatch &spriteBatch, - Adagio::RenderingServices &services); - -#endif //GL_ADAGIO_WALLOPERRENDERERSYSTEM_H diff --git a/src/game/systems/ship.cpp b/src/game/systems/ship.cpp index 7ab58a5..fe2068e 100644 --- a/src/game/systems/ship.cpp +++ b/src/game/systems/ship.cpp @@ -3,7 +3,10 @@ #include "../components/Position.h" #include "../components/PlayerShip.h" #include "../components/UserProjectile.h" -#include "../components/WallopRenderer.h" +#include "../components/Sprite.h" +#include "../components/SpriteClip.h" +#include "../components/SpriteScale.h" +#include "../components/SpriteAnimation.h" #include float lowerVelocity(float v); @@ -33,8 +36,14 @@ void ShipSystem(entt::registry ®istry, Adagio::GameStats &stats, Adagio::Stat if (IsKeyPressed(KEY_SPACE)) { const auto wallop = registry.create(); registry.emplace(wallop, 6); - registry.emplace(wallop, ship.wallopTexture, 0, 0); registry.emplace(wallop, Adagio::Vector2{pos.position.x + 27 - 16, pos.position.y}); + registry.emplace(wallop, ship.wallopTexture, Adagio::Vector2d{0, 0}, 0); + registry.emplace(wallop); + registry.emplace(wallop, Adagio::Vector2f{0.5, 0.5}); + SpriteAnimation& anim = registry.emplace(wallop); + anim.frameLength = 4; + anim.loop = true; + anim.frames = ship.wallopFrames; } ship.velocity = normalizeVelocity(ship.velocity, speed); pos.position.x += ship.velocity.x; diff --git a/test/game/EcsTestingHarness.cpp b/test/game/EcsTestingHarness.cpp index 8ce5537..20b65fe 100644 --- a/test/game/EcsTestingHarness.cpp +++ b/test/game/EcsTestingHarness.cpp @@ -2,6 +2,7 @@ EcsTestingHarness::EcsTestingHarness() { renderingServices = {&spriteBatch, graphicsDevice.getTextureManager(), &stats}; + stateMachine = new Adagio::StateMachine(&spriteBatch, &renderingServices); } void EcsTestingHarness::reset() { @@ -16,3 +17,7 @@ void EcsTestingHarness::testRendererFrame(Adagio::RendererFn renderer) { renderer(registry, spriteBatch, renderingServices); spriteBatch.end(); } + +void EcsTestingHarness::testSystemFrame(Adagio::SystemFn system) { + system(registry, stats, stateMachine); +} diff --git a/test/game/EcsTestingHarness.h b/test/game/EcsTestingHarness.h index b975d14..9b9d48b 100644 --- a/test/game/EcsTestingHarness.h +++ b/test/game/EcsTestingHarness.h @@ -7,6 +7,7 @@ #include "harness/MockGameStats.h" #include "entt/entt.hpp" #include "../../src/state/EntityGameState.h" +#include "../../src/state/StateMachine.h" class EcsTestingHarness { public: @@ -15,12 +16,14 @@ class EcsTestingHarness { MockGameStats stats; entt::registry registry; Adagio::RenderingServices renderingServices{}; + Adagio::StateMachine *stateMachine; EcsTestingHarness(); void reset(); void testRendererFrame(Adagio::RendererFn renderer); + void testSystemFrame(Adagio::SystemFn system); }; diff --git a/test/game/systems/AnimateSprite.test.cpp b/test/game/systems/AnimateSprite.test.cpp new file mode 100644 index 0000000..639c890 --- /dev/null +++ b/test/game/systems/AnimateSprite.test.cpp @@ -0,0 +1,191 @@ +#include +#include "../EcsTestingHarness.h" +#include "../../../src/game/components/SpriteAnimation.h" +#include "../../../src/game/components/Sprite.h" +#include "../../../src/game/components/SpriteClip.h" +#include "../../../src/game/systems/AnimateSprite.h" + +static EcsTestingHarness harness; + +static void assertRectEquals(SpriteClip &clip, int x, int y, int w, int h) { + REQUIRE(clip.source.x() == x); + REQUIRE(clip.source.y() == y); + REQUIRE(clip.source.width() == w); + REQUIRE(clip.source.height() == h); +} + +TEST_CASE("AnimateSprite: No components defined", "[renderer][AnimateSprite]") { + harness.reset(); + REQUIRE_NOTHROW(AnimateSprite(harness.registry, harness.stats, harness.stateMachine)); +} + +TEST_CASE("AnimateSprite does nothing if no frame data defined", "[renderer][AnimateSprite]") { + harness.reset(); + + auto sprite = harness.registry.create(); + harness.registry.emplace(sprite); + SpriteClip& clip = harness.registry.emplace(sprite, Adagio::RectF{1,2,3,4}); + harness.registry.emplace(sprite); + + harness.testSystemFrame(AnimateSprite); + harness.stats.advanceTime(1); + harness.testSystemFrame(AnimateSprite); + + REQUIRE(clip.source.x() == 1); + REQUIRE(clip.source.y() == 2); + REQUIRE(clip.source.width() == 3); + REQUIRE(clip.source.height() == 4); +} + +TEST_CASE("AnimateSprite applies clipping rect when one frame defined", "[renderer][AnimateSprite]") { + harness.reset(); + + auto sprite = harness.registry.create(); + harness.registry.emplace(sprite); + SpriteClip& clip = harness.registry.emplace(sprite, Adagio::RectF{1,2,3,4}); + AnimationFrame frames[] = { + {1,Adagio::RectI{5,6,7,8}} + }; + SpriteAnimation &animation = harness.registry.emplace(sprite); + animation.frameLength = 1; + animation.frames = frames; + + harness.testSystemFrame(AnimateSprite); + + REQUIRE(clip.source.x() == 5); + REQUIRE(clip.source.y() == 6); + REQUIRE(clip.source.width() == 7); + REQUIRE(clip.source.height() == 8); +} + +TEST_CASE("AnimateSprite picks the correct frame between two frames", "[renderer][AnimateSprite]") { + harness.reset(); + + auto sprite = harness.registry.create(); + harness.registry.emplace(sprite); + SpriteClip& clip = harness.registry.emplace(sprite, Adagio::RectF{1,2,3,4}); + AnimationFrame frames[] = { + {1,Adagio::RectI{5,6,7,8}}, + {1, Adagio::RectI{9,10,11,12}}, + }; + SpriteAnimation &animation = harness.registry.emplace(sprite); + animation.frameLength = 2; + animation.frames = frames; + + harness.testSystemFrame(AnimateSprite); + assertRectEquals(clip, 5,6,7,8); + harness.stats.advanceTime(1.01); + harness.testSystemFrame(AnimateSprite); + assertRectEquals(clip, 9, 10, 11, 12); +} + +TEST_CASE("AnimateSprite will not go past frame boundaries", "[system][AnimatedSprite]") { + harness.reset(); + + auto sprite = harness.registry.create(); + harness.registry.emplace(sprite); + SpriteClip& clip = harness.registry.emplace(sprite, Adagio::RectF{1,2,3,4}); + AnimationFrame frames[] = { + {1,Adagio::RectI{5,6,7,8}}, + {100, Adagio::RectI{0xff,0xff,0xff,0xff}}, + // The last frame here is just a "padding frame" that should never be reached + }; + SpriteAnimation &animation = harness.registry.emplace(sprite); + animation.frameLength = 1; + animation.frames = frames; + + harness.testSystemFrame(AnimateSprite); + harness.stats.advanceTime(50); + harness.testSystemFrame(AnimateSprite); + + assertRectEquals(clip, 5, 6, 7, 8); +} + +TEST_CASE("AnimateSprite will skip frames if enough time passes between calls", "[system][AnimatedSprite]") { + harness.reset(); + + auto sprite = harness.registry.create(); + harness.registry.emplace(sprite); + SpriteClip& clip = harness.registry.emplace(sprite, Adagio::RectF{1,2,3,4}); + AnimationFrame frames[] = { + {1,Adagio::RectI{5,6,7,8}}, + {1, Adagio::RectI {9,10,11,12}}, + {100, Adagio::RectI{0xff,0xff,0xff,0xff}}, + }; + SpriteAnimation& animation = harness.registry.emplace(sprite); + animation.frameLength = 3; + animation.frames = frames; + + harness.stats.advanceTime(50); + harness.testSystemFrame(AnimateSprite); + + assertRectEquals(clip, 0xff, 0xff, 0xff, 0xff); +} + +TEST_CASE("AnimateSprite can loop animations", "[system][AnimateSprite]") { + harness.reset(); + + auto sprite = harness.registry.create(); + harness.registry.emplace(sprite); + SpriteClip& clip = harness.registry.emplace(sprite); + AnimationFrame frames[] = { + {1,Adagio::RectI{5,6,7,8}}, + {1, Adagio::RectI {9,10,11,12}}, + }; + SpriteAnimation &animation = harness.registry.emplace(sprite); + animation.frameLength = 2; + animation.frames = frames; + animation.loop = true; + + harness.stats.advanceTime(1.1); + harness.testSystemFrame(AnimateSprite); + harness.stats.advanceTime(1.1); + harness.testSystemFrame(AnimateSprite); + + + assertRectEquals(clip, 5, 6, 7, 8); +} + +TEST_CASE("AnimateSprite will stop animating non-looping animations", "[system][AnimateSprite]") { + harness.reset(); + + auto sprite = harness.registry.create(); + harness.registry.emplace(sprite); + SpriteClip& clip = harness.registry.emplace(sprite); + AnimationFrame frames[] = { + {1,Adagio::RectI{5,6,7,8}}, + {1, Adagio::RectI {9,10,11,12}}, + }; + SpriteAnimation& animation = harness.registry.emplace(sprite); + animation.frameLength = 2; + animation.frames = frames; + + harness.stats.advanceTime(1.5); + harness.testSystemFrame(AnimateSprite); + harness.stats.advanceTime(1); + harness.testSystemFrame(AnimateSprite); + + assertRectEquals(clip, 9, 10, 11, 12); + REQUIRE(animation.done); +} + +TEST_CASE("AnimateSprite will not process an animation that is marked as done", "[system][AnimatedSprite]") { + harness.reset(); + + auto sprite = harness.registry.create(); + harness.registry.emplace(sprite); + SpriteClip& clip = harness.registry.emplace(sprite, Adagio::RectF{1,2,3,4}); + AnimationFrame frames[] = { + {1,Adagio::RectI{5,6,7,8}}, + {1, Adagio::RectI {9,10,11,12}}, + }; + SpriteAnimation& animation = harness.registry.emplace(sprite); + animation.frameLength = 2; + animation.frames = frames; + animation.done = true; + + harness.stats.advanceTime(1.5); + harness.testSystemFrame(AnimateSprite); + + assertRectEquals(clip, 1, 2, 3, 4); +} \ No newline at end of file