diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 06f96bf..f23e631 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -1,14 +1,51 @@ -add_executable(cpp_game - colour.cpp - entity.cpp - main.cpp - rectangle.cpp - vector2.cpp - window.cpp +cmake_minimum_required(VERSION 3.10) +project(cpp_game) + +set(CMAKE_CXX_STANDARD 20) + +# SDL2 +set(SDL2_DIR "C:/libs/SDL2-2.24.2") +set(SDL2_INCLUDE_DIR "${SDL2_DIR}/include") +set(SDL2_LIB_DIR "${SDL2_DIR}/lib/x64") +# C:/libs/SDL2-2.24.2/include +# C:/libs/SDL2-2.24.2/lib/x64 +# SDL2_ttf +set(SDL2_TTF_DIR "C:/libs/SDL2_ttf-2.22.0") +set(SDL2_TTF_INCLUDE_DIR "${SDL2_TTF_DIR}/include") +set(SDL2_TTF_LIB_DIR "${SDL2_TTF_DIR}/lib/x64") + +# Include both SDL2 and SDL2_ttf headers +include_directories(${SDL2_INCLUDE_DIR} ${SDL2_TTF_INCLUDE_DIR}) +link_directories(${SDL2_LIB_DIR} ${SDL2_TTF_LIB_DIR}) + +file(GLOB SOURCES + *.cpp + *.h ) -target_link_directories(cpp_game PRIVATE ${sdl_BINARY_DIR}) -target_include_directories(cpp_game PRIVATE ${sdl_SOURCE_DIR}/include) -target_compile_features(cpp_game PRIVATE cxx_std_20) +add_executable(cpp_game ${SOURCES}) + +# Link against both SDL2 and SDL2_ttf libraries +target_link_libraries(cpp_game SDL2.lib SDL2main.lib SDL2_ttf.lib) -target_link_libraries(cpp_game SDL2::SDL2-static) +# Copy both SDL2 and SDL2_ttf DLLs after build +add_custom_command(TARGET cpp_game POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${SDL2_LIB_DIR}/SDL2.dll" + "${SDL2_TTF_LIB_DIR}/SDL2_ttf.dll" + $ +) + +# Create resources directory and copy font file +add_custom_command(TARGET cpp_game POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory + $/resources + +) + +# Copy font file to resources directory +add_custom_command(TARGET cpp_game POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_SOURCE_DIR}/resources/MinecraftTen-VGORe.ttf" + $/resources/ +) diff --git a/cpp/README.md b/cpp/README.md new file mode 100644 index 0000000..d657fed --- /dev/null +++ b/cpp/README.md @@ -0,0 +1,185 @@ +# 🎮 BreakOut Game + +
+ +[![C++](https://img.shields.io/badge/C%2B%2B-00599C?style=for-the-badge&logo=c%2B%2B&logoColor=white)](https://en.cppreference.com/) +[![SDL2](https://img.shields.io/badge/SDL2-FFD700?style=for-the-badge&logo=SDL&logoColor=black)](https://www.libsdl.org/) +[![CMake](https://img.shields.io/badge/CMake-064F8C?style=for-the-badge&logo=cmake&logoColor=white)](https://cmake.org/) +
+ +## 📝 Description + +A classic arcade-style BreakOut game built with modern C++ and SDL2. Break all the bricks with your ball while controlling the paddle to prevent the ball from falling! Features multiple brick types, progressive difficulty, and high score tracking. + +## 🖼️ Game Screens + +
+ +### 🎮 Start Screen +![Start Screen](./screenshots/start_screen.png) + +### 🎲 Playing Screen +![Playing Screen](./screenshots/playing_screen.png) + +### 🏆 Win Screen +![Win Screen](./screenshots/win_screen.png) + +### ❌ Game Over Screen +![Game Over Screen](./screenshots/game_over_screen.png) + +
+ +## 🎯 Game Features + +- 🏓 Smooth paddle controls +- 🔴 Multiple brick types with different hit points +- ⚡ Dynamic ball physics with speed progression +- 🎨 Clean visual design with animations +- 🏆 Score system and high score tracking +- 🎮 Multiple game states (Title, Playing, Win, Game Over) +- 💾 Persistent high score storage + +## 🎮 Controls + +| Key | Action | +|-----|--------| +| ⬅️ Left Arrow | Move paddle left | +| ➡️ Right Arrow | Move paddle right | +| ⏎ Space | Start/Restart game | +| ❌ Escape | Exit game | + +## 🧱 Brick Types + +| Color | Hits Required | Points | +|-------|---------------|--------| +| 🟢 Green | 1 | 100 | +| 🟠 Orange | 2 | 200 | +| 🔴 Red | 3 | 300 | + +## 🎯 Game Mechanics + +- Ball speed increases as bricks are destroyed +- Different brick types require multiple hits +- Score based on brick type and destruction +- High score persistence between sessions +- Dynamic paddle collision angles + +## 🔄 Game States + +```mermaid +graph TD + Start[Start Game] -->|Initialize| Init[Initialize Game State] + Init -->|Enter Main Loop| Loop[Game Loop] + Loop -->|Process Input| Input[Handle Player Input] + Input -->|Update| Update[Update Game State] + Update -->|Render| Render[Render Graphics] + Render -->|Check| Check[Check Game State] + Check -->|Win Condition Met| Win[Display Win Screen] + Check -->|Game Over Condition Met| GameOver[Display Game Over Screen] + Check -->|Continue| Loop + Win -->|Restart| Start + GameOver -->|Restart| Start +``` + +## 📁 Project Structure + +### 🎯 Core Files +- `main.cpp` - 🎮 Game loop and core logic +- `window.cpp/h` - 🖼️ SDL window management +- `entity.cpp/h` - 🎲 Game objects (paddle, ball, bricks) + +### 🛠️ Support Files +- `vector2.cpp/h` - ➡️ 2D vector mathematics +- `rectangle.cpp/h` - 📦 Collision shapes +- `colour.cpp/h` - 🎨 Color management +- `key_event.h` - ⌨️ Input handling + +## 🚀 Building the Game + +### Prerequisites +- 📌 C++20 compiler +- 📌 SDL2 library +- 📌 CMake 3.10+ + +### Build Steps + +```bash +# 1. Clone the repository +git clone https://github.com/Prince-Patel84/asm_c_cpp/tree/develop/cpp + +# 2. Create build directory +mkdir build +cd build + +# 3. Generate build files +cmake .. + +# 4. Build the game +cmake --build . +``` + +## 🎮 How to Play + +1. 🚀 Launch the game +2. 🏓 Use left and right arrows to move the paddle +3. 🎯 Bounce the ball to break all bricks +4. 🏆 Try to clear all bricks without losing the ball +5. 📊 Score points based on brick type +6. 💾 Beat your high score! + +## 🔧 Technical Implementation + +### Physics System 🎯 +- Accurate ball bouncing mechanics +- Precise collision detection +- Dynamic paddle reflection angles +- Progressive ball speed system + +### Rendering Engine 🎨 +- Hardware-accelerated graphics +- Smooth animations +- Efficient frame rendering +- Dynamic color transitions + +### Game State Management 🎮 +- Title screen with animations +- Playing state with score display +- Win condition with final score +- Game over state with restart option + +### Score System 📊 +- Points based on brick type +- High score tracking +- Persistent storage +- Visual score display + +## 🛠️ Future Enhancements + +- [ ] 🔊 Sound effects +- [ ] 🎯 Multiple levels +- [ ] ⚡ Power-ups +- [ ] 💖 Lives system +- [ ] 🎨 Custom themes + +## 👥 Team Motion Minds + +
+ +### 🎮 Game Development Team + +| Name | Student ID | +|------|------------| +| Prince Patel | 202401151 | +| Vishwa Prajapati | 202401163 | +| Dhruv Patel | 202401142 | + +Built with 💖 and lots of 🎮 +
+ +--- + +
+ +### 🌟 Star this repository if you find it helpful! + +
\ No newline at end of file diff --git a/cpp/colour.cpp b/cpp/colour.cpp index 04926a9..2a4319c 100644 --- a/cpp/colour.cpp +++ b/cpp/colour.cpp @@ -25,7 +25,7 @@ Colour::Colour(std::uint8_t r, std::uint8_t g, std::uint8_t b) } Colour::Colour(std::uint32_t colour) - : Colour((colour >> 16) & 0xff, (colour >> 7) & 0xff, colour & 0xff) + : Colour((colour >> 16) & 0xff, (colour >> 8) & 0xff, colour & 0xff) { } diff --git a/cpp/key_event.h b/cpp/key_event.h index ca33eaf..fd9c5c5 100644 --- a/cpp/key_event.h +++ b/cpp/key_event.h @@ -26,6 +26,7 @@ enum class Key ESCAPE, LEFT, RIGHT, + SPACE, }; /** diff --git a/cpp/main.cpp b/cpp/main.cpp index 032feb2..7ad91da 100644 --- a/cpp/main.cpp +++ b/cpp/main.cpp @@ -6,6 +6,10 @@ #include #include +#include +#include +#include +#include #include "colour.h" #include "entity.h" @@ -17,25 +21,72 @@ namespace { +enum class GameState +{ + TITLE_SCREEN, + PLAYING, + GAME_OVER, + WIN +}; + +float title_animation_time = 0.0f; +const float ANIMATION_SPEED = 0.05f; + +// Add these near other global variables +int current_score = 0; +const int BRICK_POINTS = 100; // Points per brick destroyed +int high_score = 0; +const std::string HIGH_SCORE_FILE = "highscore.txt"; + +// Add near other global variables +struct Brick { + cpp::Entity entity; + int hits_required; +}; + +// Different brick colors based on hits required +const uint32_t BRICK_COLORS[] = { + 0x00FF00, // Green (1 hit) + 0xFFA500, // Orange (2 hits) + 0xFF0000 // Red (3 hits) +}; + +// Points awarded based on brick strength +const int BRICK_POINTS_BY_STRENGTH[] = { + 100, // 1-hit brick + 200, // 2-hit brick + 300 // 3-hit brick +}; + +// Add near other global variables +const float INITIAL_BALL_SPEED = 2.0f; +const float MAX_BALL_SPEED = 4.0f; +const float SPEED_INCREMENT = 0.1f; +float current_ball_speed = INITIAL_BALL_SPEED; + +// Add this new variable to scale paddle speed with ball speed +const float PADDLE_SPEED_MULTIPLIER = 0.5f; // Adjust this multiplier as needed + /** * Helper function to create a row of 10 bricks. * - * @param entities + * @param bricks * Collection to add new entities to. * * @param y * Y coordinate of row. * - * @param colour - * Colour of bricks. + * @param hits_required + * Number of hits required to destroy the brick */ -void create_brick_row(std::vector &entities, float y, const cpp::Colour &colour) +void create_brick_row(std::vector &bricks, float y, int hits_required) { auto x = 20.0f; - + for (auto i = 0u; i < 10u; ++i) { - entities.push_back({{{x, y}, 58.0f, 20.0f}, colour}); + cpp::Entity entity{{{x, y}, 58.0f, 20.0f}, BRICK_COLORS[hits_required - 1]}; + bricks.push_back({entity, hits_required}); x += 78.0f; } } @@ -52,16 +103,16 @@ void create_brick_row(std::vector &entities, float y, const cpp::Co * @param paddle * Paddle to check for collisions with. * - * @param entities + * @param bricks * Collection of all entities, brick entities will be removed if a collision is detected. */ void check_collisions( const cpp::Entity &ball, cpp::Vector2 &ball_velocity, const cpp::Entity &paddle, - std::vector &entities) + std::vector &bricks) { - // check and handle ball and paddle collision + // Paddle collision if (paddle.intersects(ball)) { const auto ball_pos = ball.rectangle().position; @@ -69,35 +120,57 @@ void check_collisions( if (ball_pos.x < paddle_pos.x + 100.0f) { - ball_velocity.x = -0.7f; - ball_velocity.y = -0.7f; + ball_velocity.x = -current_ball_speed * 0.7f; + ball_velocity.y = -current_ball_speed * 0.7f; } else if (ball_pos.x < paddle_pos.x + 200.0f) { - ball_velocity.x = 0.0f; - ball_velocity.y = -1.0f; + // ball_velocity.x = 0.0f; + ball_velocity.y = -current_ball_speed; } else { - ball_velocity.x = 0.7f; - ball_velocity.y = -0.7f; + ball_velocity.x = current_ball_speed * 0.7f; + ball_velocity.y = -current_ball_speed * 0.7f; } } else { - // only check brick intersections if we didn't intersect the paddle, unlikely these will both happen in the same - // frame due to the layout of the game - - // iterate over all entities, skipping the first two as these are the paddle and ball - auto bricks_view = entities | std::views::drop(2u); - auto hit_brick = - std::ranges::find_if(bricks_view, [&ball](const auto &brick) { return ball.intersects(brick); }); - - if (hit_brick != std::ranges::end(bricks_view)) + // Check brick collisions + for (auto it = bricks.begin(); it != bricks.end(); ++it) { - // we hit a brick so update ball velocity and remove brick entity - ball_velocity.y *= -1.0f; - entities.erase(hit_brick); + if (it->entity.intersects(ball)) + { + // Reduce hits required and update color + it->hits_required--; + + if (it->hits_required <= 0) + { + // Add score based on original brick strength + current_score += BRICK_POINTS_BY_STRENGTH[it->hits_required]; + bricks.erase(it); + + // Increase ball speed + current_ball_speed = std::min(current_ball_speed + SPEED_INCREMENT, MAX_BALL_SPEED); + + // Adjust current velocity to match new speed while maintaining direction + float current_speed = std::sqrt(ball_velocity.x * ball_velocity.x + + ball_velocity.y * ball_velocity.y); + ball_velocity.x = (ball_velocity.x / current_speed) * current_ball_speed; + ball_velocity.y = (ball_velocity.y / current_speed) * current_ball_speed; + } + else + { + // Update brick color based on remaining hits + it->entity = cpp::Entity{ + it->entity.rectangle(), + BRICK_COLORS[it->hits_required - 1] + }; + } + + ball_velocity.y *= -1.0f; + break; + } } } } @@ -118,12 +191,12 @@ void update_ball(cpp::Entity &ball, cpp::Vector2 &velocity) const auto ball_pos = ball.rectangle().position; - if ((ball_pos.y > 800.0f) || (ball_pos.y < 0.0f)) + if (ball_pos.y < 0.0f) { velocity.y *= -1.0f; } - if ((ball_pos.x > 800.0f) || (ball_pos.x < 0.0f)) + if ((ball_pos.x + 10.0f > 800.0f) || (ball_pos.x < 0.0f)) { velocity.x *= -1.0f; } @@ -140,7 +213,167 @@ void update_ball(cpp::Entity &ball, cpp::Vector2 &velocity) */ void update_paddle(cpp::Entity &paddle, const cpp::Vector2 &velocity) { - paddle.translate(velocity); + // Calculate paddle speed based on current ball speed + float paddle_speed = current_ball_speed * PADDLE_SPEED_MULTIPLIER; + paddle.translate(cpp::Vector2{velocity.x * paddle_speed, 0.0f}); +} + +/** + * Helper function to check if the game is over (ball falls below paddle) + * + * @param ball + * Ball entity to check position + * + * @returns + * True if game is over, false otherwise + */ +bool is_game_over(const cpp::Entity &ball) +{ + return ball.rectangle().position.y > 800.0f; +} + +/** + * Helper function to reset the ball to initial position + * + * @param ball + * Ball entity to reset + * + * @param ball_velocity + * Ball velocity to reset + */ +void reset_ball(cpp::Entity &ball, cpp::Vector2 &ball_velocity) +{ + ball = cpp::Entity{{{420.0f, 400.0f}, 10.0f, 10.0f}, 0xFFFFFF}; + ball_velocity = cpp::Vector2{0.0f, current_ball_speed}; +} + +/** + * Helper function to reset the paddle to initial position + * + * @param paddle + * Paddle entity to reset + * + * @param paddle_velocity + * Paddle velocity to reset + */ +void reset_paddle(cpp::Entity &paddle, cpp::Vector2 &paddle_velocity) +{ + paddle = cpp::Entity{{{300.0f, 780.0f}, 300.0f, 20.0f}, 0xFFFFFF}; + paddle_velocity = cpp::Vector2{0.0f, 0.0f}; +} + +/** + * Helper function to render the animated title screen + * + * @param window + * Window to render to + * + * @param animation_time + * Current animation time + */ +void render_title_screen(const cpp::Window& window, float& animation_time, const std::vector &entities) +{ + // Update animation time + animation_time += ANIMATION_SPEED; + + // Render game entities in background with dimmed effect + window.render(entities); + + // Semi-transparent overlay to dim the background + window.render_overlay(0x000000, 180); // Black with alpha 180 (partially transparent) + + // Rainbow colors for BREAKOUT letters + const uint32_t colors[] = { + 0xFF0000, // Red + 0xFF7F00, // Orange + 0xFFFF00, // Yellow + 0x00FF00, // Green + 0x0000FF, // Blue + 0x4B0082, // Indigo + 0x9400D3, // Violet + 0xFF1493 // Pink + }; + + // Render each letter of BREAKOUT with different colors and offsets + const char* letters = "BREAKOUT"; + int base_x = 150; + int letter_spacing = 70; + + for (int i = 0; letters[i] != '\0'; i++) + { + char letter[2] = {letters[i], '\0'}; + float letter_offset = std::sin(animation_time + i * 0.5f) * 15.0f; + window.render_text(letter, + base_x + i * letter_spacing, + 200 + static_cast(letter_offset), + 100, + colors[i % 8]); + } + + // Only show the essential text with pulsing effect + float scale = 1.0f + std::sin(animation_time * 2.0f) * 0.1f; + window.render_text("Press SPACE to Start", + 200, + 500 + static_cast(std::sin(animation_time) * 20.0f), + 48, + 0x00FF00); // Green +} + +// Add this helper function to reset score +void reset_score() +{ + current_score = 0; +} + +/** + * Helper function to check if all bricks are destroyed + * + * @param bricks + * Collection of all entities + * + * @returns + * True if no bricks remain, false otherwise + */ +bool check_win_condition(const std::vector &bricks) +{ + return bricks.empty(); +} + +// Helper function to load high score +void load_high_score() { + std::ifstream file(HIGH_SCORE_FILE); + if (file.is_open()) { + file >> high_score; + file.close(); + } +} + +// Helper function to save high score +void save_high_score() { + std::ofstream file(HIGH_SCORE_FILE); + if (file.is_open()) { + file << high_score; + file.close(); + } +} + +// Helper function to update high score +void update_high_score() { + if (current_score > high_score) { + high_score = current_score; + save_high_score(); + } +} + +// Add this helper function in the anonymous namespace +void render_entity(const cpp::Window& window, const cpp::Entity& entity) { + std::vector temp{entity}; + window.render(temp); +} + +// Helper function to reset ball speed +void reset_ball_speed() { + current_ball_speed = INITIAL_BALL_SPEED; } } @@ -149,22 +382,31 @@ int main() { std::cout << "hello world\n"; + // Load high score at startup + load_high_score(); + const cpp::Window window{}; auto running = true; - std::vector entities{ - {{{300.0f, 780.0f}, 300.0f, 20.0f}, 0xFFFFFF}, {{{420.0f, 400.0f}, 10.0f, 10.0f}, 0xFFFFFF}}; + // Initialize game state to title screen + GameState game_state = GameState::TITLE_SCREEN; - create_brick_row(entities, 50.0f, 0xff0000); - create_brick_row(entities, 80.0f, 0xff0000); - create_brick_row(entities, 110.0f, 0xffa500); - create_brick_row(entities, 140.0f, 0xffa500); - create_brick_row(entities, 170.0f, 0x00ff00); - create_brick_row(entities, 200.0f, 0x00ff00); + // Change entities vector to separate paddle, ball, and bricks + cpp::Entity paddle{{{300.0f, 780.0f}, 300.0f, 20.0f}, 0xFFFFFF}; + cpp::Entity ball{{{420.0f, 400.0f}, 10.0f, 10.0f}, 0xFFFFFF}; + std::vector bricks; + + // Create rows with different hit requirements + create_brick_row(bricks, 50.0f, 3); // Red bricks (3 hits) + create_brick_row(bricks, 80.0f, 3); + create_brick_row(bricks, 110.0f, 2); // Orange bricks (2 hits) + create_brick_row(bricks, 140.0f, 2); + create_brick_row(bricks, 170.0f, 1); // Green bricks (1 hit) + create_brick_row(bricks, 200.0f, 1); cpp::Vector2 ball_velocity{0.0f, 1.0f}; cpp::Vector2 paddle_velocity{0.0f, 0.0f}; - const float paddle_speed = 1.0f; + const float paddle_speed = 2.0f; auto left_press = false; auto right_press = false; @@ -181,13 +423,50 @@ int main() { running = false; } - else if (event->key == LEFT) + else if ((event->key_state == DOWN) && (event->key == SPACE)) { - left_press = (event->key_state == DOWN) ? true : false; + if (game_state == GameState::TITLE_SCREEN) + { + game_state = GameState::PLAYING; + reset_ball_speed(); + reset_ball(ball, ball_velocity); + reset_paddle(paddle, paddle_velocity); + reset_score(); + left_press = false; + right_press = false; + } + else if (game_state == GameState::GAME_OVER || game_state == GameState::WIN) + { + game_state = GameState::PLAYING; + // Reset everything + bricks.clear(); + + // Recreate all bricks + create_brick_row(bricks, 50.0f, 3); + create_brick_row(bricks, 80.0f, 3); + create_brick_row(bricks, 110.0f, 2); + create_brick_row(bricks, 140.0f, 2); + create_brick_row(bricks, 170.0f, 1); + create_brick_row(bricks, 200.0f, 1); + + reset_ball_speed(); + reset_ball(ball, ball_velocity); + reset_paddle(paddle, paddle_velocity); + reset_score(); + left_press = false; + right_press = false; + } } - else if (event->key == RIGHT) + else if (game_state == GameState::PLAYING) { - right_press = (event->key_state == DOWN) ? true : false; + if (event->key == LEFT) + { + left_press = (event->key_state == DOWN) ? true : false; + } + else if (event->key == RIGHT) + { + right_press = (event->key_state == DOWN) ? true : false; + } } } else @@ -196,34 +475,125 @@ int main() } } - if ((left_press && right_press) || (!left_press && !right_press)) - { - paddle_velocity.x = 0.0f; - } - else if (left_press) - { - paddle_velocity.x = -paddle_speed; - } - else if (right_press) - { - paddle_velocity.x = paddle_speed; - } + // Clear screen at start of frame + window.clear(); - // scope the references to the paddle and ball, if check_collisions results in an entity being removed then - // that will invalidate our references, so prevent them from being accidentally used later + switch (game_state) { - auto &paddle = entities[0]; - auto &ball = entities[1]; + case GameState::TITLE_SCREEN: + { + // Create a vector with all entities for the background + std::vector all_entities; + all_entities.push_back(paddle); + all_entities.push_back(ball); + for (const auto& brick : bricks) { + all_entities.push_back(brick.entity); + } + + render_title_screen(window, title_animation_time, all_entities); + // Add high score display on title screen + std::string high_score_text = "High Score: " + std::to_string(high_score); + window.render_text(high_score_text, 300, 600, 36, 0xFFD700); // Gold color + break; + } + case GameState::PLAYING: + { + // Update game state + if ((left_press && right_press) || (!left_press && !right_press)) + { + paddle_velocity.x = 0.0f; + } + else if (left_press) + { + paddle_velocity.x = -paddle_speed; + } + else if (right_press) + { + paddle_velocity.x = paddle_speed; + } + + // Update entities + update_paddle(paddle, paddle_velocity); + update_ball(ball, ball_velocity); + check_collisions(ball, ball_velocity, paddle, bricks); - update_paddle(paddle, paddle_velocity); - update_ball(ball, ball_velocity); - check_collisions(ball, ball_velocity, paddle, entities); + if (is_game_over(ball)) + { + game_state = GameState::GAME_OVER; + } + else if (check_win_condition(bricks)) + { + game_state = GameState::WIN; + } + + // Render entities + render_entity(window, paddle); + render_entity(window, ball); + for (const auto& brick : bricks) + { + render_entity(window, brick.entity); + } + + // Display scores + std::string score_text = "Score: " + std::to_string(current_score); + window.render_text(score_text, 10, 10, 24, 0xFFFFFF); + std::string high_score_text = "High Score: " + std::to_string(high_score); + window.render_text(high_score_text, 600, 10, 24, 0xFFD700); + break; + } + case GameState::GAME_OVER: + { + render_entity(window, paddle); + render_entity(window, ball); + for (const auto& brick : bricks) + { + render_entity(window, brick.entity); + } + window.render_text("Game Over!", 200, 250, 72); + + // Update high score before displaying + update_high_score(); + + std::string final_score = "Final Score: " + std::to_string(current_score); + window.render_text(final_score, 200, 350, 48, 0xFFFF00); + + // Display high score + std::string high_score_text = "High Score: " + std::to_string(high_score); + window.render_text(high_score_text, 200, 400, 48, 0xFFD700); + + window.render_text("Press Space to Restart", 200, 450, 36); + break; + } + case GameState::WIN: + { + render_entity(window, paddle); + render_entity(window, ball); + for (const auto& brick : bricks) + { + render_entity(window, brick.entity); + } + window.render_overlay(0x000000, 180); + window.render_text("YOU WIN!", 200, 250, 72, 0x00FF00); + + // Update high score before displaying + update_high_score(); + + std::string final_score = "Final Score: " + std::to_string(current_score); + window.render_text(final_score, 200, 350, 48, 0xFFFF00); + + // Display high score + std::string high_score_text = "High Score: " + std::to_string(high_score); + window.render_text(high_score_text, 200, 400, 48, 0xFFD700); + + window.render_text("Press Space to Play Again", 180, 450, 36, 0xFFFFFF); + break; + } } - window.render(entities); + // Present the frame + window.present(); } std::cout << "goodbye\n"; - return 0; } diff --git a/cpp/resources/MinecraftTen-VGORe.ttf b/cpp/resources/MinecraftTen-VGORe.ttf new file mode 100644 index 0000000..9a539dd Binary files /dev/null and b/cpp/resources/MinecraftTen-VGORe.ttf differ diff --git a/cpp/screenshots/game_over_screen.png b/cpp/screenshots/game_over_screen.png new file mode 100644 index 0000000..5e13973 Binary files /dev/null and b/cpp/screenshots/game_over_screen.png differ diff --git a/cpp/screenshots/playing_screen.png b/cpp/screenshots/playing_screen.png new file mode 100644 index 0000000..4e4d7f9 Binary files /dev/null and b/cpp/screenshots/playing_screen.png differ diff --git a/cpp/screenshots/start_screen.png b/cpp/screenshots/start_screen.png new file mode 100644 index 0000000..1a15685 Binary files /dev/null and b/cpp/screenshots/start_screen.png differ diff --git a/cpp/screenshots/win_screen.png b/cpp/screenshots/win_screen.png new file mode 100644 index 0000000..4faf42e Binary files /dev/null and b/cpp/screenshots/win_screen.png differ diff --git a/cpp/window.cpp b/cpp/window.cpp index 3a813ee..f8d6ba0 100644 --- a/cpp/window.cpp +++ b/cpp/window.cpp @@ -10,8 +10,10 @@ #include #include #include +#include #include "SDL.h" +#include "SDL_ttf.h" #include "entity.h" #include "key_event.h" @@ -37,6 +39,7 @@ std::optional map_sdl_key(SDL_Keycode sdl_code) case SDLK_ESCAPE: return ESCAPE; case SDLK_LEFT: return LEFT; case SDLK_RIGHT: return RIGHT; + case SDLK_SPACE: return SPACE; default: return std::nullopt; } } @@ -49,12 +52,18 @@ namespace cpp Window::Window() : window_(nullptr, &SDL_DestroyWindow) , renderer_(nullptr, &SDL_DestroyRenderer) + , font_(nullptr, &TTF_CloseFont) { if (::SDL_Init(SDL_INIT_VIDEO) != 0) { throw std::runtime_error("failed to init SDL"); } + if (::TTF_Init() != 0) + { + throw std::runtime_error("failed to init SDL_ttf"); + } + window_.reset( ::SDL_CreateWindow("cpp_game", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 800, 800, SDL_WINDOW_SHOWN)); if (!window_) @@ -67,6 +76,13 @@ Window::Window() { throw std::runtime_error("failed to create renderer"); } + + // Load font from resources directory + font_.reset(::TTF_OpenFont("resources/MinecraftTen-VGORe.ttf", 48)); + if (!font_) + { + throw std::runtime_error("failed to load font"); + } } std::optional Window::get_event() const @@ -103,16 +119,6 @@ std::optional Window::get_event() const void Window::render(const std::vector &entities) const { - if (::SDL_SetRenderDrawColor(renderer_.get(), 0x0, 0x0, 0x0, 0xff) != 0) - { - throw std::runtime_error("failed to set render draw colour"); - } - - if (::SDL_RenderClear(renderer_.get()) != 0) - { - throw std::runtime_error("failed to clear renderer"); - } - for (const auto &entity : entities) { const auto entity_rect = entity.rectangle(); @@ -135,8 +141,80 @@ void Window::render(const std::vector &entities) const throw std::runtime_error("failed to draw filled rect"); } } +} + +void Window::render_text(const std::string& text, int x, int y, int size) const +{ + // Default to white color + render_text(text, x, y, size, 0xFFFFFF); +} + +void Window::render_text(const std::string& text, int x, int y, int size, uint32_t color) const +{ + TTF_Font* temp_font = TTF_OpenFont("resources/MinecraftTen-VGORe.ttf", size); + if (!temp_font) + { + return; + } + + SDL_Color sdl_color = { + static_cast((color >> 16) & 0xFF), // R + static_cast((color >> 8) & 0xFF), // G + static_cast(color & 0xFF), // B + 255 // A + }; + + SDL_Surface* surface = TTF_RenderText_Solid(temp_font, text.c_str(), sdl_color); + TTF_CloseFont(temp_font); + if (!surface) + { + return; + } + + SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer_.get(), surface); + SDL_FreeSurface(surface); + + if (!texture) + { + return; + } + + SDL_Rect dest = {x, y, surface->w, surface->h}; + SDL_RenderCopy(renderer_.get(), texture, NULL, &dest); + SDL_DestroyTexture(texture); +} + +void Window::clear() const +{ + if (::SDL_SetRenderDrawColor(renderer_.get(), 0x0, 0x0, 0x0, 0xff) != 0) + { + throw std::runtime_error("failed to set render draw colour"); + } + + if (::SDL_RenderClear(renderer_.get()) != 0) + { + throw std::runtime_error("failed to clear renderer"); + } +} + +void Window::present() const +{ ::SDL_RenderPresent(renderer_.get()); } +void Window::render_overlay(uint32_t color, uint8_t alpha) const +{ + SDL_SetRenderDrawBlendMode(renderer_.get(), SDL_BLENDMODE_BLEND); + SDL_SetRenderDrawColor(renderer_.get(), + static_cast((color >> 16) & 0xFF), // R + static_cast((color >> 8) & 0xFF), // G + static_cast(color & 0xFF), // B + alpha); + + SDL_Rect fullscreen = {0, 0, 800, 800}; + SDL_RenderFillRect(renderer_.get(), &fullscreen); + SDL_SetRenderDrawBlendMode(renderer_.get(), SDL_BLENDMODE_NONE); +} + } diff --git a/cpp/window.h b/cpp/window.h index 3faf927..0f41995 100644 --- a/cpp/window.h +++ b/cpp/window.h @@ -9,12 +9,16 @@ #include #include #include +#include #include "entity.h" #include "key_event.h" struct SDL_Window; struct SDL_Renderer; +struct SDL_Texture; +struct _TTF_Font; +typedef struct _TTF_Font TTF_Font; using SDLWindowDelete = void (*)(SDL_Window *); using SDLRendererDelete = void (*)(SDL_Renderer *); @@ -32,7 +36,7 @@ class Window * Construct a new Window. */ Window(); - + ~Window() = default; Window(const Window &) = delete; Window &operator=(const Window &) = delete; @@ -55,12 +59,86 @@ class Window */ void render(const std::vector &entities) const; + /** + * Render text on screen + * + * @param text + * Text to render + * + * @param x + * X position + * + * @param y + * Y position + */ + void render_text(const std::string& text, int x, int y) const; + + /** + * Render text on screen with specific size + * + * @param text + * Text to render + * + * @param x + * X position + * + * @param y + * Y position + * + * @param size + * Font size + */ + void render_text(const std::string& text, int x, int y, int size) const; + + /** + * Render text on screen with specific size and color + * + * @param text + * Text to render + * + * @param x + * X position + * + * @param y + * Y position + * + * @param size + * Font size + * + * @param color + * Text color + */ + void render_text(const std::string& text, int x, int y, int size, uint32_t color) const; + + /** + * Clear the render surface + */ + void clear() const; + + /** + * Present the current frame + */ + void present() const; + + /** + * Render a semi-transparent overlay + * + * @param color + * Color of overlay + * + * @param alpha + * Transparency level (0-255) + */ + void render_overlay(uint32_t color, uint8_t alpha) const; + private: /** SDL window object. */ std::unique_ptr window_; /** SDL renderer object. */ std::unique_ptr renderer_; + + std::unique_ptr font_; }; }