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 <vector>
|
||||
#include <fstream>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
@ -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<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;
|
||||
}
|
||||
};
|
||||
#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> 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<PlayerRenderer>();
|
||||
sky = std::make_unique<Sky>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user