1
0
game/client/main.cpp
2025-09-08 12:18:01 -05:00

292 lines
8.2 KiB
C++

#include <raylib.h>
#include <raymath.h>
#include <iostream>
#include <vector>
#include <fstream>
#include <unordered_map>
#include <cstdint>
#include "PlayerController.hpp"
#include "net/NetworkManager.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;
mesh.vertices[idx * 3] = (float(x) / (size - 1) - 0.5f) * WORLD_SIZE;
mesh.vertices[idx * 3 + 1] = data[idx];
mesh.vertices[idx * 3 + 2] = (float(z) / (size - 1) - 0.5f) * WORLD_SIZE;
mesh.texcoords[idx * 2] = static_cast<float>(x) / size;
mesh.texcoords[idx * 2 + 1] = static_cast<float>(z) / size;
}
}
// 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;
}
};
class Game {
PlayerController playerController;
Model terrainModel;
Model playerModel;
Heightmap heightmap;
NetworkManager network;
Vector3 playerPos{0, 0, 0};
Texture2D terrainTexture;
std::unordered_map<std::string, Texture2D> playerTextures;
std::unordered_map<uint32_t, Model> remotePlayerModels;
public:
Game() {
InitWindow(1280, 720, "Multiplayer Terrain Game");
SetTargetFPS(60);
// Load heightmap
if (!heightmap.load("../assets/heightmap.bin")) {
std::cerr << "Failed to load heightmap\n";
}
// 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);
// Connect to server
network.sendLogin();
}
~Game() {
UnloadTexture(terrainTexture);
for (auto& [color, texture] : playerTextures) {
UnloadTexture(texture);
}
UnloadModel(terrainModel);
UnloadModel(playerModel);
for (auto& [id, model] : remotePlayerModels) {
UnloadModel(model);
}
CloseWindow();
}
void run() {
while (!WindowShouldClose()) {
update();
render();
}
}
private:
void update() {
if (!network.isConnected()) {
if (IsKeyPressed(KEY_SPACE)) {
network.sendLogin();
}
return;
}
// Get server position and update player controller
playerPos = network.getPosition();
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
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;
}
}
// 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);
}
// 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]);
}
}
void render() {
BeginDrawing();
ClearBackground(SKYBLUE);
BeginMode3D(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);
}
}
EndMode3D();
// UI
DrawText(network.isConnected() ? "Connected" : "Press SPACE to connect", 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", 10, 60, 20, WHITE);
if (network.isConnected()) {
std::string colorText = "Your color: " + network.getPlayerColor();
DrawText(colorText.c_str(), 10, 85, 20, WHITE);
}
DrawFPS(10, 110);
EndDrawing();
}
};
int main() {
Game game;
game.run();
return 0;
}