diff --git a/assets/heightmap.bin b/assets/heightmap.bin index 9416b45..38b45a1 100644 Binary files a/assets/heightmap.bin and b/assets/heightmap.bin differ diff --git a/client/main.cpp b/client/main.cpp index f389df7..95edad2 100644 --- a/client/main.cpp +++ b/client/main.cpp @@ -6,8 +6,6 @@ #include #include -#include -#include #include #include #include @@ -18,101 +16,8 @@ #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 data; - int size{0}; - - float getHeight(float x, float z) const { - auto hx = static_cast((x / WORLD_SIZE + 0.5f) * (size - 1)); - auto hz = static_cast((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(&fileSize), sizeof(fileSize)); - size = fileSize; - - data.resize(size * size); - file.read(reinterpret_cast(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; - } -}; +#include "terrain/Heightmap.hpp" +#include "config.hpp" enum GameState { STATE_LOGIN, @@ -121,6 +26,7 @@ enum GameState { }; class Game { + GameConfig config; PlayerController playerController; std::unique_ptr playerRenderer; RenderContext renderContext; @@ -140,24 +46,35 @@ class Game { // 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); + 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(); sky = std::make_unique(); - // Load heightmap - if (!heightmap.load("../assets/heightmap.bin")) { + // 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 @@ -272,7 +189,7 @@ private: // Send periodic heartbeats when not moving float currentTime = GetTime(); - if (currentTime - lastHeartbeatTime >= HEARTBEAT_INTERVAL) { + if (currentTime - lastHeartbeatTime >= config.heartbeatInterval) { network.sendHeartbeat(); lastHeartbeatTime = currentTime; } @@ -452,8 +369,18 @@ private: } }; -int main() { - Game game; +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; } diff --git a/client/utils/Coords.hpp b/client/utils/Coords.hpp index 2d1b283..f52950c 100644 --- a/client/utils/Coords.hpp +++ b/client/utils/Coords.hpp @@ -5,9 +5,24 @@ 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; + // Default world bounds (can be overridden) + static inline float WORLD_BOUNDS_X = 50.0f; + static inline float WORLD_BOUNDS_Z = 50.0f; + static inline float WORLD_MIN_HEIGHT = 0.0f; + static inline float WORLD_MAX_HEIGHT = 100.0f; + + // Set world bounds from terrain data + static void setWorldBounds(float halfWidth, float halfHeight, float maxY) { + WORLD_BOUNDS_X = halfWidth; + WORLD_BOUNDS_Z = halfHeight; + WORLD_MAX_HEIGHT = maxY; + } + + static void setWorldBounds(const Vector3& bounds) { + WORLD_BOUNDS_X = bounds.x * 0.5f; + WORLD_BOUNDS_Z = bounds.z * 0.5f; + WORLD_MAX_HEIGHT = bounds.y; + } static Vector3 worldToLocal(const Vector3& worldPos, const Vector3& origin) { return Vector3Subtract(worldPos, origin); @@ -19,16 +34,16 @@ public: static Vector3 clampToWorldBounds(const Vector3& pos) { return { - Clamp(pos.x, -WORLD_BOUNDS, WORLD_BOUNDS), + Clamp(pos.x, -WORLD_BOUNDS_X, WORLD_BOUNDS_X), Clamp(pos.y, WORLD_MIN_HEIGHT, WORLD_MAX_HEIGHT), - Clamp(pos.z, -WORLD_BOUNDS, WORLD_BOUNDS) + Clamp(pos.z, -WORLD_BOUNDS_Z, WORLD_BOUNDS_Z) }; } static bool isInWorldBounds(const Vector3& pos) { - return pos.x >= -WORLD_BOUNDS && pos.x <= WORLD_BOUNDS && + return pos.x >= -WORLD_BOUNDS_X && pos.x <= WORLD_BOUNDS_X && pos.y >= WORLD_MIN_HEIGHT && pos.y <= WORLD_MAX_HEIGHT && - pos.z >= -WORLD_BOUNDS && pos.z <= WORLD_BOUNDS; + pos.z >= -WORLD_BOUNDS_Z && pos.z <= WORLD_BOUNDS_Z; } static Matrix buildTransformMatrix(const Vector3& position, const Vector3& rotation, const Vector3& scale) { @@ -57,7 +72,7 @@ public: 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, + WORLD_BOUNDS_X * 2, WORLD_MAX_HEIGHT, WORLD_BOUNDS_Z * 2, boundsColor); } }; \ No newline at end of file diff --git a/server/net/server.go b/server/net/server.go index 8713934..bdc3ee2 100644 --- a/server/net/server.go +++ b/server/net/server.go @@ -38,6 +38,7 @@ type Server struct { usersByName map[string]*Player // Track by username for preventing duplicates userData map[string]*UserData // Persistent user data heightmap [][]float32 + worldConfig *WorldConfig mutex sync.RWMutex nextID uint32 timeOfDay float32 @@ -56,12 +57,22 @@ func NewServer(port string, heightmap [][]float32) (*Server, error) { return nil, fmt.Errorf("failed to listen on UDP: %w", err) } + // Create world configuration from heightmap + // Default to 1 unit per sample (1 meter per sample) + worldConfig := NewWorldConfig(heightmap, 1.0) + log.Printf("World configured: %dx%d samples, %.1fx%.1f units, bounds: (%.1f,%.1f) to (%.1f,%.1f)", + worldConfig.SamplesPerSide, worldConfig.SamplesPerSide, + worldConfig.WorldWidth, worldConfig.WorldHeight, + worldConfig.MinBounds.X, worldConfig.MinBounds.Z, + worldConfig.MaxBounds.X, worldConfig.MaxBounds.Z) + server := &Server{ conn: conn, players: make(map[uint32]*Player), usersByName: make(map[string]*Player), userData: make(map[string]*UserData), heightmap: heightmap, + worldConfig: worldConfig, nextID: 0, timeOfDay: 0.0, startTime: time.Now(), @@ -192,9 +203,10 @@ func (s *Server) handleLogin(data []byte, addr *net.UDPAddr) { colorIndex := (playerID - 1) % uint32(len(colors)) color := colors[colorIndex] - x := rand.Float32()*100 - 50 - z := rand.Float32()*100 - 50 - y := s.getHeightAt(x, z) + 1.0 + // Spawn within world bounds + x := rand.Float32()*s.worldConfig.WorldWidth - s.worldConfig.WorldWidth/2 + z := rand.Float32()*s.worldConfig.WorldHeight - s.worldConfig.WorldHeight/2 + y := s.worldConfig.GetHeightAt(s.heightmap, x, z) + 1.0 userData = &UserData{ Username: username, @@ -270,21 +282,21 @@ func (s *Server) handleMove(data []byte, addr *net.UDPAddr) { // Server-authoritative movement deltaTime := float32(0.016) // 60fps - newX := player.Position.X + delta.X*15.0*deltaTime - newZ := player.Position.Z + delta.Z*15.0*deltaTime + newPos := Vec3{ + X: player.Position.X + delta.X*15.0*deltaTime, + Y: player.Position.Y, + Z: player.Position.Z + delta.Z*15.0*deltaTime, + } - // Clamp to world bounds - newX = float32(math.Max(-50, math.Min(50, float64(newX)))) - newZ = float32(math.Max(-50, math.Min(50, float64(newZ)))) + // Clamp to world bounds using WorldConfig + newPos = s.worldConfig.ClampPosition(newPos) // Set Y to terrain height with smoothing - targetY := s.getHeightAt(newX, newZ) + 1.0 + targetY := s.worldConfig.GetHeightAt(s.heightmap, newPos.X, newPos.Z) + 1.0 smoothFactor := float32(0.15) - newY := player.Position.Y + (targetY-player.Position.Y)*smoothFactor + newPos.Y = player.Position.Y + (targetY-player.Position.Y)*smoothFactor - player.Position.X = newX - player.Position.Y = newY - player.Position.Z = newZ + player.Position = newPos player.LastSeen = time.Now() // Update persistent user data @@ -371,38 +383,7 @@ func (s *Server) handleColorChange(data []byte, addr *net.UDPAddr) { s.broadcastColorChanged(playerID, newColor) } -func (s *Server) getHeightAt(x, z float32) float32 { - // Convert world coords to heightmap coords with bilinear interpolation - size := float32(len(s.heightmap)) - fx := (x/100 + 0.5) * (size - 1) - fz := (z/100 + 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 -} +// getHeightAt is deprecated - use WorldConfig.GetHeightAt instead func (s *Server) broadcastUpdate(player *Player) { msg := EncodeUpdatePacket(player.ID, player.Position)