Files
tactical-shooter/docs/netfox/guides/network-time.md
T
shawn b0c83af092 Fresh start: replace with naxIO/netfox-cs-sample foundation
Complete replacement of the tactical-shooter project with the
netfox-cs-sample (MIT) — a CS 1.6 inspired multiplayer FPS built
with Godot 4 and netfox.

## What's new
- Full CS-style gameplay: teams (T/CT), rounds, economy, buy menu
- 6 weapons: Knife, Glock, USP, AK-47, M4A1, AWP
- Bomb plant/defuse with 2 bombsites
- Flashbang & smoke grenades
- Proper netfox rollback netcode at 64 tick
- Network popup UI for host/join
- HUD, crosshair, round timer, scoreboard
- All netfox singletons registered as autoloads (works in exported builds)

## Architecture
- Listen-server (host from client, no dedicated server binary)
- Multiplayer-fps game lives at examples/multiplayer-fps/
- Netfox addons registered as autoloads for exported build compat
- Godot 4.7 with Forward+ renderer

## Removed
- Old headless-server architecture (client_main, server_main, player.gd, etc.)
- Custom netfox bootstrap with ENet fallback
- Old ChaffGames FPS template (2,420 lines, 844 KB)
- SimulationServer GDExtension stub
- Godot-jolt physics (netfox sample uses default Godot physics)
- Duplicate weapon_data.gd, anti_cheat.gd, round_manager.gd, etc.
- Server browser API Python venv (87 MB)
- test_range map and modular assets

## Preserved
- Git history
- Server config at config/default_server_config.cfg
- Windows export preset
- Build directory (gitignored)

Co-authored-by: naxIO <naxIO@users.noreply.github.com>
2026-07-02 20:55:20 -04:00

258 lines
9.1 KiB
Markdown

# NetworkTime
Tracks shared network time between players, and provides an event loop for
synchronized game updates. Provided as an autoload.
A separate timer is provided for network ticks, making the network game update
rate independent from rendering or physics frames.
## Network tick loop
*NetworkTime* provides its own independent event loop by exposing signals. This
makes networked game logic independent of current FPS, and makes it run at a
consistent rate. Connect handlers to *NetworkTime*'s signals to implement
networked game logic.
During each frame, *NetworkTime* checks how much time has elapsed since the
last tick loop. When more time has elapsed than a single tick's duration, the
*network tick loop* will run:
```puml
@startuml
start
:before_tick_loop;
while (Ticks to simulate) is (>0)
:before_tick;
:on_tick;
:after_tick;
endwhile (0)
:after_tick_loop;
stop
@enduml
```
The tick loop will run as long as it catches up on ticks to run. Every loop is
limited to run at most `max_ticks_per_frame` ticks to avoid overwhelming the
CPU.
To tie the network tick loop to Godot's physics process, enable
`sync_to_physics`. This will result in the tick loop running a single tick in
every physics update.
To move your game logic to the network tick loop, use the *on_tick* event:
```gdscript
extends Node3D
@export var speed = 4.0
func _ready():
NetworkTime.on_tick.connect(_tick)
func _tick(delta, tick):
# Move forward
position += basis.z * delta * speed
```
> By convention, *on_tick* handlers are named *_tick*.
## Starting and stopping
By default, *NetworkTime* does not run the tick loop at all. This lets you
control when the network tick loop, and thus the game starts and stops.
To start the tick loop, call the `NetworkTime.start()` coroutine. On servers,
this will start the tick loop and return immediately. On clients, it will first
synchronize the time to the server, start the network tick loop, and only then
return. Use this when starting the game.
> Starting the tick loop before starting multiplayer is not supported.
To stop the tick loop, call `NetworkTime.stop()`. This will immediately stop
the tick loop and return. Use this when the player leaves a game.
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.
!!!warning
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, it is recommended to pause and unpause
at the same time on all peers.
## Tickrate matching
The idea of a shared time also implies matching tickrates. If one peer were to
run at a higher tickrate than the rest, that peer would inevitably get ahead in
ticks, and get out of sync. If it were to run at a lower tickrate, it would get
behind and out of sync.
For games where both the server and client are built from the same project,
this doesn't usually happen, since they share the same tickrate configuration.
If it does happen, by default it will be considered a configuration error, and
a warning will be emitted:
```
[WRN][@43][#1][_][netfox::NetworkTickrateHandshake] Local tickrate 24tps differs from tickrate of peer #1366785595 at 36tps! Make sure that tickrates are correctly configured in the Project settings! See netfox/Time/Tickrate.
```
This behavior is configurable, with the following options available:
Warn
: Emit a warning about the tickrate mismatch, but do nothing. Useful for
development.
Disconnect
: Disconnect clients with mismatching tickrates. This is enforced by the
host.
Adjust
: Adjust the client's tickrate to match the host's.
Signal
: Emit a signal about the detected mismatches, so custom behavior can be
implemented.
See the [settings](#settings) for the appropriate configuration.
## Time synchronization
*NetworkTime* runs a time synchronization loop on clients, in the background.
Synchronizing time makes sure that all players have a shared idea of time and
can exchange timing-related data.
The synchronization itself is handled by [NetworkTimeSynchronizer].
*NetworkTime* provides different kinds of time, each for different use cases.
Each time can be accessed as ticks or seconds. Both advance after every network
tick.
### Synchronized time
* `NetworkTime.time`
* `NetworkTime.tick`
Marks the current network game time. This is continuously synchronized, making
sure that these values are as close to each other on all peers as possible.
Use this whenever a notion of game time is needed.
### Local time
!!! warning
*Deprecated since netfox v1.9.0.* Use [synchronized time] instead.
* `NetworkTime.local_time`
* `NetworkTime.local_tick`
Marks the current time in reference to the local machine. Starts at zero when
the network tick loop starts.
Useful for logic that is tied to the tick loop, but is not synchronized over
the network. A good example is visual effects.
Not suitable for synchronizing data, as the local time is different at each
player.
### Remote time
!!! warning
*Deprecated since netfox v1.9.0.* Use [synchronized time] instead.
* `NetworkTime.remote_tick`
* `NetwokrTime.remote_time`
* `NetworkTime.remote_rtt`
Marks the current *estimated* time of the server. This is a regularly updated
estimate.
Note that on each update, the remote time may jump forwards or even backwards.
The estimate is based on the measured roundtrip time ( *remote_rtt* ) and the
assumption that the latency is exactly half of that.
Can be used as a base for comparisons ( e.g. latency ), but *not recommended*
for tying game logic to it.
To get notified when a time synchronization happens and the remote time is
updated, use the `NetworkTime.after_sync` signal.
## Settings
Settings are found in the Project Settings, under Netfox > Time:
![NetworkTime Settings](../assets/network-time-settings.png)
*Tickrate* specifies the number of ticks every second in the network tick loop.
*Max Ticks Per Frame* sets the maximum number of frames to simulate per tick loop. Used to avoid freezing the game under load.
*Recalibrate Threshold* is the largest allowed time discrepancy in seconds. If
the difference between the remote clock and reference clock is larger than this
setting, the reference clock will be reset to the remote clock. See
[NetworkTimeSynchronizer] for more details.
*Stall Threshold* is the amount of time in seconds that can pass between two
frames until it is considered a stall. This is used to detect game freezes or
OS-level pauses ( e.g. the window gets minimized ). If a stall is detected, it
is compensated by adjusting the game clock.
*Sync Interval* is the resting time in seconds between sampling the remote
clock.
*Sync Samples* is the number of measurements to use for time synchronization.
This includes measuring roundtrip time and estimating clock offsets.
*Sync Adjust Steps* is the number of iterations to use when adjusting the
reference clock. Larger values result in more stable clocks but slower
convergence, while smaller values synchronize more aggressively.
*Sync Sample Interval* *deprecated in netfox v1.9.0*. Originally used as the
resting time between roundtrip measurements.
*Sync to Physics* ensures that the network tick loop runs in Godot's physics
process when enabled. This can be useful in cases where a lot of physics
operations need to be done as part of the tick- or the rollback loop.
*Tickrate Mismatch Action* indicates what to do when a tickrate mismatch is
detected. See [Tickrate matching](#tickrate-matching) on what the individual
options do.
*Suppress Offline Peer Warning* suppresses warning when `NetworkTime.start()` is
called with the active [multiplayer peer] being an [OfflineMultiplayerPeer]. In
most cases, this warning means that the tick loop was unintentionally started
before connecting to a game or hosting one. When this settings is enabled, the
warning is not printed, instead assuming the [OfflineMultiplayerPeer] is
intentional.
[NetworkTimeSynchronizer]: ./network-time-synchronizer.md
[synchronized time]: #synchronized-time
[multiplayer peer]: https://docs.godotengine.org/en/stable/classes/class_multiplayerapi.html#class-multiplayerapi-property-multiplayer-peer
[OfflineMultiplayerPeer]: https://docs.godotengine.org/en/stable/classes/class_offlinemultiplayerpeer.html#class-offlinemultiplayerpeer