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

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

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

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

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

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

162 lines
4.3 KiB
Markdown

# 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