Quellcode durchsuchen

:sparkles: lots of quality improvements

 * WASD are now supported
 * Pause-menu can be entered/left via escape (this existed) and pause (this is new)
 * menu and game are more colorful
 * the game is still visible while in the pause menu
 * :construction: started preparations for adding high scores and credits
Felix Bytow vor 1 Jahr
Ursprung
Commit
7576922a6b
7 geänderte Dateien mit 139 neuen und 68 gelöschten Zeilen
  1. 2 0
      game/GameStateManager.hxx
  2. 47 6
      game/MenuState.cxx
  3. 11 3
      game/MenuState.hxx
  4. 61 44
      game/PlayingState.cxx
  5. 5 5
      game/PlayingState.hxx
  6. 1 1
      game/ui/UiColor.cxx
  7. 12 9
      main.cxx

+ 2 - 0
game/GameStateManager.hxx

@@ -19,6 +19,8 @@ enum class GameStates {
   MainMenu,
   Game,
   GameOver,
+  HighScores,
+  Credits,
 };
 
 class GameStateManager final : private boost::noncopyable {

+ 47 - 6
game/MenuState.cxx

@@ -1,9 +1,17 @@
 #include "MenuState.hxx"
+#include "PlayingState.hxx"
 #include "GameStateManager.hxx"
 #include "../SDLRenderer.hxx"
 
 void MenuState::on_enter(GameStateManager& gsm)
 {
+  game_.reset();
+  if (auto const parent = gsm.parent(); parent!=nullptr) {
+    if (auto const game = dynamic_cast<PlayingState*>(parent); game!=nullptr) {
+      game_ = game;
+    }
+  }
+
   new_game_button_.set_on_click([&gsm] {
     if (gsm.parent()!=nullptr)
       gsm.pop_state();
@@ -14,6 +22,14 @@ void MenuState::on_enter(GameStateManager& gsm)
     gsm.pop_state();
   });
 
+  high_score_button_.set_on_click([&gsm] {
+    gsm.push_state(GameStates::HighScores);
+  });
+
+  credits_button_.set_on_click([&gsm] {
+    gsm.push_state(GameStates::Credits);
+  });
+
   quit_button_.set_on_click([&gsm] {
     while (gsm.current()!=nullptr) {
       gsm.pop_state();
@@ -23,8 +39,18 @@ void MenuState::on_enter(GameStateManager& gsm)
 
 void MenuState::on_event(GameStateManager& gsm, SDL_Event const& evt)
 {
-  if (evt.type==SDL_KEYUP && evt.key.keysym.scancode==SDL_SCANCODE_ESCAPE) {
-    gsm.pop_state();
+  if (evt.type==SDL_KEYUP) {
+    switch (evt.key.keysym.scancode) {
+    default:
+      break;
+    case SDL_SCANCODE_PAUSE:
+      if (gsm.parent()==nullptr)
+        break;
+      [[fallthrough]];
+    case SDL_SCANCODE_ESCAPE:
+      gsm.pop_state();
+      break;
+    }
   }
 }
 
@@ -36,6 +62,8 @@ void MenuState::update(GameStateManager& gsm, std::chrono::milliseconds delta_ti
 
   new_game_button_.update();
   continue_button_.update();
+  high_score_button_.update();
+  credits_button_.update();
   quit_button_.update();
 }
 
@@ -44,20 +72,33 @@ void MenuState::render(SDLRenderer& renderer)
   int screen_w, screen_h;
   SDL_GetRendererOutputSize(renderer, &screen_w, &screen_h);
 
-  int const button_count = continue_button_.is_visible() ? 3 : 2;
+  int const button_count = continue_button_.is_visible() ? 5 : 4;
 
   int const x = (screen_w-BUTTON_WIDTH)/2;
-  int const y = (screen_h-(button_count*BUTTON_HEIGHT+(button_count-1)*20))/2;
+  int y = (screen_h-(button_count*BUTTON_HEIGHT+(button_count-1)*20))/2;
 
   new_game_button_.move(x, y);
-  continue_button_.move(x, y+BUTTON_HEIGHT+20);
-  quit_button_.move(x, y+(button_count-1)*(BUTTON_HEIGHT+20));
+  if (continue_button_.is_visible()) {
+    continue_button_.move(x, y += BUTTON_HEIGHT+20);
+  }
+  high_score_button_.move(x, y += BUTTON_HEIGHT+20);
+  credits_button_.move(x, y += BUTTON_HEIGHT+20);
+  quit_button_.move(x, y + BUTTON_HEIGHT+20);
 
   SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
   SDL_RenderClear(renderer);
 
+  if (game_.has_value()) {
+    game_.value()->render_game(renderer, false);
+  }
+
+  SDL_SetRenderDrawColor(renderer, 0, 0, 0, 200);
+  SDL_RenderFillRect(renderer, nullptr);
+
   new_game_button_.render(renderer);
   continue_button_.render(renderer);
+  high_score_button_.render(renderer);
+  credits_button_.render(renderer);
   quit_button_.render(renderer);
 
   SDL_RenderPresent(renderer);

+ 11 - 3
game/MenuState.hxx

@@ -6,6 +6,10 @@
 #include "GameState.hxx"
 #include "ui/Button.hxx"
 
+#include <optional>
+
+class PlayingState;
+
 class MenuState final : public GameState {
 public:
   void on_enter(GameStateManager& gsm) override;
@@ -18,11 +22,15 @@ public:
 
 private:
   static int constexpr BUTTON_HEIGHT = 80;
-  static int constexpr BUTTON_WIDTH = 300;
+  static int constexpr BUTTON_WIDTH = 350;
 
   Button new_game_button_{"New game", 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, UiColor::Green};
-  Button continue_button_{"Continue", 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT};
-  Button quit_button_{"Quit", 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT};
+  Button continue_button_{"Continue", 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, UiColor::Blue};
+  Button high_score_button_{"High Scores", 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT};
+  Button credits_button_{"Credits", 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT};
+  Button quit_button_{"Quit", 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, UiColor::Red};
+
+  std::optional<PlayingState*> game_{};
 };
 
 #endif // SNAKE_MENUSTATE_HXX

+ 61 - 44
game/PlayingState.cxx

@@ -30,8 +30,6 @@ static bool operator!=(SDL_Point const& lhs, SDL_Point const& rhs)
   return lhs.x!=rhs.x || lhs.y!=rhs.y;
 }
 
-unsigned PlayingState::last_high_score_{0};
-
 PlayingState::PlayingState()
     :generator_{std::random_device{}()}, font_{"kenney_pixel.ttf"}
 {
@@ -56,38 +54,48 @@ void PlayingState::on_enter(GameStateManager& gsm)
 
 void PlayingState::on_event(GameStateManager& gsm, SDL_Event const& evt)
 {
-  if (evt.type==SDL_KEYUP && evt.key.keysym.scancode==SDL_SCANCODE_ESCAPE) {
-    gsm.push_state(GameStates::MainMenu);
+  if (evt.type==SDL_KEYUP) {
+    auto const scancode = evt.key.keysym.scancode;
+    if (scancode==SDL_SCANCODE_ESCAPE || scancode==SDL_SCANCODE_PAUSE)
+      gsm.push_state(GameStates::MainMenu);
   }
   else if (evt.type==SDL_KEYDOWN) {
     switch (evt.key.keysym.scancode) {
     default:
       break;
     case SDL_SCANCODE_UP:
+      [[fallthrough]];
+    case SDL_SCANCODE_W:
       if (direction_==Direction::Left || direction_==Direction::Right) {
-        direction_ = Direction::Up;
+        new_direction_ = Direction::Up;
       }
       break;
+    case SDL_SCANCODE_S:
+      [[fallthrough]];
     case SDL_SCANCODE_DOWN:
       if (direction_==Direction::Left || direction_==Direction::Right) {
-        direction_ = Direction::Down;
+        new_direction_ = Direction::Down;
       }
       break;
+    case SDL_SCANCODE_A:
+      [[fallthrough]];
     case SDL_SCANCODE_LEFT:
       if (direction_==Direction::Up || direction_==Direction::Down) {
-        direction_ = Direction::Left;
+        new_direction_ = Direction::Left;
       }
       break;
+    case SDL_SCANCODE_D:
+      [[fallthrough]];
     case SDL_SCANCODE_RIGHT:
       if (direction_==Direction::Up || direction_==Direction::Down) {
-        direction_ = Direction::Right;
+        new_direction_ = Direction::Right;
       }
       break;
     }
   }
 }
 
-void PlayingState::update(GameStateManager& gsm, std::chrono::milliseconds delta_time)
+void PlayingState::update(GameStateManager& gsm, std::chrono::milliseconds const delta_time)
 {
   auto const distance = speed_*static_cast<float>(delta_time.count());
   if (distance>MAX_DISTANCE) {
@@ -95,8 +103,10 @@ void PlayingState::update(GameStateManager& gsm, std::chrono::milliseconds delta
     return;
   }
 
+  auto const direction = new_direction_.value_or(direction_);
+
   SDL_FPoint new_head = head_;
-  switch (direction_) {
+  switch (direction) {
   case Direction::Up:
     new_head.y -= distance;
     break;
@@ -114,14 +124,18 @@ void PlayingState::update(GameStateManager& gsm, std::chrono::milliseconds delta
   auto const old_pos = ::head_position(head_);
   auto const new_pos = ::head_position(new_head);
   if (old_pos!=new_pos) {
+    if (new_direction_.has_value()) {
+      direction_ = new_direction_.value();
+      new_direction_.reset();
+    }
+
     if (new_pos==target_) {
       ++length_;
-      speed_ *= ACCELERATION;
+      speed_ = std::min(MAX_SPEED, speed_*ACCELERATION);
       place_target();
     }
 
     if (detect_death(new_pos)) {
-      last_high_score_ = length_;
       gsm.replace_state(GameStates::GameOver);
     }
 
@@ -135,33 +149,7 @@ void PlayingState::update(GameStateManager& gsm, std::chrono::milliseconds delta
 
 void PlayingState::render(SDLRenderer& renderer)
 {
-
-  int width, height;
-  SDL_GetRendererOutputSize(renderer, &width, &height);
-
-  SDL_Rect playing_field;
-  double const ratio = static_cast<double>(CELLS_X)/CELLS_Y;
-
-  if (width<height*ratio) {
-    playing_field.w = width-20;
-    playing_field.h = static_cast<int>(playing_field.w/ratio);
-  }
-  else {
-    playing_field.h = height-70;
-    playing_field.w = static_cast<int>(playing_field.h*ratio);
-  }
-
-  playing_field.x = (width-playing_field.w)/2;
-  playing_field.y = 50;
-
-  SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
-  SDL_RenderClear(renderer);
-
-  render_ui(renderer, playing_field);
-  render_target(renderer, playing_field);
-  render_snake(renderer, playing_field);
-
-  SDL_RenderPresent(renderer);
+  render_game(renderer);
 }
 
 void PlayingState::render_ui(SDLRenderer& renderer, SDL_Rect const& playing_field)
@@ -178,7 +166,7 @@ void PlayingState::render_ui(SDLRenderer& renderer, SDL_Rect const& playing_fiel
   SDL_RenderCopy(renderer, text, nullptr, &render_quad);
   SDL_DestroyTexture(text);
 
-  SDL_SetRenderDrawColor(renderer, 255, 255, 255, SDL_ALPHA_OPAQUE);
+  SDL_SetRenderDrawColor(renderer, 249, 95, 0, SDL_ALPHA_OPAQUE);
   SDL_RenderDrawRect(renderer, &playing_field);
 }
 
@@ -211,7 +199,7 @@ void PlayingState::render_target(SDLRenderer& renderer, SDL_Rect const& playing_
       .h = static_cast<int>(ratio),
   };
 
-  SDL_SetRenderDrawColor(renderer, 0, 255, 0, SDL_ALPHA_OPAQUE);
+  SDL_SetRenderDrawColor(renderer, 76, 208, 45, SDL_ALPHA_OPAQUE);
   SDL_RenderFillRect(renderer, &target_rect);
 }
 
@@ -234,13 +222,14 @@ void PlayingState::render_snake(SDLRenderer& renderer, SDL_Rect const& playing_f
   };
 
   SDL_SetRenderDrawColor(renderer, 255, 255, 255, SDL_ALPHA_OPAQUE);
-  render_dot(::head_position(head_), 1.0);
   double size = 1.0;
   double const decay = 1.0/static_cast<double>(tail_.size()+1);
   for (auto const& particle: tail_) {
     size = std::max(0.0, size-decay);
     render_dot(particle, size);
   }
+  SDL_SetRenderDrawColor(renderer, 0, 170, 231, SDL_ALPHA_OPAQUE);
+  render_dot(::head_position(head_), 1.0);
 }
 
 bool PlayingState::detect_death(SDL_Point const& position)
@@ -255,7 +244,35 @@ bool PlayingState::detect_death(SDL_Point const& position)
   }));
 }
 
-unsigned PlayingState::last_high_score()
+void PlayingState::render_game(SDLRenderer& renderer, bool is_current_state)
 {
-  return last_high_score_;
+  int width, height;
+  SDL_GetRendererOutputSize(renderer, &width, &height);
+
+  SDL_Rect playing_field;
+  double const ratio = static_cast<double>(CELLS_X)/CELLS_Y;
+
+  if (width<height*ratio) {
+    playing_field.w = width-20;
+    playing_field.h = static_cast<int>(playing_field.w/ratio);
+  }
+  else {
+    playing_field.h = height-70;
+    playing_field.w = static_cast<int>(playing_field.h*ratio);
+  }
+
+  playing_field.x = (width-playing_field.w)/2;
+  playing_field.y = 50;
+
+  if (is_current_state) {
+    SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
+    SDL_RenderClear(renderer);
+  }
+
+  render_ui(renderer, playing_field);
+  render_snake(renderer, playing_field);
+  render_target(renderer, playing_field);
+
+  if (is_current_state)
+    SDL_RenderPresent(renderer);
 }

+ 5 - 5
game/PlayingState.hxx

@@ -7,6 +7,7 @@
 #include "AssetManager.hxx"
 
 #include <deque>
+#include <optional>
 #include <random>
 
 enum class Direction {
@@ -35,7 +36,7 @@ public:
 
   void render(SDLRenderer& renderer) override;
 
-  static unsigned last_high_score();
+  void render_game(SDLRenderer& renderer, bool is_current_state = true);
 
 private:
 
@@ -51,16 +52,15 @@ private:
 
   bool detect_death(SDL_Point const& position);
 
-  static unsigned last_high_score_;
-
   std::default_random_engine generator_;
   std::uniform_int_distribution<int> distribution_position_x_{0, CELLS_X-1};
   std::uniform_int_distribution<int> distribution_position_y_{0, CELLS_Y-1};
 
-  SDL_Point target_;
+  SDL_Point target_{};
   unsigned length_{0u};
   Direction direction_{Direction::Left};
-  SDL_FPoint head_;
+  std::optional<Direction> new_direction_{};
+  SDL_FPoint head_{};
   std::deque<SDL_Point> tail_;
   float speed_{0.001f};
 

+ 1 - 1
game/ui/UiColor.cxx

@@ -9,7 +9,7 @@ std::string ui_image(std::string_view name, UiColor const color)
     return std::format("blue_{}.png", name);
   case UiColor::Green:
     return std::format("green_{}.png", name);
-  case UiColor::Grey:
+  default:
     return std::format("grey_{}.png", name);
   case UiColor::Red:
     return std::format("red_{}.png", name);

+ 12 - 9
main.cxx

@@ -21,22 +21,25 @@ void main_loop(SDLRenderer& renderer)
       if (evt.type==SDL_QUIT)
         return;
 
-      auto const state = gsm.current();
-      if (state==nullptr)
+      if (auto const state = gsm.current(); state!=nullptr)
+        state->on_event(gsm, evt);
+      else
         return;
-      state->on_event(gsm, evt);
     }
 
-    auto const state = gsm.current();
-    if (state==nullptr)
-      return;
-
     auto const end = high_resolution_clock::now();
     auto const delta_time = duration_cast<milliseconds>(end-start);
     start = end;
 
-    state->update(gsm, delta_time);
-    state->render(renderer);
+    if (auto const state = gsm.current(); state != nullptr)
+      state->update(gsm, delta_time);
+    else
+      return;
+
+    if (auto const state = gsm.current(); state != nullptr)
+      state->render(renderer);
+    else
+      return;
   }
 }