#include "PlayerRollback.hpp" #include "world/JoltPhysicsWorld.hpp" #include "world/CharacterBody.hpp" #include "world/Transform.hpp" #include #include #include namespace tw::net { PlayerRollback::PlayerRollback(JoltPhysicsWorld* physics) : m_physics(physics), m_last_reconciled_ack(0), m_rollback_count(0), m_last_correction_distance(0.0f), m_last_replayed_frames(0), m_last_ack_frame(0) { for (auto& record : m_records) { record.frame = 0; record.valid = false; record.input = glm::vec3(0.0f); record.predicted_position = glm::vec3(0.0f); } } void PlayerRollback::set_input(uint32_t frame, glm::vec3 input) { size_t idx = frame % RING_SIZE; m_records[idx].frame = frame; m_records[idx].valid = true; m_records[idx].input = input; } void PlayerRollback::record_prediction(uint32_t frame, glm::vec3 position) { size_t idx = frame % RING_SIZE; if (m_records[idx].frame == frame && m_records[idx].valid) { m_records[idx].predicted_position = position; } } bool PlayerRollback::apply_correction(uint32_t ack_frame, glm::vec3 authoritative_position, entt::entity player, entt::registry* registry, uint32_t current_frame) { if (ack_frame == 0 || ack_frame <= m_last_reconciled_ack || ack_frame >= current_frame) { return false; } m_last_reconciled_ack = ack_frame; m_last_ack_frame = ack_frame; Record& record = m_records[ack_frame % RING_SIZE]; const bool has_prediction = record.valid && record.frame == ack_frame; // With a prediction to compare against, an answer that already matches costs // nothing further. This is the case almost every frame. if (has_prediction) { float distance = glm::distance(record.predicted_position, authoritative_position); m_last_correction_distance = distance; if (distance < kPositionEpsilon) { return false; } } // Restoring the frame the answer describes keeps everything the simulation // derived from it, so only the character has to be moved. Without a stored // frame there is nothing to restore and the answer is taken as it stands. const bool restored = has_prediction && m_physics->rollback(ack_frame); place_character(player, registry, authoritative_position, !restored); replay_from(ack_frame, player, registry, current_frame); m_last_replayed_frames = current_frame - 1 - ack_frame; m_rollback_count++; spdlog::debug("Corrected at frame {}: distance {}, replayed {}, restored {}", ack_frame, m_last_correction_distance, m_last_replayed_frames, restored); return true; } void PlayerRollback::place_character(entt::entity player, entt::registry* registry, glm::vec3 position, bool clear_velocity) { CharacterBody* body = registry->try_get(player); Transform* transform = registry->try_get(player); if (!body) { return; } body->m_character->SetPosition(JPH::RVec3(position.x, position.y, position.z)); transform->set_position(position); if (clear_velocity) { body->m_character->SetLinearVelocity(JPH::Vec3::sZero()); body->m_desired_velocity = JPH::Vec3::sZero(); } } void PlayerRollback::replay_from(uint32_t from_frame, entt::entity player, entt::registry* registry, uint32_t current_frame) { for (uint32_t f = from_frame + 1; f < current_frame; ++f) { m_physics->step(f, tw::JoltPhysicsWorld::FIXED_DELTA_TIME, true); Transform* transform = registry->try_get(player); if (!transform) { continue; } // Frames the ring never saw still need an entry, or the answer to them // arrives with nothing to compare against and forces another correction. Record& replayed = m_records[f % RING_SIZE]; replayed.frame = f; replayed.valid = true; replayed.predicted_position = transform->position(); } } void PlayerRollback::reset_at(uint32_t frame) { m_last_reconciled_ack = frame; for (auto& record : m_records) { record.valid = false; } } }