Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/images/eye_lid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/snake_mouth.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 changes: 60 additions & 0 deletions lib/include/chomper/animations/head_animation.hpp
Original file line number Diff line number Diff line change
@@ -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 <glm/vec2.hpp>

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<le::RenderInstance const> 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 const*> directionProvider, gsl::not_null<CollectibleProvider const*> collectibleProvider,
gsl::not_null<Engine const*> engine);
void tick(kvf::Seconds dt) final;
void draw(le::IRenderer& renderer) const final;

private:
klib::TypedLogger<HeadAnimation> m_log;

gsl::not_null<DirectionProvider const*> m_directionProvider;
gsl::not_null<CollectibleProvider const*> m_collectibleProvider;

le::drawable::Sprite m_mouth{};
std::unique_ptr<Eye> m_leftEye{};
std::unique_ptr<Eye> m_rightEye{};
klib::Ptr<le::ITexture> m_eyeLidTexture{};

bool m_shouldDraw{};
};
} // namespace animation
} // namespace chomper
1 change: 1 addition & 0 deletions lib/include/chomper/animator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Animator {
void play(std::unique_ptr<IAnimation> animation);

void tick(kvf::Seconds dt);
void stopAll();

void draw(le::IRenderer& renderer) const;

Expand Down
13 changes: 1 addition & 12 deletions lib/include/chomper/controller.hpp
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
#pragma once
#include "chomper/heading.hpp"
#include <klib/base_types.hpp>
#include <klib/enum_array.hpp>
#include <kvf/time.hpp>
#include <cstdint>
#include <gsl/pointers>
#include <string_view>

namespace chomper {
// represents a direction.
enum class Heading : std::int8_t {
East,
North,
West,
South,
COUNT_
};
constexpr auto headingName_v = klib::EnumArray<Heading, std::string_view>{"East", "North", "West", "South"};

// Heading controller interface.
class IController : public klib::Polymorphic {
public:
Expand Down
21 changes: 21 additions & 0 deletions lib/include/chomper/heading.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once
#include <glm/trigonometric.hpp>
#include <glm/vec2.hpp>
#include <klib/enum_array.hpp>

namespace chomper {
// represents a direction.
enum class Heading : std::int8_t {
East,
North,
West,
South,
COUNT_
};

constexpr auto headingName_v = klib::EnumArray<Heading, std::string_view>{"East", "North", "West", "South"};
constexpr auto oppositeHeading_v = klib::EnumArray<Heading, Heading>{Heading::West, Heading::South, Heading::East, Heading::North};
constexpr auto headingToDir_v = klib::EnumArray<Heading, glm::ivec2>{glm::ivec2{1, 0}, glm::ivec2{0, 1}, glm::ivec2{-1, 0}, glm::ivec2{0, -1}};
constexpr klib::EnumArray<Heading, float> headingToRot_v{glm::radians(90.f), glm::radians(0.f), glm::radians(270.f), glm::radians(180.f)};

} // namespace chomper
15 changes: 13 additions & 2 deletions lib/include/chomper/player.hpp
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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 const*> engine);
explicit Player(le::input::ScopedActionMapping& mapping, gsl::not_null<Engine const*> engine,
gsl::not_null<animation::CollectibleProvider const*> collectibleProvider);

void tick(kvf::Seconds dt);
void draw(le::IRenderer& renderer) const;
Expand All @@ -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;
Expand Down
7 changes: 6 additions & 1 deletion lib/include/chomper/runtimes/game.hpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#pragma once
#include "chomper/animations/head_animation.hpp"
#include "chomper/collectibles.hpp"
#include "chomper/engine.hpp"
#include "chomper/player.hpp"
Expand All @@ -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*> engine);

[[nodiscard]] std::span<le::RenderInstance const> getCollectibles() const final {
return m_collectibles->getInstances();
}

private:
// all Game-level input actions.
struct Actions {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#include "chomper/animations/deathAnimation.hpp"
#include "chomper/animations/death_animation.hpp"
#include "chomper/world_space.hpp"
#include <numbers>

Expand Down
108 changes: 108 additions & 0 deletions lib/src/animations/head_animation.cpp
Original file line number Diff line number Diff line change
@@ -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<glm::vec2> findAdjacentCollectible(std::span<le::RenderInstance const> 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 const*> directionProvider, gsl::not_null<CollectibleProvider const*> collectibleProvider,
gsl::not_null<Engine const*> engine)
: m_directionProvider(directionProvider), m_collectibleProvider(collectibleProvider) {
m_mouth.set_base_size(tileSize_v);
m_mouth.set_texture(engine->getResources().load<le::ITexture>("images/snake_mouth.png"));

m_eyeLidTexture = engine->getResources().load<le::ITexture>("images/eye_lid.png");
if (!m_eyeLidTexture) {
return;
}
m_leftEye = std::make_unique<Eye>(*m_eyeLidTexture);
m_rightEye = std::make_unique<Eye>(*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
4 changes: 4 additions & 0 deletions lib/src/animator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
11 changes: 7 additions & 4 deletions lib/src/player.cpp
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -9,13 +9,15 @@
namespace chomper {
namespace {
constexpr auto moveSpeed_v = kvf::Seconds{0.135f};
constexpr auto oppositeHeading_v = klib::EnumArray<Heading, Heading>{Heading::West, Heading::South, Heading::East, Heading::North};
constexpr auto headingToDir_v = klib::EnumArray<Heading, glm::ivec2>{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 const*> engine) : m_engine(engine) {
Player::Player(le::input::ScopedActionMapping& mapping, gsl::not_null<Engine const*> engine,
gsl::not_null<animation::CollectibleProvider const*> collectibleProvider)
: m_engine(engine) {
createController(mapping);
updateScoreText();

m_animator.play(std::make_unique<animation::HeadAnimation>(this, collectibleProvider, engine));
}

void Player::tick(kvf::Seconds dt) {
Expand Down Expand Up @@ -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<animation::DeathAnimation>(m_snake.getSegments()));
} else {
m_graceMove = true;
Expand Down
2 changes: 1 addition & 1 deletion lib/src/runtimes/game.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Player>(m_mapping, m_engine);
m_player = std::make_unique<Player>(m_mapping, m_engine, this);
}

void Game::createCollectibles() {
Expand Down
4 changes: 0 additions & 4 deletions lib/src/snake.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@
#include <le2d/render_instance.hpp>

namespace chomper {
namespace {
constexpr auto headingToDir_v = klib::EnumArray<Heading, glm::ivec2>{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;
Expand Down