Skip to content

Commit

Permalink
feat: Use OS time for tick loop (#225)
Browse files Browse the repository at this point in the history
Use OS time for tick loop to avoid time drifting between clients. Also
supports pausing both from editor and via SceneTree.pause.

Refs #177
  • Loading branch information
elementbound authored Jun 3, 2024
1 parent 72dfc24 commit c971417
Show file tree
Hide file tree
Showing 6 changed files with 49 additions and 11 deletions.
2 changes: 1 addition & 1 deletion addons/netfox.extras/plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
name="netfox.extras"
description="Game-specific utilities for Netfox"
author="Tamas Galffy"
version="1.6.1"
version="1.7.0"
script="netfox-extras.gd"
2 changes: 1 addition & 1 deletion addons/netfox.internals/plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
name="netfox.internals"
description="Shared internals for netfox addons"
author="Tamas Galffy"
version="1.6.1"
version="1.7.0"
script="plugin.gd"
2 changes: 1 addition & 1 deletion addons/netfox.noray/plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
name="netfox.noray"
description="Bulletproof your connectivity with noray integration for netfox"
author="Tamas Galffy"
version="1.6.1"
version="1.7.0"
script="netfox-noray.gd"
29 changes: 22 additions & 7 deletions addons/netfox/network-time.gd
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ var ticktime: float:
var tick_factor: float:
get:
if not sync_to_physics:
return 1.0 - clampf(_next_tick * tickrate, 0, 1)
return 1.0 - clampf((_next_tick_time - _last_process_time) * tickrate, 0, 1)
else:
return Engine.get_physics_interpolation_fraction()
set(v):
Expand Down Expand Up @@ -257,11 +257,13 @@ signal after_sync()
signal after_client_sync(peer_id: int)

var _tick: int = 0
var _next_tick: float = 0
var _active: bool = false
var _initial_sync_done = false
var _process_delta: float = 0

var _next_tick_time: float = 0
var _last_process_time: float = 0.

var _remote_rtt: float = 0
var _remote_tick: int = 0
var _local_tick: int = 0
Expand Down Expand Up @@ -302,12 +304,14 @@ func start():
_local_tick = _remote_tick
_initial_sync_done = true
_active = true
_next_tick_time = _get_os_time()
after_sync.emit()

rpc_id(1, "_submit_sync_success")
else:
_active = true
_initial_sync_done = true
_next_tick_time = _get_os_time()
after_sync.emit()

# Remove clients from the synced cache when disconnected
Expand Down Expand Up @@ -359,26 +363,34 @@ func _ready():
NetworkTimeSynchronizer.on_sync.connect(_handle_sync)

func _process(delta):
# Use OS delta to determine if the game's paused from editor, or through the SceneTree
var os_delta = _get_os_time() - _last_process_time
var is_delta_mismatch = os_delta / delta > 4. and os_delta > .5

# Adjust next tick time if the game is paused, so we don't try to "catch up" after unpausing
if (is_delta_mismatch and Engine.is_editor_hint()) or get_tree().paused:
_next_tick_time += os_delta

_process_delta = delta
_last_process_time += os_delta

# Run tick loop if needed
if _active and not sync_to_physics:
_next_tick -= delta

var ticks_in_loop = 0
while _next_tick < 0 and ticks_in_loop < max_ticks_per_frame:
while _next_tick_time < _last_process_time and ticks_in_loop < max_ticks_per_frame:
if ticks_in_loop == 0:
before_tick_loop.emit()

_run_tick()

ticks_in_loop += 1
_next_tick += ticktime
_next_tick_time += ticktime

if ticks_in_loop > 0:
after_tick_loop.emit()

func _physics_process(delta):
if _active and sync_to_physics:
if _active and sync_to_physics and not get_tree().paused:
# Run a single tick every physics frame
before_tick_loop.emit()
_run_tick()
Expand All @@ -393,6 +405,9 @@ func _run_tick():
_remote_tick +=1
_local_tick += 1

func _get_os_time() -> float:
return Time.get_ticks_msec() / 1000.

func _handle_sync(server_time: float, server_tick: int, rtt: float):
_remote_tick = server_tick
_remote_rtt = rtt
Expand Down
2 changes: 1 addition & 1 deletion addons/netfox/plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
name="netfox"
description="Shared internals for netfox addons"
author="Tamas Galffy"
version="1.6.1"
version="1.7.0"
script="netfox.gd"
23 changes: 23 additions & 0 deletions docs/netfox/guides/network-time.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,29 @@ To get notified when a client successfully syncs their time and starts the tick
loop, use the `NetworkTime.after_client_sync(peer_id)` signal. This is fired
once per client, and only on the server.

## Pausing

*NetworkTime* also supports pausing the game, if needed. There's two cases
where pauses are considered.

When running ( and pausing ) the game from the editor, the network tick loop
is automatically paused. As there's currently no API to detect the editor
pausing the game, *NetworkTime* checks if Godot's `_process` delta and actual
delta is mismatching, and if so, considers the game paused. In some cases, this
can result in false positives when the game simply hangs for a bit, e.g. when
loading resources.

This pause detection only happens when the game is run from the editor, to
avoid false positives in production builds.

The other supported case is pausing the game from the engine itself. Whenever
`SceneTree.paused` is set to true, *NetworkTime* won't run the tick loop.

> *Note* that pausing the tick loop can cause desynchronization between peers,
and could lead to clients fast-forwarding ticks to catch up, or time
recalibrations. If the game is paused via SceneTree, make sure it is paused and
unpaused at the same time on all peers.

## Time synchronization

*NetworkTime* runs a time synchronization loop on clients, in the background.
Expand Down

0 comments on commit c971417

Please sign in to comment.