feat: implement client-side prediction and reconciliation system
Add client-side prediction (t_p1_pred) with: - scripts/network/snapshot.gd — lightweight snapshot resource with to_dict/from_dict serialization for RPC. Stores position (Vector3), rotation (Quaternion), velocity (Vector3), grounded (bool). - scripts/network/client_prediction.gd — prediction/reconciliation controller. 64-entry ring buffer of local snapshots, sends inputs to server each physics tick (128Hz, ENet channel 0), detects misprediction on server state arrival, rewinds and re-applies unconfirmed inputs. Also supports remote player interpolation. - scripts/network/network_manager.gd — new RPC endpoints for client prediction: send_client_input (client->server, ch 0) and send_server_state (server->client, ch 1). New signals for routing. - client/characters/character/fps_character_controller.gd — prediction hooks in _physics_process: on_before_tick() captures pre-input snapshot, on_after_tick() sends input to server. Client prediction path uses local movement (instant feedback) instead of reading from SimulationServer entity. Architecture: Each tick: client applies input → predicts new state locally → sends input to server → server returns authoritative state → client compares and reconciles if mismatch.
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
## Snapshot — Lightweight world-state snapshot for client prediction & reconciliation.
|
||||
##
|
||||
## Stores a point-in-time record of a player's physical state.
|
||||
## Used by ClientPrediction to build its ring buffer, send inputs,
|
||||
## and reconcile against authoritative server state.
|
||||
##
|
||||
## Serialization: to_dict() / from_dict() for RPC transport over ENet.
|
||||
class_name Snapshot
|
||||
extends RefCounted
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fields
|
||||
# ---------------------------------------------------------------------------
|
||||
## Server tick / local tick number this snapshot corresponds to.
|
||||
var timestamp: int = 0
|
||||
|
||||
## World-space position of the character's feet / body origin.
|
||||
var position: Vector3 = Vector3.ZERO
|
||||
|
||||
## Character body rotation as a quaternion (avoids gimbal lock in interpolation).
|
||||
var rotation: Quaternion = Quaternion.IDENTITY
|
||||
|
||||
## Linear velocity in world space.
|
||||
var velocity: Vector3 = Vector3.ZERO
|
||||
|
||||
## Whether the character was on the ground (is_on_floor()) at capture time.
|
||||
var grounded: bool = false
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Serialization
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Convert to a Dictionary suitable for RPC transfer.
|
||||
## Arrays are used instead of Vector3/Quaternion because Godot's RPC
|
||||
## serialization handles basic types more reliably.
|
||||
func to_dict() -> Dictionary:
|
||||
return {
|
||||
"timestamp": timestamp,
|
||||
"position": [position.x, position.y, position.z],
|
||||
"rotation": [rotation.x, rotation.y, rotation.z, rotation.w],
|
||||
"velocity": [velocity.x, velocity.y, velocity.z],
|
||||
"grounded": grounded,
|
||||
}
|
||||
|
||||
## Deserialize from a Dictionary produced by to_dict().
|
||||
static func from_dict(data: Dictionary) -> Snapshot:
|
||||
var s = Snapshot.new()
|
||||
s.timestamp = data.get("timestamp", 0)
|
||||
|
||||
var p: Array = data.get("position", [0.0, 0.0, 0.0])
|
||||
s.position = Vector3(p[0], p[1], p[2])
|
||||
|
||||
var r: Array = data.get("rotation", [0.0, 0.0, 0.0, 1.0])
|
||||
s.rotation = Quaternion(r[0], r[1], r[2], r[3])
|
||||
|
||||
var v: Array = data.get("velocity", [0.0, 0.0, 0.0])
|
||||
s.velocity = Vector3(v[0], v[1], v[2])
|
||||
|
||||
s.grounded = data.get("grounded", false)
|
||||
return s
|
||||
Reference in New Issue
Block a user