mirror of
https://github.com/zhenyan121/Cubed.git
synced 2026-08-09 02:07:04 +08:00
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.
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
#include "Cubed/gameplay/ecs/animation.hpp"
|
#include "Cubed/gameplay/ecs/animation.hpp"
|
||||||
#include "Cubed/gameplay/ecs/transform.hpp"
|
#include "Cubed/gameplay/ecs/transform.hpp"
|
||||||
#include "Cubed/gameplay/model.hpp"
|
#include "Cubed/gameplay/model.hpp"
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
namespace Cubed {
|
namespace Cubed {
|
||||||
struct BaseClientCreature {
|
struct BaseClientCreature {
|
||||||
|
|
||||||
@@ -16,4 +18,14 @@ struct SoundTime {
|
|||||||
float next_call_time;
|
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<ClientEntitySnapshot> history;
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace Cubed
|
} // namespace Cubed
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
namespace Cubed {
|
namespace Cubed {
|
||||||
namespace Tools {
|
namespace Tools {
|
||||||
|
// return ms
|
||||||
inline uint64_t get_time_ticks() { return SDL_GetTicks(); }
|
inline uint64_t get_time_ticks() { return SDL_GetTicks(); }
|
||||||
} // namespace Tools
|
} // namespace Tools
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,74 @@
|
|||||||
#include "Cubed/tools/cubed_assert.hpp"
|
#include "Cubed/tools/cubed_assert.hpp"
|
||||||
#include "Cubed/tools/math_tools.hpp"
|
#include "Cubed/tools/math_tools.hpp"
|
||||||
#include "Cubed/tools/net_utils.hpp"
|
#include "Cubed/tools/net_utils.hpp"
|
||||||
|
#include "Cubed/tools/time_tools.hpp"
|
||||||
using namespace google::protobuf;
|
using namespace google::protobuf;
|
||||||
|
|
||||||
namespace Cubed {
|
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<ClientEntitySnapshot>& 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) {}
|
ClientEntityManager::ClientEntityManager(ClientWorld& world) : m_world(world) {}
|
||||||
void ClientEntityManager::update(float dt) {
|
void ClientEntityManager::update(float dt) {
|
||||||
handle_task(dt);
|
handle_task(dt);
|
||||||
|
{
|
||||||
|
auto view = m_registry.view<ClientEntityState, RenderTransform>();
|
||||||
|
double render_time = static_cast<double>(Tools::get_time_ticks()) -
|
||||||
|
ENTITY_RENDER_DELAY_MS;
|
||||||
|
for (auto e : view) {
|
||||||
|
auto& state = view.get<ClientEntityState>(e);
|
||||||
|
auto& r = view.get<RenderTransform>(e);
|
||||||
|
auto snap = interpolate_snapshot(state.history, render_time);
|
||||||
|
r.position.value = snap.pos;
|
||||||
|
r.direction.value = snap.dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
auto view = m_registry.view<BaseClientCreature>();
|
auto view = m_registry.view<BaseClientCreature>();
|
||||||
for (auto e : view) {
|
for (auto e : view) {
|
||||||
@@ -35,10 +97,10 @@ void ClientEntityManager::init() {
|
|||||||
BaseClientCreature c;
|
BaseClientCreature c;
|
||||||
c.model = ModelManager::instance().get_model_id("cubed:pig");
|
c.model = ModelManager::instance().get_model_id("cubed:pig");
|
||||||
float next_call_time = m_random.random_float(8.0f, 25.0f);
|
float next_call_time = m_random.random_float(8.0f, 25.0f);
|
||||||
create_entity_in_registry(id, Entity{id, EntityType::CREATURE},
|
create_entity_in_registry(
|
||||||
EntityInfo{"cubed:pig", ""}, std::move(c),
|
id, Entity{id, EntityType::CREATURE}, EntityInfo{"cubed:pig", ""},
|
||||||
PigTag{}, RenderTransform{},
|
std::move(c), PigTag{}, RenderTransform{},
|
||||||
SoundTime{next_call_time});
|
SoundTime{next_call_time}, ClientEntityState{});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// not thread safe
|
// not thread safe
|
||||||
@@ -55,9 +117,13 @@ void ClientEntityManager::handle_entity_create(EntityID id,
|
|||||||
auto* r = m_registry.try_get<RenderTransform>(a->second);
|
auto* r = m_registry.try_get<RenderTransform>(a->second);
|
||||||
ASSERT(r);
|
ASSERT(r);
|
||||||
r->position.value = pos;
|
r->position.value = pos;
|
||||||
|
auto* s = m_registry.try_get<ClientEntityState>(a->second);
|
||||||
|
ASSERT(s);
|
||||||
|
s->history.emplace_back(static_cast<double>(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;
|
entt::entity e;
|
||||||
{
|
{
|
||||||
cacc a;
|
cacc a;
|
||||||
@@ -72,13 +138,14 @@ void ClientEntityManager::handle_entity_update(UpdateInfo& info, float dt) {
|
|||||||
creature->transform.position.value = info.pos;
|
creature->transform.position.value = info.pos;
|
||||||
creature->transform.direction.value = info.direction;
|
creature->transform.direction.value = info.direction;
|
||||||
creature->pose.gait = info.gait;
|
creature->pose.gait = info.gait;
|
||||||
auto r = m_registry.try_get<RenderTransform>(e);
|
|
||||||
ASSERT(r);
|
auto state = m_registry.try_get<ClientEntityState>(e);
|
||||||
float alpha = 1 - std::exp(-dt / 0.1);
|
ASSERT(state);
|
||||||
r->direction.value = glm::mix(r->direction.value,
|
state->history.push_back({static_cast<double>(Tools::get_time_ticks()),
|
||||||
creature->transform.direction.value, alpha);
|
info.pos, info.direction});
|
||||||
r->position.value =
|
while (state->history.size() > ENTITY_SNAPSHOT_MAX) {
|
||||||
glm::mix(r->position.value, creature->transform.position.value, alpha);
|
state->history.pop_front();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientEntityManager::receive_entity_create(S2CEntityCreate& s2c) {
|
void ClientEntityManager::receive_entity_create(S2CEntityCreate& s2c) {
|
||||||
|
|||||||
Reference in New Issue
Block a user