#1 - ClientWorldController refactoring
This commit is contained in:
@@ -7,6 +7,7 @@ file(GLOB FILES
|
||||
src/app/*.cpp
|
||||
src/network/*.cpp
|
||||
src/world/*.cpp
|
||||
src/world/controllers/*.cpp
|
||||
src/io/*.cpp
|
||||
src/draw/*.cpp
|
||||
src/draw/RenderPasses/*.cpp
|
||||
@@ -39,8 +40,8 @@ target_link_libraries(${PROJECT_NAME}
|
||||
Jolt
|
||||
Tracy::TracyClient
|
||||
TracyClient
|
||||
pqxx
|
||||
pq
|
||||
# pqxx
|
||||
# pq
|
||||
)
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
+10
-8
@@ -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) {
|
||||
+6
-10
@@ -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);
|
||||
};
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user