Skip to content

Commit ebcf60e

Browse files
committed
misc: improve map marker speed
1 parent 3988407 commit ebcf60e

File tree

2 files changed

+109
-14
lines changed

2 files changed

+109
-14
lines changed

Code/client/Services/Generic/PartyMapOverlayService.cpp

Lines changed: 105 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
#include <iomanip>
2222
#include <chrono>
2323
#include <cmath>
24+
#include <limits>
25+
#include <glm/gtx/norm.hpp>
2426

2527
#include <Games/Skyrim/Interface/UI.h>
2628
#include <Games/Skyrim/Camera/PlayerCamera.h>
@@ -97,7 +99,8 @@ void PartyMapOverlayService::OnPartyPositions(const NotifyPartyPositions& aMsg)
9799

98100
for (const auto& e : aMsg.Entries)
99101
{
100-
m_last[e.PlayerId] = LastInfo{glm::vec3{e.Position.x, e.Position.y, e.Position.z}, tick};
102+
glm::vec3 pos{e.Position.x, e.Position.y, e.Position.z};
103+
StoreLastInfo(e.PlayerId, pos, tick);
101104

102105
WorldspaceInfo info{};
103106
if (e.WorldSpaceId)
@@ -115,7 +118,7 @@ void PartyMapOverlayService::OnPartyPositions(const NotifyPartyPositions& aMsg)
115118
// Track last known position per worldspace for cross-world projection
116119
if (info.HasWorld)
117120
{
118-
m_lastPerWorld[e.PlayerId][info.WorldSpaceFormId] = glm::vec3{e.Position.x, e.Position.y, e.Position.z};
121+
m_lastPerWorld[e.PlayerId][info.WorldSpaceFormId] = pos;
119122
}
120123
}
121124
}
@@ -175,13 +178,13 @@ void PartyMapOverlayService::OnUpdate(const UpdateEvent&) noexcept
175178
{
176179
const auto& pc = view.get<PlayerComponent>(e);
177180
const auto& interp = view.get<InterpolationComponent>(e);
178-
m_last[pc.Id] = LastInfo{interp.Position, tick};
181+
StoreLastInfo(pc.Id, interp.Position, tick);
179182
}
180183

181184
// Periodically send our own position to the server for party tracking
182185
{
183186
static auto s_lastPosSend = std::chrono::steady_clock::time_point{};
184-
constexpr auto cPosInterval = std::chrono::milliseconds(250);
187+
constexpr auto cPosInterval = std::chrono::milliseconds(100);
185188
const auto now = std::chrono::steady_clock::now();
186189
if (now - s_lastPosSend >= cPosInterval)
187190

@@ -222,7 +225,7 @@ void PartyMapOverlayService::OnUpdate(const UpdateEvent&) noexcept
222225

223226
// Build and send pins to CEF overlay at a throttled rate
224227
static auto s_lastSend = std::chrono::steady_clock::time_point{};
225-
constexpr auto cDelayBetweenUpdates = std::chrono::milliseconds(33); // ~30 FPS for smoother pins
228+
constexpr auto cDelayBetweenUpdates = std::chrono::milliseconds(16); // ~60 FPS for tighter tracking
226229
const auto now = std::chrono::steady_clock::now();
227230
if (now - s_lastSend < cDelayBetweenUpdates)
228231
return;
@@ -271,6 +274,7 @@ void PartyMapOverlayService::OnUpdate(const UpdateEvent&) noexcept
271274
os.setf(std::ios::fixed); os << std::setprecision(1);
272275
os << "[";
273276
bool first = true;
277+
const auto sampleNow = std::chrono::steady_clock::now();
274278

275279
for (uint32_t pid : members)
276280
{
@@ -285,11 +289,29 @@ void PartyMapOverlayService::OnUpdate(const UpdateEvent&) noexcept
285289
const bool hasWorld = (itWs != m_worlds.end()) && itWs->second.HasWorld && itWs->second.WorldSpaceFormId != 0;
286290
float sx = 0.f, sy = 0.f;
287291
bool drew = false;
292+
const auto& lastInfo = itPos->second;
293+
294+
glm::vec3 worldPos = lastInfo.Pos;
295+
float dtSeconds = 0.0f;
296+
if (tick > lastInfo.Tick)
297+
dtSeconds = static_cast<float>(tick - lastInfo.Tick) / 1000.0f;
298+
299+
if (lastInfo.SampleTime.time_since_epoch().count() != 0)
300+
{
301+
const float realDt = std::chrono::duration_cast<std::chrono::duration<float>>(sampleNow - lastInfo.SampleTime).count();
302+
dtSeconds = std::max(dtSeconds, realDt);
303+
}
304+
305+
constexpr float cMaxPredictionSeconds = 1.0f;
306+
dtSeconds = std::clamp(dtSeconds, 0.0f, cMaxPredictionSeconds);
307+
const bool usingPrediction = dtSeconds > 0.0f && glm::length2(lastInfo.Velocity) > std::numeric_limits<float>::epsilon();
308+
if (usingPrediction)
309+
worldPos += lastInfo.Velocity * dtSeconds;
288310

289311
// 1) If member already in display worldspace, project directly
290312
if (hasWorld && itWs->second.WorldSpaceFormId == dispWsId)
291313
{
292-
NiPoint3 wpos(itPos->second.Pos);
314+
NiPoint3 wpos(worldPos);
293315
NiPoint3 spos{};
294316
if (HUDMenuUtils::WorldPtToScreenPt3(wpos, spos))
295317
{
@@ -305,7 +327,7 @@ void PartyMapOverlayService::OnUpdate(const UpdateEvent&) noexcept
305327
if (auto* pFromWs = static_cast<TESWorldSpace*>(TESForm::GetById(itWs->second.WorldSpaceFormId)))
306328
{
307329
glm::vec3 dstPos{};
308-
if (WorldMapProjector::Convert(pFromWs, itPos->second.Pos, pDispWs, dstPos))
330+
if (WorldMapProjector::Convert(pFromWs, worldPos, pDispWs, dstPos))
309331
{
310332
NiPoint3 wpos(dstPos);
311333
NiPoint3 spos{};
@@ -323,7 +345,7 @@ void PartyMapOverlayService::OnUpdate(const UpdateEvent&) noexcept
323345
if (!drew && hasWorld && dispWsId != 0 && itWs->second.WorldSpaceFormId != dispWsId)
324346
{
325347
glm::vec3 dstPos{};
326-
if (ComputeCrossWorldApprox(pid, itWs->second.WorldSpaceFormId, itPos->second.Pos, dispWsId, dstPos))
348+
if (ComputeCrossWorldApprox(pid, itWs->second.WorldSpaceFormId, worldPos, dispWsId, dstPos))
327349
{
328350
NiPoint3 wpos(dstPos);
329351
NiPoint3 spos{};
@@ -379,11 +401,24 @@ void PartyMapOverlayService::OnUpdate(const UpdateEvent&) noexcept
379401
if (itLS != m_lastScreen.end())
380402
{
381403
const uint64_t dt = tick - itLS->second.Tick;
382-
// Time constant ~150ms for quick but smooth convergence
383-
float alpha = 1.0f - std::exp(-static_cast<float>(dt) / 150.0f);
384-
if (alpha < 0.f) alpha = 0.f; if (alpha > 1.f) alpha = 1.f;
385-
sx = itLS->second.sx + (sx - itLS->second.sx) * alpha;
386-
sy = itLS->second.sy + (sy - itLS->second.sy) * alpha;
404+
float alpha = 1.0f;
405+
if (dt > 0)
406+
{
407+
constexpr float cSmoothingMs = 45.0f;
408+
alpha = 1.0f - std::exp(-static_cast<float>(dt) / cSmoothingMs);
409+
alpha = std::clamp(alpha, 0.7f, 1.0f);
410+
}
411+
412+
const float dx = sx - itLS->second.sx;
413+
const float dy = sy - itLS->second.sy;
414+
constexpr float cTeleportPixelsSq = 600.0f * 600.0f;
415+
if ((dx * dx + dy * dy) > cTeleportPixelsSq)
416+
alpha = 1.0f;
417+
else if (usingPrediction && dtSeconds >= 0.2f)
418+
alpha = 1.0f;
419+
420+
sx = itLS->second.sx + dx * alpha;
421+
sy = itLS->second.sy + dy * alpha;
387422
}
388423

389424
// Cache last projected (smoothed) position
@@ -444,4 +479,61 @@ void PartyMapOverlayService::SetWaypointFor(uint32_t aPlayerId) noexcept
444479
m_world.GetDispatcher().trigger(SetWaypointEvent(pos, itWs->second.WorldSpaceFormId));
445480
}
446481

482+
void PartyMapOverlayService::StoreLastInfo(uint32_t aPlayerId, const glm::vec3& aPos, uint64_t aTick) noexcept
483+
{
484+
constexpr float cMinDtSeconds = 0.001f;
485+
constexpr float cMaxDtSeconds = 6.0f;
486+
constexpr float cTeleportResetDistanceSq = 4096.0f * 4096.0f;
487+
constexpr float cMaxSpeed = 16000.0f; // units per second, avoids runaway extrapolation
488+
constexpr float cMaxSpeedSq = cMaxSpeed * cMaxSpeed;
489+
constexpr float cVelocityBlend = 0.25f; // keep some of the previous vector to reduce jitter
490+
491+
const auto now = std::chrono::steady_clock::now();
492+
493+
LastInfo info{};
494+
info.Pos = aPos;
495+
info.Tick = aTick;
496+
info.Velocity = {};
497+
info.SampleTime = now;
498+
499+
auto itPrev = m_last.find(aPlayerId);
500+
if (itPrev != m_last.end())
501+
{
502+
const uint64_t tickDelta = (aTick > itPrev->second.Tick) ? (aTick - itPrev->second.Tick) : 0ull;
503+
float dtSeconds = tickDelta > 0 ? static_cast<float>(tickDelta) / 1000.0f : 0.0f;
504+
505+
if (itPrev->second.SampleTime.time_since_epoch().count() != 0)
506+
{
507+
const float realDtSeconds = std::chrono::duration_cast<std::chrono::duration<float>>(now - itPrev->second.SampleTime).count();
508+
if (realDtSeconds >= cMinDtSeconds)
509+
dtSeconds = std::max(dtSeconds, realDtSeconds);
510+
}
511+
512+
if (dtSeconds < cMinDtSeconds)
513+
{
514+
info.Velocity = itPrev->second.Velocity;
515+
}
516+
else
517+
{
518+
const glm::vec3 delta = aPos - itPrev->second.Pos;
519+
const float distSq = glm::length2(delta);
447520

521+
if (dtSeconds <= cMaxDtSeconds && distSq <= cTeleportResetDistanceSq)
522+
{
523+
glm::vec3 newVelocity = delta / dtSeconds;
524+
if (glm::length2(newVelocity) > cMaxSpeedSq)
525+
{
526+
newVelocity = {};
527+
}
528+
else if (glm::length2(itPrev->second.Velocity) > 0.0f)
529+
{
530+
newVelocity = itPrev->second.Velocity * cVelocityBlend + newVelocity * (1.0f - cVelocityBlend);
531+
}
532+
533+
info.Velocity = newVelocity;
534+
}
535+
}
536+
}
537+
538+
m_last[aPlayerId] = info;
539+
}

Code/client/Services/PartyMapOverlayService.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <unordered_map>
44
#include <unordered_set>
5+
#include <chrono>
56

67
#include <Events/UpdateEvent.h>
78
#include <Events/DisconnectedEvent.h>
@@ -30,6 +31,8 @@ struct PartyMapOverlayService
3031
{
3132
glm::vec3 Pos{};
3233
uint64_t Tick{0};
34+
glm::vec3 Velocity{};
35+
std::chrono::steady_clock::time_point SampleTime{};
3336
};
3437

3538
struct WorldspaceInfo
@@ -55,6 +58,7 @@ struct PartyMapOverlayService
5558
void PruneNonPartyEntries() noexcept;
5659

5760
void SetWaypointFor(uint32_t aPlayerId) noexcept;
61+
void StoreLastInfo(uint32_t aPlayerId, const glm::vec3& aPos, uint64_t aTick) noexcept;
5862

5963
// Approximate cross-world conversion using per-player anchors
6064
bool ComputeCrossWorldApprox(uint32_t aPlayerId, uint32_t aSrcWsId, const glm::vec3& aSrcPos,
@@ -78,4 +82,3 @@ struct PartyMapOverlayService
7882
entt::scoped_connection m_cellChangedConnection;
7983
entt::scoped_connection m_positionsConnection;
8084
};
81-

0 commit comments

Comments
 (0)