#5 - fixed interpolation & rollback
This commit is contained in:
@@ -27,8 +27,8 @@ $ conan install . --output-folder=build --build=missing -s build_type=Debug
|
|||||||
Configure and build using the generated preset:
|
Configure and build using the generated preset:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ cmake --preset conan-debug
|
$ cmake -S . -B ./build -DCMAKE_TOOLCHAIN_FILE="build/conan_toolchain.cmake" -DCMAKE_BUILD_TYPE=Debug
|
||||||
$ cmake --build --preset conan-debug
|
$ cmake --build ./build
|
||||||
```
|
```
|
||||||
|
|
||||||
For a release build, repeat both steps with `-s build_type=Release` and the
|
For a release build, repeat both steps with `-s build_type=Release` and the
|
||||||
|
|||||||
+2
-11
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
timescaledb:
|
timescaledb:
|
||||||
image: timescale/timescaledb:latest-pg16
|
image: timescale/timescaledb:latest-pg15
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: mmo
|
POSTGRES_USER: mmo
|
||||||
@@ -9,7 +9,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- timescaledb_data:/var/lib/postgresql/data
|
- timescaledb_data:/var/lib/towards_db/data
|
||||||
|
|
||||||
grafana:
|
grafana:
|
||||||
image: grafana/grafana:latest
|
image: grafana/grafana:latest
|
||||||
@@ -24,15 +24,6 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- timescaledb
|
- timescaledb
|
||||||
|
|
||||||
# The zone server is not containerised yet: it needs libpqxx >= 7.7 for
|
|
||||||
# pqxx::params and jammy carries 6.4.
|
|
||||||
|
|
||||||
# ── e2e harness ────────────────────────────────────────────────────────────
|
|
||||||
# Opt in with `--profile e2e`, so a plain `docker compose up` still brings up
|
|
||||||
# the database and dashboards on their own.
|
|
||||||
#
|
|
||||||
# docker compose --profile e2e up --build \
|
|
||||||
# --abort-on-container-exit --exit-code-from chat-mock-client
|
|
||||||
chat-server:
|
chat-server:
|
||||||
profiles: [e2e]
|
profiles: [e2e]
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
layout(location = 0) out vec4 outColor;
|
layout(location = 0) out vec4 outColor;
|
||||||
|
|
||||||
|
layout(location = 0) in vec3 inNormal;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
float diffuse = max(0.1, dot(vec3(0.8, 1.5, -1.0), inNormal));
|
||||||
outColor = vec4(1.0, 0.0, 0.0, 1.0);
|
outColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
layout(location = 0) in vec3 pos;
|
layout(location = 0) in vec3 pos;
|
||||||
layout(location = 1) in vec3 norm;
|
layout(location = 1) in vec3 norm;
|
||||||
|
layout(location = 0) out vec3 outNormal;
|
||||||
|
|
||||||
layout(set = 0, binding = 0) uniform Camera {
|
layout(set = 0, binding = 0) uniform Camera {
|
||||||
mat4 proj;
|
mat4 proj;
|
||||||
@@ -21,5 +22,6 @@ void main() {
|
|||||||
// outPos = pos.xyz * 0.05;
|
// outPos = pos.xyz * 0.05;
|
||||||
// outNormal = norm.xyz;
|
// outNormal = norm.xyz;
|
||||||
// outUV = uv;
|
// outUV = uv;
|
||||||
|
outNormal = norm;
|
||||||
gl_Position = cam.proj * cam.view * PushConstants.transform * vec4(pos, 1.0);
|
gl_Position = cam.proj * cam.view * PushConstants.transform * vec4(pos, 1.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,10 +101,54 @@ class tw::dbg::ComponentGui<tw::net::EntityPositionInterpolation> {
|
|||||||
private:
|
private:
|
||||||
using Clock = std::chrono::high_resolution_clock;
|
using Clock = std::chrono::high_resolution_clock;
|
||||||
|
|
||||||
|
/** How far back the plot reaches. */
|
||||||
|
static constexpr float WINDOW_IN_SECONDS = 10.0f;
|
||||||
|
|
||||||
|
/** Ten seconds of frames at sixty a second. */
|
||||||
|
static constexpr size_t CAPACITY = 600;
|
||||||
|
|
||||||
|
inline static const net::EntityPositionInterpolation* m_subject = nullptr;
|
||||||
|
|
||||||
|
inline static int m_last_frame = -1;
|
||||||
|
inline static size_t m_head = 0;
|
||||||
|
inline static size_t m_count = 0;
|
||||||
|
|
||||||
|
inline static std::vector<Clock::time_point> m_read_times;
|
||||||
|
inline static std::vector<glm::vec3> m_read_values;
|
||||||
|
|
||||||
static int64_t millis_since(Clock::time_point point) {
|
static int64_t millis_since(Clock::time_point point) {
|
||||||
return std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - point).count();
|
return std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - point).count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void restart(const net::EntityPositionInterpolation* instance) {
|
||||||
|
m_subject = instance;
|
||||||
|
m_last_frame = -1;
|
||||||
|
m_head = 0;
|
||||||
|
m_count = 0;
|
||||||
|
|
||||||
|
m_read_times.resize(CAPACITY);
|
||||||
|
m_read_values.resize(CAPACITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps one reading per frame, so drawing twice does not double up. The
|
||||||
|
* whole value is kept rather than the axis on show, so switching axis
|
||||||
|
* still shows the trace that was already gathered.
|
||||||
|
*/
|
||||||
|
void sample(Clock::time_point time, glm::vec3 value) {
|
||||||
|
const int frame = ImGui::GetFrameCount();
|
||||||
|
if(m_last_frame == frame) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_last_frame = frame;
|
||||||
|
|
||||||
|
m_read_times[m_head] = time;
|
||||||
|
m_read_values[m_head] = value;
|
||||||
|
|
||||||
|
m_head = (m_head + 1) % CAPACITY;
|
||||||
|
m_count = std::min(m_count + 1, CAPACITY);
|
||||||
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void draw(net::EntityPositionInterpolation* instance) {
|
void draw(net::EntityPositionInterpolation* instance) {
|
||||||
ImGui::SeparatorText("Received Positions");
|
ImGui::SeparatorText("Received Positions");
|
||||||
@@ -122,5 +166,73 @@ public:
|
|||||||
instance->values()[0].y,
|
instance->values()[0].y,
|
||||||
instance->values()[0].z,
|
instance->values()[0].z,
|
||||||
millis_since(instance->times()[0]));
|
millis_since(instance->times()[0]));
|
||||||
|
|
||||||
|
static int axis = 0;
|
||||||
|
ImGui::Combo("Axis##received", &axis, "X\0Y\0Z\0");
|
||||||
|
|
||||||
|
/** How far behind the present the reader looks; matches the caller. */
|
||||||
|
static int delay_in_millis = 1000;
|
||||||
|
ImGui::SliderInt("Delay (ms)", &delay_in_millis, 0, 2000);
|
||||||
|
|
||||||
|
if(m_subject != instance) {
|
||||||
|
restart(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The buffer wraps, so the index says nothing about when a sample
|
||||||
|
// arrived. Placing each one at its own age puts it where it belongs
|
||||||
|
// without having to know where the ring currently starts. Slots
|
||||||
|
// nothing was pushed into are left out.
|
||||||
|
const auto now = Clock::now();
|
||||||
|
const size_t count = instance->size();
|
||||||
|
|
||||||
|
std::vector<float> ages(count);
|
||||||
|
std::vector<float> positions(count);
|
||||||
|
|
||||||
|
for(size_t i = 0; i < count; i++) {
|
||||||
|
ages[i] = -std::chrono::duration<float>(now - instance->times()[i]).count();
|
||||||
|
positions[i] = instance->values()[i][axis];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ImPlot::BeginPlot("Buffer", ImVec2(-1.0f, 180.0f))) {
|
||||||
|
ImPlot::SetupAxes("seconds ago", "position", ImPlotAxisFlags_None, ImPlotAxisFlags_AutoFit);
|
||||||
|
ImPlot::SetupAxisLimits(ImAxis_X1,
|
||||||
|
-WINDOW_IN_SECONDS, 0.0f, ImGuiCond_Always);
|
||||||
|
|
||||||
|
ImPlot::PlotScatter("Received", ages.data(), positions.data(), (int)count);
|
||||||
|
|
||||||
|
// Reading the same way the position is read for drawing shows
|
||||||
|
// which two samples the delay lands between, and how far the
|
||||||
|
// result sits from either of them.
|
||||||
|
const auto point = now - std::chrono::milliseconds(delay_in_millis);
|
||||||
|
const auto [from, to, alpha] = instance->get_values_around(point);
|
||||||
|
|
||||||
|
const float read_at = -std::chrono::duration<float>(now - point).count();
|
||||||
|
const float read_out = glm::mix(from, to, alpha)[axis];
|
||||||
|
|
||||||
|
ImPlot::SetNextMarkerStyle(ImPlotMarker_Circle, 5.0f);
|
||||||
|
ImPlot::PlotScatter("Interpolated", &read_at, &read_out, 1);
|
||||||
|
|
||||||
|
// Each reading is kept at the moment it was read for, so the trace
|
||||||
|
// ends on the marker above and runs back along the same timeline
|
||||||
|
// the received samples sit on.
|
||||||
|
sample(point, glm::mix(from, to, alpha));
|
||||||
|
|
||||||
|
std::vector<float> trace_ages(m_count);
|
||||||
|
std::vector<float> trace_positions(m_count);
|
||||||
|
|
||||||
|
const size_t oldest = m_count == CAPACITY ? m_head : 0;
|
||||||
|
|
||||||
|
for(size_t i = 0; i < m_count; i++) {
|
||||||
|
const size_t index = (oldest + i) % CAPACITY;
|
||||||
|
|
||||||
|
trace_ages[i] = -std::chrono::duration<float>(now - m_read_times[index]).count();
|
||||||
|
trace_positions[i] = m_read_values[index][axis];
|
||||||
|
}
|
||||||
|
|
||||||
|
ImPlot::PlotLine("Interpolated Trace",
|
||||||
|
trace_ages.data(), trace_positions.data(), (int)m_count);
|
||||||
|
|
||||||
|
ImPlot::EndPlot();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ void EntityPositionInterpolator::set_position(Clock::time_point time_point, entt
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
interpolation->push(Clock::now(), position);
|
interpolation->push(time_point, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
glm::vec3 EntityPositionInterpolator::get_position(Clock::time_point time_point, entt::entity entity) {
|
glm::vec3 EntityPositionInterpolator::get_position(Clock::time_point time_point, entt::entity entity) {
|
||||||
@@ -35,9 +35,7 @@ glm::vec3 EntityPositionInterpolator::get_position(Clock::time_point time_point,
|
|||||||
return glm::vec3();
|
return glm::vec3();
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
return glm::mix(from, to, value);
|
return glm::mix(from, to, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +43,10 @@ void EntityPositionInterpolator::interpolate_smoothed_entities(Clock::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 [from, to, value] = interpolation.get_values_around(time_point);
|
auto [from, to, value] = interpolation.get_values_around(time_point);
|
||||||
|
spdlog::info("From: {} {} {} - To: {} {} {} - By: {}",
|
||||||
|
from.x, from.y, from.z,
|
||||||
|
to.x, to.y, to.z,
|
||||||
|
value);
|
||||||
ts.set_position(glm::mix(from, to, value));
|
ts.set_position(glm::mix(from, to, value));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,21 @@
|
|||||||
|
|
||||||
#include "common.hpp"
|
#include "common.hpp"
|
||||||
|
|
||||||
template<typename T, int length = 3>
|
template<typename T, int length = 10>
|
||||||
class InterpolatedProperty {
|
class InterpolatedProperty {
|
||||||
using Clock = std::chrono::high_resolution_clock;
|
using Clock = std::chrono::high_resolution_clock;
|
||||||
|
|
||||||
std::array<T, length> m_buffer;
|
std::vector<T> m_buffer;
|
||||||
std::array<Clock::time_point, length> m_time_buffer;
|
std::vector<Clock::time_point> m_time_buffer;
|
||||||
|
|
||||||
|
uint32_t m_head;
|
||||||
|
uint32_t m_size;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
InterpolatedProperty(T initial_value) {
|
InterpolatedProperty(T initial_value) :
|
||||||
|
m_buffer(length),
|
||||||
|
m_time_buffer(length),
|
||||||
|
m_size(0), m_head(0) {
|
||||||
for(int i = 0; i < length; i++) {
|
for(int i = 0; i < length; i++) {
|
||||||
m_buffer[i] = initial_value;
|
m_buffer[i] = initial_value;
|
||||||
m_time_buffer[i] = Clock::now();
|
m_time_buffer[i] = Clock::now();
|
||||||
@@ -23,7 +29,21 @@ public:
|
|||||||
GET_REF(m_buffer, values);
|
GET_REF(m_buffer, values);
|
||||||
GET_REF(m_time_buffer, times);
|
GET_REF(m_time_buffer, times);
|
||||||
|
|
||||||
|
/** How many slots hold something that was pushed. */
|
||||||
|
inline uint32_t size() const {
|
||||||
|
return m_size;
|
||||||
|
}
|
||||||
|
|
||||||
void push(Clock::time_point point, T value) {
|
void push(Clock::time_point point, T value) {
|
||||||
|
// m_buffer[m_head] = std::move(value);
|
||||||
|
// m_time_buffer[m_head] = point;
|
||||||
|
//
|
||||||
|
// m_head = (m_head + 1) % m_buffer.size();
|
||||||
|
//
|
||||||
|
// if (m_size < m_buffer.size())
|
||||||
|
// {
|
||||||
|
// ++m_size;
|
||||||
|
// }
|
||||||
if(point < m_time_buffer.at(0)) {
|
if(point < m_time_buffer.at(0)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -64,6 +84,46 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline std::tuple<T, T, float> get_values_around(Clock::time_point point) const {
|
inline std::tuple<T, T, float> get_values_around(Clock::time_point point) const {
|
||||||
|
/* const std::size_t capacity = m_buffer.size();
|
||||||
|
const std::size_t oldest = (m_head + capacity - m_size) % capacity;
|
||||||
|
|
||||||
|
if (m_size == 0)
|
||||||
|
{
|
||||||
|
return { T(), T(), 0.0f };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asking for a time the buffer no longer reaches back to. Standing on
|
||||||
|
// the oldest is wrong by however much was missed, but it is wrong in
|
||||||
|
// the direction the values were heading, unlike the newest.
|
||||||
|
if (point < m_time_buffer[oldest])
|
||||||
|
{
|
||||||
|
return { m_buffer[oldest], m_buffer[oldest], 0.0f };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i + 1 < m_size; ++i)
|
||||||
|
{
|
||||||
|
const std::size_t idx0 = (oldest + i) % capacity;
|
||||||
|
const std::size_t idx1 = (oldest + i + 1) % capacity;
|
||||||
|
|
||||||
|
const auto t0 = m_time_buffer[idx0];
|
||||||
|
const auto t1 = m_time_buffer[idx1];
|
||||||
|
|
||||||
|
if (point >= t0 && point <= t1)
|
||||||
|
{
|
||||||
|
const auto total =
|
||||||
|
std::chrono::duration<float>(t1 - t0).count();
|
||||||
|
const auto elapsed =
|
||||||
|
std::chrono::duration<float>(point - t0).count();
|
||||||
|
|
||||||
|
const float alpha = total > 0.0f ? elapsed / total : 0.0f;
|
||||||
|
|
||||||
|
return { m_buffer[idx0], m_buffer[idx1], alpha };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle exact last sample (or clamp)
|
||||||
|
const std::size_t last = (oldest + m_size - 1) % capacity;
|
||||||
|
return { m_buffer[last], m_buffer[last], 0.0f }; */
|
||||||
T prev = m_buffer.at(0);
|
T prev = m_buffer.at(0);
|
||||||
Clock::time_point prev_point = m_time_buffer.at(0);
|
Clock::time_point prev_point = m_time_buffer.at(0);
|
||||||
|
|
||||||
|
|||||||
@@ -151,6 +151,10 @@ void ReplicatorClient::handle_snapshot_entity(
|
|||||||
// return;
|
// return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Applies correction from record_frame_idx to current_frame_idx (if any) on entity. Compares previous position on
|
||||||
|
// frame record_frame_idx with p
|
||||||
|
m_rollback.apply_correction(record_frame_idx, p, entity.value(), &m_world->registry(), current_frame_idx);
|
||||||
|
|
||||||
//snap_player_to(current_frame_idx, entity.value(), p);
|
//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);
|
// bool reconcile_happened = m_reconciler.reconcile(record_frame_idx, p, entity.value(), &m_world->registry(), current_frame_idx);
|
||||||
@@ -168,15 +172,29 @@ void ReplicatorClient::handle_snapshot_entity(
|
|||||||
// }
|
// }
|
||||||
// m_network_metrics->record_ack_lag(ack_lag);
|
// m_network_metrics->record_ack_lag(ack_lag);
|
||||||
// }
|
// }
|
||||||
}
|
} else {
|
||||||
|
m_entity_interpolator->set_position(std::chrono::high_resolution_clock::now(), entity.value(), p);
|
||||||
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) {
|
void ReplicatorClient::handle_snapshot(uint32_t current_frame_idx, serial::WorldStateReader& reader) {
|
||||||
auto header = reader.read_header();
|
auto header = reader.read_header();
|
||||||
measure_response_time(current_frame_idx, header.frame_idx);
|
measure_response_time(current_frame_idx, header.frame_idx);
|
||||||
|
|
||||||
|
// Snapshots are sent unreliably, so one can overtake another. Taking a
|
||||||
|
// late one would put entities back where they have already been seen.
|
||||||
|
if(header.tick_idx <= m_latest_tick_idx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_latest_tick_idx = header.tick_idx;
|
||||||
|
|
||||||
|
// Arrival is the only time the client can be sure of, and it grows with
|
||||||
|
// every snapshot, which is what reading the buffer back relies on. What it
|
||||||
|
// costs is spacing: the gaps between samples carry the jitter of the way
|
||||||
|
// here rather than the even gaps the server sent them on.
|
||||||
|
const auto taken_at = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
while(reader.has_entity()) {
|
while(reader.has_entity()) {
|
||||||
auto entity_r = reader.read_entity();
|
auto entity_r = reader.read_entity();
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ private:
|
|||||||
net::PlayerRollback m_rollback;
|
net::PlayerRollback m_rollback;
|
||||||
World *m_world;
|
World *m_world;
|
||||||
net::ServerConnection* m_server_connection;
|
net::ServerConnection* m_server_connection;
|
||||||
|
uint32_t m_latest_tick_idx = 0;
|
||||||
|
|
||||||
std::optional<entt::entity> m_player_entity;
|
std::optional<entt::entity> m_player_entity;
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,12 @@ void ThirdPersonPlayerController::update(const tw::io::InputManager* input, doub
|
|||||||
m_camera_rotation = yaw * m_camera_rotation * pitch;
|
m_camera_rotation = yaw * m_camera_rotation * pitch;
|
||||||
|
|
||||||
glm::vec3 offset = m_camera_rotation * glm::vec3(0, 0, m_camera_zoom);
|
glm::vec3 offset = m_camera_rotation * glm::vec3(0, 0, m_camera_zoom);
|
||||||
m_camera->view().look_at(get_target_position() + offset, get_target_position());
|
|
||||||
|
glm::vec3 from = m_camera->view().position();
|
||||||
|
glm::vec3 to = get_target_position() + offset;
|
||||||
|
glm::vec3 interpolated = glm::mix(from, to, 0.8f);
|
||||||
|
|
||||||
|
m_camera->view().look_at(interpolated, get_target_position());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,6 @@
|
|||||||
|
|
||||||
namespace tw {
|
namespace tw {
|
||||||
|
|
||||||
typedef HistoryBuffer<long, glm::vec3> EntityPositionHistory;
|
|
||||||
|
|
||||||
entt::entity
|
entt::entity
|
||||||
ClientWorldController::create_entity(const std::string& name, glm::vec3 position) {
|
ClientWorldController::create_entity(const std::string& name, glm::vec3 position) {
|
||||||
const auto entity = m_world->registry().create();
|
const auto entity = m_world->registry().create();
|
||||||
@@ -82,8 +80,8 @@ void ClientWorldController::spawn_entity(const std::string& name, uint32_t serve
|
|||||||
if(m_controlled_server_id.has_value() && m_controlled_server_id.value() == server_id) {
|
if(m_controlled_server_id.has_value() && m_controlled_server_id.value() == server_id) {
|
||||||
try_bind_player_entity();
|
try_bind_player_entity();
|
||||||
} else {
|
} else {
|
||||||
|
m_interpolator.register_entity(entity);
|
||||||
}
|
}
|
||||||
m_interpolator.register_entity(entity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientWorldController::ClientWorldController(
|
ClientWorldController::ClientWorldController(
|
||||||
@@ -165,9 +163,9 @@ void ClientWorldController::try_bind_player_entity() {
|
|||||||
position
|
position
|
||||||
));
|
));
|
||||||
|
|
||||||
// if(m_world->registry().all_of<net::EntityPositionInterpolation>(entity)) {
|
if(m_world->registry().all_of<net::EntityPositionInterpolation>(entity)) {
|
||||||
// m_world->registry().remove<net::EntityPositionInterpolation>(entity);
|
m_world->registry().remove<net::EntityPositionInterpolation>(entity);
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -187,7 +185,6 @@ void ClientWorldController::update_network() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
m_replicator_client.record_prediction(m_frame_idx);
|
|
||||||
|
|
||||||
|
|
||||||
// m_world->registry().view<Transform>()
|
// m_world->registry().view<Transform>()
|
||||||
@@ -216,10 +213,11 @@ void ClientWorldController::update(double delta_time) {
|
|||||||
if(controller) {
|
if(controller) {
|
||||||
// controller->set_input(m_network_frame_idx, input);
|
// controller->set_input(m_network_frame_idx, input);
|
||||||
m_replicator_client.set_input(m_network_frame_idx, input);
|
m_replicator_client.set_input(m_network_frame_idx, input);
|
||||||
|
m_replicator_client.record_prediction(m_network_frame_idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// m_physics_world->step(m_network_frame_idx, JoltPhysicsWorld::FIXED_DELTA_TIME, true);
|
m_physics_world->step(m_network_frame_idx, JoltPhysicsWorld::FIXED_DELTA_TIME, true);
|
||||||
m_network_frame_idx++;
|
m_network_frame_idx++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +228,7 @@ void ClientWorldController::update(double delta_time) {
|
|||||||
// TODO: The third person controller could pull
|
// TODO: The third person controller could pull
|
||||||
Transform* player_transform = m_world->registry().try_get<Transform>(m_player_entity.value());
|
Transform* player_transform = m_world->registry().try_get<Transform>(m_player_entity.value());
|
||||||
if(player_transform) {
|
if(player_transform) {
|
||||||
m_player_controller.set_target(player_transform->position());
|
// m_player_controller.set_target(player_transform->position());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class WorldStateWriter {
|
|||||||
public:
|
public:
|
||||||
explicit WorldStateWriter(BinaryBuffer& buf) noexcept : m_w(buf) {}
|
explicit WorldStateWriter(BinaryBuffer& buf) noexcept : m_w(buf) {}
|
||||||
|
|
||||||
void begin(uint32_t frame_idx, uint32_t message_type) noexcept {
|
void begin(uint32_t frame_idx, uint32_t tick_idx, uint32_t message_type) noexcept {
|
||||||
m_entity_count = 0;
|
m_entity_count = 0;
|
||||||
|
|
||||||
// message_type — lets the receiver dispatch without peeking further
|
// message_type — lets the receiver dispatch without peeking further
|
||||||
@@ -35,6 +35,10 @@ public:
|
|||||||
// frame_idx
|
// frame_idx
|
||||||
m_w.encode<uint32_t>(frame_idx);
|
m_w.encode<uint32_t>(frame_idx);
|
||||||
|
|
||||||
|
// tick_idx — counts the states that went out, so the receiver can tell
|
||||||
|
// a late one from a new one. Says nothing about the frame answered.
|
||||||
|
m_w.encode<uint32_t>(tick_idx);
|
||||||
|
|
||||||
// entity_count placeholder — patched when end() is called
|
// entity_count placeholder — patched when end() is called
|
||||||
m_entity_count_offset = m_w.reserve_u32();
|
m_entity_count_offset = m_w.reserve_u32();
|
||||||
}
|
}
|
||||||
@@ -85,6 +89,7 @@ public:
|
|||||||
struct WorldStateHeader {
|
struct WorldStateHeader {
|
||||||
uint32_t packet_type;
|
uint32_t packet_type;
|
||||||
uint32_t frame_idx;
|
uint32_t frame_idx;
|
||||||
|
uint32_t tick_idx;
|
||||||
uint32_t entity_count;
|
uint32_t entity_count;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,10 +114,11 @@ public:
|
|||||||
explicit WorldStateReader(std::span<const std::byte> data) noexcept
|
explicit WorldStateReader(std::span<const std::byte> data) noexcept
|
||||||
: m_r(data) {}
|
: m_r(data) {}
|
||||||
|
|
||||||
/** Read the 12-byte header. Must be called first. */
|
/** Read the header. Must be called first. */
|
||||||
WorldStateHeader read_header() noexcept {
|
WorldStateHeader read_header() noexcept {
|
||||||
// m_header.packet_type = m_r.decode<uint32_t>();
|
// m_header.packet_type = m_r.decode<uint32_t>();
|
||||||
m_header.frame_idx = m_r.decode<uint32_t>();
|
m_header.frame_idx = m_r.decode<uint32_t>();
|
||||||
|
m_header.tick_idx = m_r.decode<uint32_t>();
|
||||||
m_header.entity_count = m_r.decode<uint32_t>();
|
m_header.entity_count = m_r.decode<uint32_t>();
|
||||||
|
|
||||||
// spawn count follows immediately
|
// spawn count follows immediately
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ void ZoneServer::player_update_handler(SessionId session_id, mmo::PlayerMoveMess
|
|||||||
if (it == m_session_zone.end()) return;
|
if (it == m_session_zone.end()) return;
|
||||||
|
|
||||||
if (auto* session = m_player_session_registry->session(session_id)) {
|
if (auto* session = m_player_session_registry->session(session_id)) {
|
||||||
|
if(session->last_received_frame >= message.frame_idx()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
session->last_received_frame = message.frame_idx();
|
session->last_received_frame = message.frame_idx();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +144,7 @@ void ZoneServer::run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_replicator->replicate(zone->registry(), zone->interest());
|
m_replicator->replicate(frame_idx, zone->registry(), zone->interest());
|
||||||
}
|
}
|
||||||
|
|
||||||
m_network_receiver->update();
|
m_network_receiver->update();
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ class StateReplicator {
|
|||||||
// Per-client backing buffers reused every frame.
|
// Per-client backing buffers reused every frame.
|
||||||
std::vector<tw::serial::BinaryBuffer> m_frames;
|
std::vector<tw::serial::BinaryBuffer> m_frames;
|
||||||
|
|
||||||
// Header(16) + spawn_hdr(4) + despawn_hdr(4) + 512 entities × 16 bytes
|
// Header(20) + spawn_hdr(4) + despawn_hdr(4) + 512 entities × 16 bytes
|
||||||
static constexpr std::size_t kHeaderCapacity = 24;
|
static constexpr std::size_t kHeaderCapacity = 28;
|
||||||
static constexpr std::size_t kInitialCapacity = kHeaderCapacity + 512 * 16;
|
static constexpr std::size_t kInitialCapacity = kHeaderCapacity + 512 * 16;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -46,6 +46,7 @@ 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(
|
void replicate(
|
||||||
|
uint32_t frame_idx,
|
||||||
const entt::registry& registry,
|
const entt::registry& registry,
|
||||||
const im::InterestSystem<Backend>* interest_manager
|
const im::InterestSystem<Backend>* interest_manager
|
||||||
) {
|
) {
|
||||||
@@ -88,7 +89,8 @@ public:
|
|||||||
|
|
||||||
m_frames[i].reserve(needed);
|
m_frames[i].reserve(needed);
|
||||||
writers[i].reset();
|
writers[i].reset();
|
||||||
writers[i].begin(session->last_received_frame, Message<mmo::WorldStateMessage>::value);
|
writers[i].begin(session->last_received_frame, frame_idx,
|
||||||
|
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());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user