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>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
# Interpolators
|
||||
|
||||
Tracks interpolators for various data types. Provided as a static class.
|
||||
|
||||
To smooth out motion between network ticks, [TickInterpolator] interpolates
|
||||
nodes' state properties between the current and the previous tick. The type of
|
||||
data to be interpolated is not known in advance, and can be any built-in or
|
||||
even custom type configured by the developer.
|
||||
|
||||
*Interpolators* provides methods to register interpolators for any data type,
|
||||
and even provides some for built-in data types.
|
||||
|
||||
## Interpolating values
|
||||
|
||||
*Interpolators* can be used to interpolate between any two values, as long as
|
||||
they hold the same data type:
|
||||
|
||||
```gdscript
|
||||
extends Node3D
|
||||
|
||||
@export var target_node
|
||||
@export var approach_time = 0.5
|
||||
|
||||
func _process(delta):
|
||||
# Approach target node, if it exists
|
||||
if target_node:
|
||||
var from_xform = global_transform
|
||||
var to_xform = target_node.global_transform
|
||||
var factor = delta / approach_time
|
||||
|
||||
global_transform = Interpolators.interpolate(from_xform, to_xform, factor)
|
||||
```
|
||||
|
||||
Note that in this case, *Interpolators* will try to look up the appropriate
|
||||
interpolator based on the provided values. If no interpolator is found, a
|
||||
fallback is used, that simply returns the value closer to *factor* - i.e. the
|
||||
starting value if *factor* is less than 0.5 and the target value otherwise.
|
||||
|
||||
## Caching interpolators
|
||||
|
||||
To avoid having to look up the right interpolator every frame, you can cache
|
||||
it:
|
||||
|
||||
```gdscript
|
||||
extends Node3D
|
||||
|
||||
@export target_node
|
||||
@export approach_time = 0.5
|
||||
|
||||
var interpolator
|
||||
|
||||
func _ready():
|
||||
interpolator = Interpolators.find_for(global_transform)
|
||||
|
||||
func _process(delta):
|
||||
# Approach target node, if it exists
|
||||
if target_node:
|
||||
var from_xform = global_transform
|
||||
var to_xform = target_node.global_transform
|
||||
var factor = delta / approach_time
|
||||
|
||||
global_transform = interpolator.call(from_xform, to_xform, factor)
|
||||
```
|
||||
|
||||
## Custom interpolators
|
||||
|
||||
*Interpolators* supports interpolators for custom data types, and even
|
||||
overriding built-in interpolators. Both can be done by registering an
|
||||
interpolator:
|
||||
|
||||
```gdscript
|
||||
Interpolators.register(
|
||||
func(a): return a is float, # Condition
|
||||
func(a, b, f): return lerpf(a, b, f * f) # Interpolation
|
||||
)
|
||||
```
|
||||
|
||||
The above registers a custom interpolator by specifying a condition function
|
||||
and an interpolation function. Since it applies to an already supported type,
|
||||
it overrides the built-in interpolator.
|
||||
|
||||
During lookup, *Interpolators* calls the *condition* function of each
|
||||
interpolator and returns the one whose *condition* function returns true. If
|
||||
multiple interpolators are applicable, *Interpolators* returns the last
|
||||
registered one.
|
||||
|
||||
The *interpolation* function receives the starting value *a*, the target value
|
||||
*b* and the interpolation factor *f*.
|
||||
|
||||
## Built-in interpolators
|
||||
|
||||
The following types are supported by default:
|
||||
|
||||
* float
|
||||
* Vector2
|
||||
* Vector3
|
||||
* Transform2D
|
||||
* Transform3D
|
||||
|
||||
[TickInterpolator]: ../nodes/tick-interpolator.md
|
||||
@@ -0,0 +1,148 @@
|
||||
# Logging
|
||||
|
||||
During runtime, it can be useful to print some diagnostic info to the console -
|
||||
this is called logging. The netfox addons include a logging system to help with
|
||||
debugging. This is useful when running the game locally, but also helps if
|
||||
there's log files players can attach with their bug reports.
|
||||
|
||||
The system produces logs like this:
|
||||
|
||||
```
|
||||
[DBG][@0][#1][_][netfox::NetworkPerformance] Network performance enabled, registering performance monitors
|
||||
[DBG][@0][#1][_][netfox.extras::WindowTiler] Tiling with sid: f2682d1, uid: 17627261006193110
|
||||
[DBG][@0][#1][_][netfox.extras::NetworkSimulator] Feature disabled
|
||||
[DBG][@0][#1][_][netfox.extras::WindowTiler] Tiling as idx 0 / 1 - 17627261006193110 in ["17627261006193110"]
|
||||
[DBG][@22][#1][_][netfox.extras::NetworkWeapon] Calling after fire hook for Bomb Projectile 5sswh7lcsgbq
|
||||
[DBG][@27][#1][_][fb::Displacer] Created explosion at (2.027323, 1.500942, -14.99592)@26
|
||||
[DBG][@34][#1][_][netfox.extras::NetworkWeapon] Calling after fire hook for Bomb Projectile u4h8opz52lin
|
||||
[DBG][@46][#1][_][fb::Displacer] Created explosion at (4.892477, 1.500942, -14.83388)@45
|
||||
[DBG][@46][#1][_][netfox.extras::NetworkWeapon] Calling after fire hook for Bomb Projectile 2u1d9n456yl1
|
||||
[DBG][@57][#1][_][fb::Displacer] Created explosion at (4.814114, 1.500942, -14.57117)@56
|
||||
```
|
||||
|
||||
This page will elaborate on how to produce your own logs, and what each part
|
||||
means.
|
||||
|
||||
## Using the logger
|
||||
|
||||
The logging system can be accessed by creating an instance of `NetfoxLogger`.
|
||||
Every logger has a name, and belongs to a module. Both of these can be
|
||||
arbitrary strings, and are included in the logged messages.
|
||||
|
||||
Messages can be logged as different *logging levels*:
|
||||
|
||||
```gd
|
||||
var logger := NetfoxLogger.new("my-game", "Player")
|
||||
logger.trace("Detailed message")
|
||||
logger.debug("Something happened")
|
||||
logger.info("Hi!")
|
||||
logger.warning("Couldn't connect")
|
||||
logger.error("Game missing?")
|
||||
```
|
||||
|
||||
To use string interpolation, you can also pass the template string and values
|
||||
separately. This can be useful to avoid substituting the values in case the
|
||||
message never gets printed because of filtering:
|
||||
|
||||
```gd
|
||||
logger.trace("Adjusted clock by %.2fms, offset: %.2fms, new time: %.4fss", [nudge * 1000., offset * 1000., _clock.get_time()])
|
||||
```
|
||||
|
||||
In the above example, there's a lot of data to be included in the message.
|
||||
However, if trace logs are disabled, that data will never be substituted,
|
||||
saving some processing time.
|
||||
|
||||
!!!tip
|
||||
This same logging system is used by netfox itself.
|
||||
|
||||
## Log levels
|
||||
|
||||
Each log message can belong to one of the following categories:
|
||||
|
||||
Error
|
||||
: Something goes irrecoverably wrong, or something that should never happen
|
||||
just happened
|
||||
|
||||
Warning
|
||||
: Something goes wrong, but can be handled
|
||||
|
||||
Info
|
||||
: Useful information on expected behaviour
|
||||
|
||||
Debug
|
||||
: Verbose messages, to help debug general code flow
|
||||
|
||||
Trace
|
||||
: Extremely verbose messages, to help follow the code flow to the smallest
|
||||
detail
|
||||
|
||||
Depending on your game, different logs may be needed. To accommodate this,
|
||||
*netfox* can be configured in the [Project Settings](#settings) to omit certain
|
||||
log messages.
|
||||
|
||||
Filtering based on log levels can also be configured from code. To set the
|
||||
global log level, set `NetfoxLogger.log_level`. To configure the log level per
|
||||
module, use the `NetfoxLogger.module_log_level` dictionary.
|
||||
|
||||
## Tags
|
||||
|
||||
Tags can be attached to the logging system. They provide pieces of information
|
||||
that appear in each log message, for every logger.
|
||||
|
||||
By default, netfox provides a few tags, to help with debugging. These are, in
|
||||
order:
|
||||
|
||||
Current tick
|
||||
: The current tick, as per `NetworkTime`
|
||||
|
||||
Peer ID
|
||||
: The currently active multiplayer peer's ID
|
||||
|
||||
Rollback status
|
||||
: Contains the current rollback stage, simulated tick, and resimulated tick
|
||||
interval.
|
||||
|
||||
The stage can be `B` for before loop, `P` for prepare tick, `S` for
|
||||
simulate tick, `R` for record tick, and `A` for after loop.
|
||||
|
||||
The current tick is in the form of `X|A>B`, meaning we're currently
|
||||
simulating tick X, in a loop going from tick A to tick B.
|
||||
|
||||
Defaults to `_` if currently not in rollback.
|
||||
|
||||
!!!note
|
||||
These default tags are subject to change between releases.
|
||||
|
||||
Custom tags can be attached by calling `NetfoxLogger.register_tag()`. In this
|
||||
sense, tags are callbacks that must return a single string, containing the tag
|
||||
data to be logged.
|
||||
|
||||
This method takes a second, `priority` parameter. This priority is used to sort
|
||||
them for logging - tags are printed from lowest priority to highest.
|
||||
|
||||
!!!warning
|
||||
Make sure to free your custom tags using `NetfoxLogger.free_tag()`. Not
|
||||
doing so might cause crashes. See [#433] for details.
|
||||
|
||||
## Settings
|
||||
|
||||

|
||||
|
||||
These settings control the *minimum* log level - e.g. if the log level is set
|
||||
to *info*, only messages at or above the info level will be logged, namely
|
||||
info, warning and error. If the setting is set to *all*, all messages are
|
||||
logged.
|
||||
|
||||
Log levels can be controlled globally and per addon. A message will be logged
|
||||
if it passes *both* logging level checks.
|
||||
|
||||
For example, if the *Log Level* setting is at *Warning* and the *Netfox Log
|
||||
Level* is at *Info*, only warning and error messages are logged for netfox.
|
||||
This happens because the *Log Level* is more restrictive than the *Netfox Log
|
||||
Level* setting.
|
||||
|
||||
Note that you don't need to install all netfox addons for the logging settings
|
||||
to work. If an addon is not installed, its log level setting is simply ignored.
|
||||
|
||||
|
||||
[#433]: https://github.com/foxssake/netfox/issues/433
|
||||
@@ -0,0 +1,98 @@
|
||||
# Netfox Sharp
|
||||
|
||||
!!!warning
|
||||
**[Netfox Sharp] is currently an experimental build and not ready for
|
||||
production. During this time, breaking changes may be introduced at any
|
||||
time.**
|
||||
|
||||
The [Netfox Sharp] addon is designed to bridge the gap between GDScript and C#
|
||||
by allowing core netfox features to be accessed in C# without having to worry
|
||||
about [Cross-Language Scripting] with GDScript directly.
|
||||
|
||||
### What Netfox Sharp Is
|
||||
|
||||
- A wrapper for netfox that uses the existing netfox codebase for its logic.
|
||||
- A way to more conveniently call netfox logic in a C# environment.
|
||||
- Partially compatible with existing codebases that use GDScript.
|
||||
|
||||
### What Netfox Sharp Isn't
|
||||
|
||||
- A standalone addon written entirely in C#.
|
||||
- A perfect 1:1 translation. Due to quirks of netfox, some code will differ,
|
||||
detailed below.
|
||||
- A wrapper for netfox.noray or netfox.extras. Support for either of those
|
||||
currently isn't planned, but may be considered based on interest.
|
||||
|
||||
## Getting Started
|
||||
|
||||
- Download the [Netfox Sharp] repo, and move the `netfox_sharp` and
|
||||
`netfox_sharp_internals` folders into the addons of a C#-enabled Godot
|
||||
project using the .NET version of Godot 4.x.
|
||||
- Install the netfox addon. See the Netfox Sharp repo for details on which
|
||||
version of netfox you need.
|
||||
- Build your project, then enable netfox and Netfox Sharp in your project
|
||||
settings.
|
||||
- Restart Godot, and you've successfully set up Netfox Sharp!
|
||||
|
||||
## Differences Between Netfox And Netfox Sharp
|
||||
|
||||
Existing documentation for netfox should be easily translatable to Netfox Sharp
|
||||
by following the below differences.
|
||||
|
||||
- Most changes follow Godot's rules for [Cross-Language Scripting], taking
|
||||
netfox as the base. In netfox, consider the following:
|
||||
|
||||
```gdscript
|
||||
# The following example is a snippet of netfox code
|
||||
func _ready():
|
||||
NetworkTime.before_tick_loop.connect(_gather)
|
||||
|
||||
func _gather():
|
||||
# Input gathering here
|
||||
pass
|
||||
|
||||
func _rollback_tick(delta, tick, is_fresh):
|
||||
# Rollback logic here
|
||||
pass
|
||||
```
|
||||
|
||||
Whereas in Netfox Sharp:
|
||||
|
||||
```cs
|
||||
// This is functionally identical Netfox Sharp code
|
||||
public override void _Ready()
|
||||
{
|
||||
// All netfox autoloads like NetworkTime are accessed through static members
|
||||
// in NetfoxSharp, to save on GetNode() calls and reduce clutter in the
|
||||
// project settings.
|
||||
|
||||
// All members like BeforeTickLoop are in PascalCase, similar to Godot's C#
|
||||
NetfoxSharp.NetworkTime.BeforeTickLoop += Gather;
|
||||
}
|
||||
|
||||
// As Gather is linked to a signal, it can be any naming convention.
|
||||
private void Gather()
|
||||
{
|
||||
// Input gathering here
|
||||
}
|
||||
|
||||
// Since _rollback_tick isn't connected to a signal and is instead handled by
|
||||
// netfox internally, netfox's naming convention must be followed.
|
||||
public void _rollback_tick(double delta, long tick, bool isFresh)
|
||||
{
|
||||
// Rollback logic here
|
||||
}
|
||||
```
|
||||
|
||||
- Nodes in the add mode menu have similar names to the GDScript version, but
|
||||
with 'Sharp' affixed, IE `RollbackSynchronizerSharp`. The GDScript versions
|
||||
of the nodes are also present in the add node menu. This is a limitation of
|
||||
how netfox interacts with Godot and cannot be removed.
|
||||
|
||||
# Other Notes
|
||||
- `RollbackSynchronizerSharp`, `StateSynchronizerSharp`, and
|
||||
`TickInterpolatorSharp` create their own respective GDScript nodes, which are
|
||||
instanced as internal children nodes and should not be accessed.
|
||||
|
||||
[Cross-Language Scripting]: https://docs.godotengine.org/en/stable/tutorials/scripting/cross_language_scripting.html
|
||||
[Netfox Sharp]: https://github.com/CyFurStudios/NetfoxSharp/
|
||||
@@ -0,0 +1,94 @@
|
||||
# NetworkCommandServer
|
||||
|
||||
Implements a simpler, lightweight alternative to RPCs. Provided as an autoload.
|
||||
|
||||
Commands consist of a single byte for ID, and the raw binary data. The ID lets
|
||||
the receiving peer decide what to execute, with the binary data serving as the
|
||||
input.
|
||||
|
||||
Being a simpler construct makes commands a good fit for regular, fundamental
|
||||
operations. For example, commands internally are used for time synchronization,
|
||||
or synchronizing state and input between peers.
|
||||
|
||||
Commands are, by default, transmitted over regular RPCs. To use less data,
|
||||
commands can also be transmitted as raw packets, using
|
||||
[SceneMultiplayer.send_bytes()]. This is an opt-in feature - if the game is
|
||||
already using [SceneMultiplayer.send_bytes()], it needs to be aware of
|
||||
commands, and must check each packet whether it's a command or one of its own
|
||||
packets. To check if a packet is a command, use `is_command_packet()`.
|
||||
|
||||
## Implementing custom commands
|
||||
|
||||
Custom commands can be registered with the *NetworkCommandServer*, using
|
||||
`register_command()`. This returns a *Command* object that provides a
|
||||
convenient interface.
|
||||
|
||||
During registration, a callback must be provided, that will be ran when the
|
||||
command is received.
|
||||
|
||||
Commands can be sent using its `send()` method.
|
||||
|
||||
```gdscript
|
||||
@onready var cmd_message := NetworkCommandServer.register_command(handle_message, MultiplayerPeer.TRANSFER_MODE_UNRELIABLE)
|
||||
|
||||
func handle_message(sender: int, data: PackedByteArray) -> void:
|
||||
var message := data.get_string_from_utf8()
|
||||
print("#%d: %s" % [sender, message])
|
||||
|
||||
func _ready() -> void:
|
||||
cmd_message.send("Hello, world!".to_utf8_buffer())
|
||||
```
|
||||
|
||||
!!!tip
|
||||
It is recommended to setup commands once, at game start. When registering
|
||||
commands from autoloads, make sure they run *after* netfox's autoloads.
|
||||
|
||||
## Differences compared to RPCs
|
||||
|
||||
Commands are a fundamentally simpler constructs compared to RPCs.
|
||||
|
||||
### Maximum 256 commands
|
||||
|
||||
Commands are limited to 256 indices - make sure to not register more than that.
|
||||
Some commands are registered by netfox on startup as well.
|
||||
|
||||
This limitation also makes commands a poor fit for registering dynamically.
|
||||
Dynamic registrations often mean registering commands as certain nodes or
|
||||
objects are created. This, in turn, makes it difficult to place an upper bound
|
||||
on the number of commands needed, which can conflict with this limitation.
|
||||
|
||||
### Commands are not tied to any node
|
||||
|
||||
Commands do not refer to any specific node or object in their content. They
|
||||
only contain a command index. Even though the API encapsulates this into
|
||||
*Command* objects, it is completely feasible to have different nodes handle the
|
||||
same command on different peers ( if the game is built as different Godot
|
||||
projects ).
|
||||
|
||||
### Commands do not track authority
|
||||
|
||||
Any peer can send any command to any other peer. It is the receiving peer's
|
||||
responsibility to check whether the sender is allowed to send such a command or
|
||||
not.
|
||||
|
||||
### Commands do not have arguments
|
||||
|
||||
To stay lightweight and to give maximum control, commands contain raw bytes
|
||||
only, no arguments.
|
||||
|
||||
In general, this can be worked around by wrapping the arguments in an array and
|
||||
converting it using [var_to_bytes()] and [bytes_to_var()].
|
||||
|
||||
However, for cases where bandwidth matters, this allows users to encode data in
|
||||
a way that fits best.
|
||||
|
||||
## Settings
|
||||
|
||||
netfox ▸ General ▸ Use Raw Commands
|
||||
|
||||
: When enabled, netfox will transmit commands as raw packets, instead of RPCs.
|
||||
|
||||
|
||||
[SceneMultiplayer.send_bytes()]: https://docs.godotengine.org/en/stable/classes/class_scenemultiplayer.html#class-scenemultiplayer-method-send-bytes
|
||||
[var_to_bytes()]: https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html#class-globalscope-method-var-to-bytes
|
||||
[bytes_to_var()]: https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html#class-globalscope-method-bytes-to-var
|
||||
@@ -0,0 +1,28 @@
|
||||
# NetworkEvents
|
||||
|
||||
Provides convenience signals for multiplayer games. Included as an autoload.
|
||||
|
||||
Keeps track of the root *multiplayer* instance and fires signals when it
|
||||
changes. Using *NetworkEvents*' signals are safe even when the *multiplayer*
|
||||
instance changes, as the signals are updated upon instance change.
|
||||
|
||||
Provides missing signals for server start and server stop events.
|
||||
|
||||
## NetworkTime
|
||||
|
||||
When enabled, *NetworkEvents* will start [NetworkTime] when it detects that a
|
||||
server or a client is started. It will stop [NetworkTime], when it detects that
|
||||
the currently running server or client is stopped.
|
||||
|
||||
## Settings
|
||||
|
||||
Settings are found in the Project Settings, under Netfox > Events:
|
||||
|
||||

|
||||
|
||||
*Enabled* toggles network events. When disabled, *NetworkEvents* will not emit
|
||||
any events or track the multiplayer instance. This may slightly improve
|
||||
performance, as it completely stops Godot from processing the *NetworkEvents*
|
||||
node.
|
||||
|
||||
[NetworkTime]: ./network-time.md
|
||||
@@ -0,0 +1,69 @@
|
||||
# NetworkPerformance
|
||||
|
||||
Provides [custom monitors] for measuring networking performance. Included as an
|
||||
autoload.
|
||||
|
||||
## Enabling monitoring
|
||||
|
||||
By default, network performance monitoring is only enabled in debug builds and
|
||||
when running from the editor.
|
||||
|
||||
Use the `netfox_noperf` feature tag to force disable network performance
|
||||
monitors.
|
||||
|
||||
Use the `netfox_perf` feature tag to force enable network performance monitors.
|
||||
|
||||
These feature tags enable customization for each export preset.
|
||||
|
||||
## Performance monitors
|
||||
|
||||
### Network loop duration
|
||||
|
||||
*Network loop duration* measures the time spent in the [network tick loop].
|
||||
Note that this includes time spent on the [rollback loop] as well.
|
||||
|
||||
This value is updated once for every tick loop, it is not reset to zero after
|
||||
the loop has run. This means that you may get a non-zero reading, even if the
|
||||
tick loop is currently not running.
|
||||
|
||||
### Rollback loop duration
|
||||
|
||||
*Rollback loop duration* measures the time spent in the last [rollback loop].
|
||||
This includes all of its steps.
|
||||
|
||||
The value of this monitor may be zero, if no players have joined, no nodes use
|
||||
rollback, or rollback is disabled.
|
||||
|
||||
### Network ticks simulated
|
||||
|
||||
*Network ticks simulated* measures the number of ticks run in the last [network
|
||||
tick loop]. If the game runs at a higher FPS than the network tickrate, this
|
||||
value should be consistently one.
|
||||
|
||||
Higher, stable values mean that the game itself runs slower than the network
|
||||
tickrate, and needs to catch up by running multiple ticks on each frame.
|
||||
|
||||
### Rollback ticks simulated
|
||||
|
||||
*Rollback ticks simulated* measures the number of rollback ticks run in the
|
||||
last [rollback loop]. Generally, this denotes the age of the oldest input *or*
|
||||
state received, depending on whether the game is running as a server or client.
|
||||
|
||||
The measurement is strongly correlated to network latency - the higher the
|
||||
latency, the older the state and input packets will be upon arrival.
|
||||
|
||||
The more rollback ticks need to be simulated, the more work the rollback tick
|
||||
has to do, which can negatively affect performance.
|
||||
|
||||
### Rollback tick duration
|
||||
|
||||
*Rollback tick duration* provides the average time spent simulating a single
|
||||
tick in the last [rollback loop].
|
||||
|
||||
This can be useful to determine if the rollback tick duration comes from too
|
||||
many ticks being simulated, or the individual ticks being expensive to
|
||||
simulate ( or both ).
|
||||
|
||||
[custom monitors]: https://docs.godotengine.org/en/latest/classes/class_performance.html#class-performance-method-add-custom-monitor
|
||||
[network tick loop]: ./network-time.md
|
||||
[rollback loop]: ./network-rollback.md
|
||||
@@ -0,0 +1,191 @@
|
||||
# NetworkRollback
|
||||
|
||||
Orchestrates the network rollback loop. Provided as an autoload.
|
||||
|
||||
Due to latency, the server may receive inputs from clients from multiple ticks
|
||||
ago. Whenever this happens, the server rewinds its time and resimulates the
|
||||
whole game from the time of the new input. The resimulated ticks are then sent
|
||||
to clients to update their state.
|
||||
|
||||
Also due to latency, clients may receive a state from the server that is
|
||||
several ticks old. Clients rewind their simulation to the time of the latest
|
||||
received state and resimulate from there.
|
||||
|
||||
On both clients and servers, simulated states are recorded for reuse later.
|
||||
|
||||
Further reading: [Client-Side Prediction and Server Reconciliation]
|
||||
|
||||
Note that most of the time you do not need to use this class - the
|
||||
[RollbackSynchronizer] node helps with writing rollback-aware behaviour.
|
||||
|
||||
## Network rollback loop
|
||||
|
||||
*NetworkRollback* runs the *network rollback loop* after every network tick,
|
||||
but before the *after tick* signal is fired.
|
||||
|
||||
The following is the network rollback loop in isolation:
|
||||
|
||||
```puml
|
||||
@startuml
|
||||
|
||||
start
|
||||
|
||||
:before_loop;
|
||||
while(Rollback)
|
||||
:on_prepare_tick;
|
||||
:after_prepare_tick;
|
||||
:on_process_tick;
|
||||
:on_record_tick;
|
||||
endwhile
|
||||
:after_loop;
|
||||
|
||||
stop
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
Signal handlers must implement the right steps for rollback to work.
|
||||
|
||||
During *before_loop*, all rollback-aware nodes must submit where to start the
|
||||
resimulation, by calling `NetworkRollback.notify_resimulation_start`.
|
||||
Resimulation will begin from the earliest tick submitted.
|
||||
|
||||
In each *on_prepare_tick(tick)* handler, nodes must rewind their state to the
|
||||
specified tick. If a state is not available for the given tick, use the latest
|
||||
tick that is earlier than the given tick. Nodes may also register themselves as
|
||||
being simulated by calling `NetworkRollback.notify_simulated`. This is not used
|
||||
by *NetworkRollback* itself, but can be used by other nodes to check which
|
||||
nodes are simulated in the current rollback tick.
|
||||
|
||||
Before processing, *after_prepare_tick(tick)* is emitted. This is where any
|
||||
additional state- or input preparation may happen, such as [input prediction].
|
||||
|
||||
For the *on_process_tick(tick)* signal, nodes must advance their simulation by
|
||||
a single tick.
|
||||
|
||||
In *on_record_tick(tick)*, nodes must record their state for the given tick.
|
||||
Note that since the simulation was advanced by one tick in the previous signal,
|
||||
the *tick* parameter is incremented here.
|
||||
|
||||
The *after_loop* signal notifies its subscribers that the resimulation is done.
|
||||
This can be used to change to the state that is appropriate for display.
|
||||
|
||||
The network rollback loop is part of the network tick loop as follows:
|
||||
|
||||
```puml
|
||||
@startuml
|
||||
|
||||
start
|
||||
|
||||
:NetworkTime.before_tick_loop;
|
||||
|
||||
while (Ticks to simulate) is (>0)
|
||||
:NetworkTime.before_tick;
|
||||
:NetworkTime.on_tick;
|
||||
:NetworkTime.after_tick;
|
||||
endwhile
|
||||
|
||||
:NetworkRollback.before_loop;
|
||||
while(Rollback)
|
||||
:NetworkRollback.on_prepare_tick;
|
||||
:NetworkRollback.after_prepare_tick;
|
||||
:NetworkRollback.on_process_tick;
|
||||
:NetworkRollback.on_record_tick;
|
||||
endwhile
|
||||
:NetworkRollback.after_loop;
|
||||
|
||||
:NetworkTime.after_tick_loop;
|
||||
|
||||
stop
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
The rollback tick loop is triggered in the `NetworkTime.after_tick_loop`
|
||||
signal. Since the rollback tick loop is the first thing connected to it, in
|
||||
practice the rollback will run *before* any user code connected to the
|
||||
`after_tick_loop` signal.
|
||||
|
||||
## Conditional simulation
|
||||
|
||||
During rollback, *NetworkRollback* loops over the full range of ticks to
|
||||
resimulate. Some nodes may not need to be resimulated for the current tick,
|
||||
e.g. because they don't have input for the current tick.
|
||||
|
||||
*NetworkRollback* can be used to track nodes that will be simulated in the
|
||||
current rollback tick. Register nodes that will be simulated by calling
|
||||
`NetworkRollback.notify_simulated`. To check if a node has been registered,
|
||||
call `NetworkRollback.is_simulated`.
|
||||
|
||||
## Rollback-awareness
|
||||
|
||||
[RollbackSynchronizer] considers nodes rollback-aware that implement the
|
||||
`_rollback_tick` method. Rollback-aware nodes are nodes that can participate in
|
||||
the rollback process, i.e. they can resimulate earlier ticks.
|
||||
|
||||
To check if a node is rollback-aware, call `NetworkRollback.is_rollback_aware`.
|
||||
To actually run a rollback tick on them, call
|
||||
`NetworkRollback.process_rollback`.
|
||||
|
||||
These methods are called by [RollbackSynchronizer] under the hood.
|
||||
|
||||
## Input Submission Status
|
||||
|
||||
In certain scenarios you may wish to delay committing to something hard to
|
||||
reverse like death, VFX or audio until its known for sure the outcome won't
|
||||
change. One way of doing this is to check which nodes have submitted input and
|
||||
are past a point of rollback.
|
||||
|
||||
You can query the status of Nodes with
|
||||
`NetworkRollback.get_latest_input_tick(root_node)` or
|
||||
`NetworkRollback.has_input_for_tick(root_node, tick)`. `root_node` being what
|
||||
the relevant [RollbackSynchronizer] has configured.
|
||||
|
||||
All tracked nodes can be retrieved from
|
||||
`NetworkRollback.get_input_submissions()` which will return the entire
|
||||
`<root_node, latest_tick>` dictionary.
|
||||
|
||||
## Settings
|
||||
|
||||

|
||||
|
||||
*Enabled* toggles network rollback. No signals are fired when disabled.
|
||||
|
||||
*History limit* is the maximum number of recorded ticks to keep. Larger values
|
||||
enable further rewinds and thus larger latencies, but consume more memory for
|
||||
each node that is recorded.
|
||||
|
||||
*Input redundancy* This is the number of previous input ticks to send along with
|
||||
the current tick. We send data unreliably over UDP for speed. In the event a packet is
|
||||
lost or arrives out of order we add some redundancy. You can calculate your target
|
||||
reliability % packet success chance by using the formula
|
||||
`1 - (1 - packet_success_rate) ^ input_redundancy`.
|
||||
|
||||
*Display offset* specifies the age of the tick to display. By displaying an
|
||||
older state instead of the latest one, games can mask adjustments if a state
|
||||
update is received from the server. The drawback is that the game will have
|
||||
some latency built-in, as it reacts to player inputs with some delay. Setting
|
||||
to zero will always display the latest game state.
|
||||
|
||||
*Input delay* specifies the delay applied to player input, in ticks. This
|
||||
results in player inputs shifted into the future, e.g. if the player starts
|
||||
moving left on tick 37, it will be sent to the server as tick 39. This way,
|
||||
even if the input takes time to arrive, it will still be up to date, as long as
|
||||
the network latency is smaller than the input latency.
|
||||
|
||||
!!!warning
|
||||
[RollbackSynchronizer]'s `is_fresh` parameter may not work as expected with
|
||||
input delay. This happens because clients already receive data for the
|
||||
current tick, which means that the tick doesn't need to be resimulated, and
|
||||
as a result, no `_rollback_tick` callbacks are ran with `is_fresh` set to
|
||||
true.
|
||||
|
||||
This happens when network latency is smaller than the input delay.
|
||||
|
||||
*Enable diff states* toggles diff states. By sending only state properties that
|
||||
have changed, netfox can reduce the bandwidth needed to synchronize the game
|
||||
between peers. See [RollbackSynchronizer] on how this is done and configured.
|
||||
|
||||
[Client-Side Prediction and Server Reconciliation]: https://www.gabrielgambetta.com/client-side-prediction-server-reconciliation.html
|
||||
[input prediction]: ../tutorials/predicting-input.md
|
||||
[RollbackSynchronizer]: ../nodes/rollback-synchronizer.md
|
||||
@@ -0,0 +1,169 @@
|
||||
# Network Schemas
|
||||
|
||||
By default, *netfox* uses Godot's [Binary serialization API] to serialize data
|
||||
before transmitting it over the network. This is designed to work under various
|
||||
circumstances, with various data types, without knowing anything about them in
|
||||
advance.
|
||||
|
||||
However, during development, developers often have knowledge about the
|
||||
individual properties, such as their type and possible range of values. In
|
||||
addition, some values may be less important as others, and thus can accept some
|
||||
loss of precision.
|
||||
|
||||
Schemas enable developers to specify how each property should be serialized,
|
||||
allowing them to use this knowledge to reduce packet sizes, and thus bandwidth
|
||||
usage.
|
||||
|
||||
## Lossless vs. lossy
|
||||
|
||||
Most serializers are either lossless or lossy. This section gives a short
|
||||
theoretical introduction on what each means and when are they useful.
|
||||
|
||||
### Lossless compression
|
||||
|
||||
When the same amount of information can be represented with less data ( bytes
|
||||
), it is *lossless compression*.
|
||||
|
||||
For example, to represent a 2D normal vector, we do not need to serialize both
|
||||
of its component ( x, y ). Since we know the vector's length to be 1 by
|
||||
definition, we can store the vector's angle compared to predetermined reference
|
||||
vector. From that, we can completely reconstruct the original vector on
|
||||
deserialization.
|
||||
|
||||
Another example is when the range of values the vector can take on is much
|
||||
smaller than its underlying datatype supports. For example, an inventory where
|
||||
items can't stack beyond 99. Instead of defaulting to a 64 bit integer, it is
|
||||
sufficient to serialize this data as a 8 bit integer. That is 1/8th of the
|
||||
original data, while still perfectly representing the range of values needed.
|
||||
|
||||
Lossless compression is an excellent tool, since the same information is kept,
|
||||
but with less data usage. Unfortunately, lossless compression is not feasible
|
||||
for every property.
|
||||
|
||||
### Lossy compression
|
||||
|
||||
If some information is lost when using less data ( bytes ) to represent a
|
||||
value, it is *lossy compression*. This can be useful in cases where the benefit
|
||||
of reduced packet size outweighs the drawbacks of lost information.
|
||||
|
||||
For example, movement vectors for NPCs may be serialized as half precision
|
||||
floats, instead of the default single precision. Since players don't directly
|
||||
control NPC's, they won't notice any difference between their original input
|
||||
and what was serialized.
|
||||
|
||||
While lossy compression can be a useful tool, it is important to judge whether
|
||||
the loss of information or precision does not detract too much from the game
|
||||
experience.
|
||||
|
||||
## Registering a schema
|
||||
|
||||
Both [RollbackSynchronizer] and [StateSynchronizer] expose a `set_schema()`
|
||||
method, that can be used to register the schema used for transmitting
|
||||
properties over the network. This method takes a dictionary, with the keys
|
||||
being property path strings, and the values being serializers:
|
||||
|
||||
```gdscript
|
||||
rollback_synchronizer.set_schema({
|
||||
":transform": NetworkSchemas.transform3f32(),
|
||||
":velocity": NetworkSchemas.vec3f32(),
|
||||
":speed": NetworkSchemas.float32(),
|
||||
":mass": NetworkSchemas.float32(),
|
||||
|
||||
"Input:movement": NetworkSchemas.vec3f32(),
|
||||
"Input:aim": NetworkSchemas.vec3f32()
|
||||
})
|
||||
```
|
||||
|
||||
## Built-in serializers
|
||||
|
||||
`NetworkSchemas` provides many built-in serializers in the form of static
|
||||
methods. Each supported type has multiple serializers for different sizes.
|
||||
|
||||
While many serializers are usable as-is, there are some generic ones that take
|
||||
other serializers as arguments. For example, `vec3t()` serializes a Vector3,
|
||||
and using the serializer passed to it to save each component of the vector.
|
||||
This way, `vec3t(float16())` will save 3 half-precision floats, ending up with
|
||||
6 bytes of data, while `vec3t(float32())` will save 3 single-precision floats,
|
||||
ending up with 12 bytes.
|
||||
|
||||
!!!note
|
||||
Many built-in serializers use half-precision floats. These are only
|
||||
supported in Godot 4.4 and up. Earlier versions fall back to
|
||||
single-precision floats.
|
||||
|
||||
For example, `float16()` may fall back to `float32()`, `vec2f16()` to
|
||||
`vec2f32()`, etc.
|
||||
|
||||
### Algebraic types
|
||||
|
||||
| Type | Methods | Size |
|
||||
|-----------------------|---------------------------------------------------------|--------------------------------------------------------------------|
|
||||
| Booleans | `bool8()` | 1 byte |
|
||||
| Signed integers | `int8()`, `int16()`, `int32()`, `int64()` | 1, 2, 4, or 8 bytes |
|
||||
| Unsigned integers | `uint8()`, `uint16()`, `uint32()`, `uint64()` | 1, 2, 4, or 8 bytes |
|
||||
| Floats | `float16()`, `float32()`, `float64()` | 2, 4, or 8 bytes |
|
||||
| Vector2 | `vec2f16()`, `vec2f32()`, `vec2f64()` | 4, 8, or 16 bytes |
|
||||
| Vector3 | `vec3f16()`, `vec3f32()`, `vec3f64()` | 6, 8, or 24 bytes |
|
||||
| Vector4 | `vec4f16()`, `vec4f32()`, `vec4f64()` | 8, 16, or 32 bytes |
|
||||
| Quaternion | `quatf16()`, `quatf32()`, `quatf64()` | 8, 16, or 32 bytes |
|
||||
| Transform2D | `transform2f16()`, `transform2f32()`, `transform2f64()` | 12, 24, or 48 bytes |
|
||||
| Transform3D | `transform3f16()`, `transform3f32()`, `transform3f64()` | 24, 48, or 96 bytes |
|
||||
|
||||
### Compressed types
|
||||
|
||||
| Type | Methods | Size |
|
||||
|-----------------------|---------------------------------------------------------|--------------------------------------------------------------------|
|
||||
| Numbers in `[0, 1]` | `ufrac8()`, `ufrac16()`, `ufrac32()` | 1, 2, or 4 bytes |
|
||||
| Numbers in `[-1, +1]` | `sfrac8()`, `sfrac16()`, `sfrac32()` | 1, 2, or 4 bytes |
|
||||
| Degrees | `degrees8()`, `degrees16()`, `degrees32()` | 1, 2, or 4 bytes |
|
||||
| Radians | `radians8()`, `radians16()`, `radians32()` | 1, 2, or 4 bytes |
|
||||
| Normalized 2D vectors | `normal2f16()`, `normal2f32()`, `normal2f64()` | 2, 4, or 8 bytes |
|
||||
| Normalized 3D vectors | `normal3f16()`, `normal3f32()`, `normal3f64()` | 4, 8, or 16 bytes |
|
||||
|
||||
### Generic types
|
||||
|
||||
| Type | Methods | Size |
|
||||
|-----------------------|---------------------------------------------------------|--------------------------------------------------------------------|
|
||||
| Vector2 | `vec2t()` | `2 * sizeof(component)` |
|
||||
| Vector3 | `vec3t()` | `3 * sizeof(component)` |
|
||||
| Vector4 | `vec4t()` | `4 * sizeof(component)` |
|
||||
| Quaternion | `quatt()` | `4 * sizeof(component)` |
|
||||
| Transform2D | `transform2t()` | `6 * sizeof(component)` |
|
||||
| Transform3D | `transform3t()` | `12 * sizeof(component)` |
|
||||
| Normalized Vector2 | `normal2t()` | `sizeof(component)` |
|
||||
| Normalized Vector3 | `normal3t()` | `2 * sizeof(component)` |
|
||||
|
||||
### Collections and others
|
||||
|
||||
| Type | Methods | Size |
|
||||
|-----------------------|---------------------------------------------------------|--------------------------------------------------------------------|
|
||||
| Arrays | `array_of()` | `sizeof(size) + array.size() * sizeof(item)` |
|
||||
| Dictionaries | `dictionary()` | `sizeof(size) + dictionary.size() * (sizeof(key) + sizeof(value))` |
|
||||
| Strings | `string()` | Size in UTF-8 + null-terminator at the end |
|
||||
| Variant | `variant()` | Same as [var_to_bytes()] |
|
||||
|
||||
## Implementing a custom serializer
|
||||
|
||||
Custom serializers are also supported. To implement one, extend the
|
||||
`NetworkSchemaSerializer` class, and implement the `encode()` and `decode()`
|
||||
methods.
|
||||
|
||||
For example, consider a `Node` serializer that encodes the node's path:
|
||||
|
||||
```gdscript
|
||||
--8<-- "examples/snippets/network-schemas/example-node-serializer.gd"
|
||||
```
|
||||
|
||||
This custom serializer can now be used in schemas:
|
||||
|
||||
```gdscript
|
||||
rollback_synchronizer.set_schema({
|
||||
"Input:target": ExampleNodeSerializer.new()
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
[Binary serialization API]: https://docs.godotengine.org/en/stable/tutorials/io/binary_serialization_api.html
|
||||
[RollbackSynchronizer]: ../nodes/rollback-synchronizer.md
|
||||
[StateSynchronizer]: ../nodes/state-synchronizer.md
|
||||
[var_to_bytes()]: https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html#class-globalscope-method-var-to-bytes
|
||||
@@ -0,0 +1,91 @@
|
||||
# NetworkTimeSynchronizer
|
||||
|
||||
Synchronizes time to the host remote. Provided as an autoload.
|
||||
|
||||
Synchronization is done by continuously pinging the host remote, and using
|
||||
these samples to figure out clock difference and network latency. These are
|
||||
then used to gradually adjust the local clock to keep in sync.
|
||||
|
||||
## The three clocks
|
||||
|
||||
The process distinguishes three different clock concepts:
|
||||
|
||||
The *Remote clock* is the clock being synchronized to, running on the host peer.
|
||||
|
||||
The *Reference clock* is a local clock, running on the client, that is getting
|
||||
adjusted to match the Remote clock as closely as possible. This clock is
|
||||
unsuitable to use for gameplay, as it being regularly adjusted can lead to
|
||||
glitchy movement.
|
||||
|
||||
The *Simulation clock* is also a local clock, and is being synchronized to the
|
||||
Reference clock. The Simulation clock is guaranteed to only move forwards in
|
||||
time. It drives the [Network tick loop].
|
||||
|
||||
Most of the time you shouldn't need to interface with this class directly,
|
||||
instead you can use [NetworkTime].
|
||||
|
||||
## Synchronizing the Reference clock
|
||||
|
||||
Synchronization is done by regularly taking samples of the remote clock, and
|
||||
deriving roundtrip time and clock offset from each sample. These samples are
|
||||
then combined into a single set of stats - offset, roundtrip time and jitter.
|
||||
|
||||
*Offset* is the difference to the remote clock. Positive values mean the remote
|
||||
clock is ahead of the reference clock. Negative values mean that the remote
|
||||
clock is behind the reference clock. May also be called theta.
|
||||
|
||||
*Roundtrip time* is the time it takes for data to travel to the remote and then
|
||||
back over the network. Smaller roundtrip times usually mean faster network
|
||||
connections. May also be called delay or delta.
|
||||
|
||||
*Jitter* is the amount of variation in measured roundtrip times. The less
|
||||
jitter, the more stable the network connection usually.
|
||||
|
||||
These stats are then used to get a good estimate of the current time on the
|
||||
remote clock. The remote clock estimate is then used to slowly adjust ( nudge )
|
||||
the reference clock towards the remote clock's value.
|
||||
|
||||
This is done iteratively, to avoid large jumps in time, and to - when possible
|
||||
- only go forward in time, not backwards.
|
||||
|
||||
When the offset gets too significant, it means that the clocks are excessively
|
||||
out of sync. In these cases, a panic occurs and the reference clock is reset.
|
||||
|
||||
This process is inspired by the [NTPv4] RFC.
|
||||
|
||||
## Synchronizing the Simulation clock
|
||||
|
||||
While the Reference clock is in sync with the Remote clock, its time is not
|
||||
linear - it is not guaranteed to advance monotonously, and technically it's
|
||||
also possible for it to move backwards. This would lead to uneven tick loops (
|
||||
e.g. sometimes 3 ticks in a single loop, sometimes 1, sometimes 5), and by
|
||||
extension, uneven and jerky movement.
|
||||
|
||||
To counteract the above, the Simulation clock is introduced. It is synced to
|
||||
the Reference clock, but instead of adjusting it by adding small offsets to it,
|
||||
it is *stretched*.
|
||||
|
||||
Whenever the Simulation clock is ahead of the Reference clock, the it will
|
||||
slightly slow down, to allow the Reference clock to catch up. When the
|
||||
Reference clock is ahead of the Simulation clock, it will run slightly faster
|
||||
to catch up with the Reference clock.
|
||||
|
||||
These stretches are subtle enough to not disturb gameplay, but effective enough
|
||||
to keep the two clocks in sync.
|
||||
|
||||
The Simulation clock is handled by [NetworkTime].
|
||||
|
||||
## Characteristics
|
||||
|
||||
The above process works well regardless of latency - very similar results can
|
||||
be achieved with 50ms latency as with 250ms.
|
||||
|
||||
Synchronization is more sensitive to jitter. Less stable network connections
|
||||
produce more varied latencies, which makes it difficult to distinguish clock
|
||||
offsets from latency variations. This in turn leads to the estimated clock
|
||||
offset changing more often, which results in more clock adjustments.
|
||||
|
||||
[Network tick loop]: ./network-time.md#network-tick-loop
|
||||
[NetworkTime]: ./network-time.md
|
||||
[NTPv4]: https://datatracker.ietf.org/doc/html/rfc5905
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
# 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:
|
||||
|
||||

|
||||
|
||||
*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
|
||||
@@ -0,0 +1,36 @@
|
||||
# Property paths
|
||||
|
||||
Multiple nodes have *properties* as their configurations. These are specified
|
||||
as *property paths*, which have a specific syntax.
|
||||
|
||||

|
||||
|
||||
These nodes have a *Root* property. During path resolution, this *Root* node is
|
||||
taken as base for relative paths.
|
||||
|
||||
## Syntax
|
||||
|
||||
Property paths are specified as follows:
|
||||
|
||||
```txt
|
||||
<node-path>:<property-name>
|
||||
```
|
||||
|
||||
Node path can be *empty* if it refers to a property on the *root* node.
|
||||
|
||||
If specified, node path will be interpreted relative to the *root* node. Any
|
||||
valid [NodePath] will work as expected.
|
||||
|
||||
Nested properties are also supported. Specify them by appending a colon and an
|
||||
additional property name.
|
||||
|
||||

|
||||
|
||||
With Brawler as root:
|
||||
|
||||
* `:position` refers to the Brawler's position
|
||||
* `Input:aim` refers to the Input's aim
|
||||
* `:velocity:x` refers to the Brawler's velocity's X component; this is a
|
||||
nested property
|
||||
|
||||
[NodePath]: https://docs.godotengine.org/en/stable/classes/class_nodepath.html
|
||||
@@ -0,0 +1,80 @@
|
||||
# Visibility Management
|
||||
|
||||
By default, *netfox* synchronizes all properties to all peers, broadcasting
|
||||
data. This may not always be the best approach. An example is competitive
|
||||
games. These games often features mechanics like fog of war, invisibility, or
|
||||
line of sight checks. If any of these obscures a player, other players should
|
||||
not receive information about them, to avoid the possibility of wallhacks and
|
||||
other similar cheats.
|
||||
|
||||
This is supported by the use of *visibility filters*. They provide three
|
||||
mechanisms to determine who should receive data and who shouldn't.
|
||||
|
||||
## Accessing the visibility filter
|
||||
|
||||
Both [RollbackSynchronizer] and [StateSynchronizer] supports visibility
|
||||
filtering. They expose a `visibility_filter` property that can be used to
|
||||
configure filtering.
|
||||
|
||||
!!!warning
|
||||
When using visibility filtering with [RollbackSynchronizer] nodes, make
|
||||
sure to disable input broadcast. Otherwise, peers might receive input data
|
||||
from the player, but no state data from the server, leading to nodes being
|
||||
simulated without up-to-date state data.
|
||||
|
||||
## Default visibility
|
||||
|
||||
If there's no settings configured, the visibility filter falls back to the
|
||||
`default_visibility`. By default it is `true`, meaning it will broadcast data
|
||||
to all peers.
|
||||
|
||||
## Per-peer override
|
||||
|
||||
Visibility can also be set individually for each peer. This overrides the
|
||||
default visibility for the given peer.
|
||||
|
||||
An override may be `true`, `false`, or not set. An override to `true` means
|
||||
that the peer will be visible regardless of the default visibility. An override
|
||||
to `false` means that the peer will not be visible regardless of the default
|
||||
visibility. An unset override means it will fall back to the
|
||||
`default_visibility`.
|
||||
|
||||
## Filter callbacks
|
||||
|
||||
Callbacks can also be registered, to filter peers dynamically. These filters
|
||||
run before the per-peer overrides. If any of the filters reject the peer, it
|
||||
will not receive data.
|
||||
|
||||
These callbacks receive the peer ID, and return a boolean:
|
||||
|
||||
```gd
|
||||
filter.add_visibility_filter(func(peer: int):
|
||||
# Forbidden trick to halve your bandwidth :P
|
||||
return (peer % 2) == 0
|
||||
)
|
||||
```
|
||||
|
||||
## Update modes
|
||||
|
||||
Visibility filters keep an internal list of visible peers. To save on compute,
|
||||
this list is only updated on certain configurable events. This is exposed as
|
||||
its `update_mode` property, which can take on the following values:
|
||||
|
||||
Never
|
||||
: Only update visibility when manually triggered using `update_visibility()`
|
||||
|
||||
On peer
|
||||
: Only update when a peer joins or leaves
|
||||
|
||||
Per tick loop
|
||||
: Update visibility before each tick loop
|
||||
|
||||
Per tick
|
||||
: Update visibility before each network tick
|
||||
|
||||
Per rollback tick
|
||||
: Update visibility *after* each rollback tick
|
||||
|
||||
|
||||
[RollbackSynchronizer]: ../nodes/rollback-synchronizer.md
|
||||
[StateSynchronizer]: ../nodes/state-synchronizer.md
|
||||
Reference in New Issue
Block a user