#1 - ClientWorldController refactoring

This commit is contained in:
Martin Slachta
2026-08-03 23:30:18 +02:00
parent f4174eb0c7
commit a097f4d4b0
35 changed files with 899 additions and 1010 deletions
+2 -2
View File
@@ -6,12 +6,12 @@
#include "debug/tools/EntityManagerGui.hpp"
#include "debug/tools/NetworkStatsGui.hpp"
#include "network/ServerConnection.hpp"
#include "world/ClientWorldController.hpp"
#include "world/controllers/ClientWorldController.hpp"
namespace tw::app {
/**
* The game world state. Owns the connection, world controller, and debug GUIs.
* The game world state.
* Responsible for updating the game simulation and rendering debug information.
*/
class GameState {
@@ -1,92 +1,126 @@
#pragma once
#include <imgui.h>
#include <implot.h>
#include <algorithm>
#include <chrono>
#include <format>
#include <ratio>
#include <cstddef>
#include <vector>
#include "debug/ComponentGui.hpp"
#include <implot.h>
#include <implot_internal.h>
#include "network/EntityInterpolation.hpp"
#include "world/Transform.hpp"
template<>
class tw::dbg::ComponentGui<tw::net::EntityPositionInterpolation> {
class tw::dbg::ComponentGui<tw::Transform> {
private:
using Clock = std::chrono::high_resolution_clock;
/** Ten seconds of frames at sixty a second. */
static constexpr size_t CAPACITY = 600;
/** How much of the trace the plot shows. */
static constexpr float WINDOW_IN_SECONDS = 10.0f;
inline static const Transform* m_subject = nullptr;
inline static int m_last_frame = -1;
inline static Clock::time_point m_started_at {};
inline static size_t m_head = 0;
inline static size_t m_count = 0;
inline static std::vector<float> m_times;
inline static std::vector<float> m_positions;
void restart(const Transform* instance) {
m_subject = instance;
m_last_frame = -1;
m_started_at = Clock::now();
m_head = 0;
m_count = 0;
m_times.resize(CAPACITY);
m_positions.resize(CAPACITY);
}
/** Keeps one sample per frame, so drawing twice does not double up. */
void sample(float time, float position) {
const int frame = ImGui::GetFrameCount();
if(m_last_frame == frame) {
return;
}
m_last_frame = frame;
m_times[m_head] = time;
m_positions[m_head] = position;
m_head = (m_head + 1) % CAPACITY;
m_count = std::min(m_count + 1, CAPACITY);
}
public:
void draw(net::EntityPositionInterpolation* instance) {
ImGui::SeparatorText("Entity Interpolation");
void draw(Transform* instance) {
ImGui::SeparatorText("Drawn Position");
bool m_is_scrolling = true;
static float m_metric_history = 10.0f;
ImGui::Checkbox("Is Scrolling", &m_is_scrolling);
if(m_is_scrolling) {
ImGui::SliderFloat("History", &m_metric_history,1,30,"%.1f s");
static int axis = 0;
ImGui::Combo("Axis", &axis, "X\0Y\0Z\0");
if(m_subject != instance) {
restart(instance);
}
static int32_t time_buffer_len = 1000;
ImGui::SliderInt("Buffer length (ms)", &time_buffer_len, 0, 1000);
const float time = std::chrono::duration<float>(Clock::now() - m_started_at).count();
const float position = instance->position()[axis];
auto now = std::chrono::high_resolution_clock::now() - std::chrono::milliseconds(time_buffer_len);
auto [from, to, mix] = instance->get_values_around(now);
auto mixed_value = glm::mix(from, to, mix);
sample(time, position);
ImGui::Text("Mixed value: (%f, %f)", mixed_value.x, mixed_value.y);
ImGui::Text("%.3f", position);
if(ImPlot::BeginPlot("Interpolation")) {
auto plot_x_from = instance->times().begin()->time_since_epoch().count();
std::vector<float> times(instance->times().max_size());
for(auto it = instance->times().begin(); it != instance->times().end(); ++it) {
times.push_back(it->time_since_epoch().count() - plot_x_from);
}
if(ImPlot::BeginPlot("Position", ImVec2(-1.0f, 180.0f))) {
ImPlot::SetupAxes("seconds", "position", ImPlotAxisFlags_None, ImPlotAxisFlags_AutoFit);
ImPlot::SetupAxisLimits(ImAxis_X1,
time - WINDOW_IN_SECONDS, time, ImGuiCond_Always);
float interpolated_time = now.time_since_epoch().count() - plot_x_from;
std::vector<float> values(instance->values().max_size());
for(auto it = instance->values().begin(); it != instance->values().end(); ++it) {
values.push_back(it->x);
}
values.push_back(mixed_value.x);
// The trace wraps around, so it is read from wherever the oldest
// sample ended up.
const int offset = m_count == CAPACITY ? (int)m_head : 0;
ImPlot::PlotLine("Drawn", m_times.data(), m_positions.data(),
(int)m_count, ImPlotLineFlags_None, offset);
ImPlot::PlotScatter("Server", times.data(), values.data(), times.size() - 1);
ImPlot::PushStyleVar(ImPlotStyleVar_FillAlpha, 0.25f);
ImPlot::SetNextMarkerStyle(ImPlotMarker_Square, 6, ImPlot::GetColormapColor(1), IMPLOT_AUTO, ImPlot::GetColormapColor(1));
ImPlot::PlotScatter("Client", &interpolated_time, &mixed_value.x, 1);
ImPlot::PopStyleVar();
ImPlot::EndPlot();
}
if(ImGui::BeginTable("entityInterpolation", 2)) {
for(int i = 0; i < instance->values().size(); i++) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
auto value = instance->values()[i];
auto now = std::chrono::high_resolution_clock::now() - std::chrono::milliseconds(time_buffer_len);
if(instance->times()[i] > now && (i == instance->values().size() - 1 || instance->times()[i + 1] <= now)) {
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::GetColorU32(ImVec4(0.7f, 0.3f, 0.3f, 0.65f)));
ImGui::Text("%f %f %f", mixed_value.x, mixed_value.y, mixed_value.z);
ImGui::TableNextColumn();
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::GetColorU32(ImVec4(0.0f, 0.0f, 0.0f, 0.0f)));
}
ImGui::Text("%f %f %f", value.x, value.y, value.z);
auto point = instance->times()[i].time_since_epoch();
auto hours = std::chrono::duration_cast<std::chrono::hours>(point);
point -= hours;
hours %= 24;
auto minutes = std::chrono::duration_cast<std::chrono::minutes>(point);
point -= minutes;
auto seconds = std::chrono::duration_cast<std::chrono::seconds>(point);
point -= seconds;
auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(point);
ImGui::TableNextColumn();
auto fmt = std::format("{}:{}:{}.{}", hours, minutes, seconds, milliseconds);
ImGui::Text(fmt.c_str());
}
ImGui::EndTable();
}
}
};
/**
* Says whether the position above stands still because nothing new arrives for
* the entity, or because what arrives is the same position over and over.
*/
template<>
class tw::dbg::ComponentGui<tw::net::EntityPositionInterpolation> {
private:
using Clock = std::chrono::high_resolution_clock;
static int64_t millis_since(Clock::time_point point) {
return std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - point).count();
}
public:
void draw(net::EntityPositionInterpolation* instance) {
ImGui::SeparatorText("Received Positions");
const size_t newest = instance->values().size() - 1;
ImGui::Text("Newest %.3f %.3f %.3f, %ld ms ago",
instance->values()[newest].x,
instance->values()[newest].y,
instance->values()[newest].z,
millis_since(instance->times()[newest]));
ImGui::Text("Oldest %.3f %.3f %.3f, %ld ms ago",
instance->values()[0].x,
instance->values()[0].y,
instance->values()[0].z,
millis_since(instance->times()[0]));
}
};
@@ -50,7 +50,8 @@ void EntityManagerGui::draw_entity_components() {
ImGui::Text("Vertex offset: %i", mesh->vertex_offset());
}
draw_debug_components<tw::CharacterBody, tw::CharacterController, tw::net::EntityPositionInterpolation>();
draw_debug_components<tw::Transform, tw::net::EntityPositionInterpolation,
tw::CharacterBody, tw::CharacterController>();
// auto character = m_world->registry().try_get<tw::CharacterBody>(m_selected);
// if(character != nullptr) {
@@ -1,10 +0,0 @@
#pragma once
/**
* Writes entity derivation message
*/
template<typename TMesg>
class EntityDerivationWriter {
};
@@ -0,0 +1,32 @@
#pragma once
#include "entt/entt.hpp"
#include <cstdint>
#include <map>
#include <optional>
namespace tw::net {
class EntityIdMap {
public:
const bool is_server_entity_registered(uint32_t server_id) const {
return m_mappings.find(server_id) != m_mappings.end();
}
const std::optional<entt::entity> get_local(uint32_t server_id) const {
if(is_server_entity_registered(server_id)) {
return m_mappings.find(server_id)->second;
}
return {};
}
void register_server_to_local(uint32_t server_id, entt::entity local) {
m_mappings[server_id] = local;
}
private:
std::map<uint32_t, entt::entity> m_mappings;
};
}
@@ -9,19 +9,16 @@
namespace tw::net {
EntityPositionInterpolator::EntityPositionInterpolator(
entt::registry* registry, entt::entity player_entity, size_t bufferingIntervalInMillis
entt::registry* registry, size_t bufferingIntervalInMillis
) : m_registry(registry),
m_player_entity(player_entity),
m_bufferingIntervalInMillis(bufferingIntervalInMillis)
{
}
{ }
void EntityPositionInterpolator::register_entity(entt::entity entity) {
m_registry->emplace<EntityPositionInterpolation>(entity, glm::vec3());
}
void EntityPositionInterpolator::add_position_for_entity(entt::entity entity, glm::vec3 position) {
void EntityPositionInterpolator::set_position(Clock::time_point time_point, entt::entity entity, glm::vec3 position) {
auto* interpolation = m_registry->try_get<EntityPositionInterpolation>(entity);
if(interpolation == nullptr) {
spdlog::warn("Attempt to add position for non-registered entity {}", (uint32_t)entity);
@@ -31,7 +28,7 @@ void EntityPositionInterpolator::add_position_for_entity(entt::entity entity, gl
interpolation->push(Clock::now(), position);
}
glm::vec3 EntityPositionInterpolator::get_position_for_entity(entt::entity entity) {
glm::vec3 EntityPositionInterpolator::get_position(Clock::time_point time_point, entt::entity entity) {
auto* interpolation = m_registry->try_get<EntityPositionInterpolation>(entity);
if(interpolation == nullptr) {
spdlog::warn("Attempt to get position for non-registered entity {}", (uint32_t)entity);
@@ -44,12 +41,10 @@ glm::vec3 EntityPositionInterpolator::get_position_for_entity(entt::entity entit
return glm::mix(from, to, value);
}
void EntityPositionInterpolator::update() {
void EntityPositionInterpolator::interpolate_smoothed_entities(Clock::time_point time_point) {
m_registry->view<EntityPositionInterpolation, Transform>()
.each([&](const auto entity, const EntityPositionInterpolation& interpolation, Transform& ts) {
auto now = Clock::now() - std::chrono::milliseconds(m_bufferingIntervalInMillis);
auto [from, to, value] = interpolation.get_values_around(now);
auto [from, to, value] = interpolation.get_values_around(time_point);
ts.set_position(glm::mix(from, to, value));
});
}
@@ -1,3 +1,4 @@
#pragma once
#include <chrono>
#include <entt/entt.hpp>
@@ -12,23 +13,20 @@ class EntityPositionInterpolator {
entt::registry* m_registry;
entt::entity m_player_entity;
size_t m_bufferingIntervalInMillis;
glm::vec3 get_position(Clock::time_point time_point, entt::entity entity);
public:
EntityPositionInterpolator(
entt::registry* registry,
entt::entity player_entity,
size_t bufferingIntervalInMillis);
void register_entity(entt::entity entity);
void add_position_for_entity(entt::entity entity, glm::vec3 position);
void set_position(Clock::time_point time_point, entt::entity entity, glm::vec3 position);
glm::vec3 get_position_for_entity(entt::entity entity);
void update();
void interpolate_smoothed_entities(Clock::time_point time_point);
};
}
@@ -1,4 +1,4 @@
#include "PlayerReconciler.hpp"
#include "PlayerRollback.hpp"
#include "world/JoltPhysicsWorld.hpp"
#include "world/CharacterBody.hpp"
@@ -9,7 +9,7 @@
namespace tw::net {
PlayerReconciler::PlayerReconciler(JoltPhysicsWorld* physics)
PlayerRollback::PlayerRollback(JoltPhysicsWorld* physics)
: m_physics(physics), m_last_reconciled_ack(0), m_rollback_count(0),
m_last_correction_distance(0.0f), m_last_replayed_frames(0), m_last_ack_frame(0)
{
@@ -21,21 +21,21 @@ PlayerReconciler::PlayerReconciler(JoltPhysicsWorld* physics)
}
}
void PlayerReconciler::record_input(uint32_t frame, glm::vec3 input) {
void PlayerRollback::set_input(uint32_t frame, glm::vec3 input) {
size_t idx = frame % RING_SIZE;
m_records[idx].frame = frame;
m_records[idx].valid = true;
m_records[idx].input = input;
}
void PlayerReconciler::record_prediction(uint32_t frame, glm::vec3 position) {
void PlayerRollback::record_prediction(uint32_t frame, glm::vec3 position) {
size_t idx = frame % RING_SIZE;
if (m_records[idx].frame == frame && m_records[idx].valid) {
m_records[idx].predicted_position = position;
}
}
bool PlayerReconciler::reconcile(uint32_t ack_frame, glm::vec3 authoritative_position,
bool PlayerRollback::apply_correction(uint32_t ack_frame, glm::vec3 authoritative_position,
entt::entity player, entt::registry* registry,
uint32_t current_frame)
{
@@ -77,14 +77,16 @@ bool PlayerReconciler::reconcile(uint32_t ack_frame, glm::vec3 authoritative_pos
return true;
}
void PlayerReconciler::place_character(entt::entity player, entt::registry* registry,
void PlayerRollback::place_character(entt::entity player, entt::registry* registry,
glm::vec3 position, bool clear_velocity) {
CharacterBody* body = registry->try_get<CharacterBody>(player);
Transform* transform = registry->try_get<Transform>(player);
if (!body) {
return;
}
body->m_character->SetPosition(JPH::RVec3(position.x, position.y, position.z));
transform->set_position(position);
if (clear_velocity) {
body->m_character->SetLinearVelocity(JPH::Vec3::sZero());
@@ -92,7 +94,7 @@ void PlayerReconciler::place_character(entt::entity player, entt::registry* regi
}
}
void PlayerReconciler::replay_from(uint32_t from_frame, entt::entity player,
void PlayerRollback::replay_from(uint32_t from_frame, entt::entity player,
entt::registry* registry, uint32_t current_frame) {
for (uint32_t f = from_frame + 1; f < current_frame; ++f) {
m_physics->step(f, tw::JoltPhysicsWorld::FIXED_DELTA_TIME, true);
@@ -111,7 +113,7 @@ void PlayerReconciler::replay_from(uint32_t from_frame, entt::entity player,
}
}
void PlayerReconciler::reset_at(uint32_t frame) {
void PlayerRollback::reset_at(uint32_t frame) {
m_last_reconciled_ack = frame;
for (auto& record : m_records) {
@@ -11,7 +11,7 @@ class JoltPhysicsWorld;
namespace tw::net {
class PlayerReconciler {
class PlayerRollback {
private:
struct Record {
uint32_t frame;
@@ -33,13 +33,14 @@ private:
uint32_t m_last_ack_frame = 0;
public:
PlayerReconciler(tw::JoltPhysicsWorld* physics);
PlayerRollback(tw::JoltPhysicsWorld* physics);
void record_input(uint32_t frame, glm::vec3 input);
void set_input(uint32_t frame, glm::vec3 input);
void record_prediction(uint32_t frame, glm::vec3 position);
bool reconcile(uint32_t ack_frame, glm::vec3 authoritative_position,
entt::entity player, entt::registry* registry, uint32_t current_frame);
bool apply_correction(
uint32_t ack_frame, glm::vec3 authoritative_position,
entt::entity player, entt::registry* registry, uint32_t current_frame);
private:
/**
@@ -56,11 +57,6 @@ private:
public:
/**
* Drops every stored frame and treats `frame` as already answered. Used when
* the player is placed outright, where nothing recorded before the placement
* describes where it now is.
*/
void reset_at(uint32_t frame);
uint64_t rollback_count() const { return m_rollback_count; }
@@ -0,0 +1,187 @@
#include "ReplicatorClient.hpp"
#include "PlayerMove.pb.h"
#include "debug/metrics/NetworkMetrics.hpp"
#include "network/EntityIdMap.hpp"
#include "world/CharacterBody.hpp"
#include "world/JoltPhysicsWorld.hpp"
#include "world/Transform.hpp"
#include <chrono>
namespace tw::net {
ReplicatorClient::ReplicatorClient(
ProtobufMessages* messages,
EntityIdMap* entity_id_map,
dbg::NetworkMetrics *network_metrics,
JoltPhysicsWorld *physics_world,
World *world,
net::ServerConnection* server_connection,
EntityPositionInterpolator* entity_writer
) :
m_messages(messages),
m_entity_id_map(entity_id_map),
m_network_metrics(network_metrics),
m_rollback(physics_world),
m_world(world),
m_server_connection(server_connection),
m_input_send_times(INPUT_SEND_TIME_COUNT),
m_entity_interpolator(entity_writer)
{
}
int64_t ReplicatorClient::now_ms() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
Clock::now().time_since_epoch()).count();
}
void ReplicatorClient::set_player_entity(entt::entity entity) {
m_player_entity = entity;
}
void ReplicatorClient::measure_response_time(uint32_t current_frame_idx, uint32_t snapshot_frame_idx) {
// Snapshots carry frame zero until the server has an input to answer, and
// repeat the same frame whenever no newer one arrived in between.
if(snapshot_frame_idx == 0 || snapshot_frame_idx <= m_last_measured_frame) {
return;
}
// Anything the send times no longer cover, including a frame we never sent,
// which underflows into a large distance.
if(current_frame_idx - snapshot_frame_idx >= INPUT_SEND_TIME_COUNT) {
return;
}
m_last_measured_frame = snapshot_frame_idx;
auto sent_at = m_input_send_times[snapshot_frame_idx % INPUT_SEND_TIME_COUNT];
m_network_metrics->record_response_time(Clock::now() - sent_at);
}
void ReplicatorClient::set_input(uint32_t frame_idx, glm::vec3 input) {
m_rollback.set_input(frame_idx, input);
mmo::PlayerMoveMessage player_move_message = {};
player_move_message.set_frame_idx(frame_idx);
mmo::PlayerInput* player_input = new mmo::PlayerInput();
player_input->set_x(input.x);
player_input->set_y(input.y);
player_input->set_z(input.z);
player_move_message.set_allocated_input(player_input);
auto send_result = m_messages->send(m_server_connection->server(), player_move_message, false);
if(!send_result) {
spdlog::error("Failed to send message: {}", send_result.error().message());
}
m_input_send_times[frame_idx % INPUT_SEND_TIME_COUNT] = Clock::now();
}
void ReplicatorClient::record_prediction(uint32_t frame_idx) {
if(!m_player_entity.has_value()) {
return;
}
Transform* transform = m_world->registry().try_get<Transform>(m_player_entity.value());
if(!transform) {
return;
}
glm::vec3 position = transform->position();
m_rollback.record_prediction(frame_idx, position);
}
void ReplicatorClient::update() {
if(!m_player_entity.has_value()) {
return;
}
// m_entity_interpolator.interpolate_smoothed_entities(std::chrono::high_resolution_clock::now() - std::chrono::milliseconds(100));
// Transform* transform = m_world->registry().try_get<Transform>(m_player_entity.value());
// if(!transform) {
// return;
// }
//
// transform->set_position(m_render_position);
}
void ReplicatorClient::snap_player_to(uint32_t frame_idx, entt::entity entity, glm::vec3 position) {
CharacterBody* body = m_world->registry().try_get<CharacterBody>(entity);
if(body) {
body->m_character->SetPosition(JPH::RVec3(position.x, position.y, position.z));
body->m_character->SetLinearVelocity(JPH::Vec3::sZero());
body->m_desired_velocity = JPH::Vec3::sZero();
}
Transform* transform = m_world->registry().try_get<Transform>(entity);
if(transform) {
transform->set_position(position);
}
m_render_position = position;
// m_rollback.reset_at(frame_idx);
}
void ReplicatorClient::handle_snapshot_entity(
uint32_t current_frame_idx,
uint32_t record_frame_idx,
serial::EntityRecord& record
) {
std::optional<entt::entity> entity = m_entity_id_map->get_local(record.id);
if(!entity.has_value()) {
spdlog::warn("Received position update for unknown entity {}", record.id);
return;
}
glm::vec3 p = {record.position.x, record.position.y, record.position.z};
if(m_player_entity.has_value() &&
entity.value() == m_player_entity.value()
) {
if(!m_player_position_initialized) {
m_player_position_initialized = true;
// snap_player_to(current_frame_idx, entity.value(), p);
// return;
}
//snap_player_to(current_frame_idx, entity.value(), p);
// bool reconcile_happened = m_reconciler.reconcile(record_frame_idx, p, entity.value(), &m_world->registry(), current_frame_idx);
//
// if(reconcile_happened) {
// m_network_metrics->record_rollback();
// m_network_metrics->record_correction_distance(m_reconciler.last_correction_distance());
// m_network_metrics->record_replayed_frames(m_reconciler.last_replayed_frames());
// }
// if(record_frame_idx != 0) {
// uint32_t ack_lag = 0;
// if(current_frame_idx >= record_frame_idx) {
// ack_lag = current_frame_idx - record_frame_idx;
// }
// m_network_metrics->record_ack_lag(ack_lag);
// }
}
m_entity_interpolator->set_position(std::chrono::high_resolution_clock::now(), entity.value(), p);
}
void ReplicatorClient::handle_snapshot(uint32_t current_frame_idx, serial::WorldStateReader& reader) {
auto header = reader.read_header();
measure_response_time(current_frame_idx, header.frame_idx);
while(reader.has_entity()) {
auto entity_r = reader.read_entity();
handle_snapshot_entity(current_frame_idx, header.frame_idx, entity_r);
}
}
}
@@ -0,0 +1,114 @@
#pragma once
#include "ProtobufMessages.hpp"
#include "WorldStateWriter.hpp"
#include "debug/metrics/NetworkMetrics.hpp"
#include "network/EntityIdMap.hpp"
#include "network/PlayerRollback.hpp"
#include "network/ServerConnection.hpp"
#include "world/JoltPhysicsWorld.hpp"
#include "network/EntityPositionInterpolator.hpp"
#include "world/World.hpp"
namespace tw::net {
/**
* Handles replication messages from server.
*/
class ReplicatorClient {
public:
ReplicatorClient(
ProtobufMessages* messages,
EntityIdMap* entity_id_map,
dbg::NetworkMetrics *network_metrics,
JoltPhysicsWorld *physics_world,
World *world,
net::ServerConnection* server_connection,
EntityPositionInterpolator* entity_writer
);
/**
* Sets player controlled entity. The behaviour for this entity will be
* little different.
*/
void set_player_entity(entt::entity entity);
/**
* Takes the input driving `frame_idx`, keeping it for the answer to that
* frame and putting it on the wire.
*/
void set_input(uint32_t frame_idx, glm::vec3 input);
/**
* Takes where the player ended up on `frame_idx`, which should represent
* the prediction taken.
*/
void record_prediction(uint32_t frame_idx);
void handle_snapshot(uint32_t current_frame_idx, serial::WorldStateReader& reader);
private:
/**
* Records how long the answer to `frame_idx` took to arrive, ignoring
* frames that were already measured or are too old to still have a send
* time.
*/
void measure_response_time(uint32_t current_frame_idx, uint32_t snapshot_frame_idx);
/**
* Places the player at an authoritative position outright, clearing the
* predicted state that led there. Used for the first position the server
* sends, which the local simulation has no history to reconcile against.
*/
void snap_player_to(uint32_t frame_idx, entt::entity entity, glm::vec3 position);
void handle_snapshot_entity(
uint32_t current_frame_idx,
uint32_t record_frame_idx,
serial::EntityRecord& record
);
void update();
glm::vec3 render_position() const {
return m_render_position;
}
private:
using Clock = std::chrono::steady_clock;
static int64_t now_ms();
ProtobufMessages *m_messages;
EntityIdMap* m_entity_id_map;
dbg::NetworkMetrics *m_network_metrics;
net::PlayerRollback m_rollback;
World *m_world;
net::ServerConnection* m_server_connection;
std::optional<entt::entity> m_player_entity;
net::EntityPositionInterpolator* m_entity_interpolator;
/**
* Whether the server has placed the player at least once. Entities are
* created before their position arrives, so the body starts at the origin
* and has to be moved once the first position shows up.
*/
bool m_player_position_initialized = false;
glm::vec3 m_render_position{0.0f};
/**
* When each input was sent, indexed by its frame. Holds the most recent
* INPUT_SEND_TIME_COUNT frames; an answer that takes longer than that goes
* unmeasured.
*/
static constexpr size_t INPUT_SEND_TIME_COUNT = 256;
std::vector<Clock::time_point> m_input_send_times;
uint32_t m_last_measured_frame = 0;
};
}
@@ -11,7 +11,6 @@ ServerConnection::ServerConnection(Address address) :
}
tl::expected<void, msg::MessageError> ServerConnection::start() {
// Create the endpoint
auto endpoint_r = msg::MessageEndpoint::create();
if(!endpoint_r) {
m_status = ConnectionStatus::Failed;
@@ -21,7 +20,6 @@ tl::expected<void, msg::MessageError> ServerConnection::start() {
m_endpoint = std::move(endpoint_r.value());
// Connect to the server
auto server_r = m_endpoint->connect(m_address.ip_string(), m_address.port());
if(!server_r) {
m_status = ConnectionStatus::Failed;
@@ -44,13 +42,11 @@ void ServerConnection::update() {
m_endpoint->update();
// Check if the connection has become established
if(m_status == ConnectionStatus::Connecting && m_server) {
if(m_server->is_established()) {
m_status = ConnectionStatus::Connected;
spdlog::info("Connected to server at {}", m_address.to_string());
} else {
// Check for timeout
auto elapsed = Clock::now() - m_started_at;
if(elapsed >= CONNECT_TIMEOUT) {
m_status = ConnectionStatus::Failed;
@@ -61,24 +57,5 @@ void ServerConnection::update() {
}
}
ConnectionStatus ServerConnection::status() const {
return m_status;
}
const std::string& ServerConnection::error() const {
return m_error;
}
const Address& ServerConnection::address() const {
return m_address;
}
msg::MessageEndpoint* ServerConnection::endpoint() const {
return m_endpoint.get();
}
msg::MessageConnection* ServerConnection::server() const {
return m_server;
}
}
@@ -20,9 +20,6 @@ enum class ConnectionStatus { Idle, Connecting, Connected, Failed };
/**
* Encapsulates a connection to a game server.
*
* Owns the endpoint and connection, managing the state of the connection
* attempt and providing non-blocking access to send/receive.
*/
class ServerConnection {
using Clock = std::chrono::steady_clock;
@@ -37,28 +34,21 @@ class ServerConnection {
static constexpr std::chrono::seconds CONNECT_TIMEOUT{5};
public:
/**
* Constructs a connection object for the given address, without starting I/O.
*/
explicit ServerConnection(Address address);
/**
* Starts the connection process by creating an endpoint and connecting to
* the server. Returns an error if the endpoint cannot be created.
*/
tl::expected<void, msg::MessageError> start();
/**
* Updates the connection state: pumps the endpoint, and checks for timeout
* or successful connection. Must be called regularly.
*/
void update();
ConnectionStatus status() const;
const std::string& error() const;
const Address& address() const;
msg::MessageEndpoint* endpoint() const;
msg::MessageConnection* server() const;
ConnectionStatus status() const { return m_status; }
const std::string& error() const { return m_error; }
const Address& address() const { return m_address; }
msg::MessageEndpoint* endpoint() const { return m_endpoint.get(); }
msg::MessageConnection* server() const { return m_server; }
};
}
@@ -1,447 +0,0 @@
#include "ClientWorldController.hpp"
#include <glm/glm.hpp>
#include <chrono>
#include <spdlog/spdlog.h>
#include "network/ServerConnection.hpp"
#include "Entity.pb.h"
#include "Login.pb.h"
#include "WorldState.pb.h"
#include "PlayerMove.pb.h"
#include "entt/entity/entity.hpp"
#include "entt/entity/fwd.hpp"
#include "messages/PlayerMoveMessage.hpp"
#include "metrics/HistoryBuffer.hpp"
#include "world/CharacterBody.hpp"
#include "world/CharacterController.hpp"
#include "world/JoltPhysicsWorld.hpp"
#include "world/WorldEntity.hpp"
#include "tw/serial/WorldStateWriter.hpp"
#include "network/EntityInterpolation.hpp"
namespace tw {
typedef HistoryBuffer<long, glm::vec3> EntityPositionHistory;
entt::entity create_player_entity(World* world, JoltPhysicsWorld* physics_world, drw::WorldRenderer* renderer) {
// entt::entity entity = world->registry().create();
// drw::Mesh mesh = renderer->add_mesh(drw::MeshData::cube(glm::vec3(1.0f)));
// world->registry()
// .emplace<Transform>(entity,
// Transform(glm::vec3(0.0f, 10.0f, 0.0f)));
// world->registry()
// .emplace<EntityPositionHistory>(entity, 0, glm::vec3(0.0f, 10.0f, 0.0f), 1000);
// world->registry()
// .emplace<tw::WorldEntity>(entity,
// tw::WorldEntity(std::string("player"), (uint32_t)entity));
// world->registry()
// .emplace<tw::drw::Mesh>(entity, mesh);
// world->registry()
// .emplace<tw::CharacterController>(entity, 20.0f);
// auto* body = &world->registry()
// .emplace<CharacterBody>(entity, physics_world->create_character(
// new JPH::BoxShape(JPH::Vec3Arg(0.5f, 0.5f, 0.5f)),
// glm::vec3(0.0f, 10.0f, 0.0f)));
// return entity;
return entt::entity(0);
}
entt::entity
ClientWorldController::create_entity(const std::string& name, glm::vec3 position) {
const auto entity = m_world->registry().create();
if(!m_mesh.has_value()) {
m_mesh = m_world_renderer->add_mesh(drw::MeshData::cube(glm::vec3(1.0f)));
}
spdlog::info("Creating entity {}", name);
m_world->registry()
.emplace<Transform>(entity,
Transform(position));
m_world->registry()
.emplace<tw::WorldEntity>(entity,
tw::WorldEntity(name, (uint32_t)entity));
m_world->registry()
.emplace<tw::drw::Mesh>(entity,
m_mesh.value());
return entity;
}
std::optional<entt::entity> ClientWorldController::map_from_server_entity(int id) {
if(m_entity_mapping.contains(id)) {
return m_entity_mapping[id];
}
return {};
}
void ClientWorldController::map_server_entity(int server_id, entt::entity local_id) {
m_entity_mapping[server_id] = local_id;
}
void ClientWorldController::apply_entity_positions(const mmo::EntityPosition* const* positions, size_t count) {
for(int i = 0; i < count; i++) {
const mmo::EntityPosition* const position = positions[i];
std::optional<entt::entity> entity = map_from_server_entity(position->id());
if(!entity.has_value()) {
spdlog::warn("Received position update for unknown entity {}", position->id());
continue;
}
glm::vec3 p = {position->x(), position->y(), position->z()};
m_entity_interpolator.add_position_for_entity(entity.value(), p);
EntityPositionHistory* history = m_world->registry().try_get<EntityPositionHistory>(entity.value());
if(history != nullptr) {
auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now().time_since_epoch()).count();
history->set(millis, p);
}
}
}
ClientWorldController::ClientWorldController(
const io::InputManager* inputs,
World* world,
JoltPhysicsWorld* physics_world,
drw::WorldRenderer* world_renderer,
net::ServerConnection* connection,
dbg::NetworkMetrics* network_metrics
) :
m_input_manager(inputs),
m_world(world),
m_physics_world(physics_world),
m_world_renderer(world_renderer),
// m_player_entity(/* create_player_entity(world, physics_world, world_renderer) */),
m_player_controller(&world_renderer->camera(), glm::vec3()),
m_connection(connection),
m_messages(connection->endpoint()),
m_network_metrics(network_metrics),
m_tick_step(20),
m_input_send_times(INPUT_SEND_TIME_COUNT),
m_position_history_exporter("/home/martin/output.csv"),
m_entity_interpolator(&m_world->registry(), (entt::entity)0, 300),
m_reconciler(physics_world)
{
m_messages.set_handler<mmo::LoginResponse>(
[this](msg::PeerId, const mmo::LoginResponse& mesg) {
spdlog::info("Logged in!");
});
m_messages.set_handler<mmo::SetControlledEntity>(
[this](msg::PeerId, const mmo::SetControlledEntity& mesg) {
spdlog::info("Setting controlled entity from server id {}", mesg.entity_id());
m_controlled_server_id = mesg.entity_id();
try_bind_player_entity();
});
m_connection->endpoint()->set_handler(Message<mmo::WorldStateMessage>::value,
[this](msg::PeerId, std::span<const std::byte> data) {
serial::WorldStateReader reader(data);
auto header = reader.read_header();
measure_response_time(header.frame_idx);
while(reader.has_spawn()) {
auto spawn = reader.read_spawn();
auto entity = create_entity("test", glm::vec3());
spdlog::info("Spawning entity {}", spawn);
map_server_entity(spawn, entity);
if(m_controlled_server_id.has_value() && m_controlled_server_id.value() == spawn) {
try_bind_player_entity();
} else {
m_entity_interpolator.register_entity(entity);
}
}
while(reader.has_entity()) {
auto entity_r = reader.read_entity();
std::optional<entt::entity> entity = map_from_server_entity(entity_r.id);
if(!entity.has_value()) {
spdlog::warn("Received position update for unknown entity {}", entity_r.id);
continue;
}
glm::vec3 p = {entity_r.position.x, entity_r.position.y, entity_r.position.z};
if(m_player_entity.has_value() && entity.value() == m_player_entity.value()) {
// The entity was created before its position was known, so the
// body sits at the origin until the server places it. There is
// no predicted history to reconcile against yet.
if(!m_player_position_initialized) {
m_player_position_initialized = true;
snap_player_to(entity.value(), p);
continue;
}
glm::vec3 position_before = glm::vec3(0.0f);
Transform* player_transform = m_world->registry().try_get<Transform>(entity.value());
if(player_transform) {
position_before = player_transform->position();
}
bool reconcile_happened = m_reconciler.reconcile(header.frame_idx, p, entity.value(), &m_world->registry(), m_frame_idx);
if(reconcile_happened) {
// Where the replay actually ended up, which is ahead of the
// acked position by the frames that were re-simulated.
glm::vec3 position_after = player_transform
? player_transform->position()
: p;
glm::vec3 correction_delta = position_before - position_after;
float correction_magnitude = glm::length(correction_delta);
if(correction_magnitude > 5.0f) {
correction_delta = glm::normalize(correction_delta) * 5.0f;
}
m_visual_error += correction_delta;
m_render_curr_position = position_after;
m_render_prev_position = position_after;
m_tick_accumulator = 0.0;
m_network_metrics->record_rollback();
m_network_metrics->record_correction_distance(m_reconciler.last_correction_distance());
m_network_metrics->record_replayed_frames(m_reconciler.last_replayed_frames());
}
if(header.frame_idx != 0) {
uint32_t ack_lag = 0;
if(m_frame_idx >= header.frame_idx) {
ack_lag = m_frame_idx - header.frame_idx;
}
m_network_metrics->record_ack_lag(ack_lag);
}
} else {
m_entity_interpolator.add_position_for_entity(entity.value(), p);
}
EntityPositionHistory* history = m_world->registry().try_get<EntityPositionHistory>(entity.value());
if(history != nullptr) {
auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now().time_since_epoch()).count();
history->set(millis, p);
}
}
// apply_entity_positions();
});
m_messages.set_handler<mmo::EntitySpawnMessage>(
[this](msg::PeerId, const mmo::EntitySpawnMessage& mesg) {
auto entity = create_entity(mesg.name(), glm::vec3());
map_server_entity(mesg.entity_id(), entity);
if(m_controlled_server_id.has_value() && m_controlled_server_id.value() == mesg.entity_id()) {
try_bind_player_entity();
} else {
m_entity_interpolator.register_entity(entity);
}
});
}
ClientWorldController::~ClientWorldController() {
}
void ClientWorldController::try_bind_player_entity() {
if(!m_controlled_server_id.has_value()) {
return;
}
auto local_entity = map_from_server_entity(m_controlled_server_id.value());
if(!local_entity.has_value()) {
return;
}
if(m_player_entity.has_value() && m_player_entity.value() == local_entity.value()) {
return;
}
entt::entity entity = local_entity.value();
spdlog::info("Binding player entity");
m_player_entity = entity;
Transform* transform = m_world->registry().try_get<Transform>(entity);
glm::vec3 position = transform ? transform->position() : glm::vec3(0.0f);
m_world->registry().emplace<CharacterController>(entity, 20.0f);
m_world->registry().emplace<CharacterBody>(entity, m_physics_world->create_character(
new JPH::BoxShape(JPH::Vec3Arg(0.5f, 0.5f, 0.5f)),
position
));
if(m_world->registry().all_of<net::EntityPositionInterpolation>(entity)) {
m_world->registry().remove<net::EntityPositionInterpolation>(entity);
}
}
void ClientWorldController::snap_player_to(entt::entity entity, glm::vec3 position) {
CharacterBody* body = m_world->registry().try_get<CharacterBody>(entity);
if(body) {
body->m_character->SetPosition(JPH::RVec3(position.x, position.y, position.z));
body->m_character->SetLinearVelocity(JPH::Vec3::sZero());
body->m_desired_velocity = JPH::Vec3::sZero();
}
Transform* transform = m_world->registry().try_get<Transform>(entity);
if(transform) {
transform->set_position(position);
}
m_render_prev_position = position;
m_render_curr_position = position;
m_tick_accumulator = 0.0;
m_visual_error = glm::vec3(0.0f);
// Frames simulated before the player was placed describe a position it never
// actually had, so answers to them must not be reconciled against.
m_reconciler.reset_at(m_frame_idx);
}
void ClientWorldController::export_entity_history() {
}
void ClientWorldController::measure_response_time(uint32_t frame_idx) {
// Snapshots carry frame zero until the server has an input to answer, and
// repeat the same frame whenever no newer one arrived in between.
if(frame_idx == 0 || frame_idx <= m_last_measured_frame) {
return;
}
// Anything the send times no longer cover, including a frame we never sent,
// which underflows into a large distance.
if(m_frame_idx - frame_idx >= INPUT_SEND_TIME_COUNT) {
return;
}
m_last_measured_frame = frame_idx;
auto sent_at = m_input_send_times[frame_idx % INPUT_SEND_TIME_COUNT];
m_network_metrics->record_response_time(Clock::now() - sent_at);
}
void ClientWorldController::update(double delta_time) {
m_player_controller.update(m_input_manager, delta_time);
ImGui::Begin("Player Controller");
if(m_player_entity.has_value()) {
ImGui::Text("Player entity ID: %d", (uint32_t)m_player_entity.value());
}
ImGui::Text("Player count: %ld", m_entity_mapping.size());
for(auto mapping : m_entity_mapping) {
ImGui::Text("%d -> %d", (uint32_t)mapping.first, mapping.second);
}
ImGui::End();
if(m_tick_step.update()) {
auto network_start = Clock::now();
m_connection->update();
m_network_metrics->record_update_time(Clock::now() - network_start);
m_network_metrics->sample({
.bytes_sent = m_connection->endpoint()->bytes_sent(),
.bytes_received = m_connection->endpoint()->bytes_received(),
.messages_sent = m_connection->endpoint()->messages_sent(),
.messages_received = m_connection->endpoint()->messages_received()
});
{
glm::vec3 input = m_player_controller.input();
if(m_player_entity.has_value()) {
CharacterController* controller = m_world->registry().try_get<CharacterController>(m_player_entity.value());
if(controller) {
controller->set_input(m_frame_idx, input);
m_reconciler.record_input(m_frame_idx, input);
}
}
m_physics_world->step(m_frame_idx, JoltPhysicsWorld::FIXED_DELTA_TIME, true);
if(m_player_entity.has_value()) {
Transform* player_transform = m_world->registry().try_get<Transform>(m_player_entity.value());
if(player_transform) {
glm::vec3 true_position = player_transform->position();
m_reconciler.record_prediction(m_frame_idx, true_position);
m_render_prev_position = m_render_curr_position;
m_render_curr_position = true_position;
m_tick_accumulator = 0.0;
}
}
mmo::PlayerMoveMessage player_move_message = {};
player_move_message.set_frame_idx(m_frame_idx);
mmo::PlayerInput* player_input = new mmo::PlayerInput();
player_input->set_x(input.x);
player_input->set_y(input.y);
player_input->set_z(input.z);
player_move_message.set_allocated_input(player_input);
auto r = m_messages.send(m_connection->server(), player_move_message, false);
m_input_send_times[m_frame_idx % INPUT_SEND_TIME_COUNT] = Clock::now();
export_entity_history();
m_frame_idx++;
m_world->registry().view<Transform>()
.each([&](const auto e, Transform& t) {
m_position_history_exporter.write((uint32_t)e,
std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now().time_since_epoch()).count(), t.position());
});
}
}
m_entity_interpolator.update();
if(m_player_entity.has_value()) {
Transform* player_transform = m_world->registry().try_get<Transform>(m_player_entity.value());
if(player_transform) {
m_visual_error *= std::exp(-delta_time * kVisualErrorDecayRate);
if(glm::length(m_visual_error) < 0.001f) {
m_visual_error = glm::vec3(0.0f);
}
m_tick_accumulator += delta_time;
float alpha = glm::clamp(
static_cast<float>(m_tick_accumulator / JoltPhysicsWorld::FIXED_DELTA_TIME),
0.0f, 1.0f
);
glm::vec3 smoothed_position = glm::mix(m_render_prev_position, m_render_curr_position, alpha) + m_visual_error;
player_transform->set_position(smoothed_position);
m_player_controller.set_target(smoothed_position);
}
}
}
}
@@ -1,150 +0,0 @@
#pragma once
#include <entt/entt.hpp>
#include <glm/gtx/io.hpp>
#include "ProtobufMessages.hpp"
#include "debug/metrics/NetworkMetrics.hpp"
#include "entt/entity/fwd.hpp"
#include "io/InputState.hpp"
#include "metrics/HistoryBufferExporter.hpp"
#include "runtime/LockStep.hpp"
#include "world/JoltPhysicsWorld.hpp"
#include "world/World.hpp"
#include "draw/WorldRenderer.hpp"
#include "world/ThirdPersonPlayerController.hpp"
#include "network/EntityPositionInterpolator.hpp"
#include "network/PlayerReconciler.hpp"
namespace tw::net {
class ServerConnection;
}
namespace tw {
/**
* Multiplayer player controller. Requires address of a server to work.
*/
class ClientWorldController {
/**
* Inputs
*/
const io::InputManager* m_input_manager;
/**
* Pointers to the world
*/
World* m_world;
drw::WorldRenderer* m_world_renderer;
JoltPhysicsWorld* m_physics_world;
std::optional<entt::entity> m_player_entity;
std::optional<uint32_t> m_controlled_server_id;
ThirdPersonPlayerController m_player_controller;
net::ServerConnection* m_connection;
ProtobufMessages m_messages;
dbg::NetworkMetrics* m_network_metrics;
LockStep m_tick_step;
uint32_t m_frame_idx = 1;
entt::entity m_entity_id;
glm::vec3 m_input;
std::optional<drw::Mesh> m_mesh;
using Clock = std::chrono::steady_clock;
void try_bind_player_entity();
/**
* Places the player at an authoritative position outright, clearing the
* predicted state that led there. Used for the first position the server
* sends, which the local simulation has no history to reconcile against.
*/
void snap_player_to(entt::entity entity, glm::vec3 position);
/**
* Whether the server has placed the player at least once. Entities are
* created before their position arrives, so the body starts at the origin
* and has to be moved once the first position shows up.
*/
bool m_player_position_initialized = false;
/**
* When each input was sent, indexed by its frame. Holds the most recent
* INPUT_SEND_TIME_COUNT frames; an answer that takes longer than that goes
* unmeasured.
*/
static constexpr size_t INPUT_SEND_TIME_COUNT = 256;
std::vector<Clock::time_point> m_input_send_times;
uint32_t m_last_measured_frame = 0;
/**
* Records how long the answer to `frame_idx` took to arrive, ignoring
* frames that were already measured or are too old to still have a send
* time.
*/
void measure_response_time(uint32_t frame_idx);
HistoryBufferExporter<long, glm::vec3> m_position_history_exporter;
net::EntityPositionInterpolator m_entity_interpolator;
net::PlayerReconciler m_reconciler;
/**
* Visual smoothing for render-rate interpolation between 20 Hz ticks.
*/
glm::vec3 m_render_prev_position{0.0f};
glm::vec3 m_render_curr_position{0.0f};
double m_tick_accumulator = 0.0;
/**
* Visual error from reconciliation corrections, decays over time.
*/
glm::vec3 m_visual_error{0.0f};
static constexpr double kVisualErrorDecayRate = 12.0;
/**
* Mapping from the server entity_id to local entity_id
* Server might have the same entity under different name
* TODO: Figure out if entt supports custUntitledom IDs to sync them
*/
std::unordered_map<uint32_t, entt::entity> m_entity_mapping;
entt::entity create_entity(const std::string& name, glm::vec3 position);
std::optional<entt::entity> map_from_server_entity(int id);
void map_server_entity(int server_id, entt::entity local_id);
void apply_entity_positions(const mmo::EntityPosition* const* positions, size_t count);
/**
* Exports entity history to CSV file for analysis
*/
void export_entity_history();
public:
ClientWorldController(
const io::InputManager* inputs,
World* world,
JoltPhysicsWorld* physics_world,
drw::WorldRenderer* world_renderer,
net::ServerConnection* connection,
dbg::NetworkMetrics* network_metrics
);
~ClientWorldController();
void update(double delta_time);
};
}
@@ -0,0 +1,242 @@
#include "ClientWorldController.hpp"
#include <glm/glm.hpp>
#include <chrono>
#include <spdlog/spdlog.h>
#include <span>
#include "network/ServerConnection.hpp"
#include "Entity.pb.h"
#include "Login.pb.h"
#include "WorldState.pb.h"
#include "PlayerMove.pb.h"
#include "entt/entity/entity.hpp"
#include "entt/entity/fwd.hpp"
#include "messages/PlayerMoveMessage.hpp"
#include "metrics/HistoryBuffer.hpp"
#include "world/CharacterBody.hpp"
#include "world/CharacterController.hpp"
#include "world/JoltPhysicsWorld.hpp"
#include "world/WorldEntity.hpp"
#include "tw/serial/WorldStateWriter.hpp"
#include "network/EntityInterpolation.hpp"
namespace tw {
typedef HistoryBuffer<long, glm::vec3> EntityPositionHistory;
entt::entity
ClientWorldController::create_entity(const std::string& name, glm::vec3 position) {
const auto entity = m_world->registry().create();
if(!m_mesh.has_value()) {
m_mesh = m_world_renderer->add_mesh(drw::MeshData::cube(glm::vec3(1.0f)));
}
spdlog::info("Creating entity {}", name);
m_world->registry()
.emplace<Transform>(entity,
Transform(position));
m_world->registry()
.emplace<tw::WorldEntity>(entity,
tw::WorldEntity(name, (uint32_t)entity));
m_world->registry()
.emplace<tw::drw::Mesh>(entity,
m_mesh.value());
return entity;
}
// void ClientWorldController::apply_entity_positions(const mmo::EntityPosition* const* positions, size_t count) {
// for(int i = 0; i < count; i++) {
// const mmo::EntityPosition* const position = positions[i];
// std::optional<entt::entity> entity = m_entity_id_map.get_local(position->id());
//
// if(!entity.has_value()) {
// spdlog::warn("Received position update for unknown entity {}", position->id());
// continue;
// }
//
// glm::vec3 p = {position->x(), position->y(), position->z()};
// m_entity_interpolator.add_position_for_entity(entity.value(), p);
//
// EntityPositionHistory* history = m_world->registry().try_get<EntityPositionHistory>(entity.value());
//
// if(history != nullptr) {
// auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now().time_since_epoch()).count();
// history->set(millis, p);
// }
// }
// }
void ClientWorldController::spawn_entity(const std::string& name, uint32_t server_id) {
auto entity = create_entity(name, glm::vec3());
spdlog::info("Spawning entity {}", server_id);
m_entity_id_map.register_server_to_local(server_id, entity);
if(m_controlled_server_id.has_value() && m_controlled_server_id.value() == server_id) {
try_bind_player_entity();
} else {
}
m_interpolator.register_entity(entity);
}
ClientWorldController::ClientWorldController(
const io::InputManager* inputs,
World* world,
JoltPhysicsWorld* physics_world,
drw::WorldRenderer* world_renderer,
net::ServerConnection* connection,
dbg::NetworkMetrics* network_metrics
) :
m_input_manager(inputs),
m_world(world),
m_physics_world(physics_world),
m_world_renderer(world_renderer),
m_player_controller(&world_renderer->camera(), glm::vec3()),
m_connection(connection),
m_messages(connection->endpoint()),
m_network_metrics(network_metrics),
m_input_update_tick_step(20),
m_interpolator(&m_world->registry(), 300),
m_replicator_client(&m_messages, &m_entity_id_map, m_network_metrics, m_physics_world, m_world, m_connection, &m_interpolator)
{
m_messages.set_handler<mmo::LoginResponse>(
[this](msg::PeerId, const mmo::LoginResponse& mesg) {
spdlog::info("Logged in!");
});
m_messages.set_handler<mmo::SetControlledEntity>(
[this](msg::PeerId, const mmo::SetControlledEntity& mesg) {
spdlog::info("Setting controlled entity from server id {}", mesg.entity_id());
m_controlled_server_id = mesg.entity_id();
try_bind_player_entity();
});
m_connection->endpoint()->set_handler(Message<mmo::WorldStateMessage>::value,
[this](msg::PeerId, std::span<const std::byte> data) {
serial::WorldStateReader reader(data);
m_replicator_client.handle_snapshot(m_network_frame_idx, reader);
});
m_messages.set_handler<mmo::EntitySpawnMessage>(
[this](msg::PeerId, const mmo::EntitySpawnMessage& mesg) {
for(auto& spawn : mesg.spawns()) {
spawn_entity(spawn.name(), spawn.entity_id());
}
});
}
ClientWorldController::~ClientWorldController() { }
void ClientWorldController::try_bind_player_entity() {
if(!m_controlled_server_id.has_value()) {
return;
}
auto local_entity = m_entity_id_map.get_local(m_controlled_server_id.value());
if(!local_entity.has_value()) {
return;
}
if(m_player_entity.has_value() && m_player_entity.value() == local_entity.value()) {
return;
}
entt::entity entity = local_entity.value();
spdlog::info("Binding player entity");
m_player_entity = entity;
m_replicator_client.set_player_entity(entity);
Transform* transform = m_world->registry().try_get<Transform>(entity);
glm::vec3 position = transform ? transform->position() : glm::vec3(0.0f);
m_world->registry().emplace<CharacterController>(entity, 20.0f);
m_world->registry().emplace<CharacterBody>(entity, m_physics_world->create_character(
new JPH::BoxShape(JPH::Vec3Arg(0.5f, 0.5f, 0.5f)),
position
));
// if(m_world->registry().all_of<net::EntityPositionInterpolation>(entity)) {
// m_world->registry().remove<net::EntityPositionInterpolation>(entity);
// }
}
void ClientWorldController::update_network() {
auto network_start = Clock::now();
// collects incoming messages
m_connection->update();
m_network_metrics->record_update_time(Clock::now() - network_start);
m_network_metrics->sample({
.bytes_sent = m_connection->endpoint()->bytes_sent(),
.bytes_received = m_connection->endpoint()->bytes_received(),
.messages_sent = m_connection->endpoint()->messages_sent(),
.messages_received = m_connection->endpoint()->messages_received()
});
m_replicator_client.record_prediction(m_frame_idx);
// m_world->registry().view<Transform>()
// .each([&](const auto e, Transform& t) {
// m_position_history_exporter.write((uint32_t)e,
// std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now().time_since_epoch()).count(), t.position());
// });
}
void ClientWorldController::update(double delta_time) {
// collects messages from network for processing
update_network();
m_player_controller.update(m_input_manager, delta_time);
// collect input every tick of the input_update_tick_step
if(m_input_update_tick_step.has_ticked()) {
// TODO: might be more like `calc_velocity_vector`.
// 1. it is not exactly *input* but input mapped to normalized velocity vector
// 2. I hate calling methods `update`
glm::vec3 input = m_player_controller.input();
if(m_player_entity.has_value()) {
CharacterController* controller = m_world->registry().try_get<CharacterController>(m_player_entity.value());
if(controller) {
// controller->set_input(m_network_frame_idx, input);
m_replicator_client.set_input(m_network_frame_idx, input);
}
}
// m_physics_world->step(m_network_frame_idx, JoltPhysicsWorld::FIXED_DELTA_TIME, true);
m_network_frame_idx++;
}
m_interpolator.interpolate_smoothed_entities(std::chrono::high_resolution_clock::now() - std::chrono::milliseconds(500));
if(m_player_entity.has_value()) {
// after interpolation happen, follow the target
// TODO: The third person controller could pull
Transform* player_transform = m_world->registry().try_get<Transform>(m_player_entity.value());
if(player_transform) {
m_player_controller.set_target(player_transform->position());
}
}
m_frame_idx++;
// m_replicator_client.update();
}
}
@@ -0,0 +1,107 @@
#pragma once
#include <entt/entt.hpp>
#include <glm/gtx/io.hpp>
#include "ProtobufMessages.hpp"
#include "debug/metrics/NetworkMetrics.hpp"
#include "entt/entity/fwd.hpp"
#include "io/InputState.hpp"
#include "metrics/HistoryBufferExporter.hpp"
#include "network/EntityIdMap.hpp"
#include "network/ReplicatorClient.hpp"
#include "runtime/LockStep.hpp"
#include "world/JoltPhysicsWorld.hpp"
#include "world/World.hpp"
#include "draw/WorldRenderer.hpp"
#include "world/ThirdPersonPlayerController.hpp"
#include "network/EntityPositionInterpolator.hpp"
namespace tw::net {
class ServerConnection;
}
namespace tw {
/**
* Multiplayer player controller. Requires address of a server to work.
*/
class ClientWorldController {
/**
* Inputs
*/
const io::InputManager* m_input_manager;
/**
* Pointers to the world
*/
World* m_world;
drw::WorldRenderer* m_world_renderer;
JoltPhysicsWorld* m_physics_world;
std::optional<entt::entity> m_player_entity;
std::optional<uint32_t> m_controlled_server_id;
ThirdPersonPlayerController m_player_controller;
net::ServerConnection* m_connection;
ProtobufMessages m_messages;
dbg::NetworkMetrics* m_network_metrics;
LockStep m_input_update_tick_step;
uint32_t m_frame_idx = 1;
uint32_t m_network_frame_idx = 1;
entt::entity m_entity_id;
glm::vec3 m_input;
std::optional<drw::Mesh> m_mesh;
net::EntityPositionInterpolator m_interpolator;
net::ReplicatorClient m_replicator_client;
net::EntityIdMap m_entity_id_map;
using Clock = std::chrono::steady_clock;
void try_bind_player_entity();
entt::entity create_entity(const std::string& name, glm::vec3 position);
void apply_entity_positions(const mmo::EntityPosition* const* positions, size_t count);
void update_network();
public:
ClientWorldController(
const io::InputManager* inputs,
World* world,
JoltPhysicsWorld* physics_world,
drw::WorldRenderer* world_renderer,
net::ServerConnection* connection,
dbg::NetworkMetrics* network_metrics
);
~ClientWorldController();
constexpr uint32_t frame_idx() const {
return m_frame_idx;
}
constexpr std::optional<entt::entity> player_entity() const {
if(!m_controlled_server_id.has_value()) {
return {};
}
return m_entity_id_map.get_local(m_controlled_server_id.value());
}
void spawn_entity(const std::string& name, uint32_t server_id);
void update(double delta_time);
};
}