1
0

improve world bounds capabilities

This commit is contained in:
Sky Johnson 2025-09-09 21:38:00 -05:00
parent 552cafb2ba
commit 663444157e
4 changed files with 82 additions and 159 deletions

Binary file not shown.

View File

@ -6,8 +6,6 @@
#include <iostream> #include <iostream>
#include <vector> #include <vector>
#include <fstream>
#include <cstdint>
#include <cstring> #include <cstring>
#include <thread> #include <thread>
#include <chrono> #include <chrono>
@ -18,101 +16,8 @@
#include "sky/Sky.hpp" #include "sky/Sky.hpp"
#include "render/RenderContext.hpp" #include "render/RenderContext.hpp"
#include "utils/Coords.hpp" #include "utils/Coords.hpp"
#include "terrain/Heightmap.hpp"
constexpr int WORLD_SIZE = 100; #include "config.hpp"
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 { enum GameState {
STATE_LOGIN, STATE_LOGIN,
@ -121,6 +26,7 @@ enum GameState {
}; };
class Game { class Game {
GameConfig config;
PlayerController playerController; PlayerController playerController;
std::unique_ptr<PlayerRenderer> playerRenderer; std::unique_ptr<PlayerRenderer> playerRenderer;
RenderContext renderContext; RenderContext renderContext;
@ -140,24 +46,35 @@ class Game {
// Heartbeat timing // Heartbeat timing
float lastHeartbeatTime = 0.0f; float lastHeartbeatTime = 0.0f;
const float HEARTBEAT_INTERVAL = 5.0f; // Send heartbeat every 5 seconds
// Debug options // Debug options
bool showDebugAxes = false; bool showDebugAxes = false;
bool showWorldBounds = false; bool showWorldBounds = false;
public: public:
Game() { Game(const GameConfig& cfg) : config(cfg) {
InitWindow(1280, 720, "Game"); InitWindow(config.windowWidth, config.windowHeight, config.windowTitle.c_str());
SetTargetFPS(60); SetTargetFPS(config.targetFPS);
// Initialize components after window is created // Initialize components after window is created
playerRenderer = std::make_unique<PlayerRenderer>(); playerRenderer = std::make_unique<PlayerRenderer>();
sky = std::make_unique<Sky>(); sky = std::make_unique<Sky>();
// Load heightmap // Configure and load heightmap
if (!heightmap.load("../assets/heightmap.bin")) { 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"; 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 // Load textures
@ -272,7 +189,7 @@ private:
// Send periodic heartbeats when not moving // Send periodic heartbeats when not moving
float currentTime = GetTime(); float currentTime = GetTime();
if (currentTime - lastHeartbeatTime >= HEARTBEAT_INTERVAL) { if (currentTime - lastHeartbeatTime >= config.heartbeatInterval) {
network.sendHeartbeat(); network.sendHeartbeat();
lastHeartbeatTime = currentTime; lastHeartbeatTime = currentTime;
} }
@ -452,8 +369,18 @@ private:
} }
}; };
int main() { int main(int argc, char* argv[]) {
Game game; 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(); game.run();
return 0; return 0;
} }

View File

@ -5,9 +5,24 @@
class Coords { class Coords {
public: public:
static constexpr float WORLD_BOUNDS = 50.0f; // Default world bounds (can be overridden)
static constexpr float WORLD_MIN_HEIGHT = 0.0f; static inline float WORLD_BOUNDS_X = 50.0f;
static constexpr float WORLD_MAX_HEIGHT = 100.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) { static Vector3 worldToLocal(const Vector3& worldPos, const Vector3& origin) {
return Vector3Subtract(worldPos, origin); return Vector3Subtract(worldPos, origin);
@ -19,16 +34,16 @@ public:
static Vector3 clampToWorldBounds(const Vector3& pos) { static Vector3 clampToWorldBounds(const Vector3& pos) {
return { 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.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) { 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.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) { static Matrix buildTransformMatrix(const Vector3& position, const Vector3& rotation, const Vector3& scale) {
@ -57,7 +72,7 @@ public:
static void drawWorldBounds() { static void drawWorldBounds() {
Color boundsColor = {255, 255, 0, 100}; Color boundsColor = {255, 255, 0, 100};
DrawCubeWires({0, WORLD_MAX_HEIGHT/2, 0}, 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); boundsColor);
} }
}; };

View File

@ -38,6 +38,7 @@ type Server struct {
usersByName map[string]*Player // Track by username for preventing duplicates usersByName map[string]*Player // Track by username for preventing duplicates
userData map[string]*UserData // Persistent user data userData map[string]*UserData // Persistent user data
heightmap [][]float32 heightmap [][]float32
worldConfig *WorldConfig
mutex sync.RWMutex mutex sync.RWMutex
nextID uint32 nextID uint32
timeOfDay float32 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) 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{ server := &Server{
conn: conn, conn: conn,
players: make(map[uint32]*Player), players: make(map[uint32]*Player),
usersByName: make(map[string]*Player), usersByName: make(map[string]*Player),
userData: make(map[string]*UserData), userData: make(map[string]*UserData),
heightmap: heightmap, heightmap: heightmap,
worldConfig: worldConfig,
nextID: 0, nextID: 0,
timeOfDay: 0.0, timeOfDay: 0.0,
startTime: time.Now(), startTime: time.Now(),
@ -192,9 +203,10 @@ func (s *Server) handleLogin(data []byte, addr *net.UDPAddr) {
colorIndex := (playerID - 1) % uint32(len(colors)) colorIndex := (playerID - 1) % uint32(len(colors))
color := colors[colorIndex] color := colors[colorIndex]
x := rand.Float32()*100 - 50 // Spawn within world bounds
z := rand.Float32()*100 - 50 x := rand.Float32()*s.worldConfig.WorldWidth - s.worldConfig.WorldWidth/2
y := s.getHeightAt(x, z) + 1.0 z := rand.Float32()*s.worldConfig.WorldHeight - s.worldConfig.WorldHeight/2
y := s.worldConfig.GetHeightAt(s.heightmap, x, z) + 1.0
userData = &UserData{ userData = &UserData{
Username: username, Username: username,
@ -270,21 +282,21 @@ func (s *Server) handleMove(data []byte, addr *net.UDPAddr) {
// Server-authoritative movement // Server-authoritative movement
deltaTime := float32(0.016) // 60fps deltaTime := float32(0.016) // 60fps
newX := player.Position.X + delta.X*15.0*deltaTime newPos := Vec3{
newZ := player.Position.Z + delta.Z*15.0*deltaTime 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 // Clamp to world bounds using WorldConfig
newX = float32(math.Max(-50, math.Min(50, float64(newX)))) newPos = s.worldConfig.ClampPosition(newPos)
newZ = float32(math.Max(-50, math.Min(50, float64(newZ))))
// Set Y to terrain height with smoothing // 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) 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 = newPos
player.Position.Y = newY
player.Position.Z = newZ
player.LastSeen = time.Now() player.LastSeen = time.Now()
// Update persistent user data // Update persistent user data
@ -371,38 +383,7 @@ func (s *Server) handleColorChange(data []byte, addr *net.UDPAddr) {
s.broadcastColorChanged(playerID, newColor) s.broadcastColorChanged(playerID, newColor)
} }
func (s *Server) getHeightAt(x, z float32) float32 { // getHeightAt is deprecated - use WorldConfig.GetHeightAt instead
// 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
}
func (s *Server) broadcastUpdate(player *Player) { func (s *Server) broadcastUpdate(player *Player) {
msg := EncodeUpdatePacket(player.ID, player.Position) msg := EncodeUpdatePacket(player.ID, player.Position)