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,166 @@
|
||||
# RewindableAction
|
||||
|
||||
!!!warning
|
||||
RewindableActions are *experimental*, meaning the API may change in
|
||||
breaking ways, and may be less stable than other features.
|
||||
|
||||
Once the class matures and finds its final form, the *experimental* mark
|
||||
will be removed. Feedback is welcome in the meanwhile!
|
||||
|
||||
Synchronizes events that happen over the network, by letting peers predict the
|
||||
event happening, and then adjusting the game based on the host's response.
|
||||
|
||||
For example, *RewindableActions* could be use to synchronize gun shots
|
||||
implemented as part of the rollback tick loop. This is implemented in the
|
||||
[rollback-fps] example.
|
||||
|
||||
## Using RewindableActions
|
||||
|
||||
To use *RewindableActions*, add them as nodes to your scenes. Once that's done,
|
||||
grab a reference to them from your scripts as you would for any other node -
|
||||
e.g. by using its NodePath, or by @export-ing it as a variable:
|
||||
|
||||
```gdscript
|
||||
@onready var rewindable_action := $RewindableAction as RewindableAction
|
||||
@export var rewindable_action: RewindableAction
|
||||
```
|
||||
|
||||
### Predicting events
|
||||
|
||||
All peers ( both hosts and clients ) should run the same simulation in their
|
||||
`_rollback_tick` implementations. During the rollback tick, peers should
|
||||
determine whether they think an event happens by calling
|
||||
`RewindableAction.set_active()` - e.g. if they think the gun was fired they should
|
||||
call `RewindableAction.set_active(true)`, otherwise call
|
||||
`RewindableAction.set_active(false)`.
|
||||
|
||||
The *RewindableAction* will keep track of the changes caused by `set_active()`.
|
||||
Clients ( i.e. peers *not* owning the *RewindableAction* ) will wait for the
|
||||
host ( i.e. peer owning the *RewindableAction* ) to broadcast the ground truth,
|
||||
noting when did the event happen, and when did it not.
|
||||
|
||||
!!!note
|
||||
Not calling `set_active()` on a specific tick means no prediction for that tick
|
||||
will be synchronized, potentially leading to desyncs.
|
||||
|
||||
### Performing events
|
||||
|
||||
With the above, *RewindableAction* will synchronize *when* something happens,
|
||||
but *what* should happen is up to the game logic.
|
||||
|
||||
For each rollback tick, to figure out what should happen, the `get_status()`
|
||||
method will return one of the following values:
|
||||
|
||||
`RewindableAction.INACTIVE`
|
||||
: The event hasn't happened yet.
|
||||
|
||||
`RewindableAction.ACTIVE`
|
||||
: The event has already happened, and this is not the first time its logic
|
||||
will run.
|
||||
|
||||
`RewindableAction.CONFIRMING`
|
||||
: The event was just set to active in this tick.
|
||||
|
||||
`RewindableAction.CANCELLING`
|
||||
: The event was just set to inactive in this tick.
|
||||
|
||||
See the following graph for a better understanding of how a *RewindableAction*
|
||||
transitions from one state to another:
|
||||
|
||||
```puml
|
||||
@startuml
|
||||
|
||||
[*] --> INACTIVE
|
||||
INACTIVE --> CONFIRMING: set_active(true)
|
||||
INACTIVE --> CONFIRMING: Host confirms
|
||||
CONFIRMING --> ACTIVE: Tick is ran again
|
||||
ACTIVE --> CANCELLING: set_active(false)
|
||||
ACTIVE --> CANCELLING: Host declines
|
||||
CANCELLING --> INACTIVE: Tick is ran again
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
Keeping with the gunfire example, if the status is `ACTIVE` or `CONFIRMING`,
|
||||
make sure to perform the firing logic - e.g. do a hitscan, and decrease the
|
||||
health of the player hit. In other words, make sure to update the *game state*.
|
||||
|
||||
If the state is `CONFIRMING`, implement logic that may spawn other objects (
|
||||
e.g. a bullet hole when hitting a wall ).
|
||||
|
||||
If the state is `CANCELLING`, undo any logic ran in `CONFIRMING`.
|
||||
|
||||
Usually no extra code is necessary for `INACTIVE`, since the game state update
|
||||
can simply be skipped, and other related code is ran in
|
||||
`CONFIRMING`/`CANCELLING`.
|
||||
|
||||
### Reacting to status changes
|
||||
|
||||
Without [mutations], nodes are not always re-ran for every tick during
|
||||
rollback. To make sure that rollback code is ran when the *RewindableAction*'s
|
||||
status changes, use `mutate()` to register the appropriate nodes to be
|
||||
*mutated* if the action's status changes, e.g.:
|
||||
|
||||
```gdscript
|
||||
@onready var rewindable_action := $RewindableAction as RewindableAction
|
||||
|
||||
func _ready():
|
||||
rewindable_action.mutate(self)
|
||||
|
||||
func _rollback_tick(delta, tick, is_fresh):
|
||||
rewindable_action.set_active(...)
|
||||
```
|
||||
|
||||
### Remembering things between tick loops
|
||||
|
||||
*RewindableActions* also provide the concept of *context*. This is any
|
||||
arbitrary value that the *RewindableAction* will remember for the given tick,
|
||||
even throughout tick loops.
|
||||
|
||||
The *context* value can be set and retrieved by the user.
|
||||
|
||||
For example, *context* can be used for weapons to remember the projectile they
|
||||
have spawned. If the action transitions to `CANCELLING`, the *context* can be
|
||||
used to remember which projectile was spawned in that tick, and in turn, which
|
||||
projectile needs to be destroyed.
|
||||
|
||||
Use `has_context()` to check if there's any context set, `get_context()` to
|
||||
retrieve it, `set_context()` to update the *context* value, and
|
||||
`erase_context()` to forget it.
|
||||
|
||||
## Handling visuals and effects
|
||||
|
||||
Performing events ( e.g. a gunshot ) often includes not just updates to the
|
||||
game state ( like decreasing health ), but also visual- and audio effects to
|
||||
communicate what's happening to the player.
|
||||
|
||||
Since a rollback tick loop may run multiple ticks in a single frame, simply
|
||||
playing sounds and other effects from the rollback tick loop can end up
|
||||
spamming particles and playing the same sound effects many times on the same
|
||||
frame.
|
||||
|
||||
Instead, one approach would be to check whether the event has happened at the
|
||||
end of each tick loop, and if so, play the appropriate sounds and run the
|
||||
appropriate effects.
|
||||
|
||||
Use `has_confirmed()` to check if the action has been confirmed since the
|
||||
beginning of the last tick loop ( i.e. had the `CONFIRMING` status ), and
|
||||
`has_cancelled()` to check if the action has been cancelled.
|
||||
|
||||
For example:
|
||||
|
||||
```gdscript
|
||||
@onready var fire_action := $"Fire Action" as RewindableAction
|
||||
|
||||
func _ready():
|
||||
NetworkTime.after_tick_loop.connect(_after_loop)
|
||||
# ...
|
||||
|
||||
func _after_loop():
|
||||
if fire_action.has_confirmed():
|
||||
sound.play()
|
||||
```
|
||||
|
||||
[rollback tick loop]: ../guides/network-rollback.md
|
||||
[rollback-fps]: https://github.com/foxssake/netfox/tree/main/examples/rollback-fps
|
||||
[mutations]: ../tutorials/modifying-objects-during-rollback.md
|
||||
Reference in New Issue
Block a user