1
0
game/client/main.cpp

387 lines
11 KiB
C++

#include <raylib.h>
#include <raymath.h>
#define RAYGUI_IMPLEMENTATION
#include "includes/raygui.h"
#include <iostream>
#include <vector>
#include <cstring>
#include <thread>
#include <chrono>
#include <memory>
#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"
#include "terrain/Heightmap.hpp"
#include "config.hpp"
enum GameState {
STATE_LOGIN,
STATE_CONNECTING,
STATE_PLAYING
};
class Game {
GameConfig config;
PlayerController playerController;
std::unique_ptr<PlayerRenderer> playerRenderer;
RenderContext renderContext;
Model terrainModel;
Heightmap heightmap;
NetworkManager network;
std::unique_ptr<Sky> sky;
Vector3 playerPos{0, 0, 0};
Texture2D terrainTexture;
// Login UI state
GameState gameState = STATE_LOGIN;
char usernameBuffer[32] = "";
bool editMode = false;
std::string loginError = "";
std::string currentUsername = "";
// Heartbeat timing
float lastHeartbeatTime = 0.0f;
// Debug options
bool showDebugAxes = false;
bool showWorldBounds = false;
public:
Game(const GameConfig& cfg) : config(cfg) {
InitWindow(config.windowWidth, config.windowHeight, config.windowTitle.c_str());
SetTargetFPS(config.targetFPS);
// Initialize components after window is created
playerRenderer = std::make_unique<PlayerRenderer>();
sky = std::make_unique<Sky>();
// Configure and load heightmap
Heightmap::Config heightConfig;
heightConfig.unitsPerSample = config.unitsPerSample;
heightConfig.heightScale = config.heightScale;
heightmap = Heightmap(heightConfig);
if (!heightmap.load(config.heightmapFile)) {
std::cerr << "Failed to load heightmap\n";
} else {
// Update world bounds based on loaded heightmap
Coords::setWorldBounds(heightmap.getWorldBounds());
std::cout << "Loaded heightmap: " << heightmap.getSamplesPerSide()
<< "x" << heightmap.getSamplesPerSide() << " samples, "
<< "world size: " << heightmap.getWorldWidth()
<< "x" << heightmap.getWorldHeight() << " units\n";
}
// Load textures
terrainTexture = LoadTexture("../assets/textures/black.png");
// Create terrain model
auto terrainMesh = heightmap.generateMesh();
terrainModel = LoadModelFromMesh(terrainMesh);
terrainModel.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = terrainTexture;
}
~Game() {
// Send logout if we're connected
if (gameState == STATE_PLAYING && network.isConnected()) {
network.sendLogout();
}
UnloadTexture(terrainTexture);
UnloadModel(terrainModel);
CloseWindow();
}
void run() {
while (!WindowShouldClose()) {
update();
render();
}
// Clean logout when window is closing
if (gameState == STATE_PLAYING && network.isConnected()) {
network.sendLogout();
// Give the network time to send the packet
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
private:
void update() {
switch (gameState) {
case STATE_LOGIN:
// Login screen - no game updates
break;
case STATE_CONNECTING:
// Check if we've received a response from the server
if (network.isConnected()) {
gameState = STATE_PLAYING;
currentUsername = std::string(usernameBuffer);
} else if (network.hasLoginError()) {
gameState = STATE_LOGIN;
loginError = network.getLoginError();
}
break;
case STATE_PLAYING:
updateGame();
break;
}
}
void updateGame() {
if (!network.isConnected()) {
// Player disconnected, go back to login
gameState = STATE_LOGIN;
loginError = "Disconnected from server";
return;
}
// Update sky with server time if connected, otherwise use local time
if (network.isConnected()) {
float serverTime = network.getServerTimeOfDay();
sky->updateFromServerTime(serverTime);
} else {
sky->update(GetFrameTime());
}
// Time of day controls (for testing)
if (IsKeyPressed(KEY_T)) {
float currentTime = sky->getTimeOfDay();
currentTime += 0.1f;
if (currentTime > 1.0f) currentTime -= 1.0f;
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);
// Clean up disconnected player models
auto remotePlayers = network.getRemotePlayers();
playerRenderer->cleanupDisconnectedPlayers(remotePlayers);
// Update player controller (handles camera)
float deltaTime = GetFrameTime();
playerController.update(deltaTime);
// Get movement input from player controller
Vector3 moveInput = playerController.getMoveInput();
// Send normalized movement direction to server (server handles speed)
if (moveInput.x != 0 || moveInput.z != 0) {
network.sendMove(moveInput.x, 0, moveInput.z);
lastHeartbeatTime = GetTime(); // Reset heartbeat timer when moving
}
// Send periodic heartbeats when not moving
float currentTime = GetTime();
if (currentTime - lastHeartbeatTime >= config.heartbeatInterval) {
network.sendHeartbeat();
lastHeartbeatTime = currentTime;
}
// Handle color change with arrow keys
static int currentColorIndex = -1;
if (IsKeyPressed(KEY_LEFT) || IsKeyPressed(KEY_RIGHT)) {
// Get current color index if not set
if (currentColorIndex == -1) {
auto currentColor = network.getPlayerColor();
for (size_t i = 0; i < NetworkManager::AVAILABLE_COLORS.size(); i++) {
if (NetworkManager::AVAILABLE_COLORS[i] == currentColor) {
currentColorIndex = i;
break;
}
}
if (currentColorIndex == -1) currentColorIndex = 0;
}
// Change color index
if (IsKeyPressed(KEY_LEFT)) {
currentColorIndex--;
if (currentColorIndex < 0) {
currentColorIndex = NetworkManager::AVAILABLE_COLORS.size() - 1;
}
} else if (IsKeyPressed(KEY_RIGHT)) {
currentColorIndex++;
if (currentColorIndex >= (int)NetworkManager::AVAILABLE_COLORS.size()) {
currentColorIndex = 0;
}
}
// Send color change to server
network.sendColorChange(NetworkManager::AVAILABLE_COLORS[currentColorIndex]);
}
// Handle logout
if (IsKeyPressed(KEY_ESCAPE)) {
network.sendLogout();
gameState = STATE_LOGIN;
loginError = "";
}
}
void render() {
BeginDrawing();
// Clear with a default color first
ClearBackground(SKYBLUE);
switch (gameState) {
case STATE_LOGIN:
renderLoginScreen();
break;
case STATE_CONNECTING:
renderConnectingScreen();
break;
case STATE_PLAYING:
renderGame();
break;
}
EndDrawing();
}
void renderLoginScreen() {
// Center the login box
int boxWidth = 300;
int boxHeight = 200;
int boxX = (GetScreenWidth() - boxWidth) / 2;
int boxY = (GetScreenHeight() - boxHeight) / 2;
// Draw login panel
GuiPanel((Rectangle){(float)boxX, (float)boxY, (float)boxWidth, (float)boxHeight}, "Login");
// Username label and text box
GuiLabel((Rectangle){(float)(boxX + 20), (float)(boxY + 50), 80, 30}, "Username:");
if (GuiTextBox((Rectangle){(float)(boxX + 100), (float)(boxY + 50), 180, 30},
usernameBuffer, 32, editMode)) {
editMode = !editMode;
}
// Login button
if (GuiButton((Rectangle){(float)(boxX + 100), (float)(boxY + 100), 100, 30}, "Login")) {
if (strlen(usernameBuffer) > 0) {
// Send login with username
network.sendLoginWithUsername(usernameBuffer);
gameState = STATE_CONNECTING;
loginError = "";
} else {
loginError = "Please enter a username";
}
}
// Error message
if (!loginError.empty()) {
DrawText(loginError.c_str(), boxX + 20, boxY + 150, 20, RED);
}
// Instructions
DrawText("Enter your username to join the game", 10, 10, 20, WHITE);
}
void renderConnectingScreen() {
DrawText("Connecting to server...",
GetScreenWidth()/2 - 100, GetScreenHeight()/2 - 10, 20, WHITE);
}
void renderGame() {
// Begin 3D rendering with render context
renderContext.begin3D(playerController.getCamera());
// 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);
}
// 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 | 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);
}
// Show time of day
float timeHour = sky->getTimeOfDay() * 24.0f;
int hour = (int)timeHour;
int minute = (int)((timeHour - hour) * 60);
DrawText(TextFormat("Time: %02d:%02d", hour, minute), 10, 135, 20, WHITE);
// 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);
}
};
int main(int argc, char* argv[]) {
GameConfig config = GameConfig::fromArgs(argc, argv);
// Show configuration if debug is enabled
if (config.showDebugInfo) {
std::cout << "Configuration:\n";
std::cout << " Heightmap: " << config.heightmapFile << "\n";
std::cout << " Units per sample: " << config.unitsPerSample << "\n";
std::cout << " Height scale: " << config.heightScale << "\n";
}
Game game(config);
game.run();
return 0;
}