Add textures, gameplay pause, and menu state placeholders.

This commit is contained in:
2026-05-20 13:59:01 +03:00
parent 67f5fcf374
commit 25a7fefa04
11 changed files with 276 additions and 47 deletions
+25 -5
View File
@@ -2,9 +2,29 @@
A classic arcade game rebuilt from the ground up using modern C++ and the Raylib library. 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.* ## 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).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

+36 -8
View File
@@ -8,6 +8,8 @@
enum class GameState { enum class GameState {
MainMenu, MainMenu,
DifficultySelect, DifficultySelect,
Multiplayer,
Settings,
Playing, Playing,
Paused, Paused,
GameOver GameOver
@@ -42,9 +44,22 @@ class Paddle : public GameObject {
public: public:
float width; float width;
float height; 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) { : 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; void Update() override;
@@ -55,9 +70,22 @@ class Ball : public GameObject {
public: public:
float radius; float radius;
Vector2 velocity; 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 }) { : 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; void Update() override;
@@ -70,8 +98,8 @@ private:
Difficulty currentDifficulty; Difficulty currentDifficulty;
public: public:
CpuPaddle(Vector2 pos, Color c, float w, float h, Difficulty diff = Difficulty::Normal) CpuPaddle(Vector2 pos, Color c, float w, float h, Difficulty diff = Difficulty::Normal, const std::string& texturePath = "")
: Paddle(pos, c, w, h) { : Paddle(pos, c, w, h, texturePath) {
SetDifficulty(diff); SetDifficulty(diff);
} }
@@ -100,11 +128,11 @@ public:
void Update() override {} void Update() override {}
void LimitMovement() { void LimitMovement() {
if (position.y <= 0) { if (position.y <= 20.0f) {
position.y = 0; position.y = 20.0f;
} }
if (position.y + height >= GetScreenHeight()) { if (position.y + height >= GetScreenHeight() - 20.0f) {
position.y = GetScreenHeight() - height; position.y = GetScreenHeight() - 20.0f - height;
} }
} }
}; };
+8
View File
@@ -145,6 +145,14 @@
<ClInclude Include="include\raymath.h" /> <ClInclude Include="include\raymath.h" />
<ClInclude Include="include\rlgl.h" /> <ClInclude Include="include\rlgl.h" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Image Include="assets\textures\ball\basic_ball_5.png" />
<Image Include="assets\textures\hud\line.png" />
<Image Include="assets\textures\paddles\basic_paddle.png" />
<Image Include="assets\textures\paddles\basic_paddle_2.png" />
<Image Include="assets\textures\spaces\basic_space.png" />
<Image Include="assets\textures\spaces\walls.png" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets"> <ImportGroup Label="ExtensionTargets">
</ImportGroup> </ImportGroup>
+26
View File
@@ -16,6 +16,12 @@
<Filter Include="Header Files\RayLib"> <Filter Include="Header Files\RayLib">
<UniqueIdentifier>{2f179593-5e9f-4095-be57-3f12f03a9705}</UniqueIdentifier> <UniqueIdentifier>{2f179593-5e9f-4095-be57-3f12f03a9705}</UniqueIdentifier>
</Filter> </Filter>
<Filter Include="Resource Files\textures">
<UniqueIdentifier>{2d4ce555-d6fc-4e9c-a30e-eadb4c1336d0}</UniqueIdentifier>
</Filter>
<Filter Include="Resource Files\audio">
<UniqueIdentifier>{5f42ff18-01e2-4266-a4e0-6dc77ba54e75}</UniqueIdentifier>
</Filter>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="src\main.cpp"> <ClCompile Include="src\main.cpp">
@@ -45,4 +51,24 @@
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Image Include="assets\textures\ball\basic_ball_5.png">
<Filter>Resource Files\textures</Filter>
</Image>
<Image Include="assets\textures\hud\line.png">
<Filter>Resource Files\textures</Filter>
</Image>
<Image Include="assets\textures\paddles\basic_paddle.png">
<Filter>Resource Files\textures</Filter>
</Image>
<Image Include="assets\textures\paddles\basic_paddle_2.png">
<Filter>Resource Files\textures</Filter>
</Image>
<Image Include="assets\textures\spaces\basic_space.png">
<Filter>Resource Files\textures</Filter>
</Image>
<Image Include="assets\textures\spaces\walls.png">
<Filter>Resource Files\textures</Filter>
</Image>
</ItemGroup>
</Project> </Project>
+30 -8
View File
@@ -13,16 +13,27 @@ void Paddle::Update() {
} }
// Limit movement // Limit movement
if (position.y <= 0) { if (position.y <= 20.0f) {
position.y = 0; position.y = 20.0f;
} }
if (position.y + height >= GetScreenHeight()) { if (position.y + height >= GetScreenHeight() - 20.0f) {
position.y = GetScreenHeight() - height; position.y = GetScreenHeight() - 20.0f - height;
} }
} }
void Paddle::Draw() { 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.x += velocity.x;
position.y += velocity.y; 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; velocity.y *= -1;
} }
} }
void Ball::Draw() { void Ball::Draw() {
// Cast to int as DrawCircle expects integers for coordinates if (texture.id > 0) {
DrawCircle((int)position.x, (int)position.y, radius, color); 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);
}
} }
+149 -26
View File
@@ -30,26 +30,39 @@ int main() {
// --- Instantiate Objects using the new Constructors --- // --- 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 }; ball.velocity = Vector2{ 7.0f, 7.0f };
Paddle player( Paddle player(
Vector2{ screen_width - 35.0f, screen_height / 2.0f - 60.0f }, Vector2{ screen_width - 20.0f - 10.0f - 25.0f, screen_height / 2.0f - 60.0f },
WHITE, 25.0f, 120.0f WHITE, 25.0f, 120.0f,
"assets/textures/paddles/basic_paddle.png"
); );
CpuPaddle cpu( 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, WHITE, 25.0f, 120.0f,
Difficulty::Normal Difficulty::Normal,
); "assets/textures/paddles/basic_paddle_2.png"
);
// --- Setup Menu and Game State --- // --- Setup Menu and Game State ---
GameState currentState = GameState::MainMenu; 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" }); 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 --- // --- Main Game Loop ---
while (WindowShouldClose() == false && currentState != GameState::GameOver) { while (WindowShouldClose() == false && currentState != GameState::GameOver) {
@@ -63,7 +76,13 @@ int main() {
if (selected == 0) { if (selected == 0) {
currentState = GameState::DifficultySelect; 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; currentState = GameState::GameOver;
} }
mainMenu.Draw(); mainMenu.Draw();
@@ -87,6 +106,8 @@ int main() {
// Reset game session stats // Reset game session stats
player_score = 0; player_score = 0;
cpu_score = 0; cpu_score = 0;
sessionPlayTime = 0.0f;
isPaused = false;
ResetBall(ball, screen_width, screen_height); ResetBall(ball, screen_width, screen_height);
currentState = GameState::Playing; currentState = GameState::Playing;
} }
@@ -99,38 +120,136 @@ int main() {
case GameState::Playing: case GameState::Playing:
{ {
ball.Update(); // Toggle pause state with 'P' key
player.Update(); if (IsKeyPressed(KEY_P)) {
cpu.Update(ball.position.y); isPaused = !isPaused;
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 })) { if (!isPaused) {
ball.velocity.x *= -1; 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) { // --- Draw Textured Court Background ---
cpu_score++; // Left Court (basic_space.png)
ResetBall(ball, screen_width, screen_height); DrawTexturePro(
} courtBackground,
if (ball.position.x - ball.radius <= 0) { Rectangle{ 0.0f, 0.0f, (float)courtBackground.width, (float)courtBackground.height },
player_score++; Rectangle{ 20.0f, 20.0f, 570.0f, 760.0f },
ResetBall(ball, screen_width, screen_height); 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); // Top Wall (walls.png)
DrawCircle(screen_width / 2, screen_height / 2, 150, Light_Green); DrawTexturePro(
DrawLine(screen_width / 2, 0, screen_width / 2, screen_height, WHITE); 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", cpu_score), screen_width / 4 - 20, 20, 80, WHITE);
DrawText(TextFormat("%i", player_score), 3 * 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(); ball.Draw();
cpu.Draw(); cpu.Draw();
player.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; break;
} }
default: default:
@@ -140,6 +259,10 @@ int main() {
EndDrawing(); EndDrawing();
} }
UnloadTexture(courtBackground);
UnloadTexture(wallsTexture);
UnloadTexture(lineTexture);
CloseWindow(); CloseWindow();
return 0; return 0;
} }
+2
View File
@@ -30,4 +30,6 @@ void Menu::Draw() {
Color textColor = (i == selectedIndex) ? YELLOW : WHITE; 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); DrawText(options[i].c_str(), screenWidth / 2 - MeasureText(options[i].c_str(), 40) / 2, screenHeight / 2 + (i * 60), 40, textColor);
} }
// Draw Control keys
} }