diff --git a/assets/wallop.png b/assets/wallop.png new file mode 100644 index 0000000..a8eff48 Binary files /dev/null and b/assets/wallop.png differ 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