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:
@@ -31,6 +31,13 @@ signal connection_failed(error_message: String)
|
||||
signal remote_player_spawned(peer_id: int, pos: Vector3)
|
||||
signal remote_player_despawned(peer_id: int)
|
||||
|
||||
# --- Client prediction signals ---
|
||||
## Emitted on the server when a client sends input. payload: {peer_id, tick, input_dict}
|
||||
signal client_input_received(peer_id: int, tick: int, input_dict: Dictionary)
|
||||
## Emitted on the client when the server sends authoritative state.
|
||||
## entity_id: the simulation entity this state belongs to.
|
||||
signal server_state_received(entity_id: int, snapshot_dict: Dictionary)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -148,6 +155,32 @@ func broadcast_despawn_player(peer_id: int) -> void:
|
||||
print("[NetworkManager] Client received despawn: peer=%d" % peer_id)
|
||||
remote_player_despawned.emit(peer_id)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Client Prediction RPCs (Phase 1 — client-side prediction)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Client → Server: send raw input for the given local tick.
|
||||
## Called by ClientPrediction.on_after_tick() each physics tick.
|
||||
## Uses ENet channel 0 (unreliable-ordered) for lowest-latency input delivery.
|
||||
@rpc("unreliable", "any_peer", "call_remote", Chan.INPUT)
|
||||
func send_client_input(tick: int, input_dict: Dictionary) -> void:
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
var peer_id: int = multiplayer.get_remote_sender_id()
|
||||
client_input_received.emit(peer_id, tick, input_dict)
|
||||
|
||||
|
||||
## Server → Client: send authoritative entity snapshot for reconciliation.
|
||||
## Called by server-side code (e.g. GameServer after each tick).
|
||||
## entity_id identifies which simulation entity this state belongs to.
|
||||
## Uses ENet channel 1 (reliable-ordered) so state corrections are not dropped.
|
||||
@rpc("unreliable", "authority", "call_remote", Chan.EVENTS)
|
||||
func send_server_state(entity_id: int, snapshot_dict: Dictionary) -> void:
|
||||
if multiplayer.is_server():
|
||||
return
|
||||
server_state_received.emit(entity_id, snapshot_dict)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Event handlers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user