diff --git a/assets/images/eye_lid.png b/assets/images/eye_lid.png new file mode 100644 index 0000000..7899cb7 Binary files /dev/null and b/assets/images/eye_lid.png differ diff --git a/assets/images/snake_mouth.png b/assets/images/snake_mouth.png new file mode 100644 index 0000000..3e6bcbe Binary files /dev/null and b/assets/images/snake_mouth.png differ diff --git a/lib/include/chomper/animations/deathAnimation.hpp b/lib/include/chomper/animations/death_animation.hpp similarity index 100% rename from lib/include/chomper/animations/deathAnimation.hpp rename to lib/include/chomper/animations/death_animation.hpp diff --git a/lib/include/chomper/animations/head_animation.hpp b/lib/include/chomper/animations/head_animation.hpp new file mode 100644 index 0000000..3042418 --- /dev/null +++ b/lib/include/chomper/animations/head_animation.hpp @@ -0,0 +1,60 @@ +#pragma once +#include "chomper/animator.hpp" +#include "chomper/heading.hpp" +#include "klib/base_types.hpp" +#include "klib/log.hpp" +#include "klib/ptr.hpp" +#include "le2d/drawable/sprite.hpp" +#include "le2d/render_instance.hpp" +#include + +namespace chomper { +class Engine; + +namespace animation { +class DirectionProvider : public klib::Polymorphic { + public: + [[nodiscard]] virtual glm::vec2 getHeadPosition() const = 0; + [[nodiscard]] virtual Heading getHeading() const = 0; +}; + +class CollectibleProvider : public klib::Polymorphic { + public: + [[nodiscard]] virtual std::span getCollectibles() const = 0; +}; + +class Eye { + public: + explicit Eye(le::ITexture& eyeLidTexture); + void setPosition(glm::vec2 position); + void lookAt(glm::vec2 target); + void draw(le::IRenderer& renderer, bool drawEyeLids) const; + + private: + le::drawable::Sprite m_eyeLid{}; + le::drawable::Circle m_eye{}; + le::drawable::Circle m_pupil{}; +}; + +class HeadAnimation : public IAnimation { + public: + explicit HeadAnimation(gsl::not_null directionProvider, gsl::not_null collectibleProvider, + gsl::not_null engine); + void tick(kvf::Seconds dt) final; + void draw(le::IRenderer& renderer) const final; + + private: + klib::TypedLogger m_log; + + gsl::not_null m_directionProvider; + gsl::not_null m_collectibleProvider; + + le::drawable::Sprite m_mouth{}; + std::unique_ptr m_leftEye{}; + std::unique_ptr m_rightEye{}; + klib::Ptr m_eyeLidTexture{}; + + bool m_shouldDraw{}; +}; +} // namespace animation +} // namespace chomper diff --git a/lib/include/chomper/animator.hpp b/lib/include/chomper/animator.hpp index ec9fbfd..6b64c89 100644 --- a/lib/include/chomper/animator.hpp +++ b/lib/include/chomper/animator.hpp @@ -23,6 +23,7 @@ class Animator { void play(std::unique_ptr animation); void tick(kvf::Seconds dt); + void stopAll(); void draw(le::IRenderer& renderer) const; diff --git a/lib/include/chomper/controller.hpp b/lib/include/chomper/controller.hpp index 81fb40d..2af5c84 100644 --- a/lib/include/chomper/controller.hpp +++ b/lib/include/chomper/controller.hpp @@ -1,22 +1,11 @@ #pragma once +#include "chomper/heading.hpp" #include #include #include -#include #include -#include namespace chomper { -// represents a direction. -enum class Heading : std::int8_t { - East, - North, - West, - South, - COUNT_ -}; -constexpr auto headingName_v = klib::EnumArray{"East", "North", "West", "South"}; - // Heading controller interface. class IController : public klib::Polymorphic { public: diff --git a/lib/include/chomper/heading.hpp b/lib/include/chomper/heading.hpp new file mode 100644 index 0000000..b086652 --- /dev/null +++ b/lib/include/chomper/heading.hpp @@ -0,0 +1,21 @@ +#pragma once +#include +#include +#include + +namespace chomper { +// represents a direction. +enum class Heading : std::int8_t { + East, + North, + West, + South, + COUNT_ +}; + +constexpr auto headingName_v = klib::EnumArray{"East", "North", "West", "South"}; +constexpr auto oppositeHeading_v = klib::EnumArray{Heading::West, Heading::South, Heading::East, Heading::North}; +constexpr auto headingToDir_v = klib::EnumArray{glm::ivec2{1, 0}, glm::ivec2{0, 1}, glm::ivec2{-1, 0}, glm::ivec2{0, -1}}; +constexpr klib::EnumArray headingToRot_v{glm::radians(90.f), glm::radians(0.f), glm::radians(270.f), glm::radians(180.f)}; + +} // namespace chomper \ No newline at end of file diff --git a/lib/include/chomper/player.hpp b/lib/include/chomper/player.hpp index 0e5f60d..7ab3d14 100644 --- a/lib/include/chomper/player.hpp +++ b/lib/include/chomper/player.hpp @@ -1,4 +1,5 @@ #pragma once +#include "chomper/animations/head_animation.hpp" #include "chomper/animator.hpp" #include "chomper/controller.hpp" #include "chomper/debug_inspector.hpp" @@ -14,14 +15,15 @@ namespace chomper { class Engine; -class Player : public IController::IListener, public IDebugInspector, public klib::Pinned { +class Player : public IController::IListener, public IDebugInspector, public klib::Pinned, public animation::DirectionProvider { public: struct Info { bool alive = true; int score{}; }; - explicit Player(le::input::ScopedActionMapping& mapping, gsl::not_null engine); + explicit Player(le::input::ScopedActionMapping& mapping, gsl::not_null engine, + gsl::not_null collectibleProvider); void tick(kvf::Seconds dt); void draw(le::IRenderer& renderer) const; @@ -35,6 +37,15 @@ class Player : public IController::IListener, public IDebugInspector, public kli return m_snake.getSegments(); } + [[nodiscard]] glm::vec2 getHeadPosition() const final { + KLIB_ASSERT(!m_snake.getSegments().empty()); + return m_snake.getSegments().back().transform.position; + } + + [[nodiscard]] Heading getHeading() const final { + return m_heading; + } + private: [[nodiscard]] bool isCollidingWithSelf(glm::ivec2 targetGrid) const; [[nodiscard]] bool isCollidingWithWall(glm::ivec2 targetGrid) const; diff --git a/lib/include/chomper/runtimes/game.hpp b/lib/include/chomper/runtimes/game.hpp index bd4f851..7dbcc21 100644 --- a/lib/include/chomper/runtimes/game.hpp +++ b/lib/include/chomper/runtimes/game.hpp @@ -1,4 +1,5 @@ #pragma once +#include "chomper/animations/head_animation.hpp" #include "chomper/collectibles.hpp" #include "chomper/engine.hpp" #include "chomper/player.hpp" @@ -15,10 +16,14 @@ namespace chomper::runtime { // driven by Engine, owner (whether indirectly) of all game things. -class Game : public IRuntime, public klib::Pinned { +class Game : public IRuntime, public klib::Pinned, public animation::CollectibleProvider { public: explicit Game(gsl::not_null engine); + [[nodiscard]] std::span getCollectibles() const final { + return m_collectibles->getInstances(); + } + private: // all Game-level input actions. struct Actions { diff --git a/lib/src/animations/deathAnimation.cpp b/lib/src/animations/death_animation.cpp similarity index 97% rename from lib/src/animations/deathAnimation.cpp rename to lib/src/animations/death_animation.cpp index b285937..c5e9aa8 100644 --- a/lib/src/animations/deathAnimation.cpp +++ b/lib/src/animations/death_animation.cpp @@ -1,4 +1,4 @@ -#include "chomper/animations/deathAnimation.hpp" +#include "chomper/animations/death_animation.hpp" #include "chomper/world_space.hpp" #include diff --git a/lib/src/animations/head_animation.cpp b/lib/src/animations/head_animation.cpp new file mode 100644 index 0000000..13c146c --- /dev/null +++ b/lib/src/animations/head_animation.cpp @@ -0,0 +1,108 @@ +#include "chomper/animations/head_animation.hpp" +#include "chomper/engine.hpp" +#include "chomper/world_space.hpp" +#include "klib/enum_array.hpp" + +namespace chomper::animation { +namespace { +constexpr auto isAdjacent(glm::ivec2 const& grid1, glm::ivec2 const& grid2) { + auto dx = klib::abs(grid1.x - grid2.x); + auto dy = klib::abs(grid1.y - grid2.y); + + return (dx <= 1 && dy <= 1) && (dx != 0 || dy != 0); +} + +std::optional findAdjacentCollectible(std::span collectibles, glm::ivec2 headPos) { + auto it = std::ranges::find_if(collectibles, [&](auto const& c) { + return isAdjacent(headPos, worldSpace::worldToGrid(c.transform.position)); + }); + if (it == collectibles.end()) { + return std::nullopt; + } + return it->transform.position; +} + +} // namespace + +HeadAnimation::HeadAnimation(gsl::not_null directionProvider, gsl::not_null collectibleProvider, + gsl::not_null engine) + : m_directionProvider(directionProvider), m_collectibleProvider(collectibleProvider) { + m_mouth.set_base_size(tileSize_v); + m_mouth.set_texture(engine->getResources().load("images/snake_mouth.png")); + + m_eyeLidTexture = engine->getResources().load("images/eye_lid.png"); + if (!m_eyeLidTexture) { + return; + } + m_leftEye = std::make_unique(*m_eyeLidTexture); + m_rightEye = std::make_unique(*m_eyeLidTexture); +} + +void HeadAnimation::tick(kvf::Seconds /*dt*/) { + auto headPos = m_directionProvider->getHeadPosition(); + auto heading = m_directionProvider->getHeading(); + + auto c = std::cos(headingToRot_v[heading]); + auto s = std::sin(headingToRot_v[heading]); + auto rotation = glm::mat2(c, -s, s, c); + + auto rightEyeOffset = rotation * glm::vec2{tileSize_v.x * 0.22f, tileSize_v.y * -0.22f}; + m_rightEye->setPosition(headPos + rightEyeOffset); + + auto leftEyeOffset = rotation * glm::vec2{tileSize_v.x * -0.22f, tileSize_v.y * -0.22f}; + m_leftEye->setPosition(headPos + leftEyeOffset); + + auto target = findAdjacentCollectible(m_collectibleProvider->getCollectibles(), worldSpace::worldToGrid(headPos)); + if (!target) { + m_shouldDraw = false; + return; + } + m_shouldDraw = true; + + auto mouthOffset = rotation * glm::vec2{0.f, tileSize_v.x * 0.5f}; + m_mouth.transform.position = headPos + mouthOffset; + m_mouth.transform.orientation = le::nvec2::from_radians(headingToRot_v[heading]); + + m_leftEye->lookAt(*target); + m_rightEye->lookAt(*target); +} + +void HeadAnimation::draw(le::IRenderer& renderer) const { + m_leftEye->draw(renderer, !m_shouldDraw); + m_rightEye->draw(renderer, !m_shouldDraw); + + if (!m_shouldDraw) { + return; + } + m_mouth.draw(renderer); +} + +Eye::Eye(le::ITexture& texture) { + m_eye.create(tileSize_v.x * 0.4f); + m_pupil.create(m_eye.get_diameter() * 0.5f); + m_pupil.tint = kvf::Color{glm::vec4{0.f, 0.f, 0.f, 1.f}}; + m_eyeLid.set_base_size(m_eye.get_size()); + + m_eyeLid.set_texture(&texture); +} + +void Eye::setPosition(glm::vec2 position) { + m_eyeLid.transform.position = position; + m_eye.transform.position = position; + m_pupil.transform.position = position; +} + +void Eye::lookAt(glm::vec2 target) { + auto dir = glm::normalize(target - m_eye.transform.position); + m_pupil.transform.position = m_eye.transform.position + dir * m_eye.get_diameter() * 0.25f; +} + +void Eye::draw(le::IRenderer& renderer, bool drawEyeLids) const { + m_eye.draw(renderer); + m_pupil.draw(renderer); + if (drawEyeLids) { + m_eyeLid.draw(renderer); + } +} + +} // namespace chomper::animation \ No newline at end of file diff --git a/lib/src/animator.cpp b/lib/src/animator.cpp index 3cab6bf..59977b1 100644 --- a/lib/src/animator.cpp +++ b/lib/src/animator.cpp @@ -14,6 +14,10 @@ void Animator::tick(kvf::Seconds dt) { } } +void Animator::stopAll() { + m_playing.clear(); +} + void Animator::draw(le::IRenderer& renderer) const { for (auto const& animation : m_playing) { animation->draw(renderer); diff --git a/lib/src/player.cpp b/lib/src/player.cpp index f536de3..603b1a6 100644 --- a/lib/src/player.cpp +++ b/lib/src/player.cpp @@ -1,5 +1,5 @@ #include "chomper/player.hpp" -#include "chomper/animations/deathAnimation.hpp" +#include "chomper/animations/death_animation.hpp" #include "chomper/controllers/player_controller.hpp" #include "chomper/engine.hpp" #include "chomper/world_size.hpp" @@ -9,13 +9,15 @@ namespace chomper { namespace { constexpr auto moveSpeed_v = kvf::Seconds{0.135f}; -constexpr auto oppositeHeading_v = klib::EnumArray{Heading::West, Heading::South, Heading::East, Heading::North}; -constexpr auto headingToDir_v = klib::EnumArray{glm::ivec2{1, 0}, glm::ivec2{0, 1}, glm::ivec2{-1, 0}, glm::ivec2{0, -1}}; } // namespace -Player::Player(le::input::ScopedActionMapping& mapping, gsl::not_null engine) : m_engine(engine) { +Player::Player(le::input::ScopedActionMapping& mapping, gsl::not_null engine, + gsl::not_null collectibleProvider) + : m_engine(engine) { createController(mapping); updateScoreText(); + + m_animator.play(std::make_unique(this, collectibleProvider, engine)); } void Player::tick(kvf::Seconds dt) { @@ -71,6 +73,7 @@ void Player::move() { if (isCollidingWithSelf(targetGrid) || isCollidingWithWall(targetGrid)) { if (m_graceMove) { m_info.alive = false; + m_animator.stopAll(); m_animator.play(std::make_unique(m_snake.getSegments())); } else { m_graceMove = true; diff --git a/lib/src/runtimes/game.cpp b/lib/src/runtimes/game.cpp index 62dcf5d..051685b 100644 --- a/lib/src/runtimes/game.cpp +++ b/lib/src/runtimes/game.cpp @@ -86,7 +86,7 @@ void Game::createPlayer() { // rebind game actions. bindActions(); // create the player, passing a reference of the logger and a reference of the input mapping to create its PlayerController. - m_player = std::make_unique(m_mapping, m_engine); + m_player = std::make_unique(m_mapping, m_engine, this); } void Game::createCollectibles() { diff --git a/lib/src/snake.cpp b/lib/src/snake.cpp index 7c24c01..60fa718 100644 --- a/lib/src/snake.cpp +++ b/lib/src/snake.cpp @@ -7,10 +7,6 @@ #include namespace chomper { -namespace { -constexpr auto headingToDir_v = klib::EnumArray{glm::ivec2{1, 0}, glm::ivec2{0, 1}, glm::ivec2{-1, 0}, glm::ivec2{0, -1}}; -} // namespace - Snake::Snake() { le::RenderInstance instance{}; instance.tint = theme::snakeBodyColor_v;