Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,10 @@ if(PERLIN_BUILD_TESTS AND NOT EMSCRIPTEN AND NOT ANDROID AND NOT PERLIN_IOS)
target_include_directories(skin_lifetime_selftest PRIVATE src "${glm_SOURCE_DIR}")
target_compile_features(skin_lifetime_selftest PRIVATE cxx_std_23)
add_test(NAME skin_lifetime_selftest COMMAND skin_lifetime_selftest)
add_executable(actor_lerp_selftest tests/actor_lerp_selftest.cpp src/client/entity/EntityTracker.cpp)
target_include_directories(actor_lerp_selftest PRIVATE src "${glm_SOURCE_DIR}")
target_compile_features(actor_lerp_selftest PRIVATE cxx_std_23)
add_test(NAME actor_lerp_selftest COMMAND actor_lerp_selftest)
add_executable(block_palette_selftest tests/block_palette_selftest.cpp
src/world/level/block/BlockPalette.cpp src/util/Json.cpp)
target_include_directories(block_palette_selftest PRIVATE src)
Expand Down
11 changes: 10 additions & 1 deletion src/client/PerlinFrame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -400,8 +400,17 @@ namespace perlin {
player.SetToggleModes(options.sprintMode == "toggle", options.sneakMode == "toggle");
hudState.moveStatus.clear();
UpdateEatingAndDrinking(f, *live);
if (!replayCtl.Playing())
if (!replayCtl.Playing()) {
player.Update(*live, inputManager, camera, levelRenderer->Blocks(), dt);
} else {
double& clock = loop->replayActorClock;
clock += std::min(static_cast<double>(dt), 0.1) * std::max(0.0, replayCtl.Speed());
for (int i = 0; clock >= physics::kTickSeconds && i < 10; ++i) {
clock -= physics::kTickSeconds;
live->Entities().TickInterpolation();
}
clock = std::min(clock, static_cast<double>(physics::kTickSeconds));
}
}

void Perlin::UpdateRiding(BedrockSession& live, const std::vector<entity::Entity>& frameEntities) {
Expand Down
2 changes: 2 additions & 0 deletions src/client/PerlinFrame.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ namespace perlin {

Zoom zoom;
float scopeFov = 1.0f;
// replays have no local player tick: actors interpolate on their own 20 Hz clock
double replayActorClock = 0.0;
TimePoint previous = Clock::now();
const TimePoint startedAt = previous;
uint64_t frameCounter = 0;
Expand Down
3 changes: 3 additions & 0 deletions src/client/PerlinWorldRender.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1106,6 +1106,9 @@ namespace perlin {
entity::EntityRenderer::FrameIn ein;
ein.time = std::chrono::duration<double>(frameStart - startedAt).count();
ein.dt = dt;
ein.partialTick = replayCtl.Playing()
? static_cast<float>(std::clamp(loop->replayActorClock / physics::kTickSeconds, 0.0, 1.0))
: player.PartialTick();
{
const glm::vec3 eyePos = camera.EyePosition();
ein.camera = {eyePos.x, eyePos.y, eyePos.z};
Expand Down
70 changes: 11 additions & 59 deletions src/client/entity/EntityRenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,6 @@ namespace perlin::entity {
struct EntityRenderer::View {
bool init = false;
glm::dvec3 pos{0.0}, lastPos{0.0};
struct Sample {
double t = 0;
glm::dvec3 pos{0.0};
float yaw = 0, headYaw = 0, pitch = 0;
};
std::deque<Sample> samples;
double interval = 0.05, jitter = 0.0, delay = 0.1;
double lastArrival = 0;
float yaw = 0, headYaw = 0, pitch = 0;
uint32_t moveSerial = 0, hurtSerial = 0, swingSerial = 0;
float limbSwing = 0, limbAmount = 0;
Expand Down Expand Up @@ -471,57 +463,17 @@ namespace perlin::entity {
const float prevYaw = v.yaw;
glm::dvec3 d = e.pos - v.pos;
if (buffered) {
if (v.samples.empty() || v.samples.back().pos != e.pos || v.samples.back().yaw != e.yaw ||
v.samples.back().headYaw != e.headYaw || v.samples.back().pitch != e.pitch) {
if (!v.samples.empty() && glm::length(e.pos - v.samples.back().pos) > 12.0) {
v.samples.clear();
v.pos = e.pos;
v.lastPos = e.pos;
v.yaw = e.yaw;
v.headYaw = e.headYaw;
v.pitch = e.pitch;
v.lastArrival = 0;
}
if (v.lastArrival > 0) {
const double gap = std::clamp(in.time - v.lastArrival, 0.005, 0.5);
const double dev = std::fabs(gap - v.interval);
v.interval += (gap - v.interval) * 0.1;
v.jitter += (dev - v.jitter) * 0.1;
}
v.lastArrival = in.time;
v.samples.push_back({in.time, e.pos, e.yaw, e.headYaw, e.pitch});
while (v.samples.size() > 64)
v.samples.pop_front();
}
const double wantDelay = std::clamp(v.interval * 1.25 + v.jitter * 2.0, 0.06, 0.25);
v.delay += (wantDelay - v.delay) * std::min(1.0, static_cast<double>(dt) * 2.0);
const double renderT = in.time - v.delay;
while (v.samples.size() > 2 && v.samples[1].t <= renderT)
v.samples.pop_front();
if (v.samples.size() >= 2 && renderT >= v.samples[0].t) {
const auto& a = v.samples[0];
const auto& b = v.samples[1];
const double span = b.t - a.t;
const double f = span > 1e-6 ? std::clamp((renderT - a.t) / span, 0.0, 1.0) : 1.0;
v.pos = a.pos + (b.pos - a.pos) * f;
const auto mix = [&](float x, float y) {
return x + WrapDegrees(y - x) * static_cast<float>(f);
};
v.yaw = mix(a.yaw, b.yaw);
v.headYaw = mix(a.headYaw, b.headYaw);
v.pitch = a.pitch + WrapDegrees(b.pitch - a.pitch) * static_cast<float>(f);
} else if (v.samples.size() >= 2) {
v.pos = v.samples[0].pos;
} else if (!v.samples.empty()) {
const double catchUp = 1.0 - std::exp(-static_cast<double>(dt) / 0.06);
v.pos += (v.samples.back().pos - v.pos) * catchUp;
const auto turnTo = [&](float cur, float target) {
return cur + WrapDegrees(target - cur) * static_cast<float>(catchUp);
};
v.yaw = turnTo(v.yaw, v.samples.back().yaw);
v.headYaw = turnTo(v.headYaw, v.samples.back().headYaw);
v.pitch += WrapDegrees(v.samples.back().pitch - v.pitch) * static_cast<float>(catchUp);
}
// the tracker moves actors once per tick like MovementInterpolatorSystem::tickClient; draw between the
// last two ticks so the drawn body is the one our hits are tested against
const double a = std::clamp(static_cast<double>(in.partialTick), 0.0, 1.0);
const glm::dvec3 step = e.pos - e.prevPos;
v.pos = glm::dot(step, step) > 144.0 ? e.pos : e.prevPos + step * a;
const auto mix = [&](float from, float to) {
return from + WrapDegrees(to - from) * static_cast<float>(a);
};
v.yaw = mix(e.prevYaw, e.yaw);
v.headYaw = mix(e.prevHeadYaw, e.headYaw);
v.pitch = mix(e.prevPitch, e.pitch);
v.yawSpeed = WrapDegrees(v.yaw - prevYaw) / dt;
} else {
v.pos = glm::length(d) > 24.0 ? e.pos : v.pos + d * follow;
Expand Down
2 changes: 2 additions & 0 deletions src/client/entity/EntityRenderer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ namespace perlin::entity {
struct FrameIn {
double time = 0;
float dt = 0.016f;
// client tick progress (0..1) since the last actor interpolation step
float partialTick = 1.0f;
glm::dvec3 camera{0.0};
std::function<glm::vec3(const glm::dvec3&)> light;
std::function<glm::ivec2(const glm::dvec3&)> lightLevels; // block, sky (0-15)
Expand Down
140 changes: 121 additions & 19 deletions src/client/entity/EntityTracker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ namespace perlin::entity {
e.pitch = SignedPitch(pitch);
e.yaw = yaw;
e.headYaw = headYaw;
e.prevPos = e.pos;
e.prevPitch = e.pitch;
e.prevYaw = e.yaw;
e.prevHeadYaw = e.headYaw;
e.lerp.steps = 0;
e.hasQueued = false;
e.haveNet = false;
e.flags = flags;
e.mainHand = mainHand;
e.isSelf = rid == selfRuntime && rid != 0;
Expand All @@ -67,6 +74,13 @@ namespace perlin::entity {
e.pitch = SignedPitch(pitch);
e.yaw = yaw;
e.headYaw = headYaw;
e.prevPos = e.pos;
e.prevPitch = e.pitch;
e.prevYaw = e.yaw;
e.prevHeadYaw = e.headYaw;
e.lerp.steps = 0;
e.hasQueued = false;
e.haveNet = false;
e.flags = flags;
e.scale = scale;
e.variant = variant;
Expand Down Expand Up @@ -131,8 +145,60 @@ namespace perlin::entity {
}
}

void EntityTracker::Move(uint64_t rid, glm::dvec3 pos, float pitch, float yaw, float headYaw,
bool onGround) {
namespace {
float Wrap(float degrees) {
degrees = std::fmod(degrees, 360.0f);
if (degrees >= 180.0f)
degrees -= 360.0f;
if (degrees < -180.0f)
degrees += 360.0f;
return degrees;
}

void PlaceNow(Entity& e, const Entity::Lerp& to) {
e.pos = e.prevPos = to.pos;
e.pitch = e.prevPitch = to.pitch;
e.yaw = e.prevYaw = to.yaw;
e.headYaw = e.prevHeadYaw = to.headYaw;
e.lerp.steps = 0;
e.hasQueued = false;
}

// Actor::lerpTo 0x142C74350: a running interpolation started with forceCompletion keeps going and the new
// target waits in QueuedMovementInterpolationComponent; anything else replaces the target
void LerpTo(Entity& e, const Entity::Lerp& to) {
if (!to.forceCompletion && e.lerp.forceCompletion && e.lerp.steps != 0) {
e.queued = to;
e.hasQueued = true;
return;
}
e.lerp = to;
}

// move_entity / move_entity_delta handler 0x1422C88B0 -> 0x1422C8D20: steps = ticks when above 3, else 3;
// a teleport of 0.5 block or more is placed at once
void Receive(Entity& e, const glm::dvec3& next, float pitch, float yaw, float headYaw,
EntityTracker::MoveKind kind, uint64_t ticks, bool forceCompletion) {
e.haveNet = true;
e.netPos = next;
e.netPitch = pitch;
e.netYaw = yaw;
e.netHeadYaw = headYaw;
const Entity::Lerp to{next, pitch, yaw, headYaw, ticks >= 4 ? static_cast<int>(std::min<uint64_t>(ticks, 1000)) : 3,
forceCompletion};
const glm::dvec3 jump = next - e.pos;
const bool farTeleport =
kind == EntityTracker::MoveKind::Teleport && glm::dot(jump, jump) >= 0.25;
// TODO: the game also places at once when the destination block is not loaded
if (kind == EntityTracker::MoveKind::Place || farTeleport || !e.ticked)
PlaceNow(e, to);
else
LerpTo(e, to);
}
}

void EntityTracker::Move(uint64_t rid, glm::dvec3 pos, float pitch, float yaw, float headYaw, bool onGround,
MoveKind kind, uint64_t ticks, bool forceCompletion) {
std::lock_guard lock(mutex);
const auto it = entities.find(rid);
if (it == entities.end())
Expand All @@ -145,16 +211,14 @@ namespace perlin::entity {
const glm::dvec3 next = e.isPlayer ? glm::dvec3(pos.x, pos.y - kPlayerEye, pos.z) : pos;
if (e.dying && e.isPlayer && glm::dot(next - e.pos, next - e.pos) > 2.25)
e.dying = false;
e.pos = next;
e.pitch = SignedPitch(pitch);
e.yaw = yaw;
e.headYaw = headYaw;
Receive(e, next, SignedPitch(pitch), yaw, headYaw, kind, ticks, forceCompletion);
e.onGround = onGround;
e.moveSerial++;
}

void EntityTracker::MoveDelta(uint64_t rid, const double* x, const double* y, const double* z,
const float* pitch, const float* yaw, const float* headYaw, bool onGround) {
const float* pitch, const float* yaw, const float* headYaw, bool onGround,
bool teleport, uint64_t ticks, bool forceCompletion) {
std::lock_guard lock(mutex);
const auto it = entities.find(rid);
if (it == entities.end())
Expand All @@ -164,22 +228,60 @@ namespace perlin::entity {
ItemServerUpdate(e, glm::dvec3(x ? *x : e.pos.x, y ? *y : e.pos.y, z ? *z : e.pos.z), onGround);
return;
}
if (x)
e.pos.x = *x;
if (y)
e.pos.y = e.isPlayer ? *y - kPlayerEye : *y;
if (z)
e.pos.z = *z;
if (pitch)
e.pitch = SignedPitch(*pitch);
if (yaw)
e.yaw = *yaw;
if (headYaw)
e.headYaw = *headYaw;
if (!e.haveNet) {
e.netPos = e.pos;
e.netPitch = e.pitch;
e.netYaw = e.yaw;
e.netHeadYaw = e.headYaw;
}
const glm::dvec3 next(x ? *x : e.netPos.x, y ? (e.isPlayer ? *y - kPlayerEye : *y) : e.netPos.y,
z ? *z : e.netPos.z);
Receive(e, next, pitch ? SignedPitch(*pitch) : e.netPitch, yaw ? *yaw : e.netYaw,
headYaw ? *headYaw : e.netHeadYaw, teleport ? MoveKind::Teleport : MoveKind::Lerp, ticks,
forceCompletion);
e.onGround = onGround;
e.moveSerial++;
}

void EntityTracker::TickInterpolation() {
std::lock_guard lock(mutex);
for (auto& [rid, e] : entities) {
e.prevPos = e.pos;
e.prevPitch = e.pitch;
e.prevYaw = e.yaw;
e.prevHeadYaw = e.headYaw;
e.ticked = true;
if (e.isSelf || (e.isItem && e.simulated) || pickups.contains(rid))
continue;
// MovementInterpolatorSystem::handleQueuedMovementInterpolation 0x143634AE0
if (e.hasQueued) {
if (e.lerp.forceCompletion && e.lerp.steps != 0) {
e.queued.steps = std::max(e.queued.steps, 2) - 1;
} else {
e.lerp = e.queued;
e.lerp.forceCompletion = false;
e.hasQueued = false;
}
}
// MovementInterpolatorSystem::tickClient 0x143630D70: 1/steps of the way, the last step lands on it
if (e.lerp.steps > 0) {
if (e.lerp.steps == 1) {
e.pos = e.lerp.pos;
e.pitch = e.lerp.pitch;
e.yaw = e.lerp.yaw;
e.headYaw = e.lerp.headYaw;
} else {
const double f = 1.0 / e.lerp.steps;
e.pos += (e.lerp.pos - e.pos) * f;
e.pitch += Wrap(e.lerp.pitch - e.pitch) * static_cast<float>(f);
e.yaw += Wrap(e.lerp.yaw - e.yaw) * static_cast<float>(f);
e.headYaw += Wrap(e.lerp.headYaw - e.headYaw) * static_cast<float>(f);
}
e.lerp.steps--;
}
}
}

void EntityTracker::SetVehicle(int64_t riderUid, int64_t vehicleUid) {
std::lock_guard lock(mutex);
const auto u = byUnique.find(riderUid);
Expand Down
28 changes: 26 additions & 2 deletions src/client/entity/EntityTracker.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,23 @@ namespace perlin::entity {
glm::dvec3 vel{0.0};
bool sleeping = false, sleepExt = false, sleepPlayer = false;
bool simulated = false;
// MovementInterpolatorComponent: pos above is the actor's tick position, moved toward the last received
// one over a few ticks; prevPos/prev* are the previous tick for render interpolation
struct Lerp {
glm::dvec3 pos{0.0};
float pitch = 0, yaw = 0, headYaw = 0;
int steps = 0;
bool forceCompletion = false;
};
Lerp lerp, queued;
bool hasQueued = false;
glm::dvec3 prevPos{0.0};
float prevYaw = 0, prevHeadYaw = 0, prevPitch = 0;
bool ticked = false;
// last absolute pose received: move_entity_delta only sends the changed lanes
glm::dvec3 netPos{0.0};
float netPitch = 0, netYaw = 0, netHeadYaw = 0;
bool haveNet = false;
bool haveServerPos = false;
glm::dvec3 lastServerPos{0.0};
std::chrono::steady_clock::time_point lastServerAt;
Expand Down Expand Up @@ -160,9 +177,16 @@ namespace perlin::entity {
void SetMotion(uint64_t rid, glm::dvec3 velocity);
void SetSleeping(uint64_t rid, bool sleeping, bool playerFlag = false);
void StepItems(const std::function<bool(int, int, int)>& solid);
void Move(uint64_t rid, glm::dvec3 pos, float pitch, float yaw, float headYaw, bool onGround);
// how a server position reaches the actor: Place sets it now (our own predicted vehicles), Lerp and
// Teleport go through Actor::lerpTo like the game's move_entity / move_entity_delta handler
enum class MoveKind { Place, Lerp, Teleport };
void Move(uint64_t rid, glm::dvec3 pos, float pitch, float yaw, float headYaw, bool onGround,
MoveKind kind = MoveKind::Place, uint64_t ticks = 0, bool forceCompletion = false);
void MoveDelta(uint64_t rid, const double* x, const double* y, const double* z, const float* pitch,
const float* yaw, const float* headYaw, bool onGround);
const float* yaw, const float* headYaw, bool onGround, bool teleport = false,
uint64_t ticks = 0, bool forceCompletion = false);
// one client tick of MovementInterpolatorSystem::tickClient for every actor
void TickInterpolation();
void SetFlags(uint64_t rid, uint64_t flags);
void SetFlagsExtended(uint64_t rid, uint64_t flags);
void SetCrawling(uint64_t rid, bool crawling);
Expand Down
11 changes: 8 additions & 3 deletions src/client/multiplayer/BedrockHandlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1337,7 +1337,9 @@ namespace perlin {
constexpr float kByte = 360.0f / 256.0f;
entities.Move(p.runtimeEntityId, {p.position.x, p.position.y, p.position.z},
p.rotation.pitch * kByte, p.rotation.yaw * kByte, p.rotation.headYaw * kByte,
(p.flags & 1) != 0);
(p.flags & 1) != 0,
(p.flags & 2) != 0 ? entity::EntityTracker::MoveKind::Teleport
: entity::EntityTracker::MoveKind::Lerp);
});

On<MoveEntityDeltaPacket>([this](const MoveEntityDeltaPacket& p) {
Expand All @@ -1362,14 +1364,17 @@ namespace perlin {
head = *p.rotZ * kByte;
entities.MoveDelta(p.runtimeEntityId, p.x ? &x : nullptr, p.y ? &y : nullptr, p.z ? &z : nullptr,
p.rotX ? &pitch : nullptr, p.rotY ? &yaw : nullptr, p.rotZ ? &head : nullptr,
p.onGround);
p.onGround, p.forceMove, p.ticks, p.forceCompletion);
});

On<MovePlayerPacket>([this](const MovePlayerPacket& p) {
if (runtimeId != 0 && p.runtimeId == runtimeId)
return;
entities.Move(p.runtimeId, {p.position.x, p.position.y, p.position.z}, p.pitch, p.yaw, p.headYaw,
p.onGround);
p.onGround,
p.mode == MovePlayerPacketMode::Normal || p.mode == MovePlayerPacketMode::Rotation
? entity::EntityTracker::MoveKind::Lerp
: entity::EntityTracker::MoveKind::Teleport);
});

On<SetEntityDataPacket>([this](const SetEntityDataPacket& p) {
Expand Down
Loading
Loading