Files
tactical-shooter/docs/netfox/tutorials/input-gathering-tips-and-tricks.md
T
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

172 lines
5.4 KiB
Markdown

# 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