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:
2026-07-02 20:55:20 -04:00
parent ce39b237c3
commit b0c83af092
4416 changed files with 57418 additions and 902676 deletions
@@ -0,0 +1,119 @@
# Configuring properties from code
In netfox, there are multiple nodes that accept [property paths] as their
configuration, for various purposes. These can be configured as lists of
strings in the editor.
In bigger projects, with many scenes and deeper class trees, manually
configuring property paths may be tedious and unscaleable. Potentially, there
may be cases where these properties are only known at runtime, not when working
in the Editor.
There are solutions for both cases.
## Adding properties from code
Properties can be added at run-time with the following methods:
* `TickInterpolator::add_property(node, property)`
* `StateSynchronizer::add_state(node, property)`
* `RollbackSynchronizer::add_state(node, property)`
* `RollbackSynchronizer::add_input(node, property)`
*node* is a reference to a node - it may be a *string* or a *[NodePath]*
pointing to an existing node, or a *[Node]* instance. When using paths, the
path itself is considered relative to the configured *root* node.
After calling any of the methods above, calling `process_settings()` is not
necessary - it will be called automatically.
!!! warning
The same as with `process_settings()`, configuration changes are not
synchronized automatically! You, the developer, must ensure that
configuration changes happen on all peers, at the same time.
Changing state- and input property configurations is not recommended during
gameplay.
## Adding properties automatically, in the editor
You can ensure that certain properties are added to netfox's nodes'
configuration by making your class a `@tool` script, and implementing the
following methods:
* TickInterpolator: `_get_interpolated_properties()`
* StateSynchronizer: `_get_synchronized_state_properties()`
* RollbackSynchronizer:
* `_get_rollback_state_properties()` for state
* `_get_rollback_input_properties()` for input
These must return an array, with each element being a string, or a two-element
array.
Strings are interpreted as property names.
Arrays are interpreted as node-property pairs. Similarly to the `add_*`
methods, the *node* may be a string, a [NodePath], or an actual [Node]
instance. When using strings or [NodePath]s, keep in mind that the path is
considered *relative to the node itself, not the configured root*.
Each of these nodes will explore nodes under their `root` node, and call the
above methods if implemented. The results will be added to the node
configuration.
This exploration is implemented in the nodes' `_get_configuration_warnings()`
method, which is called when the node tree changes ( i.e. nodes are added /
removed ), and when opening the scene.
The exploration also runs when before saving the scene, to make sure that any
updates are picked up.
!!! tip
To make sure that the updated methods are picked up, save your scene. The
exploration is ran before every scene save.
An example implementation for the above methods:
```gdscript
func _get_interpolated_properties():
# Specify a list of properties
return ["position", "quaternion"]
func _get_synchronized_state_properties() -> Array:
# Specify inherited properties and more
return super() + [
"health", "name",
[weapon, "ammo"], # Specify a property on another node
["Hand/Weapon", "ammo"] # Specify node by path
]
func _get_rollback_state_properties() -> Array:
return [
"transform", # Specify a property on self
[weapon, "ammo"] # Specify a property on another node
]
func _get_rollback_input_propertes() -> Array:
# Specify a list of properties
return ["movement", "is_jumping"]
```
See the [Property configuration example].
!!! note
In general, it's best practice to only specify node's own properties. An
exception is when the given node has no script attached.
### Caveats
**Node renames and removals** are not tracked. Unless fixed manually, they will
result in invalid property warnings.
A workaround is to reset the node's state/input/property configuration to an
empty array and save again. This will gather the tracked properties with the
right node names.
[property paths]: ../guides/property-paths.md
[NodePath]: https://docs.godotengine.org/en/stable/classes/class_nodepath.html
[Node]: https://docs.godotengine.org/en/stable/classes/class_node.html
[Property configuration example]: https://github.com/foxssake/netfox/tree/main/examples/property-configuration
@@ -0,0 +1,171 @@
# Input gathering tips and tricks
In the [Responsive player movement] tutorial, we've seen a basic example on how
to gather input. This tutorial will elaborate on how input gathering works
under the hood, and how that affects some common input patterns in games.
!!!note
You can find the full project [in the repository].
## Understanding input gathering
To have a shared notion of time, *netfox* provides its own time synchronization
and a *tick loop*. The *tick loop* will check how much time has passed since
the last network tick, and will run as many ticks as needed to catch up. Most
often this is a single tick every few frames, but in special cases it might
need to run multiple ticks in a single loop.
To have input available for each tick, *RollbackSynchronizer*s record input
after every network tick.
Since multiple ticks may be ran in a single tick loop, it makes no sense to
gather input for each tick - the hardware wouldn't update, since the ticks are
run one after the other.
Instead, input is gathered *before* each tick loop, and then reused for each
tick in the loop. This explains why special measures need to be taken in some
cases.
To read more about *netfox*'s *tick loop*, see the [Network tick loop].
## Continuous inputs
Consider player movement - if the player holds the button *up*, the character
will move north, right for east, *down* for south, *left* for west. If the
player holds two directions, the character will move diagonally.
Since the player needs to *hold* the buttons for movement to happen, it is
considered a *continuous* input.
Checking the inputs pressed at the point of gather works:
```gdscript
extends BaseNetInput
class_name PlayerInput
var movement: Vector3 = Vector3.ZERO
func _gather():
movement = Vector3(
Input.get_axis("move_west", "move_east"),
Input.get_action_strength("move_jump"),
Input.get_axis("move_north", "move_south")
)
```
However, consider what happens if inputs change between two ticks. Let's
visualize one such case on a timeline:
```puml
@startuml
concise "Player Input" as P
@P
0 is Up
3 is Right: Tick
4 is Up
6 is Right: Tick
```
Even though the player alternated between pressing Up and Right, only Right was
recorded as an input. This is gets worse considering that the player was
pressing Up *the majority of the time*.
The solution is to sample player input on every `_process()` frame, and average
the samples collected before each tick loop.
```gdscript
--8<-- "examples/snippets/input-gathering-tutorial/continuous-sampled-input.gd"
```
This way, every known input is taken into account.
This method shines the best in cases where the network tickrate is considerably
lower than the actual FPS at which the game runs. For example, in case the
network tick loop runs at 30 ticks per second, but the game consistently runs
and renders at 60fps, or even more.
With 30tps and 60fps, we take on average two input samples per tick.
## One-off inputs
Depending on game design, there are cases where the game needs the player to
press a button to take an action. If the button is held, the action still
happens only once, as it was pressed only once. If the player needs to perform
the action multiple times, they need to press the relevant button multiple
times.
These are considered *one-off inputs*.
Godot provides methods such as [Input.is_action_just_pressed()] to check if a
given input was just pressed. Counterintuitively, this does not work as
expected - the method recognizes the current frame ( `_process` ) or physics
tick ( `_physics_process` ), but not *netfox* ticks. Let's see it on a
timeline:
```puml
@startuml
concise "Player Input" as P
@P
0 is Empty
2 is Jump: Pressed
5 is Jump: Tick
```
Even though the input was pressed on frame 2, input gathering only ran on frame
5, by which time the input is *held*, not *just pressed*. This means, that the
*just pressed* check will only register if the player manages to press the
button on the exact same frame as the input gathering is running.
A different issue pops up when the game slows down a bit, and *netfox* needs to
run multiple ticks in a single loop to catch up. Let's visualize this with a
timeline, showing both the user input in real-time, and what netfox records as
input:
```puml
@startuml
concise "Player Input" as P
concise "Recorded Input" as R
@0
P is Empty
R is Empty
@3
P is Jump
R is Jump: Start loop
@4
P is Empty
@6
R is Empty: End loop
```
The player pressed Jump on a single frame, which was recorded. Then, this
single recorded input was used for each tick in the tick loop. Resulting in the
player trying to jump for multiple ticks, even though they pressed the button
only on a single frame.
To solve both of these issues, *one-off inputs* can be buffered similarly to
*continuous inputs*. The difference is that we reset the input value after it's
gathered - this way, the input will be true for *at most* a single tick:
```gdscript
--8<-- "examples/snippets/input-gathering-tutorial/one-off-input.gd"
```
!!!tip
The same principle of using buffer variables and accumulating input samples
can be implemented in the `_input()` callback as well.
[in the repository]: https://github.com/foxssake/netfox/tree/main/examples/input-gathering
[Responsive player movement]: ./responsive-player-movement.md
[Network tick loop]: ../guides/network-time.md#network-tick-loop
[Input.is_action_just_pressed()]: https://docs.godotengine.org/en/stable/classes/class_input.html#class-input-method-is-action-just-pressed
@@ -0,0 +1,39 @@
# Interpolation caveats
While netfox runs netcode at a fixed rate, the game may render frames at a
higher, varying framerate. Interpolation smooths out the difference between
tickrate and framerate, when using [TickInterpolator].
Below are some aspects that may catch users off guard.
### Interpolate only visuals
A node's state may consist of multiple properties, some of which affect its
appearance ( e.g. position, rotation, scale ), some are only relevant to the
simulation - e.g. most objects look the same regardless of their velocity, even
though it's important for simulating their behavior.
Since interpolation matters only for the game's visuals, it's enough to
interpolate only the properties that affect the game's visuals.
### Rotation vs. Quaternion vs. Transform
Interpolating `rotation` may lead to glitchy results when an object makes a
full turn. This stems from the way `rotation` works - it represents the amount
of rotation per axis, in Euler angles. Using Euler angles to interpolate
rotations doesn't work well, as they can end up interpolating from -180 degrees
to +180 numerically. The expected behavior would be to go from -180 to +180
instantly, since they represent the same rotation. The same thing happens in
animation software as well, when trying to interpolate with Euler angles.
What to do instead:
* Interpolate the whole `transform`
* Interpolate `quaternion` - represents rotation, but better suited to
interpolation
For more, see Godot docs on [3D transforms]
[TickInterpolator]: ../nodes/tick-interpolator.md
[3D transforms]: https://docs.godotengine.org/en/stable/tutorials/3d/using_transforms.html
@@ -0,0 +1,161 @@
# Modifying objects during rollback
There are cases where two objects interact and modify each other during
rollback. For example:
* Players shoving another
* An explosion displacing objects around it
* Two cars colliding
* A player shooting at another - if player stats are managed as part of
rollback
## Using Mutations
!!!warning
Mutations are *experimental*, meaning the API may change in breaking ways,
and may be less stable than other features.
Once the API matures and finds its final form, the *experimental* mark will
be removed. Feedback is welcome in the meanwhile!
Mutations enable modifying objects during rollback, in a way that is taken into
account by netfox.
When an object is modified during rollback, call `NetworkRollback.mutate()`,
passing said object as an argument.
As a result, the changes made to the object in the current tick will be
recorded. Since its history has changed, it will be resimulated from the point
of change - i.e. for all ticks after the change was made.
!!!note
Make sure that `mutate()` is only called on objects that need it - otherwise,
ticks will be resimulated for objects that don't need it, resulting in worse
performance.
### Example code
To see this in action, take a snippet from Forest Brawl:
```gdscript
for brawler in _get_overlapping_brawlers():
var diff := brawler.global_position - global_position
var f := clampf(1.0 / (1.0 + diff.length_squared()), 0.0, 1.0)
var offset := Vector3(diff.x, max(0, diff.y), diff.z).normalized()
offset *= strength_factor * strength * f * NetworkTime.ticktime
brawler.shove(offset)
NetworkRollback.mutate(brawler)
```
The script calculates which direction to shove the player in, and with what
force. This is then applied by calling `shove()`.
Then, on the last line, these changes are saved by calling
`NetworkRollback.mutate(brawler)`.
Calling `mutate()` is all that's needed to use this feature.
## The problem with naive implementations
The simplest way to implement these mechanics is to just update the affected
object, without using mutations. For example, when one player shoves another,
the shove direction can simply be added to the target player's position. Doing
this will not work unfortunately.
Let's say that Player A is shoving Player B. With Player A being the local
player, we have input for its actions. With Player B being a remote player, it
won't be simulated. So even though its position was modified, this change will
not be recorded, and will be overridden by its last *known* position.
```puml
@startuml
concise "Player A" as PA
concise "Player B" as PB
@0
PA is Restored
PB is Restored
@8
PA is Simulated
@10
PA -> PB: shove()
@enduml
```
In the example above, even though Player A shoved Player B on tick 10, Player B
is not simulated in that given tick, so it won't be recorded. Player A's shove
is not saved to history.
This may partially be fixed by enabling [prediction] for players.
Take another case, where Player B wants to shove Player A. With Player B being
a remote player, we only receive its input a few ticks after the fact. So we
need to resimulate Player B from an earlier tick. In one of these earlier tick,
Player A gets shoved.
```puml
@startuml
concise "Player A" as PA
concise "Player B" as PB
@0
PA is Restored
PB is Restored
@6
PB is Simulated
@7
PB -> PA: shove()
@8
PA is Simulated
@enduml
```
In this example, we've received input for Player B for tick 6 onwards. On tick
7, Player B shoves Player A. Since we've already simulated Player A for the
given tick, we don't need to simulate it again. This means that any changes for
the tick will not be recorded. The shove will not be saved to history.
Since Player A was already simulated and recorded for this earlier tick, it
being shoved will not be recorded.
In both cases, we need to use mutations to tell netfox that a given object has
been modified ( *mutated* ), and its state history should be updated.
Let's try the previous example, but now with `mutate()` added:
```puml
@startuml
concise "Player A" as PA
concise "Player B" as PB
@0
PA is Restored
PB is Restored
@6
PB is Simulated
@7
PB -> PA: shove()\nmutate()
PA is Simulated
@enduml
```
Player A will be resimulated from the point of shoving, and the shove itself
will be recorded.
[prediction]: ./predicting-input.md
+161
View File
@@ -0,0 +1,161 @@
# 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:
![Node configuration](../assets/rollback-enable-predict.png)
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
@@ -0,0 +1,151 @@
# Responsive player movement
To compensate for latency, *netfox* implements [Client-side prediction and
Server reconciliation]. This documentation also refers to it as rollback.
One use case is player movement - with CSP we don't need to wait for the
server's response before the player's avatar can be updated.
## Gathering input
For CSP, input is separated from player state. In practice, this means that
there's a separate node with its own script that manages input. The job of this
script is to manage properties related to input - for example, which direction
the player wants to move:
```gdscript
extends Node
class_name PlayerInput
var movement = Vector3.ZERO
```
These *input properties* must be updated based on player input. Hook into the
[network tick loop]'s *before_tick_loop* signal to update input properties:
```gdscript
func _ready():
NetworkTime.before_tick_loop.connect(_gather)
func _gather():
if not is_multiplayer_authority():
return
movement = Vector3(
Input.get_axis("move_west", "move_east"),
Input.get_action_strength("move_jump"),
Input.get_axis("move_north", "move_south")
)
```
It is important to only update input properties if we have authority over the
node. Otherwise we would try to change some other player's input with our own
actions.
### Using BaseNetInput
The same can be accomplished with [BaseNetInput], with slightly less code:
```gdscript
extends BaseNetInput
class_name PlayerInput
var movement: Vector3 = Vector3.ZERO
func _gather():
movement = Vector3(
Input.get_axis("move_west", "move_east"),
Input.get_action_strength("move_jump"),
Input.get_axis("move_north", "move_south")
)
```
## Applying movement
The other part of the equation is *state*. Use the same approach as you would
with your character controller, with the game logic being implemented in
`_rollback_tick` instead of `_process` or `_physics_process`:
```gdscript
extends CharacterBody3D
@export var speed = 4.0
@export var input: PlayerInput
func _rollback_tick(delta, tick, is_fresh):
velocity = input.movement.normalized() * speed
velocity *= NetworkTime.physics_factor
move_and_slide()
velocity /= NetworkTime.physics_factor
```
Note the usage of `physics_factor` - this is explained in [the caveats].
## Configuring rollback
Create a reusable player scene with the following layout:
![Node layout](../assets/tutorial-nodes.png)
The root is a *CharacterBody3D* with the player controller script attached.
The *Input* child manages player input and has the player input script
attached.
The [RollbackSynchronizer] node manages the rollback logic, making the player
motion responsive while also keeping it [server-authoritative].
Configure the *RollbackSynchronizer* with the following input- and state
properties:
![RollbackSynchronizer settings](../assets/tutorial-rollback-settings.png)
## Ownership
Make sure that all of the player nodes are owned by the server. The exception
is the *Input* node, which must be owned by the player who the avatar belongs
to.
After setting ownerships, **make sure** to call `process_settings` on
*RollbackSynchronizer*. This call is necessary after every ownership change.
*RollbackSynchronizer* sorts properties based on ownership, but this sorting is
only done in `process_settings`.
For example:
```gdscript
@onready var rollback_synchronizer = $RollbackSynchronizer
var peer_id = 0
func _ready():
# Wait a frame so peer_id is set
await get_tree().process_frame
# Set owner
set_multiplayer_authority(1)
input.set_multiplayer_authority(peer_id)
rollback_synchronizer.process_settings()
```
Note that `peer_id` needs to be set from the outside during spawn.
## Smooth motion
Currently, state is only updated on network ticks. If the tickrate is less than
the FPS the game is running on, motion may get choppy.
Add a [TickInterpolator] node and configure it with the same *state properties*
as the *RollbackSynchronizer*:
![TickInterpolator settings](../assets/tutorial-tick-interpolator-settings.png)
This will ensure smooth motion, regardless of FPS and tickrate.
[Client-side prediction and Server reconciliation]: https://www.gabrielgambetta.com/client-side-prediction-server-reconciliation.html
[BaseNetInput]: ../../netfox.extras/guides/base-net-input.md
[network tick loop]: ../guides/network-time.md#network-tick-loop
[RollbackSynchronizer]: ../nodes/rollback-synchronizer.md
[server-authoritative]: ../concepts/authoritative-servers.md
[the caveats]: ./rollback-caveats.md#characterbody-velocity
[TickInterpolator]: ../nodes/tick-interpolator.md
+121
View File
@@ -0,0 +1,121 @@
# Rollback caveats
As with most things, rollback has some drawbacks along with its benefits.
### CharacterBody velocity
Godot's `move_and_slide()` uses the `velocity` property, which is set in
meters/second. The method assumes a delta time based on what kind of frame is
being run. However, it is not aware of *netfox*'s network ticks, which means
that movement speed will be off.
To counteract this, multiply velocity with `NetworkTime.physics_factor`, which
will adjust for the difference between Godot's *assumed* delta time and the
delta time *netfox* is using.
If you don't want to lose your original velocity ( e.g. because it accumulates
acceleration over time ), divide by the same property after using any built-in
method. For example:
```gdscript
# Apply movement
velocity *= NetworkTime.physics_factor
move_and_slide()
velocity /= NetworkTime.physics_factor
```
### CharacterBody on floor
CharacterBodies only update their `is_on_floor()` state only after a
`move_and_slide()` call.
This means that during rollback, the position is updated, but the
`is_on_floor()` state is not.
As a work-around, do a zero-velocity move before checking if the node is on the
floor:
```gdscript
extends CharacterBody3D
func _rollback_tick(delta, tick, is_fresh):
# Add the gravity.
_force_update_is_on_floor()
if not is_on_floor():
velocity.y -= gravity * delta
# ...
func _force_update_is_on_floor():
var old_velocity = velocity
velocity = Vector3.ZERO
move_and_slide()
velocity = old_velocity
```
### Physics updates
Godot's physics system is updated only during `_physics_process`, while
rollback updates the game state multiple times during a single frame.
Unfortunately, Godot does not support manually updating or stepping the physics
system, [at least at the time of writing](https://github.com/godotengine/godot/pull/76462).
This means that:
* Rollback and physics-based games ( RigidBodies ) don't work at the moment
* Collision detection can work, but with workarounds
If there's a way to force an update for your given node type, it should work,
i.e.
* ShapeCast (2D and 3D) - [force_shapecast_update()]
* ChacacterBody (2D and 3D) - [move_and_collide()] ( which has a test only
mode )
While kinematic nodes like `CharacterBody3D` can be used with rollback, physics
queries can still cause issues (e.g.
`PhysicsDirectSpaceState3D.intersect_shape()`). This is due to the lack of
updates mentioned earlier. To work around this, run the following for each
`CollisionObject` that has its position rolled back before each tick of the
rollback loop:
```gdscript
# Works for both Jolt and GodotPhysics3D.
func _force_update_physics_transform():
PhysicsServer3D.body_set_mode(get_rid(), PhysicsServer3D.BODY_MODE_STATIC)
PhysicsServer3D.body_set_state(get_rid(), PhysicsServer3D.BODY_STATE_TRANSFORM, global_transform)
PhysicsServer3D.body_set_mode(get_rid(), PhysicsServer3D.BODY_MODE_KINEMATIC)
```
The above forces an update by setting the object to static, updating its
transform, and then setting it back to its original, kinematic state.
Note that the above code needs to run for any kinematic object that is to be
detected by the query and is manipulated during rollback.
!!!tip
The *netfox.extras* addon provides optional support for physics simulation
with rollback. See [Physics](../../netfox.extras/guides/physics.md)
### State Machines
State machines don't usually expect to be updated multiple times in a single
frame or be snapped back to a previous point in time. Be cautious of:
- Safeguards that implement a cooldown to changes.
- States based on values not updated in `_rollback_tick`.
- Transitions that enforce a specific order to state changes.
- Transitions that trigger on any state change.
The key concept to keep in mind is that netfox stores the configured states for
each processed tick. When it rolls back everything is snapped back to that
point in time and then played forward to the present in a single frame.
!!!tip
The *netfox.extras* module provides an implementation of state machines
compatible with rollback. See
[RewindableStateMachine](../../netfox.extras/guides/rewindable-state-machine.md)
[force_shapecast_update()]: https://docs.godotengine.org/en/stable/classes/class_shapecast3d.html#class-shapecast3d-method-force-shapecast-update
[move_and_collide()]: https://docs.godotengine.org/en/stable/classes/class_physicsbody3d.html#class-physicsbody3d-method-move-and-collide
@@ -0,0 +1,26 @@
# Using RollbackSynchronizer without inputs
In certain cases, a component needs to participate in rollback, but is not
driven by any input. One example could be more complex NPCs. These need to be
part of the rollback tick loop, but they are not controlled by any player.
In these cases, you can use RollbackSynchronizer as described earlier in
[Responsive player movement], but without the input. This means not needing an
input node, and not configuring any input properties. State properties still
need to be configured, and the gameplay logic must be implemented in
`_rollback_tick()`.
!!!tip
An example project featuring a simple NPC using an inputless
RollbackSynchronizer can be found at [examples/rollback-npc].
Under the hood, *netfox* will simulate these inputless nodes whenever it
encounters a tick that has no state for the inputless node. On the server, this
means inputless nodes will be simulated only for new ticks. On clients, this
means never being simulated, since all state is received from the server. If
prediction is enabled, clients will simulate inputless nodes for ticks they
don't have data from the server.
[Responsive player movement]: ./responsive-player-movement.md
[examples/rollback-npc]: https://github.com/foxssake/netfox/tree/main/examples/rollback-npc