460 lines
12 KiB
C++
460 lines
12 KiB
C++
#include <raylib.h>
|
|
#include <raymath.h>
|
|
|
|
#define RAYGUI_IMPLEMENTATION
|
|
#include "includes/raygui.h"
|
|
|
|
#include <iostream>
|
|
#include <vector>
|
|
#include <fstream>
|
|
#include <cstdint>
|
|
#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"
|
|
|
|
constexpr int WORLD_SIZE = 100;
|
|
constexpr float WORLD_SCALE = 10.0f;
|
|
constexpr float MOVE_SPEED = 15.0f;
|
|
|
|
struct Heightmap {
|
|
std::vector<float> data;
|
|
int size{0};
|
|
|
|
float getHeight(float x, float z) const {
|
|
auto hx = static_cast<int>((x / WORLD_SIZE + 0.5f) * (size - 1));
|
|
auto hz = static_cast<int>((z / WORLD_SIZE + 0.5f) * (size - 1));
|
|
|
|
if (hx < 0 || hx >= size || hz < 0 || hz >= size) return 0.0f;
|
|
return data[hz * size + hx];
|
|
}
|
|
|
|
bool load(const std::string& filename) {
|
|
std::ifstream file(filename, std::ios::binary);
|
|
if (!file) return false;
|
|
|
|
int32_t fileSize;
|
|
file.read(reinterpret_cast<char*>(&fileSize), sizeof(fileSize));
|
|
size = fileSize;
|
|
|
|
data.resize(size * size);
|
|
file.read(reinterpret_cast<char*>(data.data()), data.size() * sizeof(float));
|
|
return true;
|
|
}
|
|
|
|
Mesh generateMesh() const {
|
|
auto mesh = Mesh{};
|
|
int vertexCount = size * size;
|
|
int triangleCount = (size - 1) * (size - 1) * 2;
|
|
|
|
mesh.vertexCount = vertexCount;
|
|
mesh.triangleCount = triangleCount;
|
|
mesh.vertices = (float*)MemAlloc(vertexCount * 3 * sizeof(float));
|
|
mesh.texcoords = (float*)MemAlloc(vertexCount * 2 * sizeof(float));
|
|
mesh.normals = (float*)MemAlloc(vertexCount * 3 * sizeof(float));
|
|
mesh.indices = (unsigned short*)MemAlloc(triangleCount * 3 * sizeof(unsigned short));
|
|
|
|
// Generate vertices
|
|
for (int z = 0; z < size; z++) {
|
|
for (int x = 0; x < size; x++) {
|
|
int idx = z * size + x;
|
|
// World position in units (meters)
|
|
float worldX = (float(x) / (size - 1) - 0.5f) * WORLD_SIZE;
|
|
float worldZ = (float(z) / (size - 1) - 0.5f) * WORLD_SIZE;
|
|
|
|
mesh.vertices[idx * 3] = worldX;
|
|
mesh.vertices[idx * 3 + 1] = data[idx];
|
|
mesh.vertices[idx * 3 + 2] = worldZ;
|
|
|
|
// Texture coordinates: 1 unit = 1 texture tile
|
|
// Since world goes from -50 to +50, we need to map accordingly
|
|
// Adding 0.5f * WORLD_SIZE to shift from [-50, 50] to [0, 100]
|
|
float texU = (worldX + 0.5f * WORLD_SIZE);
|
|
float texV = (worldZ + 0.5f * WORLD_SIZE);
|
|
|
|
mesh.texcoords[idx * 2] = texU;
|
|
mesh.texcoords[idx * 2 + 1] = texV;
|
|
}
|
|
}
|
|
|
|
// Generate indices
|
|
int triIdx = 0;
|
|
for (int z = 0; z < size - 1; z++) {
|
|
for (int x = 0; x < size - 1; x++) {
|
|
int topLeft = z * size + x;
|
|
int topRight = topLeft + 1;
|
|
int bottomLeft = (z + 1) * size + x;
|
|
int bottomRight = bottomLeft + 1;
|
|
|
|
mesh.indices[triIdx++] = topLeft;
|
|
mesh.indices[triIdx++] = bottomLeft;
|
|
mesh.indices[triIdx++] = topRight;
|
|
|
|
mesh.indices[triIdx++] = topRight;
|
|
mesh.indices[triIdx++] = bottomLeft;
|
|
mesh.indices[triIdx++] = bottomRight;
|
|
}
|
|
}
|
|
|
|
// Calculate normals
|
|
for (int i = 0; i < vertexCount; i++) {
|
|
mesh.normals[i * 3] = 0;
|
|
mesh.normals[i * 3 + 1] = 1;
|
|
mesh.normals[i * 3 + 2] = 0;
|
|
}
|
|
|
|
UploadMesh(&mesh, false);
|
|
return mesh;
|
|
}
|
|
};
|
|
|
|
enum GameState {
|
|
STATE_LOGIN,
|
|
STATE_CONNECTING,
|
|
STATE_PLAYING
|
|
};
|
|
|
|
class Game {
|
|
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;
|
|
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 components after window is created
|
|
playerRenderer = std::make_unique<PlayerRenderer>();
|
|
sky = std::make_unique<Sky>();
|
|
|
|
// Load heightmap
|
|
if (!heightmap.load("../assets/heightmap.bin")) {
|
|
std::cerr << "Failed to load heightmap\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 >= HEARTBEAT_INTERVAL) {
|
|
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() {
|
|
Game game;
|
|
game.run();
|
|
return 0;
|
|
}
|