[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);
|
||||
|
||||
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_position(const BettolaMath::Vec3& pos);
|
||||
void set_yaw(float yaw) { _yaw = yaw; }
|
||||
@ -24,6 +24,8 @@ public:
|
||||
const BettolaMath::Vec3& get_position(void) const { return _position; }
|
||||
float get_yaw(void) const { return _yaw; }
|
||||
|
||||
void update(double dt);
|
||||
|
||||
protected:
|
||||
unsigned int _id;
|
||||
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;
|
||||
float x;
|
||||
float y;
|
||||
float z;
|
||||
float yaw;
|
||||
unsigned int last_processed_sequence;
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#include <cmath>
|
||||
#include "bettola/game/player_base.h"
|
||||
#include "bettola/network/player_input_message.h"
|
||||
#include "game/player.h"
|
||||
|
||||
/* Use a static variable for ID generation across all player types. */
|
||||
static unsigned int next_player_id = 1;
|
||||
@ -13,11 +14,21 @@ PlayerBase::PlayerBase(void) :
|
||||
_speed(20.0f) {}
|
||||
|
||||
void PlayerBase::update(double dt) {
|
||||
/* Apply gravity force. */
|
||||
_velocity.y -= 30.0f * dt;
|
||||
|
||||
_position.x += _velocity.x * dt;
|
||||
_position.y += _velocity.y * 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) {
|
||||
_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);
|
||||
if(move_mag > 0.0f) {
|
||||
_velocity.x = (move_dir.x / move_mag) * _speed;
|
||||
_velocity.y = 0.0;
|
||||
_velocity.z = (move_dir.z / move_mag) * _speed; /* y velocity is for z-axis. */
|
||||
_velocity.z = (move_dir.z / move_mag) * _speed;
|
||||
} else {
|
||||
_velocity.x = 0.0f;
|
||||
_velocity.y = 0.0f;
|
||||
_velocity.z = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include <SDL3/SDL_video.h>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include "bettola/math/vec3.h"
|
||||
#include "game/player.h"
|
||||
#include "graphics/camera.h"
|
||||
|
||||
|
||||
@ -1,20 +1,21 @@
|
||||
#include "remote_player.h"
|
||||
|
||||
RemotePlayer::RemotePlayer(unsigned int id, float x, float y) :
|
||||
PlayerBase(), _target_x(x), _target_y(y), _target_yaw(0.0f) {
|
||||
RemotePlayer::RemotePlayer(unsigned int id, float x, float y, float z) :
|
||||
PlayerBase(), _target_x(x), _target_y(y), _target_z(z), _target_yaw(0.0f) {
|
||||
_id = id; /* Manually set id from the server. */
|
||||
_position = {x, 0.0f, y };
|
||||
_position = {x, y, z};
|
||||
}
|
||||
|
||||
void RemotePlayer::update(double dt) {
|
||||
const float interp_speed = 15.0f;
|
||||
|
||||
_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. */
|
||||
_yaw = _target_yaw;
|
||||
}
|
||||
|
||||
void RemotePlayer::set_target_position(float x, float y, float yaw) {
|
||||
_target_x = x; _target_y = y; _target_yaw = yaw;
|
||||
void RemotePlayer::set_target_position(float x, float y, float z, float yaw) {
|
||||
_target_x = x; _target_y = y; _target_z = z; _target_yaw = yaw;
|
||||
}
|
||||
|
||||
@ -4,13 +4,14 @@
|
||||
|
||||
class RemotePlayer : public PlayerBase {
|
||||
public:
|
||||
RemotePlayer(unsigned int id, float x, float y);
|
||||
RemotePlayer(unsigned int id, float x, float y, float z);
|
||||
|
||||
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:
|
||||
float _target_x;
|
||||
float _target_y;
|
||||
float _target_z;
|
||||
float _target_yaw;
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#include "world.h"
|
||||
#include <cstring>
|
||||
#include "bettola/game/chunk.h"
|
||||
#include "bettola/game/terrain.h"
|
||||
#include "graphics/chunk_mesh.h"
|
||||
|
||||
World::~World(void) {
|
||||
@ -19,5 +20,24 @@ void World::add_chunk(int chunk_x, int chunk_z, const float* heightmap) {
|
||||
BettolaLib::Game::Chunk chunk;
|
||||
memcpy(chunk.heightmap.data(), heightmap, chunk.heightmap.size()*sizeof(float));
|
||||
|
||||
_chunks[pos] = 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 "bettola/game/chunk.h"
|
||||
#include "graphics/chunk_mesh.h"
|
||||
|
||||
/* Allow the use of a pair of ints as a map key. */
|
||||
@ -18,6 +19,7 @@ class World {
|
||||
public:
|
||||
~World(void);
|
||||
|
||||
float get_height(float world_x, float world_z) const;
|
||||
void add_chunk(int chunk_x, int chunk_z, const float* heightmap);
|
||||
const std::map<ChunkPos, ChunkMesh*>& get_chunk_meshes(void) const {
|
||||
return _chunk_meshes;
|
||||
@ -25,4 +27,5 @@ public:
|
||||
|
||||
private:
|
||||
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) {
|
||||
/* 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. */
|
||||
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
|
||||
* seen yet. This'll bring the client up to date with most recent inputs.
|
||||
*/
|
||||
for(const auto& input : _pending_inputs) {
|
||||
_player.apply_input(input);
|
||||
for(const auto& input_msg : _pending_inputs) {
|
||||
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 {
|
||||
/* 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()) {
|
||||
/* 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 {
|
||||
/* 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) {
|
||||
_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) {
|
||||
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 };
|
||||
player->set_velocity_direction(input, cam_front);
|
||||
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();
|
||||
const auto& pos = _players[i]->get_position();
|
||||
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].last_processed_sequence = _players[i]->get_last_processed_sequence();
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ public:
|
||||
World(void);
|
||||
|
||||
BettolaLib::Game::Chunk& get_chunk(int x, int z);
|
||||
float get_height(float world_x, float world_z);
|
||||
|
||||
private:
|
||||
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