#1 - ClientWorldController refactoring
This commit is contained in:
@@ -7,6 +7,7 @@ file(GLOB FILES
|
|||||||
src/app/*.cpp
|
src/app/*.cpp
|
||||||
src/network/*.cpp
|
src/network/*.cpp
|
||||||
src/world/*.cpp
|
src/world/*.cpp
|
||||||
|
src/world/controllers/*.cpp
|
||||||
src/io/*.cpp
|
src/io/*.cpp
|
||||||
src/draw/*.cpp
|
src/draw/*.cpp
|
||||||
src/draw/RenderPasses/*.cpp
|
src/draw/RenderPasses/*.cpp
|
||||||
@@ -39,8 +40,8 @@ target_link_libraries(${PROJECT_NAME}
|
|||||||
Jolt
|
Jolt
|
||||||
Tracy::TracyClient
|
Tracy::TracyClient
|
||||||
TracyClient
|
TracyClient
|
||||||
pqxx
|
# pqxx
|
||||||
pq
|
# pq
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(${PROJECT_NAME}
|
target_include_directories(${PROJECT_NAME}
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
#include "debug/tools/EntityManagerGui.hpp"
|
#include "debug/tools/EntityManagerGui.hpp"
|
||||||
#include "debug/tools/NetworkStatsGui.hpp"
|
#include "debug/tools/NetworkStatsGui.hpp"
|
||||||
#include "network/ServerConnection.hpp"
|
#include "network/ServerConnection.hpp"
|
||||||
#include "world/ClientWorldController.hpp"
|
#include "world/controllers/ClientWorldController.hpp"
|
||||||
|
|
||||||
namespace tw::app {
|
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.
|
* Responsible for updating the game simulation and rendering debug information.
|
||||||
*/
|
*/
|
||||||
class GameState {
|
class GameState {
|
||||||
|
|||||||
@@ -1,92 +1,126 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
#include <implot.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <format>
|
#include <cstddef>
|
||||||
#include <ratio>
|
#include <vector>
|
||||||
|
|
||||||
#include "debug/ComponentGui.hpp"
|
#include "debug/ComponentGui.hpp"
|
||||||
#include <implot.h>
|
|
||||||
#include <implot_internal.h>
|
|
||||||
#include "network/EntityInterpolation.hpp"
|
#include "network/EntityInterpolation.hpp"
|
||||||
|
#include "world/Transform.hpp"
|
||||||
|
|
||||||
|
|
||||||
template<>
|
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:
|
public:
|
||||||
void draw(net::EntityPositionInterpolation* instance) {
|
void draw(Transform* instance) {
|
||||||
ImGui::SeparatorText("Entity Interpolation");
|
ImGui::SeparatorText("Drawn Position");
|
||||||
|
|
||||||
bool m_is_scrolling = true;
|
static int axis = 0;
|
||||||
static float m_metric_history = 10.0f;
|
ImGui::Combo("Axis", &axis, "X\0Y\0Z\0");
|
||||||
ImGui::Checkbox("Is Scrolling", &m_is_scrolling);
|
|
||||||
if(m_is_scrolling) {
|
if(m_subject != instance) {
|
||||||
ImGui::SliderFloat("History", &m_metric_history,1,30,"%.1f s");
|
restart(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int32_t time_buffer_len = 1000;
|
const float time = std::chrono::duration<float>(Clock::now() - m_started_at).count();
|
||||||
ImGui::SliderInt("Buffer length (ms)", &time_buffer_len, 0, 1000);
|
const float position = instance->position()[axis];
|
||||||
|
|
||||||
auto now = std::chrono::high_resolution_clock::now() - std::chrono::milliseconds(time_buffer_len);
|
sample(time, position);
|
||||||
auto [from, to, mix] = instance->get_values_around(now);
|
|
||||||
auto mixed_value = glm::mix(from, to, mix);
|
|
||||||
|
|
||||||
ImGui::Text("Mixed value: (%f, %f)", mixed_value.x, mixed_value.y);
|
ImGui::Text("%.3f", position);
|
||||||
|
|
||||||
if(ImPlot::BeginPlot("Interpolation")) {
|
if(ImPlot::BeginPlot("Position", ImVec2(-1.0f, 180.0f))) {
|
||||||
auto plot_x_from = instance->times().begin()->time_since_epoch().count();
|
ImPlot::SetupAxes("seconds", "position", ImPlotAxisFlags_None, ImPlotAxisFlags_AutoFit);
|
||||||
std::vector<float> times(instance->times().max_size());
|
ImPlot::SetupAxisLimits(ImAxis_X1,
|
||||||
for(auto it = instance->times().begin(); it != instance->times().end(); ++it) {
|
time - WINDOW_IN_SECONDS, time, ImGuiCond_Always);
|
||||||
times.push_back(it->time_since_epoch().count() - plot_x_from);
|
|
||||||
}
|
|
||||||
|
|
||||||
float interpolated_time = now.time_since_epoch().count() - plot_x_from;
|
// The trace wraps around, so it is read from wherever the oldest
|
||||||
std::vector<float> values(instance->values().max_size());
|
// sample ended up.
|
||||||
for(auto it = instance->values().begin(); it != instance->values().end(); ++it) {
|
const int offset = m_count == CAPACITY ? (int)m_head : 0;
|
||||||
values.push_back(it->x);
|
ImPlot::PlotLine("Drawn", m_times.data(), m_positions.data(),
|
||||||
}
|
(int)m_count, ImPlotLineFlags_None, offset);
|
||||||
values.push_back(mixed_value.x);
|
|
||||||
|
|
||||||
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();
|
ImPlot::EndPlot();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if(ImGui::BeginTable("entityInterpolation", 2)) {
|
};
|
||||||
for(int i = 0; i < instance->values().size(); i++) {
|
|
||||||
ImGui::TableNextRow();
|
/**
|
||||||
|
* Says whether the position above stands still because nothing new arrives for
|
||||||
ImGui::TableNextColumn();
|
* the entity, or because what arrives is the same position over and over.
|
||||||
auto value = instance->values()[i];
|
*/
|
||||||
auto now = std::chrono::high_resolution_clock::now() - std::chrono::milliseconds(time_buffer_len);
|
template<>
|
||||||
if(instance->times()[i] > now && (i == instance->values().size() - 1 || instance->times()[i + 1] <= now)) {
|
class tw::dbg::ComponentGui<tw::net::EntityPositionInterpolation> {
|
||||||
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::GetColorU32(ImVec4(0.7f, 0.3f, 0.3f, 0.65f)));
|
private:
|
||||||
ImGui::Text("%f %f %f", mixed_value.x, mixed_value.y, mixed_value.z);
|
using Clock = std::chrono::high_resolution_clock;
|
||||||
ImGui::TableNextColumn();
|
|
||||||
ImGui::TableNextRow();
|
static int64_t millis_since(Clock::time_point point) {
|
||||||
ImGui::TableNextColumn();
|
return std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - point).count();
|
||||||
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::GetColorU32(ImVec4(0.0f, 0.0f, 0.0f, 0.0f)));
|
}
|
||||||
}
|
|
||||||
|
public:
|
||||||
ImGui::Text("%f %f %f", value.x, value.y, value.z);
|
void draw(net::EntityPositionInterpolation* instance) {
|
||||||
|
ImGui::SeparatorText("Received Positions");
|
||||||
auto point = instance->times()[i].time_since_epoch();
|
|
||||||
auto hours = std::chrono::duration_cast<std::chrono::hours>(point);
|
const size_t newest = instance->values().size() - 1;
|
||||||
point -= hours;
|
|
||||||
hours %= 24;
|
ImGui::Text("Newest %.3f %.3f %.3f, %ld ms ago",
|
||||||
auto minutes = std::chrono::duration_cast<std::chrono::minutes>(point);
|
instance->values()[newest].x,
|
||||||
point -= minutes;
|
instance->values()[newest].y,
|
||||||
auto seconds = std::chrono::duration_cast<std::chrono::seconds>(point);
|
instance->values()[newest].z,
|
||||||
point -= seconds;
|
millis_since(instance->times()[newest]));
|
||||||
auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(point);
|
|
||||||
|
ImGui::Text("Oldest %.3f %.3f %.3f, %ld ms ago",
|
||||||
ImGui::TableNextColumn();
|
instance->values()[0].x,
|
||||||
auto fmt = std::format("{}:{}:{}.{}", hours, minutes, seconds, milliseconds);
|
instance->values()[0].y,
|
||||||
ImGui::Text(fmt.c_str());
|
instance->values()[0].z,
|
||||||
}
|
millis_since(instance->times()[0]));
|
||||||
ImGui::EndTable();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ void EntityManagerGui::draw_entity_components() {
|
|||||||
ImGui::Text("Vertex offset: %i", mesh->vertex_offset());
|
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);
|
// auto character = m_world->registry().try_get<tw::CharacterBody>(m_selected);
|
||||||
// if(character != nullptr) {
|
// 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 {
|
namespace tw::net {
|
||||||
|
|
||||||
EntityPositionInterpolator::EntityPositionInterpolator(
|
EntityPositionInterpolator::EntityPositionInterpolator(
|
||||||
entt::registry* registry, entt::entity player_entity, size_t bufferingIntervalInMillis
|
entt::registry* registry, size_t bufferingIntervalInMillis
|
||||||
) : m_registry(registry),
|
) : m_registry(registry),
|
||||||
m_player_entity(player_entity),
|
|
||||||
m_bufferingIntervalInMillis(bufferingIntervalInMillis)
|
m_bufferingIntervalInMillis(bufferingIntervalInMillis)
|
||||||
{
|
{ }
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void EntityPositionInterpolator::register_entity(entt::entity entity) {
|
void EntityPositionInterpolator::register_entity(entt::entity entity) {
|
||||||
m_registry->emplace<EntityPositionInterpolation>(entity, glm::vec3());
|
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);
|
auto* interpolation = m_registry->try_get<EntityPositionInterpolation>(entity);
|
||||||
if(interpolation == nullptr) {
|
if(interpolation == nullptr) {
|
||||||
spdlog::warn("Attempt to add position for non-registered entity {}", (uint32_t)entity);
|
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);
|
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);
|
auto* interpolation = m_registry->try_get<EntityPositionInterpolation>(entity);
|
||||||
if(interpolation == nullptr) {
|
if(interpolation == nullptr) {
|
||||||
spdlog::warn("Attempt to get position for non-registered entity {}", (uint32_t)entity);
|
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);
|
return glm::mix(from, to, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntityPositionInterpolator::update() {
|
void EntityPositionInterpolator::interpolate_smoothed_entities(Clock::time_point time_point) {
|
||||||
m_registry->view<EntityPositionInterpolation, Transform>()
|
m_registry->view<EntityPositionInterpolation, Transform>()
|
||||||
.each([&](const auto entity, const EntityPositionInterpolation& interpolation, Transform& ts) {
|
.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(time_point);
|
||||||
|
|
||||||
auto [from, to, value] = interpolation.get_values_around(now);
|
|
||||||
ts.set_position(glm::mix(from, to, value));
|
ts.set_position(glm::mix(from, to, value));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <entt/entt.hpp>
|
#include <entt/entt.hpp>
|
||||||
@@ -12,23 +13,20 @@ class EntityPositionInterpolator {
|
|||||||
|
|
||||||
entt::registry* m_registry;
|
entt::registry* m_registry;
|
||||||
|
|
||||||
entt::entity m_player_entity;
|
|
||||||
|
|
||||||
size_t m_bufferingIntervalInMillis;
|
size_t m_bufferingIntervalInMillis;
|
||||||
|
|
||||||
|
glm::vec3 get_position(Clock::time_point time_point, entt::entity entity);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
EntityPositionInterpolator(
|
EntityPositionInterpolator(
|
||||||
entt::registry* registry,
|
entt::registry* registry,
|
||||||
entt::entity player_entity,
|
|
||||||
size_t bufferingIntervalInMillis);
|
size_t bufferingIntervalInMillis);
|
||||||
|
|
||||||
void register_entity(entt::entity entity);
|
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 interpolate_smoothed_entities(Clock::time_point time_point);
|
||||||
|
|
||||||
void update();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-8
@@ -1,4 +1,4 @@
|
|||||||
#include "PlayerReconciler.hpp"
|
#include "PlayerRollback.hpp"
|
||||||
|
|
||||||
#include "world/JoltPhysicsWorld.hpp"
|
#include "world/JoltPhysicsWorld.hpp"
|
||||||
#include "world/CharacterBody.hpp"
|
#include "world/CharacterBody.hpp"
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
namespace tw::net {
|
namespace tw::net {
|
||||||
|
|
||||||
PlayerReconciler::PlayerReconciler(JoltPhysicsWorld* physics)
|
PlayerRollback::PlayerRollback(JoltPhysicsWorld* physics)
|
||||||
: m_physics(physics), m_last_reconciled_ack(0), m_rollback_count(0),
|
: 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)
|
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;
|
size_t idx = frame % RING_SIZE;
|
||||||
m_records[idx].frame = frame;
|
m_records[idx].frame = frame;
|
||||||
m_records[idx].valid = true;
|
m_records[idx].valid = true;
|
||||||
m_records[idx].input = input;
|
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;
|
size_t idx = frame % RING_SIZE;
|
||||||
if (m_records[idx].frame == frame && m_records[idx].valid) {
|
if (m_records[idx].frame == frame && m_records[idx].valid) {
|
||||||
m_records[idx].predicted_position = position;
|
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,
|
entt::entity player, entt::registry* registry,
|
||||||
uint32_t current_frame)
|
uint32_t current_frame)
|
||||||
{
|
{
|
||||||
@@ -77,14 +77,16 @@ bool PlayerReconciler::reconcile(uint32_t ack_frame, glm::vec3 authoritative_pos
|
|||||||
return true;
|
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) {
|
glm::vec3 position, bool clear_velocity) {
|
||||||
CharacterBody* body = registry->try_get<CharacterBody>(player);
|
CharacterBody* body = registry->try_get<CharacterBody>(player);
|
||||||
|
Transform* transform = registry->try_get<Transform>(player);
|
||||||
if (!body) {
|
if (!body) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
body->m_character->SetPosition(JPH::RVec3(position.x, position.y, position.z));
|
body->m_character->SetPosition(JPH::RVec3(position.x, position.y, position.z));
|
||||||
|
transform->set_position(position);
|
||||||
|
|
||||||
if (clear_velocity) {
|
if (clear_velocity) {
|
||||||
body->m_character->SetLinearVelocity(JPH::Vec3::sZero());
|
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) {
|
entt::registry* registry, uint32_t current_frame) {
|
||||||
for (uint32_t f = from_frame + 1; f < current_frame; ++f) {
|
for (uint32_t f = from_frame + 1; f < current_frame; ++f) {
|
||||||
m_physics->step(f, tw::JoltPhysicsWorld::FIXED_DELTA_TIME, true);
|
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;
|
m_last_reconciled_ack = frame;
|
||||||
|
|
||||||
for (auto& record : m_records) {
|
for (auto& record : m_records) {
|
||||||
+5
-9
@@ -11,7 +11,7 @@ class JoltPhysicsWorld;
|
|||||||
|
|
||||||
namespace tw::net {
|
namespace tw::net {
|
||||||
|
|
||||||
class PlayerReconciler {
|
class PlayerRollback {
|
||||||
private:
|
private:
|
||||||
struct Record {
|
struct Record {
|
||||||
uint32_t frame;
|
uint32_t frame;
|
||||||
@@ -33,12 +33,13 @@ private:
|
|||||||
uint32_t m_last_ack_frame = 0;
|
uint32_t m_last_ack_frame = 0;
|
||||||
|
|
||||||
public:
|
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);
|
void record_prediction(uint32_t frame, glm::vec3 position);
|
||||||
|
|
||||||
bool reconcile(uint32_t ack_frame, glm::vec3 authoritative_position,
|
bool apply_correction(
|
||||||
|
uint32_t ack_frame, glm::vec3 authoritative_position,
|
||||||
entt::entity player, entt::registry* registry, uint32_t current_frame);
|
entt::entity player, entt::registry* registry, uint32_t current_frame);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -56,11 +57,6 @@ private:
|
|||||||
|
|
||||||
public:
|
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);
|
void reset_at(uint32_t frame);
|
||||||
|
|
||||||
uint64_t rollback_count() const { return m_rollback_count; }
|
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() {
|
tl::expected<void, msg::MessageError> ServerConnection::start() {
|
||||||
// Create the endpoint
|
|
||||||
auto endpoint_r = msg::MessageEndpoint::create();
|
auto endpoint_r = msg::MessageEndpoint::create();
|
||||||
if(!endpoint_r) {
|
if(!endpoint_r) {
|
||||||
m_status = ConnectionStatus::Failed;
|
m_status = ConnectionStatus::Failed;
|
||||||
@@ -21,7 +20,6 @@ tl::expected<void, msg::MessageError> ServerConnection::start() {
|
|||||||
|
|
||||||
m_endpoint = std::move(endpoint_r.value());
|
m_endpoint = std::move(endpoint_r.value());
|
||||||
|
|
||||||
// Connect to the server
|
|
||||||
auto server_r = m_endpoint->connect(m_address.ip_string(), m_address.port());
|
auto server_r = m_endpoint->connect(m_address.ip_string(), m_address.port());
|
||||||
if(!server_r) {
|
if(!server_r) {
|
||||||
m_status = ConnectionStatus::Failed;
|
m_status = ConnectionStatus::Failed;
|
||||||
@@ -44,13 +42,11 @@ void ServerConnection::update() {
|
|||||||
|
|
||||||
m_endpoint->update();
|
m_endpoint->update();
|
||||||
|
|
||||||
// Check if the connection has become established
|
|
||||||
if(m_status == ConnectionStatus::Connecting && m_server) {
|
if(m_status == ConnectionStatus::Connecting && m_server) {
|
||||||
if(m_server->is_established()) {
|
if(m_server->is_established()) {
|
||||||
m_status = ConnectionStatus::Connected;
|
m_status = ConnectionStatus::Connected;
|
||||||
spdlog::info("Connected to server at {}", m_address.to_string());
|
spdlog::info("Connected to server at {}", m_address.to_string());
|
||||||
} else {
|
} else {
|
||||||
// Check for timeout
|
|
||||||
auto elapsed = Clock::now() - m_started_at;
|
auto elapsed = Clock::now() - m_started_at;
|
||||||
if(elapsed >= CONNECT_TIMEOUT) {
|
if(elapsed >= CONNECT_TIMEOUT) {
|
||||||
m_status = ConnectionStatus::Failed;
|
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.
|
* 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 {
|
class ServerConnection {
|
||||||
using Clock = std::chrono::steady_clock;
|
using Clock = std::chrono::steady_clock;
|
||||||
@@ -37,28 +34,21 @@ class ServerConnection {
|
|||||||
static constexpr std::chrono::seconds CONNECT_TIMEOUT{5};
|
static constexpr std::chrono::seconds CONNECT_TIMEOUT{5};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/**
|
|
||||||
* Constructs a connection object for the given address, without starting I/O.
|
|
||||||
*/
|
|
||||||
explicit ServerConnection(Address address);
|
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();
|
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();
|
void update();
|
||||||
|
|
||||||
ConnectionStatus status() const;
|
ConnectionStatus status() const { return m_status; }
|
||||||
const std::string& error() const;
|
|
||||||
const Address& address() const;
|
const std::string& error() const { return m_error; }
|
||||||
msg::MessageEndpoint* endpoint() const;
|
|
||||||
msg::MessageConnection* server() const;
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "Address.hpp"
|
#include "Address.hpp"
|
||||||
|
#include "Entity.pb.h"
|
||||||
#include "Login.pb.h"
|
#include "Login.pb.h"
|
||||||
#include "PlayerMove.pb.h"
|
#include "PlayerMove.pb.h"
|
||||||
#include "ProtobufMessages.hpp"
|
#include "ProtobufMessages.hpp"
|
||||||
@@ -63,6 +64,9 @@ public:
|
|||||||
m_messages.set_handler<mmo::WorldStateMessage>(
|
m_messages.set_handler<mmo::WorldStateMessage>(
|
||||||
[this](tw::msg::PeerId, const mmo::WorldStateMessage& mesg) { });
|
[this](tw::msg::PeerId, const mmo::WorldStateMessage& mesg) { });
|
||||||
|
|
||||||
|
m_messages.set_handler<mmo::EntitySpawnMessage>(
|
||||||
|
[this](tw::msg::PeerId, const mmo::EntitySpawnMessage& mesg) { });
|
||||||
|
|
||||||
m_messages.set_handler<mmo::LoginResponse>(
|
m_messages.set_handler<mmo::LoginResponse>(
|
||||||
[this](tw::msg::PeerId, const mmo::LoginResponse& mesg) {
|
[this](tw::msg::PeerId, const mmo::LoginResponse& mesg) {
|
||||||
if(!m_is_connected) {
|
if(!m_is_connected) {
|
||||||
@@ -103,12 +107,13 @@ public:
|
|||||||
player_input->set_z(m_velocity.z);
|
player_input->set_z(m_velocity.z);
|
||||||
player_move_mesg.set_allocated_input(player_input);
|
player_move_mesg.set_allocated_input(player_input);
|
||||||
auto r = m_messages.send(m_server, player_move_mesg, false);
|
auto r = m_messages.send(m_server, player_move_mesg, false);
|
||||||
|
m_frame_idx++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
const int NUM_CLIENTS = 600;
|
const int NUM_CLIENTS = 10;
|
||||||
tw::net::Address address = {"127.0.0.1", 8101};
|
tw::net::Address address = {"127.0.0.1", 8101};
|
||||||
|
|
||||||
std::vector<std::thread> threads;
|
std::vector<std::thread> threads;
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ syntax = "proto3";
|
|||||||
|
|
||||||
package mmo;
|
package mmo;
|
||||||
|
|
||||||
message EntitySpawnMessage {
|
message EntitySpawn {
|
||||||
uint32 entity_id = 1;
|
uint32 entity_id = 1;
|
||||||
bool is_player = 2;
|
bool is_player = 2;
|
||||||
string name = 3;
|
string name = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message EntitySpawnMessage {
|
||||||
|
repeated EntitySpawn spawns = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message EntityDespawnMessage {
|
message EntityDespawnMessage {
|
||||||
uint32 entity_id = 1;
|
uint32 entity_id = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
/**
|
|
||||||
* WorldStateWriter / WorldStateReader
|
|
||||||
* =====================================
|
|
||||||
* Project-specific serialisation for the per-frame world-state snapshot
|
|
||||||
* sent from the server to each connected client.
|
|
||||||
*
|
|
||||||
* Wire format (all values little-endian):
|
|
||||||
*
|
|
||||||
* ┌──────────────────────────────────────────────────────────┐
|
|
||||||
* │ Header (16 bytes) │
|
|
||||||
* │ message_type : uint32 (supplied by the caller) │
|
|
||||||
* │ sequence : uint32 (always 0, no reply expected) │
|
|
||||||
* │ frame_idx : uint32 │
|
|
||||||
* │ entity_count : uint32 (number of position records) │
|
|
||||||
* ├──────────────────────────────────────────────────────────┤
|
|
||||||
* │ Spawns section │
|
|
||||||
* │ spawn_count : uint32 │
|
|
||||||
* │ spawn[i].id : uint32 × spawn_count │
|
|
||||||
* ├──────────────────────────────────────────────────────────┤
|
|
||||||
* │ Despawns section │
|
|
||||||
* │ despawn_count : uint32 │
|
|
||||||
* │ despawn[i].id : uint32 × despawn_count │
|
|
||||||
* ├──────────────────────────────────────────────────────────┤
|
|
||||||
* │ Entity positions (hot path — tightly packed) │
|
|
||||||
* │ [ id:uint32, x:float, y:float, z:float ] × entity_count│
|
|
||||||
* └──────────────────────────────────────────────────────────┘
|
|
||||||
*
|
|
||||||
* Total minimum size : 20 bytes (header + empty spawns + empty despawns)
|
|
||||||
* Per entity : 16 bytes
|
|
||||||
* 300 entities : 20 + 300×16 = 4820 bytes (well under MTU for segmented)
|
|
||||||
*
|
|
||||||
* Usage (server side, called once per client per frame):
|
|
||||||
*
|
|
||||||
* tw::serial::WorldStateWriter w(buffer); // buffer is a BinaryBuffer
|
|
||||||
* w.begin(frame_idx);
|
|
||||||
* w.write_spawns(interest.spawns());
|
|
||||||
* w.write_despawns(interest.despawns());
|
|
||||||
* w.begin_entities(num_entities); // writes entity_count slot
|
|
||||||
* for each entity in interest.entities():
|
|
||||||
* w.write_entity(entity_id, position);
|
|
||||||
* w.end(); // patches entity_count
|
|
||||||
* // buffer.view() is ready to send
|
|
||||||
*
|
|
||||||
* Usage (client side):
|
|
||||||
*
|
|
||||||
* tw::serial::WorldStateReader r(payload_span);
|
|
||||||
* auto header = r.read_header(); // frame_idx + counts
|
|
||||||
* for (auto id : r.read_spawns()) { ... }
|
|
||||||
* for (auto id : r.read_despawns()) { ... }
|
|
||||||
* while (r.has_entity()) {
|
|
||||||
* auto [id, pos] = r.read_entity();
|
|
||||||
* ...
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "Codec.hpp"
|
#include "Codec.hpp"
|
||||||
#include "GlmCodec.hpp"
|
#include "GlmCodec.hpp"
|
||||||
@@ -67,10 +13,6 @@
|
|||||||
|
|
||||||
namespace tw::serial {
|
namespace tw::serial {
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────
|
|
||||||
// WorldStateWriter
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
class WorldStateWriter {
|
class WorldStateWriter {
|
||||||
BinaryWriter m_w;
|
BinaryWriter m_w;
|
||||||
|
|
||||||
@@ -81,13 +23,6 @@ class WorldStateWriter {
|
|||||||
public:
|
public:
|
||||||
explicit WorldStateWriter(BinaryBuffer& buf) noexcept : m_w(buf) {}
|
explicit WorldStateWriter(BinaryBuffer& buf) noexcept : m_w(buf) {}
|
||||||
|
|
||||||
/**
|
|
||||||
* Write the header. Call once per frame, before everything else.
|
|
||||||
* entity_count is patched in end().
|
|
||||||
*
|
|
||||||
* message_type is supplied by the caller so that this module stays free of
|
|
||||||
* the address the message is delivered to.
|
|
||||||
*/
|
|
||||||
void begin(uint32_t frame_idx, uint32_t message_type) noexcept {
|
void begin(uint32_t frame_idx, uint32_t message_type) noexcept {
|
||||||
m_entity_count = 0;
|
m_entity_count = 0;
|
||||||
|
|
||||||
@@ -104,11 +39,6 @@ public:
|
|||||||
m_entity_count_offset = m_w.reserve_u32();
|
m_entity_count_offset = m_w.reserve_u32();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Spawns ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write the spawn list. Pass any range of entt::entity.
|
|
||||||
*/
|
|
||||||
template<typename Range>
|
template<typename Range>
|
||||||
void write_spawns(const Range& spawns) noexcept {
|
void write_spawns(const Range& spawns) noexcept {
|
||||||
auto count = static_cast<uint32_t>(std::size(spawns));
|
auto count = static_cast<uint32_t>(std::size(spawns));
|
||||||
@@ -118,8 +48,6 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Despawns ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
template<typename Range>
|
template<typename Range>
|
||||||
void write_despawns(const Range& despawns) noexcept {
|
void write_despawns(const Range& despawns) noexcept {
|
||||||
auto count = static_cast<uint32_t>(std::size(despawns));
|
auto count = static_cast<uint32_t>(std::size(despawns));
|
||||||
@@ -129,16 +57,6 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Entity positions (the hot path) ───────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write a single entity position record.
|
|
||||||
* id : raw uint32 of the entity handle
|
|
||||||
* pos : world-space position (x, y, z)
|
|
||||||
*
|
|
||||||
* This is the innermost loop of the replicator — every byte matters.
|
|
||||||
* The compiler will inline both calls down to two contiguous memcpys.
|
|
||||||
*/
|
|
||||||
void write_entity(uint32_t id, const glm::vec3& pos) noexcept {
|
void write_entity(uint32_t id, const glm::vec3& pos) noexcept {
|
||||||
m_w.write(id);
|
m_w.write(id);
|
||||||
// Write x, y, z as 3 contiguous floats
|
// Write x, y, z as 3 contiguous floats
|
||||||
@@ -146,20 +64,14 @@ public:
|
|||||||
++m_entity_count;
|
++m_entity_count;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convenience overload accepting an entt::entity handle directly
|
|
||||||
void write_entity(entt::entity entity, const glm::vec3& pos) noexcept {
|
void write_entity(entt::entity entity, const glm::vec3& pos) noexcept {
|
||||||
write_entity(static_cast<uint32_t>(entity), pos);
|
write_entity(static_cast<uint32_t>(entity), pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Patch the entity_count field written in begin() and finalise the
|
|
||||||
* buffer. Must be called exactly once after all write_entity() calls.
|
|
||||||
*/
|
|
||||||
void end() noexcept {
|
void end() noexcept {
|
||||||
m_w.patch_u32(m_entity_count_offset, m_entity_count);
|
m_w.patch_u32(m_entity_count_offset, m_entity_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Expose the underlying buffer view (e.g. to pass to send_message). */
|
|
||||||
std::span<const std::byte> view() noexcept {
|
std::span<const std::byte> view() noexcept {
|
||||||
return m_w.buffer().view();
|
return m_w.buffer().view();
|
||||||
}
|
}
|
||||||
@@ -170,10 +82,6 @@ public:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────
|
|
||||||
// WorldStateReader (client-side / test use)
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
struct WorldStateHeader {
|
struct WorldStateHeader {
|
||||||
uint32_t packet_type;
|
uint32_t packet_type;
|
||||||
uint32_t frame_idx;
|
uint32_t frame_idx;
|
||||||
|
|||||||
@@ -8,19 +8,22 @@
|
|||||||
|
|
||||||
namespace tw::net {
|
namespace tw::net {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session with the player's client.
|
||||||
|
*/
|
||||||
struct PlayerSession {
|
struct PlayerSession {
|
||||||
public:
|
public:
|
||||||
SessionId session_id;
|
SessionId session_id;
|
||||||
|
|
||||||
msg::MessageConnection* connection;
|
msg::MessageConnection* connection;
|
||||||
|
|
||||||
uint32_t last_frame;
|
uint32_t last_received_frame;
|
||||||
uint32_t acked_frame;
|
uint32_t acked_frame;
|
||||||
|
|
||||||
PlayerSession(SessionId session_id, msg::MessageConnection* connection) :
|
PlayerSession(SessionId session_id, msg::MessageConnection* connection) :
|
||||||
session_id(session_id),
|
session_id(session_id),
|
||||||
connection(connection),
|
connection(connection),
|
||||||
last_frame(0),
|
last_received_frame(0),
|
||||||
acked_frame(0)
|
acked_frame(0)
|
||||||
{ }
|
{ }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,11 +12,6 @@
|
|||||||
|
|
||||||
namespace tw::net {
|
namespace tw::net {
|
||||||
|
|
||||||
// Listens on a dedicated port for incoming zone-server peer connections and
|
|
||||||
// opens outgoing connections to known peers.
|
|
||||||
//
|
|
||||||
// Incoming and outgoing peers are held together so that broadcast messages
|
|
||||||
// (ZoneHello, ZoneBye) reach every peer uniformly.
|
|
||||||
class ZoneClusterLink {
|
class ZoneClusterLink {
|
||||||
std::unique_ptr<msg::MessageEndpoint> m_endpoint;
|
std::unique_ptr<msg::MessageEndpoint> m_endpoint;
|
||||||
ProtobufMessages m_messages;
|
ProtobufMessages m_messages;
|
||||||
@@ -24,13 +19,10 @@ class ZoneClusterLink {
|
|||||||
public:
|
public:
|
||||||
explicit ZoneClusterLink(int port);
|
explicit ZoneClusterLink(int port);
|
||||||
|
|
||||||
// Poll for messages and accept any pending peer connections.
|
|
||||||
void update();
|
void update();
|
||||||
|
|
||||||
// Connect to a remote zone peer and register it in the peer list.
|
|
||||||
msg::MessageConnection* connect_to_peer(const std::string& host, int port);
|
msg::MessageConnection* connect_to_peer(const std::string& host, int port);
|
||||||
|
|
||||||
// Send a protobuf message to a single peer.
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
void send_mesg(msg::MessageConnection* peer, const T& msg) {
|
void send_mesg(msg::MessageConnection* peer, const T& msg) {
|
||||||
auto send_r = m_messages.send(peer, msg, false);
|
auto send_r = m_messages.send(peer, msg, false);
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ ZoneManager::ZoneManager(im::AreaBounds zone_bounds)
|
|||||||
))
|
))
|
||||||
{}
|
{}
|
||||||
|
|
||||||
// ── ZoneProxy interface ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
im::AreaBounds ZoneManager::area() const {
|
im::AreaBounds ZoneManager::area() const {
|
||||||
return {
|
return {
|
||||||
{ (float)m_spatial_backend->world_min_x(), (float)m_spatial_backend->world_min_z() },
|
{ (float)m_spatial_backend->world_min_x(), (float)m_spatial_backend->world_min_z() },
|
||||||
@@ -33,8 +31,6 @@ void ZoneManager::transfer_entity(EntityInfo&& info) {
|
|||||||
spawn_entity(std::move(info));
|
spawn_entity(std::move(info));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Zone management ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
entt::entity ZoneManager::spawn_entity(EntityInfo&& info) {
|
entt::entity ZoneManager::spawn_entity(EntityInfo&& info) {
|
||||||
entt::entity entity = m_world->registry().create();
|
entt::entity entity = m_world->registry().create();
|
||||||
|
|
||||||
@@ -104,8 +100,6 @@ uint32_t ZoneManager::acked_input_frame(im::InterestId interest_id) const {
|
|||||||
return it != m_session_acked_frame.end() ? it->second : 0;
|
return it != m_session_acked_frame.end() ? it->second : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Private helpers ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
void ZoneManager::check_neighbor_transfers() {
|
void ZoneManager::check_neighbor_transfers() {
|
||||||
ZoneScopedN("ZoneManager::check_neighbor_transfers");
|
ZoneScopedN("ZoneManager::check_neighbor_transfers");
|
||||||
|
|
||||||
@@ -161,9 +155,7 @@ void ZoneManager::check_neighbor_transfers() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tick ──────────────────────────────────────────────────────────────────────
|
void ZoneManager::update(uint32_t frame_idx, float delta_time) {
|
||||||
|
|
||||||
void ZoneManager::tick(uint32_t frame_idx, float delta_time) {
|
|
||||||
ZoneScopedN("ZoneManager::tick");
|
ZoneScopedN("ZoneManager::tick");
|
||||||
|
|
||||||
FrameMarkStart("Interest System");
|
FrameMarkStart("Interest System");
|
||||||
@@ -176,21 +168,7 @@ void ZoneManager::tick(uint32_t frame_idx, float delta_time) {
|
|||||||
m_world->step(delta_time);
|
m_world->step(delta_time);
|
||||||
FrameMarkEnd("World step");
|
FrameMarkEnd("World step");
|
||||||
|
|
||||||
// Re-stamp each client entity's newest input onto the server's frame index.
|
|
||||||
// The input ring is keyed by client frame numbers (independent counter),
|
|
||||||
// but physics lookup uses the server's frame counter. By re-stamping onto
|
|
||||||
// the server frame, input(server_frame_idx) resolves by exact match rather
|
|
||||||
// than fallback, ensuring deterministic and correct input consumption.
|
|
||||||
for (const auto& [interest_id, entity] : m_client_entities) {
|
|
||||||
auto* controller = m_world->registry().try_get<CharacterController>(entity);
|
|
||||||
if (!controller) continue;
|
|
||||||
controller->set_input(frame_idx, controller->input());
|
|
||||||
}
|
|
||||||
|
|
||||||
FrameMarkStart("Physics step");
|
FrameMarkStart("Physics step");
|
||||||
// Fixed step, not the measured interval: the simulation has to advance by the
|
|
||||||
// same amount every frame for a replay of the same inputs to land in the same
|
|
||||||
// place.
|
|
||||||
m_physics_world.step(frame_idx, JoltPhysicsWorld::FIXED_DELTA_TIME);
|
m_physics_world.step(frame_idx, JoltPhysicsWorld::FIXED_DELTA_TIME);
|
||||||
FrameMarkEnd("Physics step");
|
FrameMarkEnd("Physics step");
|
||||||
|
|
||||||
|
|||||||
@@ -18,30 +18,28 @@
|
|||||||
|
|
||||||
namespace tw::net {
|
namespace tw::net {
|
||||||
|
|
||||||
// 500-unit cells, 500-unit view radius → 3×3 neighbourhood, no per-entity distance checks.
|
|
||||||
// World bounds are supplied to the constructor at runtime.
|
|
||||||
using SpatialBackendType = im::FixedGrid<500, 500>;
|
using SpatialBackendType = im::FixedGrid<500, 500>;
|
||||||
|
|
||||||
// How far into this zone a neighbour's interest area is expanded.
|
|
||||||
// Entities within this margin are tracked by the interest system before transfer.
|
|
||||||
static constexpr float kNeighborBorderOverlap = 100.0f;
|
static constexpr float kNeighborBorderOverlap = 100.0f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages one zone: contains state of all entities inside and does logic for them.
|
||||||
|
*/
|
||||||
class ZoneManager : public ZoneProxy {
|
class ZoneManager : public ZoneProxy {
|
||||||
struct NeighborEntry {
|
struct NeighborEntry {
|
||||||
ZoneProxy* proxy;
|
ZoneProxy* proxy;
|
||||||
im::InterestId interest_id;
|
im::InterestId interest_id;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Declaration order == initialisation order.
|
|
||||||
std::unique_ptr<World> m_world;
|
std::unique_ptr<World> m_world;
|
||||||
JoltPhysicsWorld m_physics_world;
|
JoltPhysicsWorld m_physics_world;
|
||||||
std::unique_ptr<SpatialBackendType> m_spatial_backend;
|
std::unique_ptr<SpatialBackendType> m_spatial_backend;
|
||||||
std::unique_ptr<im::InterestSystem<SpatialBackendType>> m_interest_system;
|
std::unique_ptr<im::InterestSystem<SpatialBackendType>> m_interest_system;
|
||||||
|
|
||||||
std::unordered_map<im::InterestId, entt::entity> m_client_entities;
|
std::unordered_map<im::InterestId, entt::entity> m_client_entities;
|
||||||
std::unordered_map<entt::entity, im::InterestId> m_entity_sessions; // reverse map
|
std::unordered_map<entt::entity, im::InterestId> m_entity_sessions;
|
||||||
std::unordered_map<im::InterestId, uint32_t> m_session_input_frame; // most recent input frame received
|
std::unordered_map<im::InterestId, uint32_t> m_session_input_frame;
|
||||||
std::unordered_map<im::InterestId, uint32_t> m_session_acked_frame; // input frame consumed by last tick
|
std::unordered_map<im::InterestId, uint32_t> m_session_acked_frame;
|
||||||
std::vector<NeighborEntry> m_neighbors;
|
std::vector<NeighborEntry> m_neighbors;
|
||||||
uint32_t m_next_neighbor_id = 0x80000000u;
|
uint32_t m_next_neighbor_id = 0x80000000u;
|
||||||
|
|
||||||
@@ -57,39 +55,23 @@ public:
|
|||||||
|
|
||||||
ZoneManager(im::AreaBounds zone_bounds);
|
ZoneManager(im::AreaBounds zone_bounds);
|
||||||
|
|
||||||
// ── ZoneProxy interface ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
// Returns the geographic area this zone owns, derived from the spatial backend bounds.
|
|
||||||
im::AreaBounds area() const override;
|
im::AreaBounds area() const override;
|
||||||
|
|
||||||
// Receive an entity transferred from a neighbouring zone.
|
|
||||||
// Spawns the entity in this zone's world; wires up input routing if session_id is set.
|
|
||||||
// NOTE: ZoneServer must also update its session→zone routing table after this call.
|
|
||||||
void transfer_entity(EntityInfo&& info) override;
|
void transfer_entity(EntityInfo&& info) override;
|
||||||
|
|
||||||
// ── Zone management ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
entt::entity spawn_entity(EntityInfo&& info);
|
entt::entity spawn_entity(EntityInfo&& info);
|
||||||
|
|
||||||
// Registers an interest for the given entity and begins tracking entities near their position.
|
|
||||||
// Uses session_id as the InterestId; the interest system is unaware of this mapping.
|
|
||||||
// Can fetch the interest using `get_interest(interest_id)`.
|
|
||||||
void add_client(im::InterestId interest_id, entt::entity entity);
|
void add_client(im::InterestId interest_id, entt::entity entity);
|
||||||
|
|
||||||
void on_player_move(SessionId session_id, mmo::PlayerMoveMessage&& message);
|
void on_player_move(SessionId session_id, mmo::PlayerMoveMessage&& message);
|
||||||
|
|
||||||
// Registers a neighbouring zone. Its geographic area is added to the interest
|
|
||||||
// manager and checked each tick; entities that cross into it are transferred.
|
|
||||||
void register_neighbor_zone(ZoneProxy* neighbor);
|
void register_neighbor_zone(ZoneProxy* neighbor);
|
||||||
|
|
||||||
// Returns the current interest state for any registered id, or nullptr.
|
|
||||||
const im::Interest* get_interest(im::InterestId id) const;
|
const im::Interest* get_interest(im::InterestId id) const;
|
||||||
|
|
||||||
// Returns the input frame that this session's entity was last simulated with, or 0 if unknown.
|
|
||||||
uint32_t acked_input_frame(im::InterestId interest_id) const;
|
uint32_t acked_input_frame(im::InterestId interest_id) const;
|
||||||
|
|
||||||
// Runs one tick: interest queries, neighbour transfer checks, world step, physics step.
|
void update(uint32_t frame_idx, float delta_time);
|
||||||
void tick(uint32_t frame_idx, float delta_time);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace tw::net
|
} // namespace tw::net
|
||||||
|
|||||||
@@ -61,10 +61,8 @@ void ZoneServer::player_update_handler(SessionId session_id, mmo::PlayerMoveMess
|
|||||||
auto it = m_session_zone.find(session_id);
|
auto it = m_session_zone.find(session_id);
|
||||||
if (it == m_session_zone.end()) return;
|
if (it == m_session_zone.end()) return;
|
||||||
|
|
||||||
// Echoed back in the next snapshot, so the client can tell how long its
|
|
||||||
// input took to come back.
|
|
||||||
if (auto* session = m_player_session_registry->session(session_id)) {
|
if (auto* session = m_player_session_registry->session(session_id)) {
|
||||||
session->last_frame = message.frame_idx();
|
session->last_received_frame = message.frame_idx();
|
||||||
}
|
}
|
||||||
|
|
||||||
it->second->on_player_move(session_id, std::move(message));
|
it->second->on_player_move(session_id, std::move(message));
|
||||||
@@ -134,11 +132,8 @@ void ZoneServer::run() {
|
|||||||
FrameMarkEnd("Update clients");
|
FrameMarkEnd("Update clients");
|
||||||
|
|
||||||
for (auto& zone : m_zones) {
|
for (auto& zone : m_zones) {
|
||||||
zone->tick(frame_idx, lock_step.delta_time());
|
zone->update(frame_idx, lock_step.delta_time());
|
||||||
|
|
||||||
// Copy acked frames from zone to sessions before replication.
|
|
||||||
// This ensures snapshots carry the input frame actually consumed by this tick,
|
|
||||||
// not frames that arrive in the trailing network update.
|
|
||||||
for (const auto& [session_id, session_zone] : m_session_zone) {
|
for (const auto& [session_id, session_zone] : m_session_zone) {
|
||||||
if (session_zone != zone.get()) continue;
|
if (session_zone != zone.get()) continue;
|
||||||
if (auto* session = m_player_session_registry->session(session_id)) {
|
if (auto* session = m_player_session_registry->session(session_id)) {
|
||||||
|
|||||||
@@ -7,28 +7,6 @@
|
|||||||
|
|
||||||
namespace tw::net::im {
|
namespace tw::net::im {
|
||||||
|
|
||||||
/**
|
|
||||||
* Bounded-world spatial grid backend.
|
|
||||||
*
|
|
||||||
* World area bounds are supplied at construction time so the same grid type
|
|
||||||
* can be reused for differently-sized zones without recompiling.
|
|
||||||
* CELL_SIZE and VIEW_RADIUS remain template parameters so kNeighborRadius
|
|
||||||
* can be computed at compile time, keeping the hot-path loop bounds constant.
|
|
||||||
*
|
|
||||||
* Template parameters:
|
|
||||||
* CELL_SIZE: size of each cell (meters). Recommended: CELL_SIZE == VIEW_RADIUS
|
|
||||||
* for maximum efficiency (no distance checks needed).
|
|
||||||
* VIEW_RADIUS: the subscription radius around a player (meters).
|
|
||||||
*
|
|
||||||
* Constructor parameters:
|
|
||||||
* min_x, max_x, min_z, max_z — world area bounds (meters).
|
|
||||||
*
|
|
||||||
* Complexity:
|
|
||||||
* begin_frame(): O(cols × rows) clearing
|
|
||||||
* insert(): O(1)
|
|
||||||
* query_neighbors(): O(kNeighborRadius²) cells × avg entities per cell
|
|
||||||
* query_area(): O(cells overlapping AABB) × avg entities per cell
|
|
||||||
*/
|
|
||||||
template<uint32_t CELL_SIZE, uint32_t VIEW_RADIUS>
|
template<uint32_t CELL_SIZE, uint32_t VIEW_RADIUS>
|
||||||
class FixedGrid {
|
class FixedGrid {
|
||||||
public:
|
public:
|
||||||
@@ -45,9 +23,6 @@ private:
|
|||||||
int32_t m_world_min_z, m_world_max_z;
|
int32_t m_world_min_z, m_world_max_z;
|
||||||
uint32_t m_cols, m_rows;
|
uint32_t m_cols, m_rows;
|
||||||
|
|
||||||
// Flat 2D vector of cell vectors indexed as [cz * m_cols + cx].
|
|
||||||
// Outer vector is allocated once at construction; inner vectors retain
|
|
||||||
// capacity across frames so no heap allocations occur in steady state.
|
|
||||||
std::vector<std::vector<entt::entity>> m_cells;
|
std::vector<std::vector<entt::entity>> m_cells;
|
||||||
|
|
||||||
[[nodiscard]] inline std::pair<uint32_t, uint32_t> world_to_cell(
|
[[nodiscard]] inline std::pair<uint32_t, uint32_t> world_to_cell(
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "Entity.pb.h"
|
||||||
#include "MessageRegistry.hpp"
|
#include "MessageRegistry.hpp"
|
||||||
|
#include "WorldState.pb.h"
|
||||||
#include "network/NetworkReceiver.hpp"
|
#include "network/NetworkReceiver.hpp"
|
||||||
#include "network/PlayerSessionRegistry.hpp"
|
#include "network/PlayerSessionRegistry.hpp"
|
||||||
#include "systems/Interest.hpp"
|
#include "systems/Interest.hpp"
|
||||||
@@ -18,14 +20,6 @@ namespace tw::net {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Decides what to replicate and to whom. Does not mutate world state.
|
* Decides what to replicate and to whom. Does not mutate world state.
|
||||||
*
|
|
||||||
* Per-frame flow:
|
|
||||||
* 1. For each session: reset its BinaryBuffer and write the header,
|
|
||||||
* spawns and despawns from the InterestSystem.
|
|
||||||
* 2. For each session: iterate its interest set and write entity positions
|
|
||||||
* 3. Patch entity_count and send each raw buffer
|
|
||||||
*
|
|
||||||
* Memory: one BinaryBuffer per session, pre-allocated and reused every frame.
|
|
||||||
*/
|
*/
|
||||||
template<im::SpatialBackend Backend>
|
template<im::SpatialBackend Backend>
|
||||||
class StateReplicator {
|
class StateReplicator {
|
||||||
@@ -51,7 +45,10 @@ public:
|
|||||||
/**
|
/**
|
||||||
* Replicates the current world state for one zone to its connected clients.
|
* Replicates the current world state for one zone to its connected clients.
|
||||||
*/
|
*/
|
||||||
void replicate(const entt::registry& registry, const im::InterestSystem<Backend>* interest_manager) {
|
void replicate(
|
||||||
|
const entt::registry& registry,
|
||||||
|
const im::InterestSystem<Backend>* interest_manager
|
||||||
|
) {
|
||||||
ZoneScopedN("Replicator");
|
ZoneScopedN("Replicator");
|
||||||
|
|
||||||
const auto& sessions = m_client_registry->sessions();
|
const auto& sessions = m_client_registry->sessions();
|
||||||
@@ -59,16 +56,12 @@ public:
|
|||||||
|
|
||||||
if (session_count == 0) return;
|
if (session_count == 0) return;
|
||||||
|
|
||||||
// ── Grow buffer pool if needed (only on new connections) ──────────
|
|
||||||
if (m_frames.size() < session_count) {
|
if (m_frames.size() < session_count) {
|
||||||
m_frames.resize(session_count);
|
m_frames.resize(session_count);
|
||||||
for (auto& buf : m_frames)
|
for (auto& buf : m_frames)
|
||||||
buf.reserve(kInitialCapacity);
|
buf.reserve(kInitialCapacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build one WorldStateWriter per client referencing the pre-allocated
|
|
||||||
// buffer. reserve() is called before this loop so no reallocation
|
|
||||||
// occurs and the buffer references inside the writers stay valid.
|
|
||||||
std::vector<tw::serial::WorldStateWriter> writers;
|
std::vector<tw::serial::WorldStateWriter> writers;
|
||||||
writers.reserve(session_count);
|
writers.reserve(session_count);
|
||||||
for (std::size_t i = 0; i < session_count; ++i)
|
for (std::size_t i = 0; i < session_count; ++i)
|
||||||
@@ -76,7 +69,6 @@ public:
|
|||||||
|
|
||||||
std::vector<const im::Interest*> client_states(session_count);
|
std::vector<const im::Interest*> client_states(session_count);
|
||||||
|
|
||||||
// ── 1. Write headers / spawns / despawns ──────────────────────────
|
|
||||||
{
|
{
|
||||||
ZoneScopedN("Preparing messages");
|
ZoneScopedN("Preparing messages");
|
||||||
|
|
||||||
@@ -96,26 +88,37 @@ public:
|
|||||||
|
|
||||||
m_frames[i].reserve(needed);
|
m_frames[i].reserve(needed);
|
||||||
writers[i].reset();
|
writers[i].reset();
|
||||||
writers[i].begin(session->acked_frame, Message<mmo::WorldStateMessage>::value);
|
writers[i].begin(session->last_received_frame, Message<mmo::WorldStateMessage>::value);
|
||||||
writers[i].write_spawns(state->spawn());
|
writers[i].write_spawns(state->spawn());
|
||||||
writers[i].write_despawns(state->despawn());
|
writers[i].write_despawns(state->despawn());
|
||||||
|
|
||||||
|
mmo::EntitySpawnMessage spawn_message = {};
|
||||||
|
|
||||||
|
for(auto& spawn : state->spawn()) {
|
||||||
|
auto spawn_item = spawn_message.add_spawns();
|
||||||
|
spawn_item->set_entity_id((uint32_t)spawn);
|
||||||
|
spawn_item->set_is_player(false);
|
||||||
|
spawn_item->set_name("Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
m_network->send_mesg(sessions[i]->session_id, spawn_message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 2. Write entity positions (hot path, O(entities × sessions)) ──
|
|
||||||
{
|
{
|
||||||
ZoneScopedN("Putting transforms into messages");
|
ZoneScopedN("Putting transforms into messages");
|
||||||
|
|
||||||
auto view = registry.view<Transform>();
|
auto view = registry.view<Transform>();
|
||||||
view.each([&](entt::entity e, const Transform& t) {
|
view.each([&](entt::entity e, const Transform& t) {
|
||||||
for (std::size_t i = 0; i < session_count; ++i) {
|
for (std::size_t i = 0; i < session_count; ++i) {
|
||||||
if (client_states[i] && client_states[i]->is_interested_in_entity(e))
|
if (client_states[i] && client_states[i]->is_interested_in_entity(e)) {
|
||||||
writers[i].write_entity(e, t.position());
|
writers[i].write_entity(e, t.position());
|
||||||
|
// spdlog::info("Entity {} is at {} {} {}", (uint32_t)e, t.position().x, t.position().y, t.position().z);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 3. Finalise and dispatch ───────────────────────────────────────
|
|
||||||
{
|
{
|
||||||
ZoneScopedN("Sending messages");
|
ZoneScopedN("Sending messages");
|
||||||
|
|
||||||
|
|||||||
@@ -12,28 +12,10 @@
|
|||||||
|
|
||||||
namespace tw::net::im {
|
namespace tw::net::im {
|
||||||
|
|
||||||
/**
|
|
||||||
* Spatial interest management: maintains per-subscription spawn/despawn deltas.
|
|
||||||
*
|
|
||||||
* Each subscription is identified by a caller-chosen InterestId (uint32_t).
|
|
||||||
* Two kinds of subscription:
|
|
||||||
* set_entity_interest(id, entity) — tracks entities in the neighbourhood of
|
|
||||||
* a world entity's position (e.g. a player).
|
|
||||||
* set_area_interest(id, bounds) — tracks entities inside an XZ quad
|
|
||||||
* (e.g. a zone border region).
|
|
||||||
*
|
|
||||||
* The caller decides how ids map to sessions, zones, or anything else.
|
|
||||||
* InterestSystem has no knowledge of that mapping.
|
|
||||||
*
|
|
||||||
* Two-phase update per frame:
|
|
||||||
* Phase 1 — Spatial rebuild (O(E)): insert all Transform entities into the backend.
|
|
||||||
* Phase 2 — Per-subscription deltas (O(N × K)): for each subscription query the
|
|
||||||
* backend, sort results, compute spawn/despawn via set_difference.
|
|
||||||
*/
|
|
||||||
template<SpatialBackend Backend>
|
template<SpatialBackend Backend>
|
||||||
class InterestSystem {
|
class InterestSystem {
|
||||||
const World* m_world;
|
const World* m_world;
|
||||||
Backend* m_backend; // non-owning; caller manages lifetime
|
Backend* m_backend;
|
||||||
std::unordered_map<InterestId, Interest> m_interests;
|
std::unordered_map<InterestId, Interest> m_interests;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -42,33 +24,26 @@ public:
|
|||||||
m_backend(backend)
|
m_backend(backend)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
// Registers or replaces a subscription tracking the neighbourhood of an entity.
|
|
||||||
void set_entity_interest(InterestId id, entt::entity entity) {
|
void set_entity_interest(InterestId id, entt::entity entity) {
|
||||||
m_interests.insert_or_assign(id, Interest(entity));
|
m_interests.insert_or_assign(id, Interest(entity));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registers or replaces a subscription tracking entities inside an XZ quad.
|
|
||||||
void set_area_interest(InterestId id, AreaBounds bounds) {
|
void set_area_interest(InterestId id, AreaBounds bounds) {
|
||||||
m_interests.insert_or_assign(id, Interest(bounds));
|
m_interests.insert_or_assign(id, Interest(bounds));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removes a subscription. No-op if id is not registered.
|
|
||||||
void remove_interest(InterestId id) {
|
void remove_interest(InterestId id) {
|
||||||
m_interests.erase(id);
|
m_interests.erase(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the current interest state for the given id, or nullptr.
|
|
||||||
const Interest* get_interest(InterestId id) const {
|
const Interest* get_interest(InterestId id) const {
|
||||||
auto it = m_interests.find(id);
|
auto it = m_interests.find(id);
|
||||||
return it != m_interests.end() ? &it->second : nullptr;
|
return it != m_interests.end() ? &it->second : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates all subscriptions for the current frame.
|
|
||||||
// Must be called once per frame after world positions have been updated.
|
|
||||||
void update() {
|
void update() {
|
||||||
ZoneScopedN("InterestSystem::update");
|
ZoneScopedN("InterestSystem::update");
|
||||||
|
|
||||||
// ── Phase 1: Rebuild spatial backend ─────────────────────────────────
|
|
||||||
{
|
{
|
||||||
ZoneScopedN("InterestSystem::Phase1_GridRebuild");
|
ZoneScopedN("InterestSystem::Phase1_GridRebuild");
|
||||||
m_backend->begin_frame();
|
m_backend->begin_frame();
|
||||||
@@ -79,7 +54,6 @@ public:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Phase 2: Per-subscription deltas ─────────────────────────────────
|
|
||||||
{
|
{
|
||||||
ZoneScopedN("InterestSystem::Phase2_Deltas");
|
ZoneScopedN("InterestSystem::Phase2_Deltas");
|
||||||
|
|
||||||
|
|||||||
@@ -39,16 +39,16 @@ int main() {
|
|||||||
tw::Transform* transformA = zoneA.registry().try_get<tw::Transform>(entityA);
|
tw::Transform* transformA = zoneA.registry().try_get<tw::Transform>(entityA);
|
||||||
transformA->set_position(glm::vec3(970.0f, 0.0f, 0.0f));
|
transformA->set_position(glm::vec3(970.0f, 0.0f, 0.0f));
|
||||||
|
|
||||||
zoneA.tick(0, 0.1f);
|
zoneA.update(0, 0.1f);
|
||||||
zoneB.tick(0, 0.1f);
|
zoneB.update(0, 0.1f);
|
||||||
|
|
||||||
interest = zoneA.get_interest(1);
|
interest = zoneA.get_interest(1);
|
||||||
|
|
||||||
transformA = zoneA.registry().try_get<tw::Transform>(entityA);
|
transformA = zoneA.registry().try_get<tw::Transform>(entityA);
|
||||||
transformA->set_position(glm::vec3(1020.0f, 0.0f, 0.0f));
|
transformA->set_position(glm::vec3(1020.0f, 0.0f, 0.0f));
|
||||||
|
|
||||||
zoneA.tick(1, 0.1f);
|
zoneA.update(1, 0.1f);
|
||||||
zoneB.tick(1, 0.1f);
|
zoneB.update(1, 0.1f);
|
||||||
|
|
||||||
interest = zoneA.get_interest(1);
|
interest = zoneA.get_interest(1);
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public:
|
|||||||
return m_last_interval_ms / (double)IN_SECOND;
|
return m_last_interval_ms / (double)IN_SECOND;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool update() {
|
bool has_ticked() {
|
||||||
auto now = Clock::now();
|
auto now = Clock::now();
|
||||||
|
|
||||||
m_last_interval_ms = duration_cast<Millis>(now - m_last_point).count();
|
m_last_interval_ms = duration_cast<Millis>(now - m_last_point).count();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <vector>
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
#include "metrics/HistoryBuffer.hpp"
|
#include "metrics/HistoryBuffer.hpp"
|
||||||
@@ -20,16 +20,15 @@ private:
|
|||||||
HistoryBuffer<Clock::time_point, glm::vec3> m_history;
|
HistoryBuffer<Clock::time_point, glm::vec3> m_history;
|
||||||
HistoryBuffer<Clock::time_point, glm::vec3> m_position_history;
|
HistoryBuffer<Clock::time_point, glm::vec3> m_position_history;
|
||||||
|
|
||||||
// Frame-indexed input ring, capacity 64
|
|
||||||
struct InputSlot {
|
struct InputSlot {
|
||||||
uint32_t frame;
|
uint32_t frame;
|
||||||
glm::vec3 input;
|
glm::vec3 input;
|
||||||
bool valid;
|
bool valid;
|
||||||
};
|
};
|
||||||
std::array<InputSlot, 64> m_input_ring;
|
// buffers inputs with history frame_idx stamp. Can be then read with get_input(frame_idx)
|
||||||
|
std::vector<InputSlot> m_input_ring;
|
||||||
|
|
||||||
uint32_t m_last_input_frame;
|
uint32_t m_last_input_frame;
|
||||||
glm::vec3 m_last_input;
|
|
||||||
|
|
||||||
uint32_t m_frame_idx;
|
uint32_t m_frame_idx;
|
||||||
|
|
||||||
@@ -42,11 +41,10 @@ public:
|
|||||||
m_speed(speed),
|
m_speed(speed),
|
||||||
m_history(Clock::now(), glm::vec3(), 10 * 20),
|
m_history(Clock::now(), glm::vec3(), 10 * 20),
|
||||||
m_position_history(Clock::now(), glm::vec3(), 10 * 20),
|
m_position_history(Clock::now(), glm::vec3(), 10 * 20),
|
||||||
|
m_input_ring(64),
|
||||||
m_last_input_frame(0),
|
m_last_input_frame(0),
|
||||||
m_last_input(0.0f),
|
|
||||||
m_frame_idx(0)
|
m_frame_idx(0)
|
||||||
{
|
{
|
||||||
// Initialize input ring
|
|
||||||
for(auto& slot : m_input_ring) {
|
for(auto& slot : m_input_ring) {
|
||||||
slot.frame = 0;
|
slot.frame = 0;
|
||||||
slot.input = glm::vec3(0.0f);
|
slot.input = glm::vec3(0.0f);
|
||||||
@@ -61,31 +59,34 @@ public:
|
|||||||
m_input_ring[idx].valid = true;
|
m_input_ring[idx].valid = true;
|
||||||
|
|
||||||
m_last_input_frame = frame_idx;
|
m_last_input_frame = frame_idx;
|
||||||
m_last_input = input;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_frame_idx(uint32_t idx) {
|
void set_frame_idx(uint32_t idx) {
|
||||||
m_frame_idx = idx;
|
m_frame_idx = idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
glm::vec3 input() const {
|
/**
|
||||||
return m_last_input;
|
* Returns the most recently received input, or zero vector if none was ever set.
|
||||||
|
* This is what the simulation consumes: frame stamps come from the client's own
|
||||||
|
* counter, so they cannot be looked up by a server-side frame number.
|
||||||
|
*/
|
||||||
|
glm::vec3 latest_input() const {
|
||||||
|
const auto& slot = m_input_ring[m_last_input_frame % m_input_ring.size()];
|
||||||
|
return slot.valid ? slot.input : glm::vec3(0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the input for the specified frame, or falls back to the most recently set input
|
/**
|
||||||
// if the frame slot has been overwritten or never written
|
* Returns input for specific frame or zero vector if not yet set
|
||||||
|
*/
|
||||||
glm::vec3 input(uint32_t frame_idx) const {
|
glm::vec3 input(uint32_t frame_idx) const {
|
||||||
size_t idx = frame_idx % m_input_ring.size();
|
size_t idx = frame_idx % m_input_ring.size();
|
||||||
const auto& slot = m_input_ring[idx];
|
const auto& slot = m_input_ring[idx];
|
||||||
|
|
||||||
// If slot contains the exact frame we're looking for, return it
|
|
||||||
if(slot.valid && slot.frame == frame_idx) {
|
if(slot.valid && slot.frame == frame_idx) {
|
||||||
return slot.input;
|
return slot.input;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, fall back to the most recent input
|
return glm::vec3(0.0f);
|
||||||
// This handles dropped packets (slot never written) or wraparound (slot overwritten)
|
|
||||||
return m_last_input;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ void JoltPhysicsWorld::update(uint32_t frame_idx, double delta_time) {
|
|||||||
bool mAllowSliding = true;
|
bool mAllowSliding = true;
|
||||||
|
|
||||||
if (player_controls_horizontal_velocity) {
|
if (player_controls_horizontal_velocity) {
|
||||||
glm::vec3 input = controller.input(frame_idx);
|
glm::vec3 input = controller.latest_input();
|
||||||
// Smooth the player input
|
// Smooth the player input
|
||||||
JPH::Vec3 vel = JPH::Vec3(input.x, input.y, input.z) * controller.speed();
|
JPH::Vec3 vel = JPH::Vec3(input.x, input.y, input.z) * controller.speed();
|
||||||
float inertia_factor = 1.0f - std::exp(-delta_time * 10.0f); // 10.0 = responsiveness tuning knob
|
float inertia_factor = 1.0f - std::exp(-delta_time * 10.0f); // 10.0 = responsiveness tuning knob
|
||||||
|
|||||||
Reference in New Issue
Block a user