initial commit
BIN
assets/heightmap.bin
Normal file
BIN
assets/heightmap.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
assets/textures/black.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/textures/green.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/textures/orange.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/textures/purple.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/textures/red.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/textures/white.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
25
client/CMakeLists.txt
Normal file
@ -0,0 +1,25 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(TerrainGame)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# Find packages
|
||||
find_package(raylib 4.0 REQUIRED)
|
||||
find_package(Boost 1.75 REQUIRED COMPONENTS system)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
# Create executable
|
||||
add_executable(game main.cpp)
|
||||
|
||||
# Link libraries
|
||||
target_link_libraries(game
|
||||
raylib
|
||||
Boost::system
|
||||
Threads::Threads
|
||||
)
|
||||
|
||||
# Compiler flags
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||
target_compile_options(game PRIVATE -Wall -Wextra -O2)
|
||||
endif()
|
||||
23
client/Makefile
Normal file
@ -0,0 +1,23 @@
|
||||
CXX = g++
|
||||
CXXFLAGS = -std=c++20 -Wall -Wextra -O2 -I.
|
||||
LDFLAGS = -lraylib -lboost_system -lpthread -lGL -lm -ldl -lrt -lX11
|
||||
|
||||
TARGET = game_client
|
||||
SOURCES = main.cpp PlayerController.cpp net/NetworkManager.cpp
|
||||
OBJECTS = $(SOURCES:.cpp=.o)
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJECTS)
|
||||
$(CXX) $(OBJECTS) -o $(TARGET) $(LDFLAGS)
|
||||
|
||||
%.o: %.cpp
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
|
||||
clean:
|
||||
rm -f $(OBJECTS) $(TARGET)
|
||||
|
||||
run: $(TARGET)
|
||||
./$(TARGET)
|
||||
|
||||
.PHONY: all clean run
|
||||
131
client/PlayerController.cpp
Normal file
@ -0,0 +1,131 @@
|
||||
#include "PlayerController.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
PlayerController::PlayerController(float distance, float height, float speed)
|
||||
: cameraDistance(distance), cameraHeight(height), moveSpeed(speed) {
|
||||
|
||||
camera.position = {0, cameraHeight, cameraDistance};
|
||||
camera.target = {0, 0, 0};
|
||||
camera.up = {0, 1, 0};
|
||||
camera.fovy = 45.0f;
|
||||
camera.projection = CAMERA_PERSPECTIVE;
|
||||
|
||||
updateCameraPosition();
|
||||
}
|
||||
|
||||
void PlayerController::update(float deltaTime) {
|
||||
handleCameraRotation();
|
||||
handleCameraZoom();
|
||||
updateCameraPosition();
|
||||
}
|
||||
|
||||
void PlayerController::setPlayerPosition(const Vector3& position) {
|
||||
playerPosition = position;
|
||||
}
|
||||
|
||||
Vector3 PlayerController::getMoveInput() const {
|
||||
Vector3 moveDir = {0, 0, 0};
|
||||
|
||||
// Forward/Backward movement (W/S) - relative to camera direction
|
||||
if (IsKeyDown(KEY_W)) {
|
||||
moveDir.x += sinf(cameraYaw);
|
||||
moveDir.z += cosf(cameraYaw);
|
||||
}
|
||||
if (IsKeyDown(KEY_S)) {
|
||||
moveDir.x -= sinf(cameraYaw);
|
||||
moveDir.z -= cosf(cameraYaw);
|
||||
}
|
||||
|
||||
// Strafe left/right (Q/E) - perpendicular to camera direction (inverted)
|
||||
if (IsKeyDown(KEY_Q)) {
|
||||
moveDir.x += cosf(cameraYaw);
|
||||
moveDir.z -= sinf(cameraYaw);
|
||||
}
|
||||
if (IsKeyDown(KEY_E)) {
|
||||
moveDir.x -= cosf(cameraYaw);
|
||||
moveDir.z += sinf(cameraYaw);
|
||||
}
|
||||
|
||||
// Also support A/D for strafing as alternative (inverted)
|
||||
if (IsKeyDown(KEY_A)) {
|
||||
moveDir.x += cosf(cameraYaw);
|
||||
moveDir.z -= sinf(cameraYaw);
|
||||
}
|
||||
if (IsKeyDown(KEY_D)) {
|
||||
moveDir.x -= cosf(cameraYaw);
|
||||
moveDir.z += sinf(cameraYaw);
|
||||
}
|
||||
|
||||
// Normalize movement vector if it has length
|
||||
float length = sqrtf(moveDir.x * moveDir.x + moveDir.z * moveDir.z);
|
||||
if (length > 0.0f) {
|
||||
moveDir.x /= length;
|
||||
moveDir.z /= length;
|
||||
}
|
||||
|
||||
return moveDir;
|
||||
}
|
||||
|
||||
void PlayerController::updateCameraPosition() {
|
||||
// Update camera target to player position
|
||||
camera.target = playerPosition;
|
||||
|
||||
// Calculate camera position based on spherical coordinates
|
||||
float cosYaw = cosf(cameraYaw);
|
||||
float sinYaw = sinf(cameraYaw);
|
||||
float cosPitch = cosf(cameraPitch);
|
||||
float sinPitch = sinf(cameraPitch);
|
||||
|
||||
camera.position.x = playerPosition.x - sinYaw * cosPitch * cameraDistance;
|
||||
camera.position.y = playerPosition.y + sinPitch * cameraDistance;
|
||||
camera.position.z = playerPosition.z - cosYaw * cosPitch * cameraDistance;
|
||||
}
|
||||
|
||||
void PlayerController::handleCameraRotation() {
|
||||
// Check for right mouse button
|
||||
if (IsMouseButtonDown(MOUSE_RIGHT_BUTTON)) {
|
||||
if (!isRightMouseDown) {
|
||||
// Just pressed - store initial mouse position
|
||||
isRightMouseDown = true;
|
||||
lastMousePos = GetMousePosition();
|
||||
DisableCursor();
|
||||
}
|
||||
|
||||
// Get mouse delta
|
||||
Vector2 currentMousePos = GetMousePosition();
|
||||
Vector2 mouseDelta = {
|
||||
currentMousePos.x - lastMousePos.x,
|
||||
currentMousePos.y - lastMousePos.y
|
||||
};
|
||||
|
||||
// Update camera angles (inverted Y-axis)
|
||||
const float sensitivity = 0.003f;
|
||||
cameraYaw -= mouseDelta.x * sensitivity;
|
||||
cameraPitch += mouseDelta.y * sensitivity;
|
||||
|
||||
// Clamp pitch to prevent camera flipping
|
||||
cameraPitch = std::clamp(cameraPitch, -1.4f, 1.4f);
|
||||
|
||||
// Wrap yaw
|
||||
if (cameraYaw > PI * 2.0f) cameraYaw -= PI * 2.0f;
|
||||
if (cameraYaw < 0.0f) cameraYaw += PI * 2.0f;
|
||||
|
||||
lastMousePos = currentMousePos;
|
||||
} else {
|
||||
if (isRightMouseDown) {
|
||||
// Just released
|
||||
isRightMouseDown = false;
|
||||
EnableCursor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerController::handleCameraZoom() {
|
||||
// Handle mouse wheel zoom
|
||||
float wheel = GetMouseWheelMove();
|
||||
if (wheel != 0) {
|
||||
cameraDistance -= wheel * 2.0f;
|
||||
// Clamp zoom distance
|
||||
cameraDistance = std::clamp(cameraDistance, 5.0f, 50.0f);
|
||||
}
|
||||
}
|
||||
37
client/PlayerController.hpp
Normal file
@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <raylib.h>
|
||||
#include <raymath.h>
|
||||
|
||||
class PlayerController {
|
||||
private:
|
||||
Camera3D camera{};
|
||||
Vector3 playerPosition{0, 0, 0};
|
||||
float cameraDistance;
|
||||
float cameraHeight;
|
||||
float moveSpeed;
|
||||
|
||||
float cameraYaw{0.0f};
|
||||
float cameraPitch{0.3f};
|
||||
|
||||
bool isRightMouseDown{false};
|
||||
Vector2 lastMousePos{0, 0};
|
||||
|
||||
public:
|
||||
PlayerController(float distance = 10.0f, float height = 5.0f, float speed = 5.0f);
|
||||
|
||||
void update(float deltaTime);
|
||||
void setPlayerPosition(const Vector3& position);
|
||||
Vector3 getPlayerPosition() const { return playerPosition; }
|
||||
|
||||
Vector3 getMoveInput() const;
|
||||
Camera3D& getCamera() { return camera; }
|
||||
|
||||
float getCameraYaw() const { return cameraYaw; }
|
||||
float getCameraPitch() const { return cameraPitch; }
|
||||
|
||||
private:
|
||||
void updateCameraPosition();
|
||||
void handleCameraRotation();
|
||||
void handleCameraZoom();
|
||||
};
|
||||
291
client/main.cpp
Normal file
@ -0,0 +1,291 @@
|
||||
#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;
|
||||
}
|
||||
243
client/net/NetworkManager.cpp
Normal file
@ -0,0 +1,243 @@
|
||||
#include "NetworkManager.hpp"
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
|
||||
const std::vector<std::string> NetworkManager::AVAILABLE_COLORS = {
|
||||
"red", "green", "orange", "purple", "white"
|
||||
};
|
||||
|
||||
NetworkManager::NetworkManager() : serverEndpoint(ip::make_address("127.0.0.1"), 9999) {
|
||||
socket.open(udp::v4());
|
||||
startReceive();
|
||||
ioThread = std::thread([this] { ioContext.run(); });
|
||||
}
|
||||
|
||||
NetworkManager::~NetworkManager() {
|
||||
ioContext.stop();
|
||||
if (ioThread.joinable()) ioThread.join();
|
||||
}
|
||||
|
||||
void NetworkManager::startReceive() {
|
||||
socket.async_receive_from(
|
||||
buffer(recvBuffer), serverEndpoint,
|
||||
[this](std::error_code ec, std::size_t bytes) {
|
||||
if (!ec && bytes > 0) {
|
||||
processMessage(recvBuffer.data(), bytes);
|
||||
}
|
||||
startReceive();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void NetworkManager::processMessage(const uint8_t* data, std::size_t size) {
|
||||
if (size == 0) return;
|
||||
|
||||
auto msgType = static_cast<MessageType>(data[0]);
|
||||
|
||||
switch (msgType) {
|
||||
case MessageType::Spawn:
|
||||
handleSpawn(data, size);
|
||||
break;
|
||||
case MessageType::Update:
|
||||
handleUpdate(data, size);
|
||||
break;
|
||||
case MessageType::PlayerJoined:
|
||||
handlePlayerJoined(data, size);
|
||||
break;
|
||||
case MessageType::PlayerLeft:
|
||||
handlePlayerLeft(data, size);
|
||||
break;
|
||||
case MessageType::PlayerList:
|
||||
handlePlayerList(data, size);
|
||||
break;
|
||||
case MessageType::ColorChanged:
|
||||
handleColorChanged(data, size);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkManager::handleSpawn(const uint8_t* data, std::size_t size) {
|
||||
// Message format: [type(1)][id(4)][x(4)][y(4)][z(4)][colorLen(1)][color(colorLen)]
|
||||
if (size < 18) return;
|
||||
|
||||
uint32_t id;
|
||||
float x, y, z;
|
||||
std::memcpy(&id, &data[1], sizeof(id));
|
||||
std::memcpy(&x, &data[5], sizeof(x));
|
||||
std::memcpy(&y, &data[9], sizeof(y));
|
||||
std::memcpy(&z, &data[13], sizeof(z));
|
||||
|
||||
uint8_t colorLen = data[17];
|
||||
if (size >= 18 + colorLen) {
|
||||
playerColor = std::string(reinterpret_cast<const char*>(&data[18]), colorLen);
|
||||
}
|
||||
|
||||
playerID = id;
|
||||
{
|
||||
std::lock_guard lock(positionMutex);
|
||||
serverPosition = {x, y, z};
|
||||
}
|
||||
connected = true;
|
||||
std::cout << "Connected as player " << id << " with color " << playerColor << "\n";
|
||||
}
|
||||
|
||||
void NetworkManager::handleUpdate(const uint8_t* data, std::size_t size) {
|
||||
if (size < 17) return;
|
||||
|
||||
uint32_t id;
|
||||
float x, y, z;
|
||||
std::memcpy(&id, &data[1], sizeof(id));
|
||||
std::memcpy(&x, &data[5], sizeof(x));
|
||||
std::memcpy(&y, &data[9], sizeof(y));
|
||||
std::memcpy(&z, &data[13], sizeof(z));
|
||||
|
||||
if (id == playerID) {
|
||||
std::lock_guard lock(positionMutex);
|
||||
serverPosition = {x, y, z};
|
||||
} else {
|
||||
std::lock_guard lock(remotePlayersMutex);
|
||||
if (remotePlayers.find(id) != remotePlayers.end()) {
|
||||
remotePlayers[id].position = {x, y, z};
|
||||
remotePlayers[id].lastUpdate = GetTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkManager::handlePlayerJoined(const uint8_t* data, std::size_t size) {
|
||||
// Message format: [type(1)][id(4)][x(4)][y(4)][z(4)][colorLen(1)][color(colorLen)]
|
||||
if (size < 18) return;
|
||||
|
||||
uint32_t id;
|
||||
float x, y, z;
|
||||
std::memcpy(&id, &data[1], sizeof(id));
|
||||
std::memcpy(&x, &data[5], sizeof(x));
|
||||
std::memcpy(&y, &data[9], sizeof(y));
|
||||
std::memcpy(&z, &data[13], sizeof(z));
|
||||
|
||||
uint8_t colorLen = data[17];
|
||||
std::string color = "red";
|
||||
if (size >= 18 + colorLen) {
|
||||
color = std::string(reinterpret_cast<const char*>(&data[18]), colorLen);
|
||||
}
|
||||
|
||||
if (id != playerID) {
|
||||
std::lock_guard lock(remotePlayersMutex);
|
||||
remotePlayers[id] = {id, {x, y, z}, color, static_cast<float>(GetTime())};
|
||||
std::cout << "Player " << id << " joined with color " << color << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkManager::handlePlayerLeft(const uint8_t* data, std::size_t size) {
|
||||
if (size < 5) return;
|
||||
|
||||
uint32_t id;
|
||||
std::memcpy(&id, &data[1], sizeof(id));
|
||||
|
||||
std::lock_guard lock(remotePlayersMutex);
|
||||
remotePlayers.erase(id);
|
||||
std::cout << "Player " << id << " left\n";
|
||||
}
|
||||
|
||||
void NetworkManager::handlePlayerList(const uint8_t* data, std::size_t size) {
|
||||
if (size < 2) return;
|
||||
|
||||
uint8_t count = data[1];
|
||||
size_t offset = 2;
|
||||
|
||||
std::lock_guard lock(remotePlayersMutex);
|
||||
remotePlayers.clear();
|
||||
|
||||
for (uint8_t i = 0; i < count && offset + 17 < size; i++) {
|
||||
uint32_t id;
|
||||
float x, y, z;
|
||||
std::memcpy(&id, &data[offset], sizeof(id));
|
||||
std::memcpy(&x, &data[offset + 4], sizeof(x));
|
||||
std::memcpy(&y, &data[offset + 8], sizeof(y));
|
||||
std::memcpy(&z, &data[offset + 12], sizeof(z));
|
||||
|
||||
uint8_t colorLen = data[offset + 16];
|
||||
std::string color = "red";
|
||||
|
||||
if (offset + 17 + colorLen <= size) {
|
||||
color = std::string(reinterpret_cast<const char*>(&data[offset + 17]), colorLen);
|
||||
}
|
||||
|
||||
offset += 17 + colorLen;
|
||||
|
||||
if (id != playerID) {
|
||||
remotePlayers[id] = {id, {x, y, z}, color, static_cast<float>(GetTime())};
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "Received list of " << (int)count << " players\n";
|
||||
}
|
||||
|
||||
void NetworkManager::sendLogin() {
|
||||
std::array<uint8_t, 1> msg{static_cast<uint8_t>(MessageType::Login)};
|
||||
socket.send_to(buffer(msg), serverEndpoint);
|
||||
}
|
||||
|
||||
void NetworkManager::sendMove(float dx, float dy, float dz) {
|
||||
if (!connected) return;
|
||||
|
||||
std::array<uint8_t, 17> msg{};
|
||||
msg[0] = static_cast<uint8_t>(MessageType::Move);
|
||||
|
||||
uint32_t id = playerID;
|
||||
std::memcpy(&msg[1], &id, sizeof(id));
|
||||
std::memcpy(&msg[5], &dx, sizeof(dx));
|
||||
std::memcpy(&msg[9], &dy, sizeof(dy));
|
||||
std::memcpy(&msg[13], &dz, sizeof(dz));
|
||||
|
||||
socket.send_to(buffer(msg), serverEndpoint);
|
||||
}
|
||||
|
||||
void NetworkManager::sendColorChange(const std::string& newColor) {
|
||||
if (!connected) return;
|
||||
|
||||
std::vector<uint8_t> msg(6 + newColor.size());
|
||||
msg[0] = static_cast<uint8_t>(MessageType::ChangeColor);
|
||||
|
||||
uint32_t id = playerID;
|
||||
std::memcpy(&msg[1], &id, sizeof(id));
|
||||
msg[5] = static_cast<uint8_t>(newColor.size());
|
||||
std::memcpy(&msg[6], newColor.data(), newColor.size());
|
||||
|
||||
socket.send_to(buffer(msg), serverEndpoint);
|
||||
}
|
||||
|
||||
Vector3 NetworkManager::getPosition() {
|
||||
std::lock_guard lock(positionMutex);
|
||||
return serverPosition;
|
||||
}
|
||||
|
||||
void NetworkManager::handleColorChanged(const uint8_t* data, std::size_t size) {
|
||||
// Message format: [type(1)][id(4)][colorLen(1)][color(colorLen)]
|
||||
if (size < 6) return;
|
||||
|
||||
uint32_t id;
|
||||
std::memcpy(&id, &data[1], sizeof(id));
|
||||
|
||||
uint8_t colorLen = data[5];
|
||||
if (size >= 6 + colorLen) {
|
||||
std::string newColor(reinterpret_cast<const char*>(&data[6]), colorLen);
|
||||
|
||||
if (id == playerID) {
|
||||
playerColor = newColor;
|
||||
std::cout << "Your color changed to " << newColor << "\n";
|
||||
} else {
|
||||
std::lock_guard lock(remotePlayersMutex);
|
||||
if (remotePlayers.find(id) != remotePlayers.end()) {
|
||||
remotePlayers[id].color = newColor;
|
||||
std::cout << "Player " << id << " changed color to " << newColor << "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_map<uint32_t, RemotePlayer> NetworkManager::getRemotePlayers() {
|
||||
std::lock_guard lock(remotePlayersMutex);
|
||||
return remotePlayers;
|
||||
}
|
||||
78
client/net/NetworkManager.hpp
Normal file
@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <raylib.h>
|
||||
#include <cstdint>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <unordered_map>
|
||||
#include <array>
|
||||
|
||||
using namespace boost::asio;
|
||||
using ip::udp;
|
||||
|
||||
enum class MessageType : uint8_t {
|
||||
Login = 0x01,
|
||||
Position = 0x02,
|
||||
Spawn = 0x03,
|
||||
Move = 0x04,
|
||||
Update = 0x05,
|
||||
PlayerJoined = 0x06,
|
||||
PlayerLeft = 0x07,
|
||||
PlayerList = 0x08,
|
||||
ChangeColor = 0x09,
|
||||
ColorChanged = 0x0A
|
||||
};
|
||||
|
||||
struct RemotePlayer {
|
||||
uint32_t id;
|
||||
Vector3 position;
|
||||
std::string color;
|
||||
float lastUpdate;
|
||||
};
|
||||
|
||||
class NetworkManager {
|
||||
public:
|
||||
NetworkManager();
|
||||
~NetworkManager();
|
||||
|
||||
void sendLogin();
|
||||
void sendMove(float dx, float dy, float dz);
|
||||
void sendColorChange(const std::string& newColor);
|
||||
|
||||
Vector3 getPosition();
|
||||
bool isConnected() const { return connected; }
|
||||
uint32_t getPlayerID() const { return playerID; }
|
||||
std::string getPlayerColor() const { return playerColor; }
|
||||
|
||||
std::unordered_map<uint32_t, RemotePlayer> getRemotePlayers();
|
||||
|
||||
// Available colors for cycling
|
||||
static const std::vector<std::string> AVAILABLE_COLORS;
|
||||
|
||||
private:
|
||||
io_context ioContext;
|
||||
udp::socket socket{ioContext};
|
||||
udp::endpoint serverEndpoint;
|
||||
std::thread ioThread;
|
||||
std::array<uint8_t, 1024> recvBuffer;
|
||||
|
||||
std::atomic<uint32_t> playerID{0};
|
||||
std::string playerColor{"red"};
|
||||
std::mutex positionMutex;
|
||||
Vector3 serverPosition{0, 0, 0};
|
||||
std::atomic<bool> connected{false};
|
||||
|
||||
std::mutex remotePlayersMutex;
|
||||
std::unordered_map<uint32_t, RemotePlayer> remotePlayers;
|
||||
|
||||
void startReceive();
|
||||
void processMessage(const uint8_t* data, std::size_t size);
|
||||
void handleSpawn(const uint8_t* data, std::size_t size);
|
||||
void handleUpdate(const uint8_t* data, std::size_t size);
|
||||
void handlePlayerJoined(const uint8_t* data, std::size_t size);
|
||||
void handlePlayerLeft(const uint8_t* data, std::size_t size);
|
||||
void handlePlayerList(const uint8_t* data, std::size_t size);
|
||||
void handleColorChanged(const uint8_t* data, std::size_t size);
|
||||
};
|
||||
3
server/go.mod
Normal file
@ -0,0 +1,3 @@
|
||||
module server
|
||||
|
||||
go 1.25.0
|
||||
509
server/main.go
Normal file
@ -0,0 +1,509 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
"slices"
|
||||
)
|
||||
|
||||
type Vec3 struct {
|
||||
X, Y, Z float32
|
||||
}
|
||||
|
||||
type Player struct {
|
||||
ID uint32
|
||||
Position Vec3
|
||||
Velocity Vec3
|
||||
Color string
|
||||
Address *net.UDPAddr
|
||||
LastSeen time.Time
|
||||
}
|
||||
|
||||
type GameServer struct {
|
||||
conn *net.UDPConn
|
||||
players map[uint32]*Player
|
||||
heightmap [][]float32
|
||||
mutex sync.RWMutex
|
||||
nextID uint32
|
||||
}
|
||||
|
||||
const (
|
||||
MSG_LOGIN = 0x01
|
||||
MSG_POSITION = 0x02
|
||||
MSG_SPAWN = 0x03
|
||||
MSG_MOVE = 0x04
|
||||
MSG_UPDATE = 0x05
|
||||
MSG_PLAYER_JOINED = 0x06
|
||||
MSG_PLAYER_LEFT = 0x07
|
||||
MSG_PLAYER_LIST = 0x08
|
||||
MSG_CHANGE_COLOR = 0x09
|
||||
MSG_COLOR_CHANGED = 0x0A
|
||||
|
||||
WORLD_SIZE = 100
|
||||
WORLD_SCALE = 10.0
|
||||
MOVE_SPEED = 15.0
|
||||
GRAVITY = -9.8
|
||||
PLAYER_HEIGHT = 1.0
|
||||
)
|
||||
|
||||
func generateHeightmap(size int) [][]float32 {
|
||||
heightmap := make([][]float32, size)
|
||||
for i := range heightmap {
|
||||
heightmap[i] = make([]float32, size)
|
||||
}
|
||||
|
||||
// Simple perlin-like noise
|
||||
for y := 0; y < size; y++ {
|
||||
for x := 0; x < size; x++ {
|
||||
nx := float64(x) / float64(size) * 4
|
||||
ny := float64(y) / float64(size) * 4
|
||||
heightmap[y][x] = float32(
|
||||
math.Sin(nx*2+rand.Float64()) * 0.5 +
|
||||
math.Cos(ny*3+rand.Float64()) * 0.3 +
|
||||
rand.Float64() * 0.2) * 10.0
|
||||
}
|
||||
}
|
||||
|
||||
// Smooth the heightmap
|
||||
for i := 0; i < 3; i++ {
|
||||
newHeightmap := make([][]float32, size)
|
||||
for y := range newHeightmap {
|
||||
newHeightmap[y] = make([]float32, size)
|
||||
for x := range newHeightmap[y] {
|
||||
sum := heightmap[y][x]
|
||||
count := float32(1)
|
||||
for dy := -1; dy <= 1; dy++ {
|
||||
for dx := -1; dx <= 1; dx++ {
|
||||
nx, ny := x+dx, y+dy
|
||||
if nx >= 0 && nx < size && ny >= 0 && ny < size {
|
||||
sum += heightmap[ny][nx]
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
newHeightmap[y][x] = sum / count
|
||||
}
|
||||
}
|
||||
heightmap = newHeightmap
|
||||
}
|
||||
|
||||
return heightmap
|
||||
}
|
||||
|
||||
func saveHeightmapPNG(heightmap [][]float32, filename string) {
|
||||
size := len(heightmap)
|
||||
img := image.NewGray(image.Rect(0, 0, size, size))
|
||||
|
||||
// Find min/max for normalization
|
||||
minH, maxH := heightmap[0][0], heightmap[0][0]
|
||||
for y := range heightmap {
|
||||
for x := range heightmap[y] {
|
||||
if heightmap[y][x] < minH {
|
||||
minH = heightmap[y][x]
|
||||
}
|
||||
if heightmap[y][x] > maxH {
|
||||
maxH = heightmap[y][x]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for y := range heightmap {
|
||||
for x := range heightmap[y] {
|
||||
normalized := (heightmap[y][x] - minH) / (maxH - minH)
|
||||
img.SetGray(x, y, color.Gray{uint8(normalized * 255)})
|
||||
}
|
||||
}
|
||||
|
||||
file, _ := os.Create(filename)
|
||||
defer file.Close()
|
||||
png.Encode(file, img)
|
||||
}
|
||||
|
||||
func saveHeightmapBinary(heightmap [][]float32, filename string) {
|
||||
size := len(heightmap)
|
||||
file, _ := os.Create(filename)
|
||||
defer file.Close()
|
||||
|
||||
binary.Write(file, binary.LittleEndian, int32(size))
|
||||
for y := range heightmap {
|
||||
for x := range heightmap[y] {
|
||||
binary.Write(file, binary.LittleEndian, heightmap[y][x])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GameServer) getHeightAt(x, z float32) float32 {
|
||||
// Convert world coords to heightmap coords with bilinear interpolation
|
||||
size := float32(len(s.heightmap))
|
||||
fx := (x/WORLD_SIZE + 0.5) * (size - 1)
|
||||
fz := (z/WORLD_SIZE + 0.5) * (size - 1)
|
||||
|
||||
// Get integer coordinates
|
||||
x0 := int(math.Floor(float64(fx)))
|
||||
z0 := int(math.Floor(float64(fz)))
|
||||
x1 := x0 + 1
|
||||
z1 := z0 + 1
|
||||
|
||||
// Clamp to bounds
|
||||
if x0 < 0 || x1 >= len(s.heightmap) || z0 < 0 || z1 >= len(s.heightmap) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Get fractional parts
|
||||
tx := fx - float32(x0)
|
||||
tz := fz - float32(z0)
|
||||
|
||||
// Bilinear interpolation
|
||||
h00 := s.heightmap[z0][x0]
|
||||
h10 := s.heightmap[z0][x1]
|
||||
h01 := s.heightmap[z1][x0]
|
||||
h11 := s.heightmap[z1][x1]
|
||||
|
||||
h0 := h00*(1-tx) + h10*tx
|
||||
h1 := h01*(1-tx) + h11*tx
|
||||
|
||||
return h0*(1-tz) + h1*tz
|
||||
}
|
||||
|
||||
func (s *GameServer) loadPlayerPositions() {
|
||||
data, err := os.ReadFile("players.json")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var savedPlayers map[uint32]Vec3
|
||||
json.Unmarshal(data, &savedPlayers)
|
||||
|
||||
for id, pos := range savedPlayers {
|
||||
if id > s.nextID {
|
||||
s.nextID = id
|
||||
}
|
||||
s.players[id] = &Player{
|
||||
ID: id,
|
||||
Position: pos,
|
||||
LastSeen: time.Now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GameServer) sendPlayerList(addr *net.UDPAddr, players []*Player) {
|
||||
if len(players) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
msg := make([]byte, 1024)
|
||||
msg[0] = MSG_PLAYER_LIST
|
||||
msg[1] = uint8(len(players))
|
||||
|
||||
offset := 2
|
||||
for _, p := range players {
|
||||
binary.LittleEndian.PutUint32(msg[offset:], p.ID)
|
||||
binary.LittleEndian.PutUint32(msg[offset+4:], math.Float32bits(p.Position.X))
|
||||
binary.LittleEndian.PutUint32(msg[offset+8:], math.Float32bits(p.Position.Y))
|
||||
binary.LittleEndian.PutUint32(msg[offset+12:], math.Float32bits(p.Position.Z))
|
||||
|
||||
colorBytes := []byte(p.Color)
|
||||
msg[offset+16] = uint8(len(colorBytes))
|
||||
copy(msg[offset+17:], colorBytes)
|
||||
|
||||
offset += 17 + len(colorBytes)
|
||||
if offset > 1000 {
|
||||
break // Prevent overflow
|
||||
}
|
||||
}
|
||||
|
||||
s.conn.WriteToUDP(msg[:offset], addr)
|
||||
}
|
||||
|
||||
func (s *GameServer) broadcastPlayerJoined(newPlayer *Player) {
|
||||
colorBytes := []byte(newPlayer.Color)
|
||||
msg := make([]byte, 18+len(colorBytes))
|
||||
msg[0] = MSG_PLAYER_JOINED
|
||||
binary.LittleEndian.PutUint32(msg[1:5], newPlayer.ID)
|
||||
binary.LittleEndian.PutUint32(msg[5:9], math.Float32bits(newPlayer.Position.X))
|
||||
binary.LittleEndian.PutUint32(msg[9:13], math.Float32bits(newPlayer.Position.Y))
|
||||
binary.LittleEndian.PutUint32(msg[13:17], math.Float32bits(newPlayer.Position.Z))
|
||||
msg[17] = uint8(len(colorBytes))
|
||||
copy(msg[18:], colorBytes)
|
||||
|
||||
s.mutex.RLock()
|
||||
for _, p := range s.players {
|
||||
if p.ID != newPlayer.ID && p.Address != nil {
|
||||
s.conn.WriteToUDP(msg, p.Address)
|
||||
}
|
||||
}
|
||||
s.mutex.RUnlock()
|
||||
}
|
||||
|
||||
func (s *GameServer) broadcastPlayerLeft(playerID uint32) {
|
||||
msg := make([]byte, 5)
|
||||
msg[0] = MSG_PLAYER_LEFT
|
||||
binary.LittleEndian.PutUint32(msg[1:5], playerID)
|
||||
|
||||
s.mutex.RLock()
|
||||
for _, p := range s.players {
|
||||
if p.ID != playerID && p.Address != nil {
|
||||
s.conn.WriteToUDP(msg, p.Address)
|
||||
}
|
||||
}
|
||||
s.mutex.RUnlock()
|
||||
}
|
||||
|
||||
func (s *GameServer) broadcastUpdate(player *Player) {
|
||||
msg := make([]byte, 17)
|
||||
msg[0] = MSG_UPDATE
|
||||
binary.LittleEndian.PutUint32(msg[1:5], player.ID)
|
||||
binary.LittleEndian.PutUint32(msg[5:9], math.Float32bits(player.Position.X))
|
||||
binary.LittleEndian.PutUint32(msg[9:13], math.Float32bits(player.Position.Y))
|
||||
binary.LittleEndian.PutUint32(msg[13:17], math.Float32bits(player.Position.Z))
|
||||
|
||||
s.mutex.RLock()
|
||||
for _, p := range s.players {
|
||||
if p.Address != nil {
|
||||
s.conn.WriteToUDP(msg, p.Address)
|
||||
}
|
||||
}
|
||||
s.mutex.RUnlock()
|
||||
}
|
||||
|
||||
func (s *GameServer) handleColorChange(data []byte, addr *net.UDPAddr) {
|
||||
if len(data) < 6 {
|
||||
return
|
||||
}
|
||||
|
||||
playerID := binary.LittleEndian.Uint32(data[1:5])
|
||||
colorLen := data[5]
|
||||
|
||||
if len(data) < 6+int(colorLen) {
|
||||
return
|
||||
}
|
||||
|
||||
newColor := string(data[6 : 6+colorLen])
|
||||
|
||||
// Validate color
|
||||
validColors := []string{"red", "green", "orange", "purple", "white"}
|
||||
isValid := slices.Contains(validColors, newColor)
|
||||
|
||||
if !isValid {
|
||||
return
|
||||
}
|
||||
|
||||
s.mutex.Lock()
|
||||
player, exists := s.players[playerID]
|
||||
if !exists {
|
||||
s.mutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
player.Color = newColor
|
||||
s.mutex.Unlock()
|
||||
|
||||
// Broadcast color change to all players
|
||||
s.broadcastColorChanged(playerID, newColor)
|
||||
|
||||
fmt.Printf("Player %d changed color to %s\n", playerID, newColor)
|
||||
}
|
||||
|
||||
func (s *GameServer) broadcastColorChanged(playerID uint32, color string) {
|
||||
colorBytes := []byte(color)
|
||||
msg := make([]byte, 6+len(colorBytes))
|
||||
msg[0] = MSG_COLOR_CHANGED
|
||||
binary.LittleEndian.PutUint32(msg[1:5], playerID)
|
||||
msg[5] = uint8(len(colorBytes))
|
||||
copy(msg[6:], colorBytes)
|
||||
|
||||
s.mutex.RLock()
|
||||
for _, p := range s.players {
|
||||
if p.Address != nil {
|
||||
s.conn.WriteToUDP(msg, p.Address)
|
||||
}
|
||||
}
|
||||
s.mutex.RUnlock()
|
||||
}
|
||||
|
||||
func (s *GameServer) savePlayerPositions() {
|
||||
s.mutex.RLock()
|
||||
savedPlayers := make(map[uint32]Vec3)
|
||||
for id, player := range s.players {
|
||||
savedPlayers[id] = player.Position
|
||||
}
|
||||
s.mutex.RUnlock()
|
||||
|
||||
data, _ := json.Marshal(savedPlayers)
|
||||
os.WriteFile("players.json", data, 0644)
|
||||
}
|
||||
|
||||
func (s *GameServer) handleLogin(addr *net.UDPAddr) {
|
||||
s.mutex.Lock()
|
||||
s.nextID++
|
||||
playerID := s.nextID
|
||||
|
||||
// Assign color based on player ID to ensure variety
|
||||
colors := []string{"red", "green", "orange", "purple", "white"}
|
||||
// Cycle through colors based on player ID
|
||||
colorIndex := (playerID - 1) % uint32(len(colors))
|
||||
color := colors[colorIndex]
|
||||
|
||||
// Spawn at random position on heightmap
|
||||
x := rand.Float32() * WORLD_SIZE - WORLD_SIZE/2
|
||||
z := rand.Float32() * WORLD_SIZE - WORLD_SIZE/2
|
||||
y := s.getHeightAt(x, z) + PLAYER_HEIGHT
|
||||
|
||||
player := &Player{
|
||||
ID: playerID,
|
||||
Position: Vec3{x, y, z},
|
||||
Color: color,
|
||||
Address: addr,
|
||||
LastSeen: time.Now(),
|
||||
}
|
||||
|
||||
// Send existing players to new player
|
||||
existingPlayers := make([]*Player, 0)
|
||||
for _, p := range s.players {
|
||||
if p.ID != playerID {
|
||||
existingPlayers = append(existingPlayers, p)
|
||||
}
|
||||
}
|
||||
|
||||
s.players[playerID] = player
|
||||
s.mutex.Unlock()
|
||||
|
||||
// Send spawn message with color
|
||||
colorBytes := []byte(color)
|
||||
msg := make([]byte, 18+len(colorBytes))
|
||||
msg[0] = MSG_SPAWN
|
||||
binary.LittleEndian.PutUint32(msg[1:5], playerID)
|
||||
binary.LittleEndian.PutUint32(msg[5:9], math.Float32bits(player.Position.X))
|
||||
binary.LittleEndian.PutUint32(msg[9:13], math.Float32bits(player.Position.Y))
|
||||
binary.LittleEndian.PutUint32(msg[13:17], math.Float32bits(player.Position.Z))
|
||||
msg[17] = uint8(len(colorBytes))
|
||||
copy(msg[18:], colorBytes)
|
||||
|
||||
s.conn.WriteToUDP(msg, addr)
|
||||
|
||||
// Send player list to new player
|
||||
s.sendPlayerList(addr, existingPlayers)
|
||||
|
||||
// Notify other players about new player
|
||||
s.broadcastPlayerJoined(player)
|
||||
|
||||
fmt.Printf("Player %d logged in at (%.2f, %.2f, %.2f) with color %s\n", playerID, x, y, z, color)
|
||||
|
||||
s.savePlayerPositions()
|
||||
}
|
||||
|
||||
func (s *GameServer) handleMove(data []byte, addr *net.UDPAddr) {
|
||||
if len(data) < 17 {
|
||||
return
|
||||
}
|
||||
|
||||
playerID := binary.LittleEndian.Uint32(data[1:5])
|
||||
dx := math.Float32frombits(binary.LittleEndian.Uint32(data[5:9]))
|
||||
// dy := math.Float32frombits(binary.LittleEndian.Uint32(data[9:13])) // Not used - Y position is determined by terrain height
|
||||
dz := math.Float32frombits(binary.LittleEndian.Uint32(data[13:17]))
|
||||
|
||||
s.mutex.Lock()
|
||||
player, exists := s.players[playerID]
|
||||
if !exists {
|
||||
s.mutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Server-authoritative movement - server decides the actual speed
|
||||
// dx/dz from client are just normalized direction vectors
|
||||
deltaTime := float32(0.016) // Assume 60fps for now
|
||||
newX := player.Position.X + dx * MOVE_SPEED * deltaTime
|
||||
newZ := player.Position.Z + dz * MOVE_SPEED * deltaTime
|
||||
|
||||
// Clamp to world bounds
|
||||
newX = float32(math.Max(float64(-WORLD_SIZE/2), math.Min(float64(WORLD_SIZE/2), float64(newX))))
|
||||
newZ = float32(math.Max(float64(-WORLD_SIZE/2), math.Min(float64(WORLD_SIZE/2), float64(newZ))))
|
||||
|
||||
// Set Y to terrain height with some smoothing
|
||||
targetY := s.getHeightAt(newX, newZ) + PLAYER_HEIGHT
|
||||
// Smooth the Y transition
|
||||
smoothFactor := float32(0.15) // How quickly to adapt to new height
|
||||
newY := player.Position.Y + (targetY - player.Position.Y) * smoothFactor
|
||||
|
||||
player.Position.X = newX
|
||||
player.Position.Y = newY
|
||||
player.Position.Z = newZ
|
||||
player.LastSeen = time.Now()
|
||||
|
||||
s.mutex.Unlock()
|
||||
|
||||
// Broadcast position update to all players
|
||||
s.broadcastUpdate(player)
|
||||
}
|
||||
|
||||
func (s *GameServer) run() {
|
||||
buffer := make([]byte, 1024)
|
||||
|
||||
// Periodic save
|
||||
go func() {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
for range ticker.C {
|
||||
s.savePlayerPositions()
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
n, addr, err := s.conn.ReadFromUDP(buffer)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if n < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
msgType := buffer[0]
|
||||
|
||||
switch msgType {
|
||||
case MSG_LOGIN:
|
||||
s.handleLogin(addr)
|
||||
case MSG_MOVE:
|
||||
s.handleMove(buffer[:n], addr)
|
||||
case MSG_CHANGE_COLOR:
|
||||
s.handleColorChange(buffer[:n], addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Generate and save heightmap
|
||||
fmt.Println("Generating heightmap...")
|
||||
heightmap := generateHeightmap(WORLD_SIZE)
|
||||
saveHeightmapPNG(heightmap, "../assets/heightmap.png")
|
||||
saveHeightmapBinary(heightmap, "../assets/heightmap.bin")
|
||||
|
||||
// Start UDP server
|
||||
addr, _ := net.ResolveUDPAddr("udp", ":9999")
|
||||
conn, err := net.ListenUDP("udp", addr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
server := &GameServer{
|
||||
conn: conn,
|
||||
players: make(map[uint32]*Player),
|
||||
heightmap: heightmap,
|
||||
nextID: 0,
|
||||
}
|
||||
|
||||
server.loadPlayerPositions()
|
||||
|
||||
fmt.Println("Server running on :9999")
|
||||
server.run()
|
||||
}
|
||||
1
server/players.json
Normal file
@ -0,0 +1 @@
|
||||
{"1":{"X":-2.5774379,"Y":0.3485479,"Z":3.2305741},"2":{"X":-1.6390398,"Y":0.5682664,"Z":1.0276936}}
|
||||