From d3d277a2317e1859dba998ca735d66550cbd825a Mon Sep 17 00:00:00 2001 From: zhenyan121 <3367366583@qq.com> Date: Fri, 7 Aug 2026 09:13:40 +0800 Subject: [PATCH] feat(client-entity): use snapshot history for entity interpolation Replace exponential smoothing with a buffered snapshot system. Each entity keeps a deque of position/direction snapshots with timestamps; the render transform is interpolated at a fixed 100 ms render delay to hide network jitter. Snapshots are capped to 16 entries. --- include/Cubed/gameplay/ecs/client_entity.hpp | 12 +++ include/Cubed/tools/time_tools.hpp | 1 + src/gameplay/client_entity_manager.cpp | 91 +++++++++++++++++--- 3 files changed, 92 insertions(+), 12 deletions(-) diff --git a/include/Cubed/gameplay/ecs/client_entity.hpp b/include/Cubed/gameplay/ecs/client_entity.hpp index 7f0d98a..98df8af 100644 --- a/include/Cubed/gameplay/ecs/client_entity.hpp +++ b/include/Cubed/gameplay/ecs/client_entity.hpp @@ -2,6 +2,8 @@ #include "Cubed/gameplay/ecs/animation.hpp" #include "Cubed/gameplay/ecs/transform.hpp" #include "Cubed/gameplay/model.hpp" + +#include namespace Cubed { struct BaseClientCreature { @@ -16,4 +18,14 @@ struct SoundTime { float next_call_time; }; +struct ClientEntitySnapshot { + double time_ms = 0.0; + glm::vec3 pos{0.0f}; + glm::vec3 dir{0.0f}; +}; + +struct ClientEntityState { + std::deque history; +}; + } // namespace Cubed \ No newline at end of file diff --git a/include/Cubed/tools/time_tools.hpp b/include/Cubed/tools/time_tools.hpp index 7d4add8..3c22df6 100644 --- a/include/Cubed/tools/time_tools.hpp +++ b/include/Cubed/tools/time_tools.hpp @@ -4,6 +4,7 @@ #include namespace Cubed { namespace Tools { +// return ms inline uint64_t get_time_ticks() { return SDL_GetTicks(); } } // namespace Tools diff --git a/src/gameplay/client_entity_manager.cpp b/src/gameplay/client_entity_manager.cpp index 7a0a72a..1af7e37 100644 --- a/src/gameplay/client_entity_manager.cpp +++ b/src/gameplay/client_entity_manager.cpp @@ -9,12 +9,74 @@ #include "Cubed/tools/cubed_assert.hpp" #include "Cubed/tools/math_tools.hpp" #include "Cubed/tools/net_utils.hpp" +#include "Cubed/tools/time_tools.hpp" using namespace google::protobuf; namespace Cubed { + +namespace { +constexpr double ENTITY_RENDER_DELAY_MS = 100.0; // two tick time +constexpr size_t ENTITY_SNAPSHOT_MAX = 16; + +ClientEntitySnapshot +interpolate_snapshot(const std::deque& history, + double render_time) { + + if (history.empty()) { + return {}; + } + + if (history.size() == 1) { + return history.front(); + } + + auto upper = std::lower_bound( + history.begin(), history.end(), render_time, + [](const ClientEntitySnapshot& s, double t) { return s.time_ms < t; }); + + if (upper == history.end()) { + return history.back(); + } + + if (upper == history.begin()) { + return *upper; + } + + auto lower = upper - 1; + + double span = upper->time_ms - lower->time_ms; + + float t = span > 0.0 ? float((render_time - lower->time_ms) / span) : 1.0f; + + t = std::clamp(t, 0.0f, 1.0f); + + ClientEntitySnapshot out; + + out.pos = glm::mix(lower->pos, upper->pos, t); + + glm::vec3 mixed = glm::mix(lower->dir, upper->dir, t); + + out.dir = + glm::length(mixed) > 1e-6f ? glm::normalize(mixed) : glm::vec3{0.0f}; + return out; +} +} // namespace + ClientEntityManager::ClientEntityManager(ClientWorld& world) : m_world(world) {} void ClientEntityManager::update(float dt) { handle_task(dt); + { + auto view = m_registry.view(); + double render_time = static_cast(Tools::get_time_ticks()) - + ENTITY_RENDER_DELAY_MS; + for (auto e : view) { + auto& state = view.get(e); + auto& r = view.get(e); + auto snap = interpolate_snapshot(state.history, render_time); + r.position.value = snap.pos; + r.direction.value = snap.dir; + } + } auto view = m_registry.view(); for (auto e : view) { @@ -35,10 +97,10 @@ void ClientEntityManager::init() { BaseClientCreature c; c.model = ModelManager::instance().get_model_id("cubed:pig"); float next_call_time = m_random.random_float(8.0f, 25.0f); - create_entity_in_registry(id, Entity{id, EntityType::CREATURE}, - EntityInfo{"cubed:pig", ""}, std::move(c), - PigTag{}, RenderTransform{}, - SoundTime{next_call_time}); + create_entity_in_registry( + id, Entity{id, EntityType::CREATURE}, EntityInfo{"cubed:pig", ""}, + std::move(c), PigTag{}, RenderTransform{}, + SoundTime{next_call_time}, ClientEntityState{}); }); } // not thread safe @@ -55,9 +117,13 @@ void ClientEntityManager::handle_entity_create(EntityID id, auto* r = m_registry.try_get(a->second); ASSERT(r); r->position.value = pos; + auto* s = m_registry.try_get(a->second); + ASSERT(s); + s->history.emplace_back(static_cast(Tools::get_time_ticks()), pos, + glm::vec3{0, 0, 0}); } -void ClientEntityManager::handle_entity_update(UpdateInfo& info, float dt) { +void ClientEntityManager::handle_entity_update(UpdateInfo& info, float) { entt::entity e; { cacc a; @@ -72,13 +138,14 @@ void ClientEntityManager::handle_entity_update(UpdateInfo& info, float dt) { creature->transform.position.value = info.pos; creature->transform.direction.value = info.direction; creature->pose.gait = info.gait; - auto r = m_registry.try_get(e); - ASSERT(r); - float alpha = 1 - std::exp(-dt / 0.1); - r->direction.value = glm::mix(r->direction.value, - creature->transform.direction.value, alpha); - r->position.value = - glm::mix(r->position.value, creature->transform.position.value, alpha); + + auto state = m_registry.try_get(e); + ASSERT(state); + state->history.push_back({static_cast(Tools::get_time_ticks()), + info.pos, info.direction}); + while (state->history.size() > ENTITY_SNAPSHOT_MAX) { + state->history.pop_front(); + } } void ClientEntityManager::receive_entity_create(S2CEntityCreate& s2c) {