From a2452e3cf68fa5b7208bd8106209a0b2cd781bf7 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Tue, 9 Sep 2025 20:52:46 -0500 Subject: [PATCH] Separation of concerns --- client/Makefile | 2 +- client/entity/player/PlayerRenderer.cpp | 96 +++++++++++++++++ client/entity/player/PlayerRenderer.hpp | 42 ++++++++ client/main.cpp | 137 +++++++++++------------- client/render/RenderContext.hpp | 104 ++++++++++++++++++ client/utils/Coords.hpp | 63 +++++++++++ 6 files changed, 371 insertions(+), 73 deletions(-) create mode 100644 client/entity/player/PlayerRenderer.cpp create mode 100644 client/entity/player/PlayerRenderer.hpp create mode 100644 client/render/RenderContext.hpp create mode 100644 client/utils/Coords.hpp diff --git a/client/Makefile b/client/Makefile index c1eaa88..4ab605f 100644 --- a/client/Makefile +++ b/client/Makefile @@ -3,7 +3,7 @@ CXXFLAGS = -std=c++20 -Wall -Wextra -O2 -I. LDFLAGS = -lraylib -lboost_system -lpthread -lGL -lm -ldl -lrt -lX11 TARGET = game -SOURCES = main.cpp entity/Entity.cpp entity/player/PlayerController.cpp net/NetworkManager.cpp sky/Sky.cpp +SOURCES = main.cpp entity/Entity.cpp entity/player/PlayerController.cpp entity/player/PlayerRenderer.cpp net/NetworkManager.cpp sky/Sky.cpp OBJECTS = $(SOURCES:.cpp=.o) all: $(TARGET) diff --git a/client/entity/player/PlayerRenderer.cpp b/client/entity/player/PlayerRenderer.cpp new file mode 100644 index 0000000..c4ac6b3 --- /dev/null +++ b/client/entity/player/PlayerRenderer.cpp @@ -0,0 +1,96 @@ +#include "PlayerRenderer.hpp" +#include "../../net/NetworkManager.hpp" + +PlayerRenderer::PlayerRenderer() { + init(); +} + +PlayerRenderer::~PlayerRenderer() { + // Unload textures + for (auto& [color, texture] : playerTextures) { + UnloadTexture(texture); + } + + // Unload models + UnloadModel(localPlayerModel); + for (auto& [id, model] : remotePlayerModels) { + UnloadModel(model); + } +} + +void PlayerRenderer::init() { + // Load all player color textures + playerTextures["red"] = LoadTexture("../assets/textures/red.png"); + playerTextures["green"] = LoadTexture("../assets/textures/green.png"); + playerTextures["orange"] = LoadTexture("../assets/textures/orange.png"); + playerTextures["purple"] = LoadTexture("../assets/textures/purple.png"); + playerTextures["white"] = LoadTexture("../assets/textures/white.png"); + + // Create local player model + auto cubeMesh = GenMeshCube(1.0f, 2.0f, 1.0f); + localPlayerModel = LoadModelFromMesh(cubeMesh); +} + +void PlayerRenderer::renderLocalPlayer(RenderContext& ctx, const Vector3& position, const std::string& color) { + // Set texture based on player color + if (playerTextures.count(color) > 0) { + localPlayerModel.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = playerTextures[color]; + } + + // Submit to render context + ctx.submitModel(localPlayerModel, position, 1.0f, WHITE, RenderContext::RenderLayer::ENTITIES); +} + +void PlayerRenderer::renderRemotePlayers(RenderContext& ctx, + const std::unordered_map& players) { + for (const auto& [id, player] : players) { + // Ensure model exists for this player + ensurePlayerModel(id); + + // Update texture based on player color + if (remotePlayerModels.count(id) > 0 && playerTextures.count(player.color) > 0) { + remotePlayerModels[id].materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = playerTextures[player.color]; + + // Submit to render context + ctx.submitModel(remotePlayerModels[id], player.position, 1.0f, WHITE, + RenderContext::RenderLayer::ENTITIES); + } + } +} + +void PlayerRenderer::ensurePlayerModel(uint32_t playerId) { + if (remotePlayerModels.find(playerId) == remotePlayerModels.end()) { + // Create new model for this player + auto cubeMesh = GenMeshCube(1.0f, 2.0f, 1.0f); + remotePlayerModels[playerId] = LoadModelFromMesh(cubeMesh); + } +} + +void PlayerRenderer::removePlayerModel(uint32_t playerId) { + auto it = remotePlayerModels.find(playerId); + if (it != remotePlayerModels.end()) { + UnloadModel(it->second); + remotePlayerModels.erase(it); + } +} + +void PlayerRenderer::cleanupDisconnectedPlayers(const std::unordered_map& currentPlayers) { + // Remove models for players who are no longer connected + for (auto it = remotePlayerModels.begin(); it != remotePlayerModels.end();) { + if (currentPlayers.find(it->first) == currentPlayers.end()) { + UnloadModel(it->second); + it = remotePlayerModels.erase(it); + } else { + ++it; + } + } +} + +Texture2D PlayerRenderer::getPlayerTexture(const std::string& color) const { + auto it = playerTextures.find(color); + if (it != playerTextures.end()) { + return it->second; + } + // Return a default texture or handle error + return Texture2D{}; +} diff --git a/client/entity/player/PlayerRenderer.hpp b/client/entity/player/PlayerRenderer.hpp new file mode 100644 index 0000000..fc5d061 --- /dev/null +++ b/client/entity/player/PlayerRenderer.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include +#include "../../render/RenderContext.hpp" + +class PlayerRenderer { +private: + Model localPlayerModel; + std::unordered_map remotePlayerModels; + std::unordered_map playerTextures; + +public: + PlayerRenderer(); + ~PlayerRenderer(); + + // Initialize player models and textures + void init(); + + // Render local player + void renderLocalPlayer(RenderContext& ctx, const Vector3& position, const std::string& color); + + // Render remote players + void renderRemotePlayers(RenderContext& ctx, const std::unordered_map& players); + + // Update player model for a specific player ID + void ensurePlayerModel(uint32_t playerId); + + // Remove model for disconnected player + void removePlayerModel(uint32_t playerId); + + // Clean up models for players not in the list + void cleanupDisconnectedPlayers(const std::unordered_map& currentPlayers); + + // Get texture for a color + Texture2D getPlayerTexture(const std::string& color) const; +}; + +// Forward declaration for RemotePlayer struct (defined in NetworkManager) +struct RemotePlayer; \ No newline at end of file diff --git a/client/main.cpp b/client/main.cpp index 1eede90..15df7e7 100644 --- a/client/main.cpp +++ b/client/main.cpp @@ -7,15 +7,17 @@ #include #include #include -#include #include #include #include #include #include #include "entity/player/PlayerController.hpp" +#include "entity/player/PlayerRenderer.hpp" #include "net/NetworkManager.hpp" #include "sky/Sky.hpp" +#include "render/RenderContext.hpp" +#include "utils/Coords.hpp" constexpr int WORLD_SIZE = 100; constexpr float WORLD_SCALE = 10.0f; @@ -110,15 +112,14 @@ enum GameState { class Game { PlayerController playerController; + std::unique_ptr playerRenderer; + RenderContext renderContext; Model terrainModel; - Model playerModel; Heightmap heightmap; NetworkManager network; std::unique_ptr sky; Vector3 playerPos{0, 0, 0}; Texture2D terrainTexture; - std::unordered_map playerTextures; - std::unordered_map remotePlayerModels; // Login UI state GameState gameState = STATE_LOGIN; @@ -131,12 +132,17 @@ class Game { float lastHeartbeatTime = 0.0f; const float HEARTBEAT_INTERVAL = 5.0f; // Send heartbeat every 5 seconds + // Debug options + bool showDebugAxes = false; + bool showWorldBounds = false; + public: Game() { InitWindow(1280, 720, "Game"); SetTargetFPS(60); - // Initialize sky after window is created + // Initialize components after window is created + playerRenderer = std::make_unique(); sky = std::make_unique(); // Load heightmap @@ -147,21 +153,10 @@ public: // Load textures terrainTexture = LoadTexture("../assets/textures/black.png"); - // Load all player color textures - playerTextures["red"] = LoadTexture("../assets/textures/red.png"); - playerTextures["green"] = LoadTexture("../assets/textures/green.png"); - playerTextures["orange"] = LoadTexture("../assets/textures/orange.png"); - playerTextures["purple"] = LoadTexture("../assets/textures/purple.png"); - playerTextures["white"] = LoadTexture("../assets/textures/white.png"); - // Create terrain model auto terrainMesh = heightmap.generateMesh(); terrainModel = LoadModelFromMesh(terrainMesh); terrainModel.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = terrainTexture; - - // Create player cube (texture will be set when we know our color) - auto cubeMesh = GenMeshCube(1.0f, 2.0f, 1.0f); - playerModel = LoadModelFromMesh(cubeMesh); } ~Game() { @@ -171,14 +166,7 @@ public: } UnloadTexture(terrainTexture); - for (auto& [color, texture] : playerTextures) { - UnloadTexture(texture); - } UnloadModel(terrainModel); - UnloadModel(playerModel); - for (auto& [id, model] : remotePlayerModels) { - UnloadModel(model); - } CloseWindow(); } @@ -244,38 +232,20 @@ private: sky->setTimeOfDay(currentTime); } + // Debug toggles + if (IsKeyPressed(KEY_F1)) showDebugAxes = !showDebugAxes; + if (IsKeyPressed(KEY_F2)) showWorldBounds = !showWorldBounds; + // Get server position and update player controller playerPos = network.getPosition(); + + // Clamp position to world bounds + playerPos = Coords::clampToWorldBounds(playerPos); playerController.setPlayerPosition(playerPos); - // Set player texture based on assigned color - if (network.isConnected() && playerTextures.count(network.getPlayerColor()) > 0) { - playerModel.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = playerTextures[network.getPlayerColor()]; - } - - // Update remote player models + // Clean up disconnected player models auto remotePlayers = network.getRemotePlayers(); - for (const auto& [id, player] : remotePlayers) { - if (remotePlayerModels.find(id) == remotePlayerModels.end()) { - // Create new model for this player - auto cubeMesh = GenMeshCube(1.0f, 2.0f, 1.0f); - remotePlayerModels[id] = LoadModelFromMesh(cubeMesh); - } - // Always update texture in case color changed - if (playerTextures.count(player.color) > 0) { - remotePlayerModels[id].materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = playerTextures[player.color]; - } - } - - // Remove models for players who left - for (auto it = remotePlayerModels.begin(); it != remotePlayerModels.end();) { - if (remotePlayers.find(it->first) == remotePlayers.end()) { - UnloadModel(it->second); - it = remotePlayerModels.erase(it); - } else { - ++it; - } - } + playerRenderer->cleanupDisconnectedPlayers(remotePlayers); // Update player controller (handles camera) float deltaTime = GetFrameTime(); @@ -404,36 +374,53 @@ private: } void renderGame() { - BeginMode3D(playerController.getCamera()); + // Begin 3D rendering with render context + renderContext.begin3D(playerController.getCamera()); - // Render skybox first (it will handle its own depth settings) - if (sky) { - sky->renderSkybox(playerController.getCamera()); - } - - // Draw terrain - DrawModel(terrainModel, {0, 0, 0}, 1.0f, WHITE); - - // Draw player - if (network.isConnected()) { - DrawModel(playerModel, playerPos, 1.0f, WHITE); - } - - // Draw remote players - auto remotePlayers = network.getRemotePlayers(); - for (const auto& [id, player] : remotePlayers) { - if (remotePlayerModels.find(id) != remotePlayerModels.end()) { - DrawModel(remotePlayerModels[id], player.position, 1.0f, WHITE); + // Submit skybox first (lowest layer) + renderContext.submitCustom([this]() { + if (sky) { + sky->renderSkybox(playerController.getCamera()); } + }, RenderContext::RenderLayer::SKYBOX); + + // Submit terrain + renderContext.submitModel(terrainModel, {0, 0, 0}, 1.0f, WHITE, + RenderContext::RenderLayer::TERRAIN); + + // Submit players if connected + if (network.isConnected()) { + // Local player + playerRenderer->renderLocalPlayer(renderContext, playerPos, network.getPlayerColor()); + + // Remote players + auto remotePlayers = network.getRemotePlayers(); + playerRenderer->renderRemotePlayers(renderContext, remotePlayers); } - EndMode3D(); + // Debug rendering + if (showDebugAxes) { + renderContext.submitCustom([this]() { + Coords::drawDebugAxes(playerPos, 2.0f); + // Draw world origin axes + Coords::drawDebugAxes({0, 0, 0}, 5.0f); + }, RenderContext::RenderLayer::TRANSPARENT); + } + + if (showWorldBounds) { + renderContext.submitCustom([]() { + Coords::drawWorldBounds(); + }, RenderContext::RenderLayer::TRANSPARENT); + } + + // Execute all render commands in order + renderContext.end3D(); // UI DrawText(TextFormat("Logged in as: %s", currentUsername.c_str()), 10, 10, 20, WHITE); DrawText("WASD: Move | Q/E: Strafe | Right-Click: Rotate Camera", 10, 35, 20, WHITE); DrawText("Left/Right Arrow: Change Color | Mouse Wheel: Zoom | ESC: Logout", 10, 60, 20, WHITE); - DrawText("T: Change Time of Day", 10, 85, 20, WHITE); + DrawText("T: Change Time | F1: Debug Axes | F2: World Bounds", 10, 85, 20, WHITE); if (network.isConnected()) { std::string colorText = "Your color: " + network.getPlayerColor(); DrawText(colorText.c_str(), 10, 110, 20, WHITE); @@ -445,7 +432,13 @@ private: int minute = (int)((timeHour - hour) * 60); DrawText(TextFormat("Time: %02d:%02d", hour, minute), 10, 135, 20, WHITE); - DrawFPS(10, 160); + // Show position if debug is on + if (showDebugAxes) { + DrawText(TextFormat("Pos: %.1f, %.1f, %.1f", playerPos.x, playerPos.y, playerPos.z), + 10, 160, 20, YELLOW); + } + + DrawFPS(10, showDebugAxes ? 185 : 160); } }; diff --git a/client/render/RenderContext.hpp b/client/render/RenderContext.hpp new file mode 100644 index 0000000..db21553 --- /dev/null +++ b/client/render/RenderContext.hpp @@ -0,0 +1,104 @@ +#pragma once + +#include +#include +#include +#include + +class RenderContext { +public: + struct RenderLayer { + enum Type { + SKYBOX = 0, + TERRAIN = 100, + ENTITIES = 200, + TRANSPARENT = 300, + UI = 400 + }; + }; + + struct RenderCommand { + int layer; + std::function command; + + bool operator<(const RenderCommand& other) const { + return layer < other.layer; + } + }; + +private: + std::vector commands; + Camera3D* activeCamera; + bool is3DMode; + +public: + RenderContext() : activeCamera(nullptr), is3DMode(false) {} + + void begin3D(Camera3D& camera) { + activeCamera = &camera; + is3DMode = true; + BeginMode3D(camera); + } + + void end3D() { + executeCommands(); + EndMode3D(); + is3DMode = false; + activeCamera = nullptr; + } + + void submitModel(const Model& model, const Vector3& position, + float scale = 1.0f, Color tint = WHITE, + int layer = RenderLayer::ENTITIES) { + commands.push_back({layer, [model, position, scale, tint]() { + DrawModel(model, position, scale, tint); + }}); + } + + void submitModelEx(const Model& model, const Vector3& position, + const Vector3& rotation, const Vector3& scale, + Color tint = WHITE, int layer = RenderLayer::ENTITIES) { + commands.push_back({layer, [model, position, rotation, scale, tint]() { + DrawModelEx(model, position, rotation, 0.0f, scale, tint); + }}); + } + + void submitCube(const Vector3& position, const Vector3& size, + Color color, int layer = RenderLayer::ENTITIES) { + commands.push_back({layer, [position, size, color]() { + DrawCube(position, size.x, size.y, size.z, color); + }}); + } + + void submitCubeWires(const Vector3& position, const Vector3& size, + Color color, int layer = RenderLayer::ENTITIES) { + commands.push_back({layer, [position, size, color]() { + DrawCubeWires(position, size.x, size.y, size.z, color); + }}); + } + + void submitLine3D(const Vector3& start, const Vector3& end, + Color color, int layer = RenderLayer::ENTITIES) { + commands.push_back({layer, [start, end, color]() { + DrawLine3D(start, end, color); + }}); + } + + void submitCustom(std::function renderFunc, int layer) { + commands.push_back({layer, renderFunc}); + } + + Camera3D* getActiveCamera() { return activeCamera; } + bool isIn3DMode() const { return is3DMode; } + +private: + void executeCommands() { + std::sort(commands.begin(), commands.end()); + + for (const auto& cmd : commands) { + cmd.command(); + } + + commands.clear(); + } +}; diff --git a/client/utils/Coords.hpp b/client/utils/Coords.hpp new file mode 100644 index 0000000..2d1b283 --- /dev/null +++ b/client/utils/Coords.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include +#include + +class Coords { +public: + static constexpr float WORLD_BOUNDS = 50.0f; + static constexpr float WORLD_MIN_HEIGHT = 0.0f; + static constexpr float WORLD_MAX_HEIGHT = 100.0f; + + static Vector3 worldToLocal(const Vector3& worldPos, const Vector3& origin) { + return Vector3Subtract(worldPos, origin); + } + + static Vector3 localToWorld(const Vector3& localPos, const Vector3& origin) { + return Vector3Add(localPos, origin); + } + + static Vector3 clampToWorldBounds(const Vector3& pos) { + return { + Clamp(pos.x, -WORLD_BOUNDS, WORLD_BOUNDS), + Clamp(pos.y, WORLD_MIN_HEIGHT, WORLD_MAX_HEIGHT), + Clamp(pos.z, -WORLD_BOUNDS, WORLD_BOUNDS) + }; + } + + static bool isInWorldBounds(const Vector3& pos) { + return pos.x >= -WORLD_BOUNDS && pos.x <= WORLD_BOUNDS && + pos.y >= WORLD_MIN_HEIGHT && pos.y <= WORLD_MAX_HEIGHT && + pos.z >= -WORLD_BOUNDS && pos.z <= WORLD_BOUNDS; + } + + static Matrix buildTransformMatrix(const Vector3& position, const Vector3& rotation, const Vector3& scale) { + Matrix matScale = MatrixScale(scale.x, scale.y, scale.z); + Matrix matRotation = MatrixRotateXYZ(rotation); + Matrix matTranslation = MatrixTranslate(position.x, position.y, position.z); + + Matrix result = MatrixMultiply(matScale, matRotation); + result = MatrixMultiply(result, matTranslation); + return result; + } + + static Vector3 forward() { return {0.0f, 0.0f, 1.0f}; } + static Vector3 backward() { return {0.0f, 0.0f, -1.0f}; } + static Vector3 right() { return {1.0f, 0.0f, 0.0f}; } + static Vector3 left() { return {-1.0f, 0.0f, 0.0f}; } + static Vector3 up() { return {0.0f, 1.0f, 0.0f}; } + static Vector3 down() { return {0.0f, -1.0f, 0.0f}; } + + static void drawDebugAxes(const Vector3& position, float size = 1.0f) { + DrawLine3D(position, Vector3Add(position, Vector3Scale(right(), size)), RED); + DrawLine3D(position, Vector3Add(position, Vector3Scale(up(), size)), GREEN); + DrawLine3D(position, Vector3Add(position, Vector3Scale(forward(), size)), BLUE); + } + + static void drawWorldBounds() { + Color boundsColor = {255, 255, 0, 100}; + DrawCubeWires({0, WORLD_MAX_HEIGHT/2, 0}, + WORLD_BOUNDS * 2, WORLD_MAX_HEIGHT, WORLD_BOUNDS * 2, + boundsColor); + } +}; \ No newline at end of file