Skip to content

Commit

Permalink
doc: Update tutorials (#128)
Browse files Browse the repository at this point in the history
Closes #122
  • Loading branch information
elementbound authored Nov 23, 2023
1 parent 1e4883a commit 9bc690c
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 478 deletions.
5 changes: 2 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,8 @@ Some links to get you up to speed:

## About the tutorials

Tutorials contain a *challenge* section - this part is to describe the actual
difficulty the feature solves. In case you are experienced with building
multiplayer games, feel free to skip these sections.
The tutorials are intended to get you started fast, but don't explain much of
how things work. For that, refer to the guides.

[Godot engine]: https://godotengine.org/
[noray]: https://github.com/foxssake/noray
Expand Down
Binary file added docs/netfox/assets/tutorial-nodes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/netfox/assets/tutorial-rollback-settings.png
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.
2 changes: 2 additions & 0 deletions docs/netfox/concepts/authoritative-servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,5 @@ For more approaches, see: [Networking for Physics Programmers]
[1500 Archers on a 28.8: Network Programming in Age of Empires and Beyond]: https://www.gamedeveloper.com/programming/1500-archers-on-a-28-8-network-programming-in-age-of-empires-and-beyond

[Networking for Physics Programmers]: https://www.gdcvault.com/play/1022195/Physics-for-Game-Programmers-Networking

[Client-side prediction and Server reconciliation]: https://www.gabrielgambetta.com/client-side-prediction-server-reconciliation.html
127 changes: 127 additions & 0 deletions docs/netfox/tutorials/responsive-player-movement.md
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
67 changes: 67 additions & 0 deletions docs/netfox/tutorials/rollback-caveats.md
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.
Loading

0 comments on commit 9bc690c

Please sign in to comment.