diff --git a/libbettola/include/bettola/game/player_base.h b/libbettola/include/bettola/game/player_base.h index 07425d7..0944f80 100644 --- a/libbettola/include/bettola/game/player_base.h +++ b/libbettola/include/bettola/game/player_base.h @@ -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; diff --git a/libbettola/include/bettola/game/terrain.h b/libbettola/include/bettola/game/terrain.h new file mode 100644 index 0000000..eeb2e55 --- /dev/null +++ b/libbettola/include/bettola/game/terrain.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#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. */ diff --git a/libbettola/include/bettola/network/player_state_message.h b/libbettola/include/bettola/network/player_state_message.h index e94d602..016c469 100644 --- a/libbettola/include/bettola/network/player_state_message.h +++ b/libbettola/include/bettola/network/player_state_message.h @@ -9,6 +9,7 @@ struct PlayerStateMessage { unsigned int player_id; float x; float y; + float z; float yaw; unsigned int last_processed_sequence; }; diff --git a/libbettola/src/bettola/game/player_base.cpp b/libbettola/src/bettola/game/player_base.cpp index 43f061b..23564e7 100644 --- a/libbettola/src/bettola/game/player_base.cpp +++ b/libbettola/src/bettola/game/player_base.cpp @@ -1,6 +1,7 @@ #include #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; } } diff --git a/src/bettola.cpp b/src/bettola.cpp index f141241..c5f0f00 100644 --- a/src/bettola.cpp +++ b/src/bettola.cpp @@ -7,6 +7,7 @@ #include #include #include +#include "bettola/math/vec3.h" #include "game/player.h" #include "graphics/camera.h" diff --git a/src/game/remote_player.cpp b/src/game/remote_player.cpp index e0daf3e..1cd1909 100644 --- a/src/game/remote_player.cpp +++ b/src/game/remote_player.cpp @@ -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; } diff --git a/src/game/remote_player.h b/src/game/remote_player.h index 8ec23b5..029864b 100644 --- a/src/game/remote_player.h +++ b/src/game/remote_player.h @@ -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; }; diff --git a/src/game/world.cpp b/src/game/world.cpp index 84dbf90..11e0075 100644 --- a/src/game/world.cpp +++ b/src/game/world.cpp @@ -1,6 +1,7 @@ #include "world.h" #include #include "bettola/game/chunk.h" +#include "bettola/game/terrain.h" #include "graphics/chunk_mesh.h" World::~World(void) { @@ -10,7 +11,7 @@ World::~World(void) { } 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)) { /* TODO: Chunk already exists, perhaps update it later.. */ return; @@ -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); +} diff --git a/src/game/world.h b/src/game/world.h index d4562c1..0b023e9 100644 --- a/src/game/world.h +++ b/src/game/world.h @@ -2,6 +2,7 @@ #include +#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& get_chunk_meshes(void) const { return _chunk_meshes; @@ -25,4 +27,5 @@ public: private: std::map _chunk_meshes; + std::map _chunks; }; diff --git a/src/game_client.cpp b/src/game_client.cpp index 6be57ca..2887221 100644 --- a/src/game_client.cpp +++ b/src/game_client.cpp @@ -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); } diff --git a/srv/game/game.cpp b/srv/game/game.cpp index 6ddef26..3d36b78 100644 --- a/srv/game/game.cpp +++ b/srv/game/game.cpp @@ -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(); } diff --git a/srv/game/world.h b/srv/game/world.h index bfd3784..c139e23 100644 --- a/srv/game/world.h +++ b/srv/game/world.h @@ -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); diff --git a/srv/game/world_collision.cpp b/srv/game/world_collision.cpp new file mode 100644 index 0000000..e3e3adc --- /dev/null +++ b/srv/game/world_collision.cpp @@ -0,0 +1,23 @@ +#include +#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); +}