# This file is part of sunder. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . ## This defines a ping manager. ## ## When added to your scene and connect the [member synchronized] signal to your ## custom handler in order to receive ping updates on a regular basis (as ## defined by [param ping_interval] and synchronize the state for each peer. ## [codeblock] ## func _ready() -> void: ## $Ping.synchronized.connect(_on_ping_sync) ## ## func _on_ping_sync(state: Dictionary) -> void: ## for peer_id in state: ## print("peer `%s` latency is %d ms" % [peer_id, state[peer_id]]) ## [/codeblock] @icon("res://assets/icons/ping.svg") class_name Ping extends Timer ## Emitted when the state is synchronized. signal synchronized(state : Dictionary) ## The size of the ping history buffer used to store past ping times for each client. @export var ping_history: int = 8: set = set_ping_history ## The ping state being synchronized to connected peers @export var _state := {} ## The dictionary of [PingInfo] for each connected peer id var _infos := {} ## The array of times when a ping request occured var _pings := [] var _last_ping := 0 ## This class defines a ping information to keep track of ping results ## and provide average ping. class PingInfo: const SIZE := 32 var samples := PackedInt32Array() var pos := 0 func _init() -> void: samples.resize(SIZE) samples.fill(0) func get_average() -> int: var value := 0 for sample in samples: value += sample @warning_ignore("integer_division") return value / SIZE if value > 0 else 0 func set_next(value: int) -> void: samples[pos] = value pos = (pos + 1) % SIZE func _ready() -> void: clear_pings() multiplayer.peer_connected.connect(_add_peer) multiplayer.peer_disconnected.connect(_del_peer) timeout.connect(_on_timeout) func _on_timeout() -> void: if _infos: # check and update ping intervals var now := Time.get_ticks_msec() if now >= _pings[_last_ping] + wait_time: _last_ping = (_last_ping + 1) % ping_history _pings[_last_ping] = now _ping.rpc(now) # reset state _state.clear() # iterate over registered clients for peer_id:int in _infos: # set client average ping state _state[peer_id] = _infos[peer_id].get_average() # send state to connected peers _synchronize.rpc(_state) ## Clears the ping history array. func clear_pings() -> void: _last_ping = 0 _pings.resize(ping_history) _pings.fill(0) ## Sets the size of the ping history buffer and clears existing ping data. func set_ping_history(value: int) -> void: if value < 1: return ping_history = value clear_pings() ## Adds a new peer to the registry func _add_peer(peer_id: int) -> void: _infos[peer_id] = PingInfo.new() ## Deletes a peer from the registry func _del_peer(peer_id: int) -> void: _infos.erase(peer_id) @rpc("authority", "call_remote", "unreliable") func _ping(time: int) -> void: _pong.rpc_id(get_multiplayer_authority(), time) @rpc("any_peer", "call_remote", "unreliable") func _pong(time: int) -> void: if not multiplayer.is_server(): return # get id of the peer sending the pong var peer_id: int = multiplayer.get_remote_sender_id() # check if peer exists in the registered clients dictionary if not _infos.has(peer_id): return # init variables var now := Time.get_ticks_msec() var last := (_last_ping + 1) % ping_history var found := ping_history * wait_time # search related ping in history for i in range(ping_history): # check if current ping matches received pong if time == _pings[last]: found = _pings[last] break # move to next ping in history last = (last + 1) % ping_history # compute round-trip time (RTT) and update ping info for this peer _infos[peer_id].set_next((now - found) / 2) @rpc("authority", "call_local", "unreliable") func _synchronize(state: Dictionary) -> void: _state = state synchronized.emit(_state)