improve world bounds capabilities
This commit is contained in:
parent
552cafb2ba
commit
663444157e
Binary file not shown.
139
client/main.cpp
139
client/main.cpp
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user