-
-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
10 changed files
with
200 additions
and
478 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
# 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() | ||
``` | ||
|
||
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. | ||
|
||
## 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# 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. This means that: | ||
|
||
* Rollback and physics-based games 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. |
Oops, something went wrong.