Преглед на файлове

:sparkles: added game over screen with name input for high score

Felix Bytow преди 1 година
родител
ревизия
b42a19ff33
променени са 10 файла, в които са добавени 347 реда и са изтрити 1 реда
  1. 2 0
      CMakeLists.txt
  2. 119 0
      game/GameOverState.cxx
  3. 32 0
      game/GameOverState.hxx
  4. 2 0
      game/GameStateManager.cxx
  5. 2 0
      game/GameStateManager.hxx
  6. 97 0
      game/HighScoreManager.cxx
  7. 67 0
      game/HighScoreManager.hxx
  8. 3 0
      game/PlayingState.cxx
  9. 18 1
      game/ui/LineInput.cxx
  10. 5 0
      game/ui/LineInput.hxx

+ 2 - 0
CMakeLists.txt

@@ -48,6 +48,8 @@ add_executable(Snake WIN32
     game/ui/Button.cxx game/ui/Button.hxx
     game/ui/LineInput.cxx game/ui/LineInput.hxx
     game/ui/UiColor.hxx game/ui/UiColor.cxx
+    game/HighScoreManager.cxx game/HighScoreManager.hxx
+    game/GameOverState.cxx game/GameOverState.hxx
 )
 
 target_link_libraries(Snake PRIVATE

+ 119 - 0
game/GameOverState.cxx

@@ -0,0 +1,119 @@
+#include "GameOverState.hxx"
+#include "GameStateManager.hxx"
+
+#include "HighScoreManager.hxx"
+#include "../SDLRenderer.hxx"
+
+GameOverState::GameOverState()
+    :font_{"kenney_pixel.ttf"}
+{
+}
+
+void GameOverState::on_enter(GameStateManager& gsm)
+{
+  SDL_ShowCursor(SDL_ENABLE);
+
+  if (HighScoreManager::instance().has_new_score()) {
+    name_input_.set_focus(true);
+    name_input_.set_value("Anon");
+    name_input_.set_visible(true);
+  }
+  else {
+    name_input_.set_focus(false);
+    name_input_.set_value("");
+    name_input_.set_visible(false);
+  }
+
+  ok_button_.set_on_click([&gsm, this] {
+    HighScoreManager::instance().provide_name_for_new_score(name_input_.value());
+    gsm.replace_state(GameStates::MainMenu);
+  });
+}
+
+void GameOverState::on_leave()
+{
+  SDL_StopTextInput();
+  SDL_ShowCursor(SDL_DISABLE);
+}
+
+void GameOverState::on_event(GameStateManager& gsm, SDL_Event const& evt)
+{
+  name_input_.on_event(evt);
+
+  if (evt.type==SDL_KEYUP) {
+    switch (evt.key.keysym.scancode) {
+    default:
+      break;
+    case SDL_SCANCODE_RETURN:
+      HighScoreManager::instance().provide_name_for_new_score(name_input_.value());
+      [[fallthrough]];
+    case SDL_SCANCODE_ESCAPE:
+      gsm.replace_state(GameStates::MainMenu);
+      break;
+    }
+  }
+}
+
+void GameOverState::update(GameStateManager& gsm, std::chrono::milliseconds const delta_time)
+{
+  ok_button_.update();
+  name_input_.update(delta_time);
+}
+
+void GameOverState::render(SDLRenderer& renderer)
+{
+  int width, height;
+  SDL_GetRendererOutputSize(renderer, &width, &height);
+
+  SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
+  SDL_RenderClear(renderer);
+
+  int base_y;
+  if (HighScoreManager::instance().has_new_score()) {
+    auto const score_text = std::format("Congratulations, you made it to the top 10!\nPlease enter your name:");
+    SDL_Surface* text_surface = TTF_RenderText_Solid_Wrapped(font_, score_text.c_str(),
+        {255, 255, 255, SDL_ALPHA_OPAQUE}, 0);
+    SDL_Texture* text = SDL_CreateTextureFromSurface(renderer, text_surface);
+    SDL_FreeSurface(text_surface);
+    int text_width, text_height;
+    SDL_QueryTexture(text, nullptr, nullptr, &text_width, &text_height);
+    // some up-scaling
+    text_width *= 2;
+    text_height *= 2;
+
+    base_y = (height-(text_height+name_input_.get_bounding_box().h+ok_button_.get_bounding_box().h+30))/2;
+
+    SDL_Rect render_quad = {(width-text_width)/2, base_y, text_width, text_height};
+    SDL_RenderCopy(renderer, text, nullptr, &render_quad);
+    SDL_DestroyTexture(text);
+
+    name_input_.move((width-name_input_.get_bounding_box().w)/2, base_y += (text_height+20));
+    name_input_.render(renderer);
+    base_y += name_input_.get_bounding_box().h+10;
+  }
+  else {
+    auto const score_text = std::format("Game over!");
+    SDL_Surface* text_surface = TTF_RenderText_Solid_Wrapped(font_, score_text.c_str(),
+        {255, 255, 255, SDL_ALPHA_OPAQUE}, 0);
+    SDL_Texture* text = SDL_CreateTextureFromSurface(renderer, text_surface);
+    SDL_FreeSurface(text_surface);
+    int text_width, text_height;
+    SDL_QueryTexture(text, nullptr, nullptr, &text_width, &text_height);
+    // some up-scaling
+    text_width *= 2;
+    text_height *= 2;
+
+    base_y = (height-(text_height+ok_button_.get_bounding_box().h+20))/2;
+
+    SDL_Rect render_quad = {(width-text_width)/2, base_y, text_width, text_height};
+    SDL_RenderCopy(renderer, text, nullptr, &render_quad);
+    SDL_DestroyTexture(text);
+
+    base_y += text_height+20;
+  }
+
+  ok_button_.move((width-ok_button_.get_bounding_box().w)/2, base_y);
+  ok_button_.render(renderer);
+
+  SDL_RenderPresent(renderer);
+}

+ 32 - 0
game/GameOverState.hxx

@@ -0,0 +1,32 @@
+#pragma once
+
+#ifndef SNAKE_GAMEOVERSTATE_HXX
+#define SNAKE_GAMEOVERSTATE_HXX
+
+#include "AssetManager.hxx"
+#include "GameState.hxx"
+#include "ui/Button.hxx"
+#include "ui/LineInput.hxx"
+
+class GameOverState final : public GameState {
+public:
+  GameOverState();
+
+  void on_enter(GameStateManager& gsm) override;
+
+  void on_leave() override;
+
+  void on_event(GameStateManager& gsm, SDL_Event const& evt) override;
+
+  void update(GameStateManager& gsm, std::chrono::milliseconds delta_time) override;
+
+  void render(SDLRenderer& renderer) override;
+
+private:
+  Button ok_button_{"OK", 0, 0, 800, 80, UiColor::Green};
+  LineInput name_input_{0, 0, 800, 80};
+
+  Asset<TTF_Font*> font_;
+};
+
+#endif // SNAKE_GAMEOVERSTATE_HXX

+ 2 - 0
game/GameStateManager.cxx

@@ -26,6 +26,8 @@ GameState* GameStateManager::enum_to_state(GameStates const state)
     return &menu_;
   case GameStates::Game:
     return &game_;
+  case GameStates::GameOver:
+    return &game_over_;
   }
 }
 

+ 2 - 0
game/GameStateManager.hxx

@@ -7,6 +7,7 @@
 #include "SplashState.hxx"
 #include "MenuState.hxx"
 #include "PlayingState.hxx"
+#include "GameOverState.hxx"
 #include "DummyState.hxx"
 
 #include <stack>
@@ -48,6 +49,7 @@ private:
   SplashState splash_;
   MenuState menu_;
   PlayingState game_;
+  GameOverState game_over_;
   DummyState dummy_;
 };
 

+ 97 - 0
game/HighScoreManager.cxx

@@ -0,0 +1,97 @@
+#include "HighScoreManager.hxx"
+
+#include <SDL.h>
+
+#include <boost/archive/binary_iarchive.hpp>
+#include <boost/archive/binary_oarchive.hpp>
+
+#include <algorithm>
+#include <filesystem>
+#include <fstream>
+
+namespace fs = std::filesystem;
+
+HighScoreManager::HighScoreManager()
+{
+  data_dir_ = SDL_GetPrefPath("draconic-bytes", "snake");
+  fs::create_directories(data_dir_);
+
+  load();
+}
+
+void HighScoreManager::load()
+{
+  auto const filename = fs::path{data_dir_}/"highscore.dat";
+  std::ifstream input{filename, std::ios::binary};
+  if (!input) {
+    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Missing high score file: %s", filename.c_str());
+    return;
+  }
+
+  boost::archive::binary_iarchive archive{input};
+  archive >> high_score_;
+  std::ranges::stable_sort(high_score_.scores_, std::greater<>{}, &Score::points_);
+
+  if (high_score_.scores_.empty()) {
+    SDL_Log("No existing high scores.");
+  }
+  else {
+    SDL_Log("Loaded high scores:");
+    for (auto const& score: high_score_.scores_) {
+      SDL_Log(" - %s: %u", score.player_name_.c_str(), score.points_);
+    }
+  }
+}
+
+void HighScoreManager::save() const
+{
+  auto const filename = fs::path{data_dir_}/"highscore.dat";
+  std::ofstream output{filename, std::ios::binary};
+  if (!output) {
+    SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Failed saving high scores to file %s", filename.c_str());
+    return;
+  }
+
+  boost::archive::binary_oarchive archive{output};
+  archive << high_score_;
+  if (high_score_.scores_.empty()) {
+    SDL_Log("No high scores saved.");
+  }
+  else {
+    SDL_Log("Saved high scores:");
+    for (auto const& score: high_score_.scores_) {
+      SDL_Log(" - %s: %u", score.player_name_.c_str(), score.points_);
+    }
+  }
+}
+
+void HighScoreManager::set_new_score(unsigned int score)
+{
+  if (high_score_.scores_.size()<MAX_SCORES || high_score_.scores_.back().points_<score) {
+    new_score_ = score;
+  }
+}
+
+bool HighScoreManager::has_new_score() const
+{
+  return new_score_.has_value();
+}
+
+void HighScoreManager::provide_name_for_new_score(std::string const& name)
+{
+  if (new_score_.has_value()) {
+    high_score_.scores_.push_back({name, new_score_.value()});
+    new_score_.reset();
+    std::ranges::stable_sort(high_score_.scores_, std::greater<>{}, &Score::points_);
+    if (high_score_.scores_.size()>MAX_SCORES) {
+      high_score_.scores_.resize(MAX_SCORES);
+    }
+    save();
+  }
+}
+
+HighScoreManager& HighScoreManager::instance()
+{
+  static HighScoreManager manager;
+  return manager;
+}

+ 67 - 0
game/HighScoreManager.hxx

@@ -0,0 +1,67 @@
+#pragma once
+
+#ifndef SNAKE_HIGHSCOREMANAGER_HXX
+#define SNAKE_HIGHSCOREMANAGER_HXX
+
+#include <boost/noncopyable.hpp>
+
+#include <boost/serialization/string.hpp>
+#include <boost/serialization/vector.hpp>
+#include <boost/serialization/version.hpp>
+
+#include <optional>
+#include <string>
+#include <vector>
+
+struct Score final {
+  std::string player_name_;
+  unsigned points_;
+
+  template<typename Archive>
+  void serialize(Archive& archive, unsigned const version)
+  {
+    (void) version;
+    archive & player_name_;
+    archive & points_;
+  }
+};
+BOOST_CLASS_VERSION(Score, 0);
+
+struct HighScores final {
+  std::vector<Score> scores_;
+
+  template<typename Archive>
+  void serialize(Archive& archive, unsigned const version)
+  {
+    (void) version;
+    archive & scores_;
+  }
+};
+BOOST_CLASS_VERSION(HighScores, 0);
+
+class HighScoreManager final : private boost::noncopyable {
+public:
+  static int constexpr MAX_SCORES = 10;
+
+  void set_new_score(unsigned score);
+
+  [[nodiscard]] bool has_new_score() const;
+
+  void provide_name_for_new_score(std::string const& name);
+
+  static HighScoreManager& instance();
+
+private:
+  HighScoreManager();
+
+  void load();
+
+  void save() const;
+
+  std::string data_dir_;
+
+  HighScores high_score_;
+  std::optional<unsigned> new_score_;
+};
+
+#endif // SNAKE_HIGHSCOREMANAGER_HXX

+ 3 - 0
game/PlayingState.cxx

@@ -1,5 +1,6 @@
 #include "PlayingState.hxx"
 
+#include "HighScoreManager.hxx"
 #include "GameStateManager.hxx"
 
 #include <algorithm>
@@ -118,11 +119,13 @@ void PlayingState::update(GameStateManager& gsm, std::chrono::milliseconds const
       speed_ = std::min(MAX_SPEED, speed_*ACCELERATION);
       if (!place_target()) {
         // technically the player finished the game at this point
+        HighScoreManager::instance().set_new_score(length_);
         gsm.replace_state(GameStates::GameOver);
       }
     }
 
     if (detect_death(new_pos)) {
+      HighScoreManager::instance().set_new_score(length_);
       gsm.replace_state(GameStates::GameOver);
     }
 

+ 18 - 1
game/ui/LineInput.cxx

@@ -39,6 +39,7 @@ namespace {
 
 LineInput::LineInput(int const x, int const y, int const w, int const h, std::string value)
     :value_{std::move(value)}, x_{x}, y_{y}, w_{w}, h_{h}, focus_{false},
+     visible_{true},
      texture_{"line_input.png"},
      font_{"kenney_pixel.ttf"}
 {
@@ -80,7 +81,7 @@ bool LineInput::has_focus() const
 
 void LineInput::on_event(SDL_Event const& evt)
 {
-  if (!focus_)
+  if (!focus_ || !visible_)
     return;
 
   if (evt.type==SDL_TEXTINPUT) {
@@ -102,6 +103,9 @@ void LineInput::on_event(SDL_Event const& evt)
 
 void LineInput::update(std::chrono::milliseconds const delta_time)
 {
+  if (!visible_)
+    return;
+
   if (focus_ && !SDL_IsTextInputActive())
     SDL_StartTextInput();
 
@@ -112,6 +116,9 @@ void LineInput::update(std::chrono::milliseconds const delta_time)
 
 void LineInput::render(SDLRenderer& renderer)
 {
+  if (!visible_)
+    return;
+
   using namespace std::chrono_literals;
 
   SDL_Texture* const background = texture_;
@@ -161,3 +168,13 @@ void LineInput::set_value(std::string value)
 {
   value_ = std::move(value);
 }
+
+bool LineInput::is_visible() const
+{
+  return visible_;
+}
+
+void LineInput::set_visible(bool visible)
+{
+  visible_ = visible;
+}

+ 5 - 0
game/ui/LineInput.hxx

@@ -33,6 +33,10 @@ public:
 
   [[nodiscard]] bool has_focus() const;
 
+  void set_visible(bool visible);
+
+  [[nodiscard]] bool is_visible() const;
+
   void set_value(std::string value);
 
   [[nodiscard]]char const* value() const;
@@ -41,6 +45,7 @@ private:
   std::string value_;
   int x_, y_, w_, h_;
   bool focus_;
+  bool visible_;
 
   Asset<SDL_Texture*> texture_;
   Asset<TTF_Font*> font_;