b0c83af092
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>
162 lines
5.2 KiB
Markdown
162 lines
5.2 KiB
Markdown
# Predicting input
|
|
|
|
Whenever clients send their inputs, it takes some time to arrive. From there,
|
|
it also takes time for the updated game state to arrive to clients.
|
|
|
|
This means that the server never knows the client's *current* input, only the
|
|
input from a few ticks ago - depending on network latency. Other clients are
|
|
even more behind, as they also need to wait for the server to broadcast the
|
|
updated game state.
|
|
|
|
Another trick *netfox* enables to hide this latency is *input prediction*.
|
|
|
|
## About prediction
|
|
|
|
By default, nodes are only simulated for ticks that we currently have enough
|
|
information for - i.e. the *input* for the current tick. If there's no input,
|
|
the node simply isn't simulated, as we can't know what the player intended to
|
|
do.
|
|
|
|
But, what if we do know? Or what if we can make a reasonable guess?
|
|
|
|
For example, in driving games, it is a safe assumption that if the player was
|
|
going full throttle three ticks ago, they are still going full throttle.
|
|
|
|
It is important to consider the last received input's *age*. The more time
|
|
passes, the harder it is to reasonably predict the player's inputs.
|
|
|
|
*Prediction* allows users to implement similar, game-specific predictions.
|
|
|
|
## Implementing input prediction
|
|
|
|
`NetworkRollback` provides the following signal:
|
|
|
|
```gdscript
|
|
signal after_prepare_tick(tick: int)
|
|
```
|
|
|
|
This is emitted during rollback, *after* the input and state is applied for the
|
|
tick about to be simulated. This is the phase where input prediction may
|
|
happen.
|
|
|
|
Firstly, call `RollbackSynchronizer.is_predicting()`, to check if any
|
|
prediction needs to be done. If none, input can be left as-is, without
|
|
predicting.
|
|
|
|
You may also check if there's *any* known input for the current tick that we
|
|
can base our prediction off of. This is done by calling
|
|
`RollbackSynchronizer.has_input()`.
|
|
|
|
For the actual prediction, consider the age of the last known input. This is
|
|
obtained by calling `RollbackSynchronizer.get_input_age()`, which will return
|
|
the applied input's age in ticks.
|
|
|
|
---
|
|
|
|
To put all of this into practice, see the following snippet:
|
|
|
|
```gdscipt
|
|
extends BaseNetInput
|
|
|
|
var movement: Vector3
|
|
var confidence: float = 1.
|
|
|
|
@onready var _rollback_synchronizer := $"../RollbackSynchronizer" as RollbackSynchronizer
|
|
|
|
func _ready():
|
|
super()
|
|
|
|
# Predict on `after_prepare_tick`
|
|
NetworkRollback.after_prepare_tick.connect(_predict)
|
|
|
|
func _gather():
|
|
# Gather input
|
|
movement = Vector3(
|
|
Input.get_axis("move_east", "move_west"),
|
|
Input.get_action_strength("move_jump"),
|
|
Input.get_axis("move_south", "move_north")
|
|
)
|
|
|
|
func _predict(_t):
|
|
if not _rollback_synchronizer.is_predicting():
|
|
# Not predicting, nothing to do
|
|
confidence = 1.
|
|
return
|
|
|
|
if not _rollback_synchronizer.has_input():
|
|
# Can't predict without input
|
|
confidence = 0.
|
|
return
|
|
|
|
# Decay input over a short time
|
|
var decay_time := NetworkTime.seconds_to_ticks(.15)
|
|
var input_age := _rollback_synchronizer.get_input_age()
|
|
|
|
# **ALWAYS** cast either side to float, otherwise the integer-integer
|
|
# division yields either 1 or 0 confidence
|
|
confidence = input_age / float(decay_time)
|
|
confidence = clampf(1. - confidence, 0., 1.)
|
|
|
|
# Modulate input based on confidence
|
|
movement *= confidence
|
|
```
|
|
|
|
In this example, a confidence value is calculated based on the input age. This
|
|
is then used to gradually fade out the input, as if the player slowly let go of
|
|
the controls.
|
|
|
|
Make sure to consider the specifics of your game and tailor your input
|
|
prediction strategy to the game's needs. Depending on the game, you may even
|
|
opt out of prediction.
|
|
|
|
## Impossible predictions
|
|
|
|
In the example above, a *confidence* value of zero means that input simply
|
|
can't be predicted currently. This usually happens when the input is too old to
|
|
use for prediction.
|
|
|
|
In this case, call `NetworkRollback.ignore_prediction(target)`. This lets
|
|
*netfox* know that the target node - usually `self` - can't be predicted. Its
|
|
simulated state will not be recorded for the current tick.
|
|
|
|
To see this in practice:
|
|
|
|
```gdscript
|
|
func _rollback_tick(dt, _t, _if):
|
|
if is_zero_approx(input.confidence):
|
|
# Can't predict, not enough confidence in input
|
|
_rollback_synchronizer.ignore_prediction(self)
|
|
return
|
|
|
|
# ... run simulation as usual ...
|
|
```
|
|
|
|
If there's not enough confidence in the input, `ignore_prediction` is called,
|
|
and we return early.
|
|
|
|
!!! note
|
|
`NetworkRollback.ignore_prediction()` can be called for multiple nodes from
|
|
the same script. This is useful in cases where a single script drives
|
|
multiple nodes, like an FPS controller updating the whole body's position
|
|
and the head's rotation independently.
|
|
|
|
## Configuring prediction
|
|
|
|
Running the game in its current state would result in no changes - *prediction
|
|
is off by default*. It can be toggled separately for each
|
|
`RollbackSynchronizer`.
|
|
|
|
To enable, check *Enable Prediction* in the `RollbackSynchronizer`'s
|
|
configuration:
|
|
|
|

|
|
|
|
With this configured, `RollbackSynchronizer` will simulate all the nodes it
|
|
manages even for ticks that *it doesn't have input for*.
|
|
|
|
## Example project
|
|
|
|
To see all of the above as one cohesive project, see the [Input prediction example].
|
|
|
|
[Input prediction example]: https://github.com/foxssake/netfox/tree/main/examples/input-prediction
|