2 Commits

Author SHA1 Message Date
d3d277a231 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.
2026-08-07 09:13:40 +08:00
6e7bef8626 feat(audio): add ambient pig sounds
Add pig call audio with randomized timing, triggered by proximity to the player as 3D positional sound.
2026-08-07 08:41:56 +08:00
5 changed files with 129 additions and 13 deletions

Binary file not shown.

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "Cubed/gameplay/ecs/entity.hpp" #include "Cubed/gameplay/ecs/entity.hpp"
#include "Cubed/gameplay/gait.hpp" #include "Cubed/gameplay/gait.hpp"
#include "Cubed/tools/cubed_random.hpp"
#include "glm/ext/vector_float3.hpp" #include "glm/ext/vector_float3.hpp"
#include "world/entity.pb.h" #include "world/entity.pb.h"
@@ -26,6 +27,8 @@ public:
const entt::registry& get_registry() const; const entt::registry& get_registry() const;
void player_sound(float dt);
private: private:
struct EntityCreateElement { struct EntityCreateElement {
EntityID id; EntityID id;
@@ -52,7 +55,7 @@ private:
EntityMap m_entities; EntityMap m_entities;
std::unordered_map<std::string_view, CreateFunc> m_factories; std::unordered_map<std::string_view, CreateFunc> m_factories;
tbb::concurrent_queue<TaskPair> m_tasks; tbb::concurrent_queue<TaskPair> m_tasks;
Random m_random;
void handle_task(float dt); void handle_task(float dt);
void handle_entity_destory(EntityID id); void handle_entity_destory(EntityID id);
// not thread safe // not thread safe

View File

@@ -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 {
@@ -11,4 +13,19 @@ struct BaseClientCreature {
ModelID model; ModelID model;
}; };
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<ClientEntitySnapshot> history;
};
} // namespace Cubed } // namespace Cubed

View File

@@ -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

View File

@@ -7,14 +7,76 @@
#include "Cubed/gameplay/ecs/transform.hpp" #include "Cubed/gameplay/ecs/transform.hpp"
#include "Cubed/render/model_manager.hpp" #include "Cubed/render/model_manager.hpp"
#include "Cubed/tools/cubed_assert.hpp" #include "Cubed/tools/cubed_assert.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) {
@@ -25,15 +87,20 @@ void ClientEntityManager::update(float dt) {
c.pose.walk_time += dt; c.pose.walk_time += dt;
} }
} }
player_sound(dt);
} }
void ClientEntityManager::init() { void ClientEntityManager::init() {
m_random.init(std::random_device()());
m_factories.emplace("cubed:pig", [this](EntityID id) { m_factories.emplace("cubed:pig", [this](EntityID id) {
BaseClientCreature c; BaseClientCreature c;
c.model = ModelManager::instance().get_model_id("cubed:pig"); c.model = ModelManager::instance().get_model_id("cubed:pig");
create_entity_in_registry(id, Entity{id, EntityType::CREATURE}, float next_call_time = m_random.random_float(8.0f, 25.0f);
EntityInfo{"cubed:pig", ""}, std::move(c), create_entity_in_registry(
PigTag{}, RenderTransform{}); id, Entity{id, EntityType::CREATURE}, EntityInfo{"cubed:pig", ""},
std::move(c), PigTag{}, RenderTransform{},
SoundTime{next_call_time}, ClientEntityState{});
}); });
} }
// not thread safe // not thread safe
@@ -50,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;
@@ -67,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) {
@@ -157,4 +229,27 @@ void ClientEntityManager::handle_entity_destory(EntityID id) {
const entt::registry& ClientEntityManager::get_registry() const { const entt::registry& ClientEntityManager::get_registry() const {
return m_registry; return m_registry;
} }
void ClientEntityManager::player_sound(float dt) {
auto& audio = m_world.get_audio();
auto player_pos = m_world.player_manager().get_local().get_player_pos();
auto view = m_registry.view<EntityInfo, BaseClientCreature, SoundTime>();
for (auto e : view) {
const auto [info, creature] =
view.get<EntityInfo, BaseClientCreature>(e);
if (Math::distance2(player_pos, creature.transform.position.value) >
10 * 10) {
continue;
}
auto& sound_time = view.get<SoundTime>(e);
sound_time.next_call_time -= dt;
if (sound_time.next_call_time <= 0.0f) {
audio.play_3d("creature/pig/call.mp3",
creature.transform.position.value);
sound_time.next_call_time = m_random.random_float(8.0f, 25.0f);
}
}
}
} // namespace Cubed } // namespace Cubed