diff --git a/README.md b/README.md index cab2d52..4cee6d1 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,29 @@ A classic arcade game rebuilt from the ground up using modern C++ and the Raylib library. -**Features:** -* Clean, Object-Oriented entity architecture. -* Game state management (Main Menu, Gameplay, Game Over). -* A CPU AI opponent with adjustable difficulty levels (Easy, Normal, Hard). +--- -*This is a project developed for UTCN - ETTI Helios Additional_activity.* \ No newline at end of file +## Credits & Attributions + +### Code Base Template +* **Original Author:** educ8s (Nick Koumaris) +* **Original Repository:** [github.com/educ8s/Cpp-Pong-Game-Raylib](https://github.com/educ8s/Cpp-Pong-Game-Raylib) +* **Description:** This project utilizes core architectural patterns and Raylib integrations adapted from the original open-source template repository listed above. + +### Asset Credits & Attribution +The multimedia resources (graphics, sprites, and audio files) used in this game are not my personal creations. They have been curated and adapted from the open-source community for educational development. +* **Original Project:** Moddable Pong +* **Author / Creator:** Endless OS Foundation & Endless Studios +* **Website:** [endlessos.org](https://endlessos.org) +* **Source Code Repository:** [github.com/endlessm/moddable-pong](https://github.com/endlessm/moddable-pong/) +* **Usage Notice:** These assets are integrated strictly for non-commercial, academic layout validation and development milestones. All rights, ownership, and copyrights belong entirely to the original creators at the Endless OS Foundation. + +--- + +## Features + +* **Clean Object-Oriented Architecture:** Developed using rigorous OOP principles, utilizing a base abstract `GameObject` class with virtual override structures for specialized modular entities (`Ball`, `Paddle`, and `CpuPaddle`). +* **Robust Game State Machine:** Features an integrated game loop managing distinct application states including `MainMenu`, `DifficultySelect`, `Multiplayer`, `Settings`, and `Playing`. +* **Dynamic AI Opponent:** A singleplayer CPU opponent equipped with configurable movement velocities tailored across three distinct difficulty tiers: Easy, Normal, and Hard. +* **Enhanced Visual Layout:** Fully upgraded with high-resolution texture mapping via Raylib's `DrawTexturePro`, featuring dedicated game court backdrops, localized wall segments, styled central dashed lines, and custom entity sprites. +* **Academic Context:** This work is part of a student engineering project within the Helios Additional_activity initiative at the Technical University of Cluj-Napoca (UTCN) - Faculty of Electronics, Telecommunications and Information Technology (ETTI). \ No newline at end of file diff --git a/assets/textures/spaces/asteroid.png b/assets/textures/spaces/asteroid.png deleted file mode 100644 index 2db0d29..0000000 Binary files a/assets/textures/spaces/asteroid.png and /dev/null differ diff --git a/assets/textures/spaces/galaxy_space.png b/assets/textures/spaces/galaxy_space.png deleted file mode 100644 index 6bec620..0000000 Binary files a/assets/textures/spaces/galaxy_space.png and /dev/null differ diff --git a/assets/textures/spaces/trap_space.png b/assets/textures/spaces/trap_space.png deleted file mode 100644 index 7842c0c..0000000 Binary files a/assets/textures/spaces/trap_space.png and /dev/null differ diff --git a/assets/textures/spaces/vortex.png b/assets/textures/spaces/vortex.png deleted file mode 100644 index 7820908..0000000 Binary files a/assets/textures/spaces/vortex.png and /dev/null differ diff --git a/include/game.h b/include/game.h index e1d8981..3b3c6a6 100644 --- a/include/game.h +++ b/include/game.h @@ -8,6 +8,8 @@ enum class GameState { MainMenu, DifficultySelect, + Multiplayer, + Settings, Playing, Paused, GameOver @@ -42,9 +44,22 @@ class Paddle : public GameObject { public: float width; float height; + Texture2D texture; - Paddle(Vector2 pos, Color c, float w, float h) +public: + Paddle(Vector2 pos, Color c, float w, float h, const std::string& texturePath = "") : GameObject(pos, c), width(w), height(h) { + if (!texturePath.empty()) { + texture = LoadTexture(texturePath.c_str()); + } else { + texture = { 0 }; + } + } + + ~Paddle() override { + if (texture.id > 0) { + UnloadTexture(texture); + } } void Update() override; @@ -55,9 +70,22 @@ class Ball : public GameObject { public: float radius; Vector2 velocity; + Texture2D texture; - Ball(Vector2 pos, Color c, float r) +public: + Ball(Vector2 pos, Color c, float r, const std::string& texturePath = "") : GameObject(pos, c), radius(r), velocity({ 5.0f, 5.0f }) { + if (!texturePath.empty()) { + texture = LoadTexture(texturePath.c_str()); + } else { + texture = { 0 }; + } + } + + ~Ball() override { + if (texture.id > 0) { + UnloadTexture(texture); + } } void Update() override; @@ -70,8 +98,8 @@ private: Difficulty currentDifficulty; public: - CpuPaddle(Vector2 pos, Color c, float w, float h, Difficulty diff = Difficulty::Normal) - : Paddle(pos, c, w, h) { + CpuPaddle(Vector2 pos, Color c, float w, float h, Difficulty diff = Difficulty::Normal, const std::string& texturePath = "") + : Paddle(pos, c, w, h, texturePath) { SetDifficulty(diff); } @@ -100,11 +128,11 @@ public: void Update() override {} void LimitMovement() { - if (position.y <= 0) { - position.y = 0; + if (position.y <= 20.0f) { + position.y = 20.0f; } - if (position.y + height >= GetScreenHeight()) { - position.y = GetScreenHeight() - height; + if (position.y + height >= GetScreenHeight() - 20.0f) { + position.y = GetScreenHeight() - 20.0f - height; } } }; \ No newline at end of file diff --git a/pong-reloaded.vcxproj b/pong-reloaded.vcxproj index 5bfe3eb..90ac5b9 100644 --- a/pong-reloaded.vcxproj +++ b/pong-reloaded.vcxproj @@ -145,6 +145,14 @@ + + + + + + + + diff --git a/pong-reloaded.vcxproj.filters b/pong-reloaded.vcxproj.filters index 03010cd..076bcbf 100644 --- a/pong-reloaded.vcxproj.filters +++ b/pong-reloaded.vcxproj.filters @@ -16,6 +16,12 @@ {2f179593-5e9f-4095-be57-3f12f03a9705} + + {2d4ce555-d6fc-4e9c-a30e-eadb4c1336d0} + + + {5f42ff18-01e2-4266-a4e0-6dc77ba54e75} + @@ -45,4 +51,24 @@ Header Files + + + Resource Files\textures + + + Resource Files\textures + + + Resource Files\textures + + + Resource Files\textures + + + Resource Files\textures + + + Resource Files\textures + + \ No newline at end of file diff --git a/src/game.cpp b/src/game.cpp index 68024d9..8515244 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -13,16 +13,27 @@ void Paddle::Update() { } // Limit movement - if (position.y <= 0) { - position.y = 0; + if (position.y <= 20.0f) { + position.y = 20.0f; } - if (position.y + height >= GetScreenHeight()) { - position.y = GetScreenHeight() - height; + if (position.y + height >= GetScreenHeight() - 20.0f) { + position.y = GetScreenHeight() - 20.0f - height; } } void Paddle::Draw() { - DrawRectangleRounded(Rectangle{ position.x, position.y, width, height }, 0.8f, 0, color); + if (texture.id > 0) { + DrawTexturePro( + texture, + Rectangle{ 0.0f, 0.0f, (float)texture.width, (float)texture.height }, + Rectangle{ position.x, position.y, width, height }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + } else { + DrawRectangleRounded(Rectangle{ position.x, position.y, width, height }, 0.8f, 0, color); + } } @@ -32,12 +43,23 @@ void Ball::Update() { position.x += velocity.x; position.y += velocity.y; - if (position.y + radius >= GetScreenHeight() || position.y - radius <= 0) { + if (position.y + radius >= GetScreenHeight() - 20.0f || position.y - radius <= 20.0f) { velocity.y *= -1; } } void Ball::Draw() { - // Cast to int as DrawCircle expects integers for coordinates - DrawCircle((int)position.x, (int)position.y, radius, color); + if (texture.id > 0) { + DrawTexturePro( + texture, + Rectangle{ 0.0f, 0.0f, (float)texture.width, (float)texture.height }, + Rectangle{ position.x - radius, position.y - radius, radius * 2.0f, radius * 2.0f }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + } else { + // Cast to int as DrawCircle expects integers for coordinates + DrawCircle((int)position.x, (int)position.y, radius, color); + } } \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index dc67d11..97dbcd1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -30,26 +30,39 @@ int main() { // --- Instantiate Objects using the new Constructors --- - Ball ball(Vector2{ screen_width / 2.0f, screen_height / 2.0f }, Yellow, 20.0f); + Ball ball( + Vector2{ screen_width / 2.0f, screen_height / 2.0f }, + Yellow, 20.0f, + "assets/textures/ball/basic_ball_5.png" + ); ball.velocity = Vector2{ 7.0f, 7.0f }; Paddle player( - Vector2{ screen_width - 35.0f, screen_height / 2.0f - 60.0f }, - WHITE, 25.0f, 120.0f + Vector2{ screen_width - 20.0f - 10.0f - 25.0f, screen_height / 2.0f - 60.0f }, + WHITE, 25.0f, 120.0f, + "assets/textures/paddles/basic_paddle.png" ); CpuPaddle cpu( - Vector2{ 10.0f, screen_height / 2.0f - 60.0f }, + Vector2{ 20.0f + 10.0f, screen_height / 2.0f - 60.0f }, WHITE, 25.0f, 120.0f, - Difficulty::Normal - ); + Difficulty::Normal, + "assets/textures/paddles/basic_paddle_2.png" + ); // --- Setup Menu and Game State --- GameState currentState = GameState::MainMenu; - Menu mainMenu("PONG RELOADED", { "Start Game", "Quit" }); + float sessionPlayTime = 0.0f; + bool isPaused = false; + Menu mainMenu("PONG RELOADED", { "Singleplayer", "Multiplayer", "Settings", "Quit" }); Menu difficultyMenu("SELECT DIFFICULTY", { "Easy", "Normal", "Hard", "Back" }); + // --- Load Background and Wall Textures --- + Texture2D courtBackground = LoadTexture("assets/textures/spaces/basic_space.png"); + Texture2D wallsTexture = LoadTexture("assets/textures/spaces/walls.png"); + Texture2D lineTexture = LoadTexture("assets/textures/hud/line.png"); + // --- Main Game Loop --- while (WindowShouldClose() == false && currentState != GameState::GameOver) { @@ -63,7 +76,13 @@ int main() { if (selected == 0) { currentState = GameState::DifficultySelect; } - else if (selected == 1) { + if (selected == 1) { + currentState = GameState::Multiplayer; + } + if (selected == 2) { + currentState = GameState::Settings; + } + else if (selected == 3) { currentState = GameState::GameOver; } mainMenu.Draw(); @@ -87,6 +106,8 @@ int main() { // Reset game session stats player_score = 0; cpu_score = 0; + sessionPlayTime = 0.0f; + isPaused = false; ResetBall(ball, screen_width, screen_height); currentState = GameState::Playing; } @@ -99,38 +120,136 @@ int main() { case GameState::Playing: { - ball.Update(); - player.Update(); - cpu.Update(ball.position.y); - - if (CheckCollisionCircleRec(ball.position, ball.radius, Rectangle{ player.position.x, player.position.y, player.width, player.height })) { - ball.velocity.x *= -1; + // Toggle pause state with 'P' key + if (IsKeyPressed(KEY_P)) { + isPaused = !isPaused; } - if (CheckCollisionCircleRec(ball.position, ball.radius, Rectangle{ cpu.position.x, cpu.position.y, cpu.width, cpu.height })) { - ball.velocity.x *= -1; + if (!isPaused) { + sessionPlayTime += GetFrameTime(); + + ball.Update(); + player.Update(); + cpu.Update(ball.position.y); + + if (CheckCollisionCircleRec(ball.position, ball.radius, Rectangle{ player.position.x, player.position.y, player.width, player.height })) { + ball.velocity.x *= -1; + } + + if (CheckCollisionCircleRec(ball.position, ball.radius, Rectangle{ cpu.position.x, cpu.position.y, cpu.width, cpu.height })) { + ball.velocity.x *= -1; + } + + if (ball.position.x + ball.radius >= screen_width - 20.0f) { + cpu_score++; + ResetBall(ball, screen_width, screen_height); + } + if (ball.position.x - ball.radius <= 20.0f) { + player_score++; + ResetBall(ball, screen_width, screen_height); + } } - if (ball.position.x + ball.radius >= screen_width) { - cpu_score++; - ResetBall(ball, screen_width, screen_height); - } - if (ball.position.x - ball.radius <= 0) { - player_score++; - ResetBall(ball, screen_width, screen_height); + // --- Draw Textured Court Background --- + // Left Court (basic_space.png) + DrawTexturePro( + courtBackground, + Rectangle{ 0.0f, 0.0f, (float)courtBackground.width, (float)courtBackground.height }, + Rectangle{ 20.0f, 20.0f, 570.0f, 760.0f }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + + // Right Court (basic_space.png) + DrawTexturePro( + courtBackground, + Rectangle{ 0.0f, 0.0f, (float)courtBackground.width, (float)courtBackground.height }, + Rectangle{ 690.0f, 20.0f, 570.0f, 760.0f }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + + // Center strip (Dark Green background area) - already cleared by ClearBackground + + // Center Circle + DrawCircle(screen_width / 2, screen_height / 2, 50.0f, Color{ 102, 51, 153, 100 }); + DrawCircleLines(screen_width / 2, screen_height / 2, 50.0f, Color{ 50, 25, 75, 250 }); + + // Tiled Center Dashed Line (line.png) + int lineY = 20; + while (lineY < 780) { + DrawTexture(lineTexture, screen_width / 2 - lineTexture.width / 2, lineY, WHITE); + lineY += lineTexture.height; } - DrawRectangle(screen_width / 2, 0, screen_width / 2, screen_height, Green); - DrawCircle(screen_width / 2, screen_height / 2, 150, Light_Green); - DrawLine(screen_width / 2, 0, screen_width / 2, screen_height, WHITE); + // Top Wall (walls.png) + DrawTexturePro( + wallsTexture, + Rectangle{ 0.0f, 0.0f, (float)wallsTexture.width, (float)wallsTexture.height }, + Rectangle{ 0.0f, 0.0f, (float)screen_width, 20.0f }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + + // Bottom Wall (walls.png) + DrawTexturePro( + wallsTexture, + Rectangle{ 0.0f, 0.0f, (float)wallsTexture.width, (float)wallsTexture.height }, + Rectangle{ 0.0f, (float)screen_height - 20.0f, (float)screen_width, 20.0f }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + + // Left Wall (walls.png) + DrawTexturePro( + wallsTexture, + Rectangle{ 0.0f, 0.0f, (float)wallsTexture.width, (float)wallsTexture.height }, + Rectangle{ 0.0f, 0.0f, 20.0f, (float)screen_height }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + + // Right Wall (walls.png) + DrawTexturePro( + wallsTexture, + Rectangle{ 0.0f, 0.0f, (float)wallsTexture.width, (float)wallsTexture.height }, + Rectangle{ (float)screen_width - 20.0f, 0.0f, 20.0f, (float)screen_height }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); DrawText(TextFormat("%i", cpu_score), screen_width / 4 - 20, 20, 80, WHITE); DrawText(TextFormat("%i", player_score), 3 * screen_width / 4 - 20, 20, 80, WHITE); + // Draw Playtime Counter + int minutes = (int)sessionPlayTime / 60; + int seconds = (int)sessionPlayTime % 60; + int timeTextWidth = MeasureText(TextFormat("%02i:%02i", minutes, seconds), 32); + + // Draw background box to block out the center line and increase contrast + DrawRectangle(screen_width / 2 - timeTextWidth / 2 - 15, 715, timeTextWidth + 30, 44, Color{ 15, 15, 15, 220 }); + DrawRectangleLines(screen_width / 2 - timeTextWidth / 2 - 15, 715, timeTextWidth + 30, 44, Color{ 100, 100, 100, 255 }); + DrawText(TextFormat("%02i:%02i", minutes, seconds), screen_width / 2 - timeTextWidth / 2, 721, 32, YELLOW); + ball.Draw(); cpu.Draw(); player.Draw(); + // Draw Pause Overlay and Text + if (isPaused) { + // Semi-transparent overlay inside the borders + DrawRectangle(20, 20, screen_width - 40, screen_height - 40, Color{ 0, 0, 0, 150 }); + + int pausedTextWidth = MeasureText("PAUSED", 60); + DrawText("PAUSED", screen_width / 2 - pausedTextWidth / 2, screen_height / 2 - 30, 60, YELLOW); + } + break; } default: @@ -140,6 +259,10 @@ int main() { EndDrawing(); } + UnloadTexture(courtBackground); + UnloadTexture(wallsTexture); + UnloadTexture(lineTexture); + CloseWindow(); return 0; } \ No newline at end of file diff --git a/src/menu.cpp b/src/menu.cpp index 657548b..0876d78 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -30,4 +30,6 @@ void Menu::Draw() { Color textColor = (i == selectedIndex) ? YELLOW : WHITE; DrawText(options[i].c_str(), screenWidth / 2 - MeasureText(options[i].c_str(), 40) / 2, screenHeight / 2 + (i * 60), 40, textColor); } + + // Draw Control keys } \ No newline at end of file