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,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
|
||||
Reference in New Issue
Block a user