From 080fb612ef8569844966fad6cf9df4de1837fb29 Mon Sep 17 00:00:00 2001 From: romain1717 Date: Sun, 18 Jan 2026 18:39:56 +0100 Subject: [PATCH 01/10] feat: update interpolated positions to use frame delta time for smoother rendering --- client/src/game/world/ClientWorld.cpp | 195 +++++++++++++++++++------- client/src/game/world/ClientWorld.hpp | 7 +- client/src/thread/ClientRuntime.cpp | 2 +- 3 files changed, 154 insertions(+), 50 deletions(-) diff --git a/client/src/game/world/ClientWorld.cpp b/client/src/game/world/ClientWorld.cpp index fecf6be2..aa887b8f 100644 --- a/client/src/game/world/ClientWorld.cpp +++ b/client/src/game/world/ClientWorld.cpp @@ -88,6 +88,23 @@ namespace World _entityLastSeen[id] = std::chrono::steady_clock::now(); } + if (!_snapshots.empty()) { + const TickSnapshot &prev = _snapshots.back(); + constexpr float dtTick = 1.f / 60.f; + const float dt = std::max(dtTick, dtTick * static_cast(snap.tick - prev.tick)); + + for (const auto &[id, st] : snap.entities) { + const auto itPrev = prev.entities.find(id); + if (itPrev == prev.entities.end()) + continue; + + const float dx = st.x - itPrev->second.x; + const float dy = st.y - itPrev->second.y; + + _velByNetId[static_cast(id)] = Vel2{dx / dt, dy / dt}; + } + } + if (!_snapshots.empty() && snap.tick <= _snapshots.back().tick) { auto it = _snapshots.end(); while (it != _snapshots.begin()) { @@ -102,7 +119,8 @@ namespace World while (_snapshots.size() > _maxSnapshots) _snapshots.pop_front(); - purgeStaleEntities(std::chrono::milliseconds(500)); + + purgeStaleEntities(std::chrono::milliseconds(3000)); } void ClientWorld::applyDestroy(const DestroyInfo &destroyInfo) @@ -256,55 +274,79 @@ namespace World posOpt->y += dy * SmoothFactor; } - void ClientWorld::updateInterpolatedPositions() - { - if (_snapshots.size() < 2) - return; +void ClientWorld::updateInterpolatedPositions(const float dt) +{ + if (_snapshots.empty()) + return; - auto &positions = _registry.getComponents(); - auto &drawables = _registry.getComponents(); - auto &renders = _registry.getComponents(); - auto &anims = _registry.getComponents(); + auto &positions = _registry.getComponents(); + auto &drawables = _registry.getComponents(); + auto &renders = _registry.getComponents(); + auto &anims = _registry.getComponents(); - const auto now = std::chrono::steady_clock::now(); - constexpr auto InterpDelay = std::chrono::milliseconds(100); + const auto now = std::chrono::steady_clock::now(); + constexpr auto InterpDelay = std::chrono::milliseconds(100); - std::optional idxA; - std::optional idxB; + if (_snapshots.size() < 2) { + const TickSnapshot &S = _snapshots.back(); - for (size_t i = 1; i < _snapshots.size(); ++i) { - if (_snapshots.at(i).arrivalTime > now - InterpDelay) { - idxA = i - 1; - idxB = i; - break; + for (const auto &[netId, bs] : S.entities) { + const uint32_t id = static_cast(netId); + if (_destroyed.contains(id)) + continue; + + if (!_entityMap.contains(netId)) + applyCreate(EntityCreate{netId, bs.x, bs.y, bs.z, bs.spriteId}); + + const Ecs::Entity e = _entityMap[netId]; + const auto entIdx = static_cast(e); + auto &posOpt = positions.at(entIdx); + if (!posOpt) + continue; + + float vx = 0.f; + float vy = 0.f; + + if (auto itV = _velByNetId.find(id); itV != _velByNetId.end()) { + vx = itV->second.vx; + vy = itV->second.vy; } - } - if (!idxA || !idxB) - return; + if (auto itT = _lastVelUpdate.find(id); itT == _lastVelUpdate.end() + || (now - itT->second) > std::chrono::milliseconds(250)) { + vx = 0.f; + vy = 0.f; + } - const TickSnapshot &A = _snapshots.at(*idxA); - const TickSnapshot &B = _snapshots.at(*idxB); + constexpr float MaxVisualStep = 12.f; - const float denom = std::chrono::duration(B.arrivalTime - A.arrivalTime).count(); - if (denom <= 0.f) - return; + posOpt->x += std::clamp(vx * dt, -MaxVisualStep, MaxVisualStep); + posOpt->y += std::clamp(vy * dt, -MaxVisualStep, MaxVisualStep); + posOpt->z = bs.z; - const float alpha = clamp(std::chrono::duration(now - InterpDelay - A.arrivalTime).count() / denom); + refreshSpriteIfChanged(e, bs.spriteId, drawables, anims, renders); + } + return; + } - for (const auto &[netId, bs] : B.entities) { - if (_destroyed.contains(static_cast(netId))) - continue; + std::optional idxA; + std::optional idxB; - if (std::cmp_equal(netId, _entityPlayerId)) { - if (!_entityMap.contains(netId)) - applyCreate(EntityCreate{netId, bs.x, bs.y, bs.z, bs.spriteId}); + for (size_t i = 1; i < _snapshots.size(); ++i) { + if (_snapshots.at(i).arrivalTime > now - InterpDelay) { + idxA = i - 1; + idxB = i; + break; + } + } - reconcileLocalPlayerWithServer(bs, positions); - if (auto it = _entityMap.find(netId); it != _entityMap.end()) - refreshSpriteIfChanged(it->second, bs.spriteId, drawables, anims, renders); + if (!idxA || !idxB) { + const TickSnapshot &S = _snapshots.back(); + + for (const auto &[netId, bs] : S.entities) { + const uint32_t id = static_cast(netId); + if (_destroyed.contains(id)) continue; - } if (!_entityMap.contains(netId)) applyCreate(EntityCreate{netId, bs.x, bs.y, bs.z, bs.spriteId}); @@ -315,29 +357,86 @@ namespace World if (!posOpt) continue; - const auto itA = A.entities.find(netId); - const NetState as = (itA != A.entities.end()) ? itA->second : bs; + float vx = 0.f; + float vy = 0.f; - constexpr float MaxVisualStep = 20.f; + if (auto itV = _velByNetId.find(id); itV != _velByNetId.end()) { + vx = itV->second.vx; + vy = itV->second.vy; + } - const float targetX = lerp(as.x, bs.x, alpha); - const float targetY = lerp(as.y, bs.y, alpha); + if (auto itT = _lastVelUpdate.find(id); itT == _lastVelUpdate.end() + || (now - itT->second) > std::chrono::milliseconds(250)) { + vx = 0.f; + vy = 0.f; + } - const float dx = targetX - posOpt->x; - const float dy = targetY - posOpt->y; + constexpr float MaxVisualStep = 12.f; - posOpt->x += std::clamp(dx, -MaxVisualStep, MaxVisualStep); - posOpt->y += std::clamp(dy, -MaxVisualStep, MaxVisualStep); + posOpt->x += std::clamp(vx * dt, -MaxVisualStep, MaxVisualStep); + posOpt->y += std::clamp(vy * dt, -MaxVisualStep, MaxVisualStep); posOpt->z = bs.z; refreshSpriteIfChanged(e, bs.spriteId, drawables, anims, renders); } + return; + } - while (_snapshots.size() > 2 && _snapshots.front().arrivalTime < A.arrivalTime) { - _snapshots.pop_front(); + const TickSnapshot &A = _snapshots.at(*idxA); + const TickSnapshot &B = _snapshots.at(*idxB); + + const float denom = std::chrono::duration(B.arrivalTime - A.arrivalTime).count(); + if (denom <= 0.f) + return; + + const float alpha = clamp(std::chrono::duration(now - InterpDelay - A.arrivalTime).count() / denom); + + for (const auto &[netId, bs] : B.entities) { + const uint32_t id = static_cast(netId); + if (_destroyed.contains(id)) + continue; + + if (std::cmp_equal(netId, _entityPlayerId)) { + if (!_entityMap.contains(netId)) + applyCreate(EntityCreate{netId, bs.x, bs.y, bs.z, bs.spriteId}); + + reconcileLocalPlayerWithServer(bs, positions); + if (auto it = _entityMap.find(netId); it != _entityMap.end()) + refreshSpriteIfChanged(it->second, bs.spriteId, drawables, anims, renders); + continue; } + + if (!_entityMap.contains(netId)) + applyCreate(EntityCreate{netId, bs.x, bs.y, bs.z, bs.spriteId}); + + const Ecs::Entity e = _entityMap[netId]; + const auto entIdx = static_cast(e); + auto &posOpt = positions.at(entIdx); + if (!posOpt) + continue; + + const auto itA = A.entities.find(netId); + const NetState as = (itA != A.entities.end()) ? itA->second : bs; + + constexpr float MaxVisualStep = 20.f; + + const float targetX = lerp(as.x, bs.x, alpha); + const float targetY = lerp(as.y, bs.y, alpha); + + const float dx = targetX - posOpt->x; + const float dy = targetY - posOpt->y; + + posOpt->x += std::clamp(dx, -MaxVisualStep, MaxVisualStep); + posOpt->y += std::clamp(dy, -MaxVisualStep, MaxVisualStep); + posOpt->z = bs.z; + + refreshSpriteIfChanged(e, bs.spriteId, drawables, anims, renders); } + while (_snapshots.size() > 2 && _snapshots.front().arrivalTime < A.arrivalTime) + _snapshots.pop_front(); +} + void ClientWorld::purgeStaleEntities(const std::chrono::milliseconds maxAge) { const auto now = std::chrono::steady_clock::now(); diff --git a/client/src/game/world/ClientWorld.hpp b/client/src/game/world/ClientWorld.hpp index 2ef52174..79f03f3b 100644 --- a/client/src/game/world/ClientWorld.hpp +++ b/client/src/game/world/ClientWorld.hpp @@ -99,7 +99,7 @@ namespace World /** * @brief Updates interpolated positions of entities for smooth rendering. */ - void updateInterpolatedPositions(); + void updateInterpolatedPositions(float dt); /** * @brief Applies local movement based on input flags for the player entity. @@ -203,5 +203,10 @@ namespace World * @param positions Sparse array of Position components. */ void reconcileLocalPlayerWithServer(const NetState &bs, Ecs::SparseArray &positions); + + struct Vel2 { float vx; float vy; }; + std::unordered_map _velByNetId; + std::unordered_map _lastVelUpdate; + }; } // namespace World diff --git a/client/src/thread/ClientRuntime.cpp b/client/src/thread/ClientRuntime.cpp index b5b65093..68c6cdc2 100644 --- a/client/src/thread/ClientRuntime.cpp +++ b/client/src/thread/ClientRuntime.cpp @@ -261,7 +261,7 @@ namespace Thread sendCombinedInput(); - _world->updateInterpolatedPositions(); + _world->updateInterpolatedPositions(frameDt); int steps = 0; while (accumulator >= FixedDt && steps < MaxStepsPerTick && clock::now() < deadline) { From 2ca454c3ea420e6c99fad1b291494b155e240a26 Mon Sep 17 00:00:00 2001 From: romain1717 Date: Sun, 18 Jan 2026 19:50:15 +0100 Subject: [PATCH 02/10] feat: enhance room join logic to account for game state --- client/src/thread/ClientRuntime.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/thread/ClientRuntime.cpp b/client/src/thread/ClientRuntime.cpp index 2dba429b..1ccdb25d 100644 --- a/client/src/thread/ClientRuntime.cpp +++ b/client/src/thread/ClientRuntime.cpp @@ -480,7 +480,7 @@ namespace Thread }); _tcpPacketRouter->sink()->onRoomJoinedSubscribe([&](uint32_t, const uint32_t) { - if (!_stateManager->is()) + if (!_stateManager->is() && !_stateManager->is()) _pendingJoinRoom.store(true, std::memory_order_release); else _pendingLobbyRefresh.store(true, std::memory_order_release); @@ -488,7 +488,7 @@ namespace Thread _tcpPacketRouter->sink()->onRoomUpdatedSubscribe([&](uint32_t, const RoomData &room) { _roomManager->setCurrentData(room); - if (!_stateManager->is()) + if (!_stateManager->is() && !_stateManager->is()) _pendingJoinRoom.store(true, std::memory_order_release); else _pendingLobbyRefresh.store(true, std::memory_order_release); From 18a95760118181601de8c8e79087f8d71f7121e1 Mon Sep 17 00:00:00 2001 From: romain1717 Date: Sun, 18 Jan 2026 20:02:44 +0100 Subject: [PATCH 03/10] feat: improve room management by adding inRoom state and refining leave logic --- client/src/thread/ClientRuntime.cpp | 36 +++++++++++++++-------------- client/src/thread/ClientRuntime.hpp | 1 + 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/client/src/thread/ClientRuntime.cpp b/client/src/thread/ClientRuntime.cpp index 1ccdb25d..00327ec8 100644 --- a/client/src/thread/ClientRuntime.cpp +++ b/client/src/thread/ClientRuntime.cpp @@ -355,8 +355,9 @@ namespace Thread _eventBus->on([this](const Engine::JoinRoomRequested &e) { const auto req = nextReqId(); _tcpClient->sendPacket(*_tcpPacketFactory.makeJoinRoom(req, e.roomId)); - if (const auto pkt = _tcpPacketFactory.makeRoomInfo(11)) + if (const auto pkt = _tcpPacketFactory.makeRoomInfo(nextReqId())) _tcpClient->sendPacket(*pkt); + _inRoom.store(true, std::memory_order_release); }); _eventBus->on([this](const Engine::ListRoomRequested &) { @@ -388,31 +389,28 @@ namespace Thread }); _eventBus->on([this](const Engine::LeaveRoomRequested &) { - if (const auto pkt = _tcpPacketFactory.makeLeaveRoom(nextReqId())) - _tcpClient->sendPacket(*pkt); - _pendingHome.store(true, std::memory_order_release); - }); + _inRoom.store(false, std::memory_order_release); - _eventBus->on([this](const Engine::UpdateRoomRequested &) { - const auto req = nextReqId(); - if (const auto pkt = _tcpPacketFactory.makeRoomInfo(req)) - _tcpClient->sendPacket(*pkt); - }); - - _eventBus->on([this](const Engine::LeaveRoomRequested &) { const auto req = nextReqId(); if (const auto pkt = _tcpPacketFactory.makeLeaveRoom(req)) - (void) _tcpClient->sendPacket(*pkt); + (void)_tcpClient->sendPacket(*pkt); + + _pendingHome.store(true, std::memory_order_release); + _world->reset(); { std::scoped_lock lock(_frameMutex); - if (_readRenderCommands) - _readRenderCommands->clear(); - if (_writeRenderCommands) - _writeRenderCommands->clear(); + if (_readRenderCommands) _readRenderCommands->clear(); + if (_writeRenderCommands) _writeRenderCommands->clear(); } }); + _eventBus->on([this](const Engine::UpdateRoomRequested &) { + const auto req = nextReqId(); + if (const auto pkt = _tcpPacketFactory.makeRoomInfo(req)) + _tcpClient->sendPacket(*pkt); + }); + _eventBus->on([this](const Engine::SendingMessage &e) { const auto req = nextReqId(); if (const auto pkt = _tcpPacketFactory.makeRoomMessage(req, e.message)) @@ -480,6 +478,8 @@ namespace Thread }); _tcpPacketRouter->sink()->onRoomJoinedSubscribe([&](uint32_t, const uint32_t) { + if (!_inRoom.load(std::memory_order_acquire)) + return; if (!_stateManager->is() && !_stateManager->is()) _pendingJoinRoom.store(true, std::memory_order_release); else @@ -487,6 +487,8 @@ namespace Thread }); _tcpPacketRouter->sink()->onRoomUpdatedSubscribe([&](uint32_t, const RoomData &room) { + if (!_inRoom.load(std::memory_order_acquire)) + return; _roomManager->setCurrentData(room); if (!_stateManager->is() && !_stateManager->is()) _pendingJoinRoom.store(true, std::memory_order_release); diff --git a/client/src/thread/ClientRuntime.hpp b/client/src/thread/ClientRuntime.hpp index 72772729..01b26bca 100644 --- a/client/src/thread/ClientRuntime.hpp +++ b/client/src/thread/ClientRuntime.hpp @@ -256,6 +256,7 @@ namespace Thread std::atomic_bool _pendingAuthOk{false}; ///> Atomic flag to indicate pending authentication OK std::atomic_bool _pendingScoreSubmit{false}; ///> Atomic flag to indicate pending score submission std::atomic_bool _pendingGameOver{false}; ///> Atomic flag to indicate pending game over + std::atomic_bool _inRoom{false}; std::atomic_uint32_t _lastScore{0}; ///> Atomic variable to store the last score From 5f9165e8be0279438e6c46d984aaf843af28daa0 Mon Sep 17 00:00:00 2001 From: romain1717 Date: Sun, 18 Jan 2026 20:10:45 +0100 Subject: [PATCH 04/10] feat: increase lobby refresh period from 500ms to 1000ms for improved performance --- client/src/ui/lobby/Lobby.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/ui/lobby/Lobby.hpp b/client/src/ui/lobby/Lobby.hpp index ace52105..45e2be95 100644 --- a/client/src/ui/lobby/Lobby.hpp +++ b/client/src/ui/lobby/Lobby.hpp @@ -196,7 +196,7 @@ namespace Engine bool _needUpdate = false; ///> Indicates if the lobby needs to be updated. std::chrono::steady_clock::time_point _lastRefresh{}; ///> Last refresh time point. - static constexpr auto refreshPeriod = std::chrono::milliseconds(500); ///> Refresh period. + static constexpr auto refreshPeriod = std::chrono::milliseconds(1000); ///> Refresh period. size_t _lobbyCapacity = 4; ///> Maximum capacity of the lobby. From 8d4fef15868b9a7640f085efe2865dcee91a1cab Mon Sep 17 00:00:00 2001 From: romain1717 Date: Sun, 18 Jan 2026 20:46:09 +0100 Subject: [PATCH 05/10] feat: refine interpolation logic and error handling during large lags --- client/src/game/world/ClientWorld.cpp | 243 +++++++++--------- client/src/game/world/ClientWorld.hpp | 10 +- client/src/thread/ClientRuntime.cpp | 31 +-- client/src/thread/ClientRuntime.hpp | 1 - client/src/ui/lobby/Lobby.hpp | 2 +- client/src/ui/rooms/RoomMenu.cpp | 3 +- .../PacketRouter/TCP/TCPPacketRouter.cpp | 1 + 7 files changed, 145 insertions(+), 146 deletions(-) diff --git a/client/src/game/world/ClientWorld.cpp b/client/src/game/world/ClientWorld.cpp index aa887b8f..0b03d644 100644 --- a/client/src/game/world/ClientWorld.cpp +++ b/client/src/game/world/ClientWorld.cpp @@ -85,9 +85,22 @@ namespace World for (const auto &[id, x, y, z, spriteId] : batch.entities) { _destroyed.erase(static_cast(id)); snap.entities[id] = NetState{x, y, z, spriteId}; - _entityLastSeen[id] = std::chrono::steady_clock::now(); + _entityLastSeen[id] = snap.arrivalTime; } + if (_hasLastArrival) { + const float iaMs = std::chrono::duration(snap.arrivalTime - _lastSnapArrival).count(); + const float targetMs = 16.666f; + const float j = std::fabs(iaMs - targetMs); + _emaJitterMs = _emaJitterMs * 0.9f + j * 0.1f; + + const float base = 100.f; + const float extra = std::clamp(_emaJitterMs * 2.0f, 0.f, 180.f); + _interpDelayMs = std::clamp(base + extra, 80.f, 280.f); + } + _lastSnapArrival = snap.arrivalTime; + _hasLastArrival = true; + if (!_snapshots.empty()) { const TickSnapshot &prev = _snapshots.back(); constexpr float dtTick = 1.f / 60.f; @@ -101,7 +114,9 @@ namespace World const float dx = st.x - itPrev->second.x; const float dy = st.y - itPrev->second.y; - _velByNetId[static_cast(id)] = Vel2{dx / dt, dy / dt}; + const uint32_t uid = static_cast(id); + _velByNetId[uid] = Vel2{dx / dt, dy / dt}; + _lastVelUpdate[uid] = snap.arrivalTime; } } @@ -114,8 +129,9 @@ namespace World it = prev; } _snapshots.insert(it, std::move(snap)); - } else + } else { _snapshots.push_back(std::move(snap)); + } while (_snapshots.size() > _maxSnapshots) _snapshots.pop_front(); @@ -274,79 +290,114 @@ namespace World posOpt->y += dy * SmoothFactor; } -void ClientWorld::updateInterpolatedPositions(const float dt) -{ - if (_snapshots.empty()) - return; + void ClientWorld::updateInterpolatedPositions(const float dt) + { + if (_snapshots.empty()) + return; - auto &positions = _registry.getComponents(); - auto &drawables = _registry.getComponents(); - auto &renders = _registry.getComponents(); - auto &anims = _registry.getComponents(); + auto &positions = _registry.getComponents(); + auto &drawables = _registry.getComponents(); + auto &renders = _registry.getComponents(); + auto &anims = _registry.getComponents(); - const auto now = std::chrono::steady_clock::now(); - constexpr auto InterpDelay = std::chrono::milliseconds(100); + const auto now = std::chrono::steady_clock::now(); + const auto interpDelay = std::chrono::milliseconds(static_cast(_interpDelayMs)); - if (_snapshots.size() < 2) { - const TickSnapshot &S = _snapshots.back(); + const auto applyExtrapOrHold = [&](const TickSnapshot &S) { + const float age = std::chrono::duration(now - S.arrivalTime).count(); + const float horizon = 0.20f; + const float k = clamp(1.f - (age / horizon)); - for (const auto &[netId, bs] : S.entities) { - const uint32_t id = static_cast(netId); - if (_destroyed.contains(id)) - continue; + for (const auto &[netId, bs] : S.entities) { + const uint32_t id = static_cast(netId); + if (_destroyed.contains(id)) + continue; - if (!_entityMap.contains(netId)) - applyCreate(EntityCreate{netId, bs.x, bs.y, bs.z, bs.spriteId}); + if (!_entityMap.contains(netId)) + applyCreate(EntityCreate{netId, bs.x, bs.y, bs.z, bs.spriteId}); - const Ecs::Entity e = _entityMap[netId]; - const auto entIdx = static_cast(e); - auto &posOpt = positions.at(entIdx); - if (!posOpt) - continue; + const Ecs::Entity e = _entityMap[netId]; + const auto entIdx = static_cast(e); + auto &posOpt = positions.at(entIdx); + if (!posOpt) + continue; - float vx = 0.f; - float vy = 0.f; + float vx = 0.f; + float vy = 0.f; - if (auto itV = _velByNetId.find(id); itV != _velByNetId.end()) { - vx = itV->second.vx; - vy = itV->second.vy; - } + if (auto itV = _velByNetId.find(id); itV != _velByNetId.end()) { + vx = itV->second.vx; + vy = itV->second.vy; + } + + if (auto itT = _lastVelUpdate.find(id); + itT == _lastVelUpdate.end() || (now - itT->second) > std::chrono::milliseconds(250)) { + vx = 0.f; + vy = 0.f; + } + + vx *= k; + vy *= k; + if (age > horizon) { + vx = 0.f; + vy = 0.f; + } - if (auto itT = _lastVelUpdate.find(id); itT == _lastVelUpdate.end() - || (now - itT->second) > std::chrono::milliseconds(250)) { - vx = 0.f; - vy = 0.f; + constexpr float MaxVisualStep = 12.f; + + posOpt->x += std::clamp(vx * dt, -MaxVisualStep, MaxVisualStep); + posOpt->y += std::clamp(vy * dt, -MaxVisualStep, MaxVisualStep); + posOpt->z = bs.z; + + refreshSpriteIfChanged(e, bs.spriteId, drawables, anims, renders); } + }; - constexpr float MaxVisualStep = 12.f; + if (_snapshots.size() < 2) { + applyExtrapOrHold(_snapshots.back()); + return; + } - posOpt->x += std::clamp(vx * dt, -MaxVisualStep, MaxVisualStep); - posOpt->y += std::clamp(vy * dt, -MaxVisualStep, MaxVisualStep); - posOpt->z = bs.z; + std::optional idxA; + std::optional idxB; - refreshSpriteIfChanged(e, bs.spriteId, drawables, anims, renders); + for (size_t i = 1; i < _snapshots.size(); ++i) { + if (_snapshots.at(i).arrivalTime > now - interpDelay) { + idxA = i - 1; + idxB = i; + break; + } + } + + if (!idxA || !idxB) { + applyExtrapOrHold(_snapshots.back()); + return; } - return; - } - std::optional idxA; - std::optional idxB; + const TickSnapshot &A = _snapshots.at(*idxA); + const TickSnapshot &B = _snapshots.at(*idxB); - for (size_t i = 1; i < _snapshots.size(); ++i) { - if (_snapshots.at(i).arrivalTime > now - InterpDelay) { - idxA = i - 1; - idxB = i; - break; + const float denom = std::chrono::duration(B.arrivalTime - A.arrivalTime).count(); + if (denom <= 0.f) { + applyExtrapOrHold(_snapshots.back()); + return; } - } - if (!idxA || !idxB) { - const TickSnapshot &S = _snapshots.back(); + const float alpha = clamp(std::chrono::duration(now - interpDelay - A.arrivalTime).count() / denom); + + for (const auto &[netId, bs] : B.entities) { + if (const auto id = static_cast(netId); _destroyed.contains(id)) + continue; + + if (std::cmp_equal(netId, _entityPlayerId)) { + if (!_entityMap.contains(netId)) + applyCreate(EntityCreate{netId, bs.x, bs.y, bs.z, bs.spriteId}); - for (const auto &[netId, bs] : S.entities) { - const uint32_t id = static_cast(netId); - if (_destroyed.contains(id)) + reconcileLocalPlayerWithServer(bs, positions); + if (auto it = _entityMap.find(netId); it != _entityMap.end()) + refreshSpriteIfChanged(it->second, bs.spriteId, drawables, anims, renders); continue; + } if (!_entityMap.contains(netId)) applyCreate(EntityCreate{netId, bs.x, bs.y, bs.z, bs.spriteId}); @@ -357,86 +408,28 @@ void ClientWorld::updateInterpolatedPositions(const float dt) if (!posOpt) continue; - float vx = 0.f; - float vy = 0.f; + const auto itA = A.entities.find(netId); + const NetState as = (itA != A.entities.end()) ? itA->second : bs; - if (auto itV = _velByNetId.find(id); itV != _velByNetId.end()) { - vx = itV->second.vx; - vy = itV->second.vy; - } + constexpr float MaxVisualStep = 20.f; - if (auto itT = _lastVelUpdate.find(id); itT == _lastVelUpdate.end() - || (now - itT->second) > std::chrono::milliseconds(250)) { - vx = 0.f; - vy = 0.f; - } + const float targetX = lerp(as.x, bs.x, alpha); + const float targetY = lerp(as.y, bs.y, alpha); - constexpr float MaxVisualStep = 12.f; + const float dx = targetX - posOpt->x; + const float dy = targetY - posOpt->y; - posOpt->x += std::clamp(vx * dt, -MaxVisualStep, MaxVisualStep); - posOpt->y += std::clamp(vy * dt, -MaxVisualStep, MaxVisualStep); + posOpt->x += std::clamp(dx, -MaxVisualStep, MaxVisualStep); + posOpt->y += std::clamp(dy, -MaxVisualStep, MaxVisualStep); posOpt->z = bs.z; refreshSpriteIfChanged(e, bs.spriteId, drawables, anims, renders); } - return; - } - - const TickSnapshot &A = _snapshots.at(*idxA); - const TickSnapshot &B = _snapshots.at(*idxB); - - const float denom = std::chrono::duration(B.arrivalTime - A.arrivalTime).count(); - if (denom <= 0.f) - return; - - const float alpha = clamp(std::chrono::duration(now - InterpDelay - A.arrivalTime).count() / denom); - - for (const auto &[netId, bs] : B.entities) { - const uint32_t id = static_cast(netId); - if (_destroyed.contains(id)) - continue; - - if (std::cmp_equal(netId, _entityPlayerId)) { - if (!_entityMap.contains(netId)) - applyCreate(EntityCreate{netId, bs.x, bs.y, bs.z, bs.spriteId}); - - reconcileLocalPlayerWithServer(bs, positions); - if (auto it = _entityMap.find(netId); it != _entityMap.end()) - refreshSpriteIfChanged(it->second, bs.spriteId, drawables, anims, renders); - continue; - } - - if (!_entityMap.contains(netId)) - applyCreate(EntityCreate{netId, bs.x, bs.y, bs.z, bs.spriteId}); - - const Ecs::Entity e = _entityMap[netId]; - const auto entIdx = static_cast(e); - auto &posOpt = positions.at(entIdx); - if (!posOpt) - continue; - - const auto itA = A.entities.find(netId); - const NetState as = (itA != A.entities.end()) ? itA->second : bs; - - constexpr float MaxVisualStep = 20.f; - - const float targetX = lerp(as.x, bs.x, alpha); - const float targetY = lerp(as.y, bs.y, alpha); - const float dx = targetX - posOpt->x; - const float dy = targetY - posOpt->y; - - posOpt->x += std::clamp(dx, -MaxVisualStep, MaxVisualStep); - posOpt->y += std::clamp(dy, -MaxVisualStep, MaxVisualStep); - posOpt->z = bs.z; - - refreshSpriteIfChanged(e, bs.spriteId, drawables, anims, renders); + while (_snapshots.size() > 2 && _snapshots.front().arrivalTime < A.arrivalTime) + _snapshots.pop_front(); } - while (_snapshots.size() > 2 && _snapshots.front().arrivalTime < A.arrivalTime) - _snapshots.pop_front(); -} - void ClientWorld::purgeStaleEntities(const std::chrono::milliseconds maxAge) { const auto now = std::chrono::steady_clock::now(); diff --git a/client/src/game/world/ClientWorld.hpp b/client/src/game/world/ClientWorld.hpp index 79f03f3b..f271b691 100644 --- a/client/src/game/world/ClientWorld.hpp +++ b/client/src/game/world/ClientWorld.hpp @@ -204,9 +204,17 @@ namespace World */ void reconcileLocalPlayerWithServer(const NetState &bs, Ecs::SparseArray &positions); - struct Vel2 { float vx; float vy; }; + struct Vel2 { + float vx; + float vy; + }; + std::unordered_map _velByNetId; std::unordered_map _lastVelUpdate; + std::chrono::steady_clock::time_point _lastSnapArrival{}; + bool _hasLastArrival = false; + float _emaJitterMs = 0.f; + float _interpDelayMs = 100.f; }; } // namespace World diff --git a/client/src/thread/ClientRuntime.cpp b/client/src/thread/ClientRuntime.cpp index 00327ec8..f1b29f8f 100644 --- a/client/src/thread/ClientRuntime.cpp +++ b/client/src/thread/ClientRuntime.cpp @@ -357,7 +357,6 @@ namespace Thread _tcpClient->sendPacket(*_tcpPacketFactory.makeJoinRoom(req, e.roomId)); if (const auto pkt = _tcpPacketFactory.makeRoomInfo(nextReqId())) _tcpClient->sendPacket(*pkt); - _inRoom.store(true, std::memory_order_release); }); _eventBus->on([this](const Engine::ListRoomRequested &) { @@ -384,28 +383,30 @@ namespace Thread }); _eventBus->on([this](const Engine::StartGameRequested &) { + if (!_stateManager->is()) + return; if (const auto pkt = _tcpPacketFactory.makeStartGame(nextReqId())) _tcpClient->sendPacket(*pkt); }); _eventBus->on([this](const Engine::LeaveRoomRequested &) { - _inRoom.store(false, std::memory_order_release); - - const auto req = nextReqId(); - if (const auto pkt = _tcpPacketFactory.makeLeaveRoom(req)) - (void)_tcpClient->sendPacket(*pkt); - + if (const auto pkt = _tcpPacketFactory.makeLeaveRoom(nextReqId())) + _tcpClient->sendPacket(*pkt); _pendingHome.store(true, std::memory_order_release); - _world->reset(); { std::scoped_lock lock(_frameMutex); - if (_readRenderCommands) _readRenderCommands->clear(); - if (_writeRenderCommands) _writeRenderCommands->clear(); + if (_readRenderCommands) + _readRenderCommands->clear(); + if (_writeRenderCommands) + _writeRenderCommands->clear(); } }); _eventBus->on([this](const Engine::UpdateRoomRequested &) { + if (!_stateManager->is() || _pendingHome) + return; + const auto req = nextReqId(); if (const auto pkt = _tcpPacketFactory.makeRoomInfo(req)) _tcpClient->sendPacket(*pkt); @@ -478,19 +479,15 @@ namespace Thread }); _tcpPacketRouter->sink()->onRoomJoinedSubscribe([&](uint32_t, const uint32_t) { - if (!_inRoom.load(std::memory_order_acquire)) - return; - if (!_stateManager->is() && !_stateManager->is()) + if (!_stateManager->is()) _pendingJoinRoom.store(true, std::memory_order_release); else _pendingLobbyRefresh.store(true, std::memory_order_release); }); _tcpPacketRouter->sink()->onRoomUpdatedSubscribe([&](uint32_t, const RoomData &room) { - if (!_inRoom.load(std::memory_order_acquire)) - return; _roomManager->setCurrentData(room); - if (!_stateManager->is() && !_stateManager->is()) + if (!_stateManager->is()) _pendingJoinRoom.store(true, std::memory_order_release); else _pendingLobbyRefresh.store(true, std::memory_order_release); @@ -573,4 +570,4 @@ namespace Thread (void) _udpClient->sendPacket(*pingPkt); } -} // namespace Thread +} // namespace Thread \ No newline at end of file diff --git a/client/src/thread/ClientRuntime.hpp b/client/src/thread/ClientRuntime.hpp index 01b26bca..72772729 100644 --- a/client/src/thread/ClientRuntime.hpp +++ b/client/src/thread/ClientRuntime.hpp @@ -256,7 +256,6 @@ namespace Thread std::atomic_bool _pendingAuthOk{false}; ///> Atomic flag to indicate pending authentication OK std::atomic_bool _pendingScoreSubmit{false}; ///> Atomic flag to indicate pending score submission std::atomic_bool _pendingGameOver{false}; ///> Atomic flag to indicate pending game over - std::atomic_bool _inRoom{false}; std::atomic_uint32_t _lastScore{0}; ///> Atomic variable to store the last score diff --git a/client/src/ui/lobby/Lobby.hpp b/client/src/ui/lobby/Lobby.hpp index 45e2be95..5ba20b43 100644 --- a/client/src/ui/lobby/Lobby.hpp +++ b/client/src/ui/lobby/Lobby.hpp @@ -195,7 +195,7 @@ namespace Engine bool _needUpdate = false; ///> Indicates if the lobby needs to be updated. - std::chrono::steady_clock::time_point _lastRefresh{}; ///> Last refresh time point. + std::chrono::steady_clock::time_point _lastRefresh{}; ///> Last refresh time point. static constexpr auto refreshPeriod = std::chrono::milliseconds(1000); ///> Refresh period. size_t _lobbyCapacity = 4; ///> Maximum capacity of the lobby. diff --git a/client/src/ui/rooms/RoomMenu.cpp b/client/src/ui/rooms/RoomMenu.cpp index b45abdda..48118933 100644 --- a/client/src/ui/rooms/RoomMenu.cpp +++ b/client/src/ui/rooms/RoomMenu.cpp @@ -414,6 +414,7 @@ namespace Engine if (_root.join->onClickReleased(mx, my, [&] { _page = Page::List; _listRooms = true; + _createRoom = false; _list.scroll = 0.f; _list.lastRefresh = {}; updateListRooms(); @@ -448,7 +449,7 @@ namespace Engine return; } if (frame.key == Key::Enter) { - _createRoom = true; + _createRoom = !_createRoom; _createRoomName = _create.roomNameField ? _create.roomNameField->value() : "default"; return; } diff --git a/server/src/packet/PacketRouter/TCP/TCPPacketRouter.cpp b/server/src/packet/PacketRouter/TCP/TCPPacketRouter.cpp index 6ec8c166..32d1770d 100644 --- a/server/src/packet/PacketRouter/TCP/TCPPacketRouter.cpp +++ b/server/src/packet/PacketRouter/TCP/TCPPacketRouter.cpp @@ -243,6 +243,7 @@ namespace Net void TCPPacketRouter::onCreateRoom(const sockaddr_in &addr, const uint32_t req, TCP::Reader &r) const { + std::cout << "Received CREATE_ROOM request\n"; if (const auto currentRoom = _rooms->getRoomIdOfPlayer(_sessions->getOrCreateSession(addr)); currentRoom != 0) (void) _rooms->removePlayer(_sessions->getOrCreateSession(addr)); std::string roomName; From 73448132816aa1f8dd0f30451065ad3cb6bad9f5 Mon Sep 17 00:00:00 2001 From: romain1717 Date: Sun, 18 Jan 2026 20:49:50 +0100 Subject: [PATCH 06/10] feat: prevent room updates when in game state to improve state management --- client/src/thread/ClientRuntime.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/thread/ClientRuntime.cpp b/client/src/thread/ClientRuntime.cpp index f1b29f8f..d6221eb5 100644 --- a/client/src/thread/ClientRuntime.cpp +++ b/client/src/thread/ClientRuntime.cpp @@ -486,6 +486,8 @@ namespace Thread }); _tcpPacketRouter->sink()->onRoomUpdatedSubscribe([&](uint32_t, const RoomData &room) { + if (_stateManager->is()) + return; _roomManager->setCurrentData(room); if (!_stateManager->is()) _pendingJoinRoom.store(true, std::memory_order_release); From ccbe9ad2968e8822e68cee839c2de92d5f85ec32 Mon Sep 17 00:00:00 2001 From: romain1717 Date: Sun, 18 Jan 2026 21:14:48 +0100 Subject: [PATCH 07/10] feat: simplify interpolation logic by removing unnecessary parameters and optimizing snapshot handling --- client/src/game/world/ClientWorld.cpp | 168 ++++++++++---------------- client/src/game/world/ClientWorld.hpp | 24 ++-- client/src/thread/ClientRuntime.cpp | 2 +- 3 files changed, 74 insertions(+), 120 deletions(-) diff --git a/client/src/game/world/ClientWorld.cpp b/client/src/game/world/ClientWorld.cpp index 0b03d644..2373c9e8 100644 --- a/client/src/game/world/ClientWorld.cpp +++ b/client/src/game/world/ClientWorld.cpp @@ -34,6 +34,12 @@ namespace World _registry.registerComponent(); _registry.registerComponent(); _registry.registerComponent(); + + // if (_soundRegistry) { + // _powerUpStandardSoundHandle = _soundRegistry->lo("sounds/powerup.wav"); + // _powerUpLaserSoundHandle = _soundRegistry->loadSound("sounds/powerup_laser.wav"); + // _powerUpBubbleSoundHandle = _soundRegistry->loadSound("sounds/powerup_bubble.wav"); + // } } void ClientWorld::step(const float dt) @@ -85,39 +91,7 @@ namespace World for (const auto &[id, x, y, z, spriteId] : batch.entities) { _destroyed.erase(static_cast(id)); snap.entities[id] = NetState{x, y, z, spriteId}; - _entityLastSeen[id] = snap.arrivalTime; - } - - if (_hasLastArrival) { - const float iaMs = std::chrono::duration(snap.arrivalTime - _lastSnapArrival).count(); - const float targetMs = 16.666f; - const float j = std::fabs(iaMs - targetMs); - _emaJitterMs = _emaJitterMs * 0.9f + j * 0.1f; - - const float base = 100.f; - const float extra = std::clamp(_emaJitterMs * 2.0f, 0.f, 180.f); - _interpDelayMs = std::clamp(base + extra, 80.f, 280.f); - } - _lastSnapArrival = snap.arrivalTime; - _hasLastArrival = true; - - if (!_snapshots.empty()) { - const TickSnapshot &prev = _snapshots.back(); - constexpr float dtTick = 1.f / 60.f; - const float dt = std::max(dtTick, dtTick * static_cast(snap.tick - prev.tick)); - - for (const auto &[id, st] : snap.entities) { - const auto itPrev = prev.entities.find(id); - if (itPrev == prev.entities.end()) - continue; - - const float dx = st.x - itPrev->second.x; - const float dy = st.y - itPrev->second.y; - - const uint32_t uid = static_cast(id); - _velByNetId[uid] = Vel2{dx / dt, dy / dt}; - _lastVelUpdate[uid] = snap.arrivalTime; - } + _entityLastSeen[id] = std::chrono::steady_clock::now(); } if (!_snapshots.empty() && snap.tick <= _snapshots.back().tick) { @@ -129,14 +103,12 @@ namespace World it = prev; } _snapshots.insert(it, std::move(snap)); - } else { + } else _snapshots.push_back(std::move(snap)); - } while (_snapshots.size() > _maxSnapshots) _snapshots.pop_front(); - - purgeStaleEntities(std::chrono::milliseconds(3000)); + purgeStaleEntities(std::chrono::milliseconds(500)); } void ClientWorld::applyDestroy(const DestroyInfo &destroyInfo) @@ -217,6 +189,11 @@ namespace World if (data.spriteId == 6 && _soundRegistry && sprite.shootSoundHandle != Graphics::InvalidAudio) _soundRegistry->playSound(sprite.shootSoundHandle); + + if (_soundRegistry) { + if (data.spriteId == 21 && _powerUpStandardSoundHandle != Graphics::InvalidAudio) + _soundRegistry->playSound(_powerUpStandardSoundHandle); + } } catch (const std::exception &e) { std::cerr << "{ClientWorld::applyCreate} " << e.what() << std::endl; } @@ -290,9 +267,9 @@ namespace World posOpt->y += dy * SmoothFactor; } - void ClientWorld::updateInterpolatedPositions(const float dt) + void ClientWorld::updateInterpolatedPositions() { - if (_snapshots.empty()) + if (_snapshots.size() < 2) return; auto &positions = _registry.getComponents(); @@ -301,92 +278,33 @@ namespace World auto &anims = _registry.getComponents(); const auto now = std::chrono::steady_clock::now(); - const auto interpDelay = std::chrono::milliseconds(static_cast(_interpDelayMs)); - - const auto applyExtrapOrHold = [&](const TickSnapshot &S) { - const float age = std::chrono::duration(now - S.arrivalTime).count(); - const float horizon = 0.20f; - const float k = clamp(1.f - (age / horizon)); - - for (const auto &[netId, bs] : S.entities) { - const uint32_t id = static_cast(netId); - if (_destroyed.contains(id)) - continue; - - if (!_entityMap.contains(netId)) - applyCreate(EntityCreate{netId, bs.x, bs.y, bs.z, bs.spriteId}); - - const Ecs::Entity e = _entityMap[netId]; - const auto entIdx = static_cast(e); - auto &posOpt = positions.at(entIdx); - if (!posOpt) - continue; - - float vx = 0.f; - float vy = 0.f; - - if (auto itV = _velByNetId.find(id); itV != _velByNetId.end()) { - vx = itV->second.vx; - vy = itV->second.vy; - } - - if (auto itT = _lastVelUpdate.find(id); - itT == _lastVelUpdate.end() || (now - itT->second) > std::chrono::milliseconds(250)) { - vx = 0.f; - vy = 0.f; - } - - vx *= k; - vy *= k; - if (age > horizon) { - vx = 0.f; - vy = 0.f; - } - - constexpr float MaxVisualStep = 12.f; - - posOpt->x += std::clamp(vx * dt, -MaxVisualStep, MaxVisualStep); - posOpt->y += std::clamp(vy * dt, -MaxVisualStep, MaxVisualStep); - posOpt->z = bs.z; - - refreshSpriteIfChanged(e, bs.spriteId, drawables, anims, renders); - } - }; - - if (_snapshots.size() < 2) { - applyExtrapOrHold(_snapshots.back()); - return; - } + constexpr auto InterpDelay = std::chrono::milliseconds(100); std::optional idxA; std::optional idxB; for (size_t i = 1; i < _snapshots.size(); ++i) { - if (_snapshots.at(i).arrivalTime > now - interpDelay) { + if (_snapshots.at(i).arrivalTime > now - InterpDelay) { idxA = i - 1; idxB = i; break; } } - if (!idxA || !idxB) { - applyExtrapOrHold(_snapshots.back()); + if (!idxA || !idxB) return; - } const TickSnapshot &A = _snapshots.at(*idxA); const TickSnapshot &B = _snapshots.at(*idxB); const float denom = std::chrono::duration(B.arrivalTime - A.arrivalTime).count(); - if (denom <= 0.f) { - applyExtrapOrHold(_snapshots.back()); + if (denom <= 0.f) return; - } - const float alpha = clamp(std::chrono::duration(now - interpDelay - A.arrivalTime).count() / denom); + const float alpha = clamp(std::chrono::duration(now - InterpDelay - A.arrivalTime).count() / denom); for (const auto &[netId, bs] : B.entities) { - if (const auto id = static_cast(netId); _destroyed.contains(id)) + if (_destroyed.contains(static_cast(netId))) continue; if (std::cmp_equal(netId, _entityPlayerId)) { @@ -396,6 +314,44 @@ namespace World reconcileLocalPlayerWithServer(bs, positions); if (auto it = _entityMap.find(netId); it != _entityMap.end()) refreshSpriteIfChanged(it->second, bs.spriteId, drawables, anims, renders); + + bool hasBubbleNow = false; + bool hasLaserNow = false; + for (const auto &[otherNetId, otherState] : B.entities) { + const float dx = otherState.x - bs.x; + const float dy = otherState.y - bs.y; + const float distSq = dx * dx + dy * dy; + + if (otherState.spriteId == 20 && distSq < 50.f * 50.f) + hasBubbleNow = true; + else if (otherState.spriteId == 17 && distSq < 100.f * 100.f) + hasLaserNow = true; + + if (hasBubbleNow && !_bubbleSoundPlaying && _soundRegistry + && _powerUpBubbleSoundHandle != Graphics::InvalidAudio) { + _activePowerUpType = 19; + _soundRegistry->playSound(_powerUpBubbleSoundHandle); + _bubbleSoundPlaying = true; + } else if (!hasBubbleNow && _bubbleSoundPlaying && _soundRegistry) { + // _soundRegistry->stopSound(_powerUpBubbleSoundHandle); + _bubbleSoundPlaying = false; + if (_activePowerUpType == 19) + _activePowerUpType = 0; + } + + if (hasLaserNow && !_laserSoundPlaying && _soundRegistry + && _powerUpLaserSoundHandle != Graphics::InvalidAudio) { + _activePowerUpType = 18; + _soundRegistry->playSound(_powerUpLaserSoundHandle); + _laserSoundPlaying = true; + } else if (!hasLaserNow && _laserSoundPlaying && _soundRegistry) { + // _soundRegistry->stopSound(_powerUpLaserSoundHandle); + _laserSoundPlaying = false; + if (_activePowerUpType == 18) + _activePowerUpType = 0; + } + continue; + } continue; } @@ -426,8 +382,9 @@ namespace World refreshSpriteIfChanged(e, bs.spriteId, drawables, anims, renders); } - while (_snapshots.size() > 2 && _snapshots.front().arrivalTime < A.arrivalTime) + while (_snapshots.size() > 2 && _snapshots.front().arrivalTime < A.arrivalTime) { _snapshots.pop_front(); + } } void ClientWorld::purgeStaleEntities(const std::chrono::milliseconds maxAge) @@ -493,3 +450,4 @@ namespace World return out; } } // namespace World + \ No newline at end of file diff --git a/client/src/game/world/ClientWorld.hpp b/client/src/game/world/ClientWorld.hpp index f271b691..19b14f33 100644 --- a/client/src/game/world/ClientWorld.hpp +++ b/client/src/game/world/ClientWorld.hpp @@ -99,7 +99,7 @@ namespace World /** * @brief Updates interpolated positions of entities for smooth rendering. */ - void updateInterpolatedPositions(float dt); + void updateInterpolatedPositions(); /** * @brief Applies local movement based on input flags for the player entity. @@ -197,24 +197,20 @@ namespace World int _entityPlayerId = -1; ///> Client session ID std::unordered_map _scoresByPlayerId; ///> Map of player IDs to their scores + Graphics::AudioHandle _powerUpStandardSoundHandle = + Graphics::InvalidAudio; ///> Handle for standard power-up sound + Graphics::AudioHandle _powerUpLaserSoundHandle = Graphics::InvalidAudio; ///> Handle for laser power-up sound + Graphics::AudioHandle _powerUpBubbleSoundHandle = Graphics::InvalidAudio; ///> Handle for bubble power-up sound + + uint32_t _activePowerUpType = 0; ///> Currently active power-up type (0=none, 18=laser, 19=bubble) + bool _bubbleSoundPlaying = false; ///> Whether bubble sound is currently playing + bool _laserSoundPlaying = false; ///> Whether laser sound is currently playing + /** * @brief Reconciles the local player entity's position with the server's authoritative state. * @param bs The network state received from the server. * @param positions Sparse array of Position components. */ void reconcileLocalPlayerWithServer(const NetState &bs, Ecs::SparseArray &positions); - - struct Vel2 { - float vx; - float vy; - }; - - std::unordered_map _velByNetId; - std::unordered_map _lastVelUpdate; - - std::chrono::steady_clock::time_point _lastSnapArrival{}; - bool _hasLastArrival = false; - float _emaJitterMs = 0.f; - float _interpDelayMs = 100.f; }; } // namespace World diff --git a/client/src/thread/ClientRuntime.cpp b/client/src/thread/ClientRuntime.cpp index d6221eb5..2f7d3dba 100644 --- a/client/src/thread/ClientRuntime.cpp +++ b/client/src/thread/ClientRuntime.cpp @@ -265,7 +265,7 @@ namespace Thread sendCombinedInput(); sendPingIfDue(); - _world->updateInterpolatedPositions(frameDt); + _world->updateInterpolatedPositions(); int steps = 0; while (accumulator >= FixedDt && steps < MaxStepsPerTick && clock::now() < deadline) { From 706992f6235296ad8d96663a64dc2cd11ed2ae3e Mon Sep 17 00:00:00 2001 From: romain1717 Date: Sun, 18 Jan 2026 21:25:53 +0100 Subject: [PATCH 08/10] feat: reset game world on start game request to ensure clean state --- client/src/game/world/ClientWorld.cpp | 3 ++- client/src/thread/ClientRuntime.cpp | 1 + server/src/packet/PacketRouter/TCP/TCPPacketRouter.cpp | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/game/world/ClientWorld.cpp b/client/src/game/world/ClientWorld.cpp index 0cfe6450..2f581402 100644 --- a/client/src/game/world/ClientWorld.cpp +++ b/client/src/game/world/ClientWorld.cpp @@ -431,7 +431,8 @@ namespace World void ClientWorld::reset() { - _registry = Ecs::Registry{}; + _registry.clear(); + _registry = Ecs::Registry(); _entityMap.clear(); _entityLastSeen.clear(); _scoresByPlayerId.clear(); diff --git a/client/src/thread/ClientRuntime.cpp b/client/src/thread/ClientRuntime.cpp index cb1e9836..ca723110 100644 --- a/client/src/thread/ClientRuntime.cpp +++ b/client/src/thread/ClientRuntime.cpp @@ -387,6 +387,7 @@ namespace Thread _eventBus->on([this](const Engine::StartGameRequested &) { if (!_stateManager->is()) return; + _world->reset(); if (const auto pkt = _tcpPacketFactory.makeStartGame(nextReqId())) _tcpClient->sendPacket(*pkt); }); diff --git a/server/src/packet/PacketRouter/TCP/TCPPacketRouter.cpp b/server/src/packet/PacketRouter/TCP/TCPPacketRouter.cpp index 05099860..8513a0fa 100644 --- a/server/src/packet/PacketRouter/TCP/TCPPacketRouter.cpp +++ b/server/src/packet/PacketRouter/TCP/TCPPacketRouter.cpp @@ -243,7 +243,6 @@ namespace Net void TCPPacketRouter::onCreateRoom(const sockaddr_in &addr, const uint32_t req, TCP::Reader &r) const { - std::cout << "Received CREATE_ROOM request\n"; if (const auto currentRoom = _rooms->getRoomIdOfPlayer(_sessions->getOrCreateSession(addr)); currentRoom != 0) (void) _rooms->removePlayer(_sessions->getOrCreateSession(addr)); std::string roomName; From e0326370646a71599331a411683f65b4ca2a670b Mon Sep 17 00:00:00 2001 From: romain1717 Date: Sun, 18 Jan 2026 21:32:16 +0100 Subject: [PATCH 09/10] feat: ensure room creation is always triggered on Enter key press --- client/src/ui/rooms/RoomMenu.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/ui/rooms/RoomMenu.cpp b/client/src/ui/rooms/RoomMenu.cpp index a0ecea95..283427b8 100644 --- a/client/src/ui/rooms/RoomMenu.cpp +++ b/client/src/ui/rooms/RoomMenu.cpp @@ -453,7 +453,7 @@ namespace Engine return; } if (frame.key == Key::Enter) { - _createRoom = !_createRoom; + _createRoom = true; _createRoomName = _create.roomNameField ? _create.roomNameField->value() : "default"; return; } From a2dd812f814dfd05b49eb0f6ba3d734b3249a16c Mon Sep 17 00:00:00 2001 From: romain1717 Date: Sun, 18 Jan 2026 21:58:31 +0100 Subject: [PATCH 10/10] feat: send accept packet multiple times to improve connection reliability --- server/src/game/gameServer/GameServer.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/game/gameServer/GameServer.cpp b/server/src/game/gameServer/GameServer.cpp index 17eb854f..fc2ac26b 100644 --- a/server/src/game/gameServer/GameServer.cpp +++ b/server/src/game/gameServer/GameServer.cpp @@ -113,8 +113,10 @@ namespace const sockaddr_in *addr = sessionsL->getUdpAddress(event.sessionId); if (!addr) return; - if (const auto pkt = factoryL->createAcceptPacket(*addr, event.netPlayerId)) - (void) serverL->sendPacket(*pkt); + if (const auto pkt = factoryL->createAcceptPacket(*addr, event.netPlayerId)) { + for (int i = 0; i < 3; i++) + (void) serverL->sendPacket(*pkt); + } }); }