diff --git a/assets/images/apple.png b/assets/images/apple.png new file mode 100644 index 0000000..f74d03b Binary files /dev/null and b/assets/images/apple.png differ diff --git a/lib/include/chomper/collectible.hpp b/lib/include/chomper/collectible.hpp new file mode 100644 index 0000000..a7cedb4 --- /dev/null +++ b/lib/include/chomper/collectible.hpp @@ -0,0 +1,22 @@ +#pragma once +#include "chomper/world_space.hpp" +#include "glm/ext/vector_float2.hpp" +#include "le2d/drawable/sprite.hpp" +#include "le2d/renderer.hpp" +#include "le2d/resource/texture.hpp" + +namespace chomper { +class Collectible { + public: + explicit Collectible(le::ITexture const& texture, glm::vec2 position); + + void draw(le::IRenderer& renderer) const; + + [[nodiscard]] glm::vec2 getGridPosition() const { + return worldSpace::worldToGrid(m_sprite.transform.position); + } + + private: + le::drawable::Sprite m_sprite{}; +}; +} // namespace chomper \ No newline at end of file diff --git a/lib/include/chomper/player.hpp b/lib/include/chomper/player.hpp index 0005964..b7d4122 100644 --- a/lib/include/chomper/player.hpp +++ b/lib/include/chomper/player.hpp @@ -2,6 +2,8 @@ #include "chomper/controller.hpp" #include "chomper/debug_inspector.hpp" #include "chomper/snake.hpp" +#include "le2d/drawable/text.hpp" +#include "le2d/render_instance.hpp" #include #include #include @@ -15,6 +17,7 @@ class Player : public IController::IListener, public IDebugInspector, public kli public: struct Info { bool alive = true; + int score{}; }; explicit Player(le::input::ScopedActionMapping& mapping, gsl::not_null engine); @@ -22,12 +25,20 @@ class Player : public IController::IListener, public IDebugInspector, public kli void tick(kvf::Seconds dt); void draw(le::IRenderer& renderer) const; - [[nodiscard]] Info const& getInfo() const; + void grow(); + + [[nodiscard]] Info const& getInfo() const { + return m_info; + } + [[nodiscard]] std::span getSegments() const { + return m_snake.getSegments(); + } private: [[nodiscard]] bool isCollidingWithSelf(glm::vec2 targetGrid) const; [[nodiscard]] bool isCollidingWithWall(glm::vec2 targetGrid) const; void move(); + void updateScoreText(); // IController::IListener void onSetHeading(Heading heading) final; @@ -44,6 +55,7 @@ class Player : public IController::IListener, public IDebugInspector, public kli std::unique_ptr m_controller{}; Snake m_snake{}; + le::drawable::Text m_scoreText{}; Info m_info{}; diff --git a/lib/include/chomper/runtimes/game.hpp b/lib/include/chomper/runtimes/game.hpp index 3769010..7e12e03 100644 --- a/lib/include/chomper/runtimes/game.hpp +++ b/lib/include/chomper/runtimes/game.hpp @@ -1,12 +1,16 @@ #pragma once +#include "chomper/collectible.hpp" #include "chomper/engine.hpp" #include "chomper/player.hpp" #include "chomper/runtime.hpp" #include "chomper/world.hpp" +#include "le2d/random.hpp" +#include "le2d/resource/texture.hpp" #include #include #include #include +#include namespace chomper::runtime { // driven by Engine, owner (whether indirectly) of all game things. @@ -28,6 +32,11 @@ class Game : public IRuntime, public klib::Pinned { void bindActions(); void createPlayer(); + void createCollectibleTexture(); + + void findEmptyTiles(); + void spawnCollectibles(); + void collideCollectibles(); void onGoBack(); @@ -38,8 +47,15 @@ class Game : public IRuntime, public klib::Pinned { le::input::ScopedActionMapping m_mapping; Actions m_actions{}; + le::Random m_random{}; + std::unordered_set m_occupied; + std::unique_ptr m_player{}; std::unique_ptr m_world{}; + std::vector m_collectibles{}; + klib::Ptr m_collectibleTexture{}; + + std::vector m_emptyTiles{}; le::drawable::Text m_countdownText{}; kvf::Seconds m_countdown{3}; diff --git a/lib/include/chomper/world_size.hpp b/lib/include/chomper/world_size.hpp index 4d139fa..3339213 100644 --- a/lib/include/chomper/world_size.hpp +++ b/lib/include/chomper/world_size.hpp @@ -3,6 +3,6 @@ #include namespace chomper { -constexpr auto worldSize_v = glm::vec2{15.f}; +constexpr auto worldSize_v = glm::vec2{16.f}; constexpr auto tileSize_v = viewport_v.world_size / worldSize_v; } // namespace chomper diff --git a/lib/include/chomper/world_space.hpp b/lib/include/chomper/world_space.hpp index ef9c337..b845b6a 100644 --- a/lib/include/chomper/world_space.hpp +++ b/lib/include/chomper/world_space.hpp @@ -19,6 +19,6 @@ constexpr auto worldToGrid(glm::vec2 worldPosition) { } constexpr auto isOutOfBounds(glm::vec2 gridPoint) { - return gridPoint.x <= 0 || gridPoint.y <= 0 || gridPoint.x > worldSize_v.x || gridPoint.y > worldSize_v.y; + return gridPoint.x < 0 || gridPoint.y < 0 || gridPoint.x >= worldSize_v.x || gridPoint.y >= worldSize_v.y; } } // namespace chomper::worldSpace \ No newline at end of file diff --git a/lib/src/collectible.cpp b/lib/src/collectible.cpp new file mode 100644 index 0000000..3573d9c --- /dev/null +++ b/lib/src/collectible.cpp @@ -0,0 +1,15 @@ +#include "chomper/collectible.hpp" +#include "chomper/world_size.hpp" +#include "le2d/renderer.hpp" + +namespace chomper { +Collectible::Collectible(le::ITexture const& texture, glm::vec2 position) { + m_sprite.set_base_size(tileSize_v); + m_sprite.set_texture(&texture); + m_sprite.transform.position = position; +} + +void Collectible::draw(le::IRenderer& renderer) const { + m_sprite.draw(renderer); +} +} // namespace chomper \ No newline at end of file diff --git a/lib/src/player.cpp b/lib/src/player.cpp index aaf5d0f..d9f7e48 100644 --- a/lib/src/player.cpp +++ b/lib/src/player.cpp @@ -1,6 +1,7 @@ #include "chomper/player.hpp" #include "chomper/controllers/player_controller.hpp" #include "chomper/engine.hpp" +#include "chomper/world_size.hpp" #include "chomper/world_space.hpp" #include @@ -13,6 +14,7 @@ constexpr auto headingToDir_v = klib::EnumArray{glm::vec2{1. Player::Player(le::input::ScopedActionMapping& mapping, gsl::not_null engine) : m_engine(engine) { createController(mapping); + updateScoreText(); } void Player::tick(kvf::Seconds dt) { @@ -30,8 +32,10 @@ void Player::tick(kvf::Seconds dt) { } } -Player::Info const& Player::getInfo() const { - return m_info; +void Player::grow() { + m_info.score++; + updateScoreText(); + m_shouldPop = false; } bool Player::isCollidingWithSelf(glm::vec2 const targetGrid) const { @@ -78,11 +82,22 @@ void Player::move() { m_snake.popTail(); } - m_graceMove = false; + m_shouldPop = true; // reset shouldPop + m_graceMove = false; // reset graceMove +} + +void Player::updateScoreText() { + static constexpr auto textParams_v = le::drawable::Text::Params{ + .height = le::TextHeight{16}, + }; + + m_scoreText.set_string(m_engine->getResources().getMainFont(), std::format("Score: {}", m_info.score), textParams_v); + m_scoreText.transform.position = worldSpace::gridToWorld({0, worldSize_v.y - 1}) + glm::vec2{m_scoreText.get_size().x / 2, 0}; } void Player::draw(le::IRenderer& renderer) const { m_snake.draw(renderer); + m_scoreText.draw(renderer); } void Player::debugInspect() { @@ -102,12 +117,12 @@ void Player::createController(le::input::ScopedActionMapping& mapping) { void Player::onSetHeading(Heading const heading) { auto lastHeading = m_headingQueue.empty() ? m_heading : m_headingQueue.back(); - if (heading == m_heading || heading == oppositeHeading_v[lastHeading]) { + if (heading == lastHeading || heading == oppositeHeading_v[lastHeading]) { return; } if (m_headingQueue.size() < 3) { - m_log.debug("changing heading from {} to {}", headingName_v[m_heading], headingName_v[heading]); + m_log.debug("changing heading from {} to {}", headingName_v[lastHeading], headingName_v[heading]); m_headingQueue.push_back(heading); } } diff --git a/lib/src/runtimes/game.cpp b/lib/src/runtimes/game.cpp index 155c3b8..16666f8 100644 --- a/lib/src/runtimes/game.cpp +++ b/lib/src/runtimes/game.cpp @@ -1,20 +1,29 @@ #include "chomper/runtimes/game.hpp" +#include "chomper/collectible.hpp" #include "chomper/im_util.hpp" #include "chomper/runtimes/entrypoint.hpp" -#include +#include "chomper/world_size.hpp" +#include "chomper/world_space.hpp" +#include +#include namespace chomper::runtime { namespace { constexpr auto countdownParams_v = le::drawable::Text::Params{ .height = le::TextHeight{60}, }; -} +constexpr auto collectibleAmount_v = 10; +} // namespace using ActionValue = le::input::action::Value; Game::Game(gsl::not_null engine) : m_engine(engine), m_mapping(&engine->getInputRouter()) { createPlayer(); m_world = std::make_unique(m_engine); + createCollectibleTexture(); + + spawnCollectibles(); + m_countdownText.set_string(engine->getResources().getMainFont(), "3", countdownParams_v); } @@ -33,6 +42,8 @@ void Game::tick(kvf::Seconds const dt) { m_player->tick(dt); + collideCollectibles(); + // On death if (!m_player->getInfo().alive) { m_engine->setNextRuntime(); @@ -42,6 +53,9 @@ void Game::tick(kvf::Seconds const dt) { void Game::render(le::IRenderer& renderer) const { m_world->draw(renderer); m_player->draw(renderer); + for (auto const& collectible : m_collectibles) { + collectible.draw(renderer); + } if (m_countdown.count() > 0) { m_countdownText.draw(renderer); } @@ -75,6 +89,69 @@ void Game::createPlayer() { m_player = std::make_unique(m_mapping, m_engine); } +void Game::createCollectibleTexture() { + m_collectibleTexture = m_engine->getResources().load("images/apple.png"); +} + +void Game::findEmptyTiles() { + m_emptyTiles.clear(); + m_emptyTiles.reserve(static_cast(worldSize_v.x * worldSize_v.y)); + for (auto i = 0; i < static_cast(worldSize_v.x * worldSize_v.y); i++) { + m_emptyTiles.push_back(i); + } + + auto const removeTile = [this](int tile) { + auto it = std::ranges::find(m_emptyTiles, tile); + if (it != m_emptyTiles.end()) { + *it = m_emptyTiles.back(); + m_emptyTiles.pop_back(); + } + }; + + for (auto const& seg : m_player->getSegments()) { + auto p = worldSpace::worldToGrid(seg.transform.position); + removeTile(static_cast((p.y * worldSize_v.x) + p.x)); + } + + for (auto const& c : m_collectibles) { + auto p = c.getGridPosition(); + removeTile(static_cast((p.y * worldSize_v.x) + p.x)); + } +} + +void Game::spawnCollectibles() { + findEmptyTiles(); + + for (auto i = m_collectibles.size(); i < collectibleAmount_v; i++) { + if (m_emptyTiles.empty()) { + return; + } + // find a random tile + auto random = m_random.next_index(m_emptyTiles.size()); + auto tile = m_emptyTiles[random]; + // remove said tile from the vector + std::erase_if(m_emptyTiles, [&](auto const& v) { + return v == m_emptyTiles[random]; + }); + // place the collectible on the tile + auto width = static_cast(worldSize_v.x); + m_collectibles.emplace_back(*m_collectibleTexture, worldSpace::gridToWorld({tile % width, tile / width})); + } +} + +void Game::collideCollectibles() { + auto it = std::ranges::find_if(m_collectibles, [&](auto const& collectible) { + return collectible.getGridPosition() == worldSpace::worldToGrid(m_player->getSegments().back().transform.position); + }); + if (it == m_collectibles.end()) { + return; + } + + m_collectibles.erase(it); + m_player->grow(); + spawnCollectibles(); +} + void Game::onGoBack() { m_log.debug("execute 'go back' action here"); } diff --git a/lib/src/snake.cpp b/lib/src/snake.cpp index 2538230..5d9bdff 100644 --- a/lib/src/snake.cpp +++ b/lib/src/snake.cpp @@ -1,5 +1,7 @@ #include "chomper/snake.hpp" +#include "chomper/world_size.hpp" #include "chomper/world_space.hpp" +#include "le2d/render_instance.hpp" #include #include @@ -10,6 +12,10 @@ constexpr auto headingToDir_v = klib::EnumArray{glm::vec2{1. } // namespace Snake::Snake() { + le::RenderInstance instance{}; + instance.tint = snakeBodyColor_v; + instance.transform.position = worldSpace::gridToWorld({0, worldSize_v.y / 2}); + m_instances.push_back(instance); while (m_instances.size() < m_baseSize) { grow({}); }