[Add] Terrain collision and gravity.
Physics and collision system to make players interact with the generated terraine without falling through the world like lemmings. It uses a shared bilinear interpolation function for terrain height for smooth and consistant height queries on the client and server.
This commit is contained in:
parent
7ba0b348bc
commit
744c41b8ce
@ -15,7 +15,7 @@ public:
|
|||||||
|
|
||||||
PlayerBase(void);
|
PlayerBase(void);
|
||||||
|
|
||||||
void update(double dt);
|
void apply_gravity_and_collision(float terrain_height);
|
||||||
void set_velocity_direction(const InputState& input, const BettolaMath::Vec3& cam_front);
|
void set_velocity_direction(const InputState& input, const BettolaMath::Vec3& cam_front);
|
||||||
void set_position(const BettolaMath::Vec3& pos);
|
void set_position(const BettolaMath::Vec3& pos);
|
||||||
void set_yaw(float yaw) { _yaw = yaw; }
|
void set_yaw(float yaw) { _yaw = yaw; }
|
||||||
@ -24,6 +24,8 @@ public:
|
|||||||
const BettolaMath::Vec3& get_position(void) const { return _position; }
|
const BettolaMath::Vec3& get_position(void) const { return _position; }
|
||||||
float get_yaw(void) const { return _yaw; }
|
float get_yaw(void) const { return _yaw; }
|
||||||
|
|
||||||
|
void update(double dt);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
unsigned int _id;
|
unsigned int _id;
|
||||||
BettolaMath::Vec3 _position;
|
BettolaMath::Vec3 _position;
|
||||||
|
|||||||
49
libbettola/include/bettola/game/terrain.h
Normal file
49
libbettola/include/bettola/game/terrain.h
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
#include "bettola/game/chunk.h"
|
||||||
|
|
||||||
|
namespace BettolaLib {
|
||||||
|
namespace Game {
|
||||||
|
namespace Terrain {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Performs bilinear interpolation on a chunk's heightmap to find
|
||||||
|
* a smooth height at a given local coord.
|
||||||
|
*/
|
||||||
|
inline float get_height_at(const BettolaLib::Game::Chunk& chunk, float local_x, float local_z) {
|
||||||
|
/*
|
||||||
|
* Mesh generation uses (WIDTH-1), so coordinates for interpolation
|
||||||
|
* should be within the range [0, WIDTH-2].
|
||||||
|
*/
|
||||||
|
float max_coord = CHUNK_WIDTH-2;
|
||||||
|
if(local_x < 0 || local_x > max_coord || local_z < 0 || local_z > max_coord) {
|
||||||
|
int x = std::max(0, std::min((int)local_x, CHUNK_WIDTH - 1));
|
||||||
|
int z = std::max(0, std::min((int)local_z, CHUNK_HEIGHT - 1));
|
||||||
|
return chunk.heightmap[z*CHUNK_WIDTH+x] * 5.0f; /* Apply height scaling. */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get integer and fracctional parts of the coords. */
|
||||||
|
int x_int = (int)local_x;
|
||||||
|
int z_int = (int)local_z;
|
||||||
|
float x_frac = local_x - x_int;
|
||||||
|
float z_frac = local_z - z_int;
|
||||||
|
|
||||||
|
/* Get heights of the four corners of the grid cell. */
|
||||||
|
float h00 = chunk.heightmap[z_int * CHUNK_WIDTH+x_int];
|
||||||
|
float h10 = chunk.heightmap[z_int * CHUNK_WIDTH+(x_int+1)];
|
||||||
|
float h01 = chunk.heightmap[(z_int+1) * CHUNK_WIDTH+x_int];
|
||||||
|
float h11 = chunk.heightmap[(z_int+1) * CHUNK_WIDTH+(x_int+1)];
|
||||||
|
|
||||||
|
/* Bilinear interpolation. */
|
||||||
|
float h_x0 = h00 * (1 - x_frac) + h10 * x_frac;
|
||||||
|
float h_x1 = h01 * (1 - x_frac) + h11 * x_frac;
|
||||||
|
float height = h_x0 * (1 - z_frac) + h_x1 * z_frac;
|
||||||
|
|
||||||
|
return height * 5.0f; /* Apply the same scaling used in ChunkMesh. */
|
||||||
|
}
|
||||||
|
|
||||||
|
} /* namespace Terrain. */
|
||||||
|
} /* namespace Game. */
|
||||||
|
} /* namespace BettolaLib. */
|
||||||
@ -9,6 +9,7 @@ struct PlayerStateMessage {
|
|||||||
unsigned int player_id;
|
unsigned int player_id;
|
||||||
float x;
|
float x;
|
||||||
float y;
|
float y;
|
||||||
|
float z;
|
||||||
float yaw;
|
float yaw;
|
||||||
unsigned int last_processed_sequence;
|
unsigned int last_processed_sequence;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include "bettola/game/player_base.h"
|
#include "bettola/game/player_base.h"
|
||||||
#include "bettola/network/player_input_message.h"
|
#include "bettola/network/player_input_message.h"
|
||||||
|
#include "game/player.h"
|
||||||
|
|
||||||
/* Use a static variable for ID generation across all player types. */
|
/* Use a static variable for ID generation across all player types. */
|
||||||
static unsigned int next_player_id = 1;
|
static unsigned int next_player_id = 1;
|
||||||
@ -13,11 +14,21 @@ PlayerBase::PlayerBase(void) :
|
|||||||
_speed(20.0f) {}
|
_speed(20.0f) {}
|
||||||
|
|
||||||
void PlayerBase::update(double dt) {
|
void PlayerBase::update(double dt) {
|
||||||
|
/* Apply gravity force. */
|
||||||
|
_velocity.y -= 30.0f * dt;
|
||||||
|
|
||||||
_position.x += _velocity.x * dt;
|
_position.x += _velocity.x * dt;
|
||||||
_position.y += _velocity.y * dt;
|
_position.y += _velocity.y * dt;
|
||||||
_position.z += _velocity.z * dt;
|
_position.z += _velocity.z * dt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlayerBase::apply_gravity_and_collision(float terrain_height) {
|
||||||
|
if(_position.y < terrain_height) {
|
||||||
|
_position.y = terrain_height;
|
||||||
|
_velocity.y = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void PlayerBase::set_position(const BettolaMath::Vec3& pos) {
|
void PlayerBase::set_position(const BettolaMath::Vec3& pos) {
|
||||||
_position = pos;
|
_position = pos;
|
||||||
}
|
}
|
||||||
@ -40,11 +51,9 @@ void PlayerBase::set_velocity_direction(const InputState& input, const BettolaMa
|
|||||||
float move_mag = sqrt(move_dir.x*move_dir.x + move_dir.z*move_dir.z);
|
float move_mag = sqrt(move_dir.x*move_dir.x + move_dir.z*move_dir.z);
|
||||||
if(move_mag > 0.0f) {
|
if(move_mag > 0.0f) {
|
||||||
_velocity.x = (move_dir.x / move_mag) * _speed;
|
_velocity.x = (move_dir.x / move_mag) * _speed;
|
||||||
_velocity.y = 0.0;
|
_velocity.z = (move_dir.z / move_mag) * _speed;
|
||||||
_velocity.z = (move_dir.z / move_mag) * _speed; /* y velocity is for z-axis. */
|
|
||||||
} else {
|
} else {
|
||||||
_velocity.x = 0.0f;
|
_velocity.x = 0.0f;
|
||||||
_velocity.y = 0.0f;
|
|
||||||
_velocity.z = 0.0f;
|
_velocity.z = 0.0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
#include <SDL3/SDL_video.h>
|
#include <SDL3/SDL_video.h>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include "bettola/math/vec3.h"
|
||||||
#include "game/player.h"
|
#include "game/player.h"
|
||||||
#include "graphics/camera.h"
|
#include "graphics/camera.h"
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,21 @@
|
|||||||
#include "remote_player.h"
|
#include "remote_player.h"
|
||||||
|
|
||||||
RemotePlayer::RemotePlayer(unsigned int id, float x, float y) :
|
RemotePlayer::RemotePlayer(unsigned int id, float x, float y, float z) :
|
||||||
PlayerBase(), _target_x(x), _target_y(y), _target_yaw(0.0f) {
|
PlayerBase(), _target_x(x), _target_y(y), _target_z(z), _target_yaw(0.0f) {
|
||||||
_id = id; /* Manually set id from the server. */
|
_id = id; /* Manually set id from the server. */
|
||||||
_position = {x, 0.0f, y };
|
_position = {x, y, z};
|
||||||
}
|
}
|
||||||
|
|
||||||
void RemotePlayer::update(double dt) {
|
void RemotePlayer::update(double dt) {
|
||||||
const float interp_speed = 15.0f;
|
const float interp_speed = 15.0f;
|
||||||
|
|
||||||
_position.x += (_target_x - _position.x) * interp_speed * dt;
|
_position.x += (_target_x - _position.x) * interp_speed * dt;
|
||||||
_position.z += (_target_y - _position.z) * interp_speed * dt; /* y is z. */
|
_position.y += (_target_y - _position.y) * interp_speed * dt;
|
||||||
|
_position.z += (_target_z - _position.z) * interp_speed * dt;
|
||||||
/* TODO: Snap the yaw, we'll interpolate later if we need. */
|
/* TODO: Snap the yaw, we'll interpolate later if we need. */
|
||||||
_yaw = _target_yaw;
|
_yaw = _target_yaw;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RemotePlayer::set_target_position(float x, float y, float yaw) {
|
void RemotePlayer::set_target_position(float x, float y, float z, float yaw) {
|
||||||
_target_x = x; _target_y = y; _target_yaw = yaw;
|
_target_x = x; _target_y = y; _target_z = z; _target_yaw = yaw;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,13 +4,14 @@
|
|||||||
|
|
||||||
class RemotePlayer : public PlayerBase {
|
class RemotePlayer : public PlayerBase {
|
||||||
public:
|
public:
|
||||||
RemotePlayer(unsigned int id, float x, float y);
|
RemotePlayer(unsigned int id, float x, float y, float z);
|
||||||
|
|
||||||
void update(double dt);
|
void update(double dt);
|
||||||
void set_target_position(float x, float y, float yaw);
|
void set_target_position(float x, float y, float z, float yaw);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
float _target_x;
|
float _target_x;
|
||||||
float _target_y;
|
float _target_y;
|
||||||
|
float _target_z;
|
||||||
float _target_yaw;
|
float _target_yaw;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#include "world.h"
|
#include "world.h"
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include "bettola/game/chunk.h"
|
#include "bettola/game/chunk.h"
|
||||||
|
#include "bettola/game/terrain.h"
|
||||||
#include "graphics/chunk_mesh.h"
|
#include "graphics/chunk_mesh.h"
|
||||||
|
|
||||||
World::~World(void) {
|
World::~World(void) {
|
||||||
@ -10,7 +11,7 @@ World::~World(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void World::add_chunk(int chunk_x, int chunk_z, const float* heightmap) {
|
void World::add_chunk(int chunk_x, int chunk_z, const float* heightmap) {
|
||||||
ChunkPos pos = { chunk_x, chunk_z };
|
ChunkPos pos = {chunk_x, chunk_z};
|
||||||
if(_chunk_meshes.count(pos)) {
|
if(_chunk_meshes.count(pos)) {
|
||||||
/* TODO: Chunk already exists, perhaps update it later.. */
|
/* TODO: Chunk already exists, perhaps update it later.. */
|
||||||
return;
|
return;
|
||||||
@ -19,5 +20,24 @@ void World::add_chunk(int chunk_x, int chunk_z, const float* heightmap) {
|
|||||||
BettolaLib::Game::Chunk chunk;
|
BettolaLib::Game::Chunk chunk;
|
||||||
memcpy(chunk.heightmap.data(), heightmap, chunk.heightmap.size()*sizeof(float));
|
memcpy(chunk.heightmap.data(), heightmap, chunk.heightmap.size()*sizeof(float));
|
||||||
|
|
||||||
|
_chunks[pos] = chunk;
|
||||||
_chunk_meshes[pos] = new ChunkMesh(chunk_x, chunk_z, chunk);
|
_chunk_meshes[pos] = new ChunkMesh(chunk_x, chunk_z, chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float World::get_height(float world_x, float world_z) const {
|
||||||
|
float chunk_width = (float)(BettolaLib::Game::CHUNK_WIDTH -1);
|
||||||
|
float chunk_height = (float)(BettolaLib::Game::CHUNK_HEIGHT-1);
|
||||||
|
|
||||||
|
int chunk_x = floor(world_x / chunk_width);
|
||||||
|
int chunk_z = floor(world_z / chunk_height);
|
||||||
|
|
||||||
|
float local_x = world_x - (chunk_x * chunk_width);
|
||||||
|
float local_z = world_z - (chunk_z * chunk_height);
|
||||||
|
|
||||||
|
auto it = _chunks.find({chunk_x, chunk_z});
|
||||||
|
if(it == _chunks.end()) {
|
||||||
|
return 0.0f; /* No chunk data. */
|
||||||
|
}
|
||||||
|
|
||||||
|
return BettolaLib::Game::Terrain::get_height_at(it->second, local_x, local_z);
|
||||||
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
|
#include "bettola/game/chunk.h"
|
||||||
#include "graphics/chunk_mesh.h"
|
#include "graphics/chunk_mesh.h"
|
||||||
|
|
||||||
/* Allow the use of a pair of ints as a map key. */
|
/* Allow the use of a pair of ints as a map key. */
|
||||||
@ -18,6 +19,7 @@ class World {
|
|||||||
public:
|
public:
|
||||||
~World(void);
|
~World(void);
|
||||||
|
|
||||||
|
float get_height(float world_x, float world_z) const;
|
||||||
void add_chunk(int chunk_x, int chunk_z, const float* heightmap);
|
void add_chunk(int chunk_x, int chunk_z, const float* heightmap);
|
||||||
const std::map<ChunkPos, ChunkMesh*>& get_chunk_meshes(void) const {
|
const std::map<ChunkPos, ChunkMesh*>& get_chunk_meshes(void) const {
|
||||||
return _chunk_meshes;
|
return _chunk_meshes;
|
||||||
@ -25,4 +27,5 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
std::map<ChunkPos, ChunkMesh*> _chunk_meshes;
|
std::map<ChunkPos, ChunkMesh*> _chunk_meshes;
|
||||||
|
std::map<ChunkPos, BettolaLib::Game::Chunk> _chunks;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -129,7 +129,7 @@ void GameClient::_process_game_state(const BettolaLib::Network::GameStateMessage
|
|||||||
|
|
||||||
if(ps.player_id == _our_player_id) {
|
if(ps.player_id == _our_player_id) {
|
||||||
/* This is our player. Reconcile. */
|
/* This is our player. Reconcile. */
|
||||||
_player.set_position({ps.x, 0.0f, ps.y});
|
_player.set_position({ps.x, ps.y, ps.z});
|
||||||
|
|
||||||
/* Remove all inputs from our history that the server has processed. */
|
/* Remove all inputs from our history that the server has processed. */
|
||||||
while(!_pending_inputs.empty() &&
|
while(!_pending_inputs.empty() &&
|
||||||
@ -140,8 +140,12 @@ void GameClient::_process_game_state(const BettolaLib::Network::GameStateMessage
|
|||||||
/* Now, re-apply any remaining inputs that the server hasn't
|
/* Now, re-apply any remaining inputs that the server hasn't
|
||||||
* seen yet. This'll bring the client up to date with most recent inputs.
|
* seen yet. This'll bring the client up to date with most recent inputs.
|
||||||
*/
|
*/
|
||||||
for(const auto& input : _pending_inputs) {
|
for(const auto& input_msg : _pending_inputs) {
|
||||||
_player.apply_input(input);
|
PlayerBase::InputState input_state = { input_msg.up, input_msg.down,
|
||||||
|
input_msg.left, input_msg.right };
|
||||||
|
BettolaMath::Vec3 cam_front = { input_msg.cam_front_x, input_msg.cam_front_y,
|
||||||
|
input_msg.cam_front_z };
|
||||||
|
_player.set_velocity_direction(input_state, cam_front);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
/* Remote player. Find if we already know about them.. */
|
/* Remote player. Find if we already know about them.. */
|
||||||
@ -151,10 +155,10 @@ void GameClient::_process_game_state(const BettolaLib::Network::GameStateMessage
|
|||||||
|
|
||||||
if(it != _remote_players.end()) {
|
if(it != _remote_players.end()) {
|
||||||
/* Found 'em! update their target pos. */
|
/* Found 'em! update their target pos. */
|
||||||
it->set_target_position(ps.x, ps.y, ps.yaw);
|
it->set_target_position(ps.x, ps.y, ps.z, ps.yaw);
|
||||||
} else {
|
} else {
|
||||||
/* They are new, add them to our list. */
|
/* They are new, add them to our list. */
|
||||||
_remote_players.emplace_back(ps.player_id, ps.x, ps.y);
|
_remote_players.emplace_back(ps.player_id, ps.x, ps.y, ps.z);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,6 +201,9 @@ void GameClient::send_input(PlayerBase::InputState& input,
|
|||||||
|
|
||||||
void GameClient::update_players(float dt) {
|
void GameClient::update_players(float dt) {
|
||||||
_player.update(dt);
|
_player.update(dt);
|
||||||
|
float terrain_height = _world.get_height(_player.get_position().x,
|
||||||
|
_player.get_position().z);
|
||||||
|
_player.apply_gravity_and_collision(terrain_height);
|
||||||
for(auto& p : _remote_players) {
|
for(auto& p : _remote_players) {
|
||||||
p.update(dt);
|
p.update(dt);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,6 +53,10 @@ void Game::process_udp_message(const char* buffer, size_t size, const sockaddr_i
|
|||||||
BettolaMath::Vec3 cam_front = { msg.cam_front_x, msg.cam_front_y, msg.cam_front_z };
|
BettolaMath::Vec3 cam_front = { msg.cam_front_x, msg.cam_front_y, msg.cam_front_z };
|
||||||
player->set_velocity_direction(input, cam_front);
|
player->set_velocity_direction(input, cam_front);
|
||||||
player->update(msg.dt);
|
player->update(msg.dt);
|
||||||
|
|
||||||
|
float terrain_height = _world.get_height(player->get_position().x,
|
||||||
|
player->get_position().z);
|
||||||
|
player->apply_gravity_and_collision(terrain_height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,7 +70,8 @@ void Game::broadcast_game_state(BettolaLib::Network::UDPSocket& udp_socket) {
|
|||||||
msg.players[i].player_id = _players[i]->get_id();
|
msg.players[i].player_id = _players[i]->get_id();
|
||||||
const auto& pos = _players[i]->get_position();
|
const auto& pos = _players[i]->get_position();
|
||||||
msg.players[i].x = pos.x;
|
msg.players[i].x = pos.x;
|
||||||
msg.players[i].y = pos.z; /* Send z as y for the 2D style network message. */
|
msg.players[i].y = pos.y;
|
||||||
|
msg.players[i].z = pos.z;
|
||||||
msg.players[i].yaw = _players[i]->get_yaw();
|
msg.players[i].yaw = _players[i]->get_yaw();
|
||||||
msg.players[i].last_processed_sequence = _players[i]->get_last_processed_sequence();
|
msg.players[i].last_processed_sequence = _players[i]->get_last_processed_sequence();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ public:
|
|||||||
World(void);
|
World(void);
|
||||||
|
|
||||||
BettolaLib::Game::Chunk& get_chunk(int x, int z);
|
BettolaLib::Game::Chunk& get_chunk(int x, int z);
|
||||||
|
float get_height(float world_x, float world_z);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void _generate_chunk(BettolaLib::Game::Chunk& chunk, int chunk_x, int chunk_z);
|
void _generate_chunk(BettolaLib::Game::Chunk& chunk, int chunk_x, int chunk_z);
|
||||||
|
|||||||
23
srv/game/world_collision.cpp
Normal file
23
srv/game/world_collision.cpp
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
#include <cmath>
|
||||||
|
#include "world.h"
|
||||||
|
#include "bettola/game/chunk.h"
|
||||||
|
#include "bettola/game/terrain.h"
|
||||||
|
|
||||||
|
float World::get_height(float world_x, float world_z) {
|
||||||
|
/*
|
||||||
|
* Mesh generation uses (WIDTH-1), so account for it
|
||||||
|
* when calculating the chunk and local coords.
|
||||||
|
*/
|
||||||
|
float chunk_width = (float)(BettolaLib::Game::CHUNK_WIDTH-1);
|
||||||
|
float chunk_height = (float)(BettolaLib::Game::CHUNK_HEIGHT-1);
|
||||||
|
|
||||||
|
int chunk_x = floor(world_x / chunk_width);
|
||||||
|
int chunk_z = floor(world_z / chunk_height);
|
||||||
|
|
||||||
|
float local_x = world_x - (chunk_x * chunk_width);
|
||||||
|
float local_z = world_z - (chunk_z * chunk_height);
|
||||||
|
|
||||||
|
BettolaLib::Game::Chunk& chunk = get_chunk(chunk_x, chunk_z);
|
||||||
|
|
||||||
|
return BettolaLib::Game::Terrain::get_height_at(chunk, local_x, local_z);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user