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:
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
@@ -0,0 +1,30 @@
|
||||
# BaseNetInput
|
||||
|
||||
Base class for Input nodes used with rollback.
|
||||
|
||||
During rollback, multiple logical ticks are simulated in the span of a single
|
||||
network tick. Since these are just logical ticks, no actual input arrives during
|
||||
them from the input devices.
|
||||
|
||||
The solution is to gather input before the tick loop, and use that input for
|
||||
any new ticks simulated during the rollback.
|
||||
|
||||
## Gathering input
|
||||
|
||||
This class provides a virtual `_gather` method that you can override. Set the
|
||||
variables configured in [RollbackSynchronizer] in your own implementation:
|
||||
|
||||
```gdscript
|
||||
extends BaseNetInput
|
||||
|
||||
var movement: Vector3 = Vector3.ZERO
|
||||
|
||||
func _gather():
|
||||
movement = Vector3(
|
||||
Input.get_axis("move_west", "move_east"),
|
||||
0,
|
||||
Input.get_axis("move_north", "move_south")
|
||||
)
|
||||
```
|
||||
|
||||
[RollbackSynchronizer]: ../../netfox/nodes/rollback-synchronizer.md
|
||||
@@ -0,0 +1,80 @@
|
||||
# NetworkSimulator
|
||||
|
||||
During testing, it is crucial to test your game under realistic network
|
||||
conditions, including latency and potentially packet loss.
|
||||
|
||||
Network conditions can be simulated using [clumsy], [netem], or with *netfox*'s
|
||||
*NetworkSimulator*. It auto-connects instances when launched from the editor,
|
||||
and simulates various network configurations.
|
||||
|
||||
## How to Use
|
||||
|
||||
Enable and configure *Autoconnect* in the project settings.
|
||||
|
||||

|
||||
|
||||
When your game launches one instance will start an [ENetMultiplayerPeer] server
|
||||
and the rest will connect to it.
|
||||
|
||||
Either a `NetworkSimulator.server_created` or
|
||||
`NetworkSimulator.client_connected` signal will be fired which you can use to
|
||||
bootstrap your game code to.
|
||||
|
||||
!!!note
|
||||
*NetworkSimulator* will only work when the game is ran from the editor.
|
||||
Otherwise it will disable itself, regardless of project settings. This is a
|
||||
failsafe to avoid affecting production builds of your games.
|
||||
|
||||
## Configuration
|
||||
|
||||
Hostname
|
||||
: The hosting address. Usually `127.0.0.1` but can be changed to `*`, if you
|
||||
want other machines to be able to join.
|
||||
|
||||
Server Port
|
||||
: Which port to listen on. A second server port with latency / loss will open
|
||||
one number higher if they are set to more than zero.
|
||||
|
||||
Use Compression
|
||||
: Will make use of ENET's range encoder to keep packet sizes down.
|
||||
|
||||
Simulated Latency ( ms )
|
||||
: Traffic delay, in milliseconds.
|
||||
|
||||
Simlated Packet Loss Chance
|
||||
: What percentage of packets will to drop, simulating bad network conditions.
|
||||
|
||||
## Running in CI and other environments
|
||||
|
||||
In certain cases, you might not need the autoconnect feature to run, even if
|
||||
it's enabled in the project settings.
|
||||
|
||||
One example could be running checks on your project in CI, or wanting to run a
|
||||
single script.
|
||||
|
||||
In these cases, Godot will still identify itself as running in editor, so the
|
||||
autoconnect feature will start hosting, keeping the process alive. This leads
|
||||
to timeout errors, or the process running indefinitely.
|
||||
|
||||
To avoid such cases, set any of the following environment variables to any
|
||||
non-empty string:
|
||||
|
||||
- `CI`
|
||||
- `NETFOX_CI`
|
||||
- `NETFOX_NO_AUTOCONNECT`
|
||||
|
||||
If *NetworkSimulator* detects any of the above environment variables, it will
|
||||
deactivate, regardless of project settings.
|
||||
|
||||
!!!tip
|
||||
Github Actions automatically sets the `CI` environment variable.
|
||||
*NetworkSimulator* will automatically disable itself when running in a
|
||||
Github Actions workflow.
|
||||
|
||||
See the [Github blog entry] for more details.
|
||||
|
||||
|
||||
[clumsy]: https://jagt.github.io/clumsy/
|
||||
[netem]: https://man7.org/linux/man-pages/man8/tc-netem.8.html
|
||||
[ENetMultiplayerPeer]: https://docs.godotengine.org/en/4.1/classes/class_enetmultiplayerpeer.html
|
||||
[Github blog entry]: https://github.blog/changelog/2020-04-15-github-actions-sets-the-ci-environment-variable-to-true/
|
||||
@@ -0,0 +1,137 @@
|
||||
# NetworkWeapon
|
||||
|
||||
Class to simplify writing networked weapons.
|
||||
|
||||
A weapon, in this context, is anything that can be fired and spawn objects (
|
||||
projectiles ) upon being fired.
|
||||
|
||||
## Responsive projectiles
|
||||
|
||||
Upon firing, sending a request to the server and waiting for the response with
|
||||
the projectile would introduce a delay. Doing a full-on state synchronization
|
||||
with [MultiplayerSynchronizer] or [RollbackSynchronizer] can be unfeasible with
|
||||
too many projectiles, and unnecessary, since most of the time, projectiles act
|
||||
and move the same way regardless of their surroundings.
|
||||
|
||||
Instead, upon firing, a projectile is spawned instantly. At the same time, a
|
||||
request is sent to the server. If the server accepts the projectile, it will
|
||||
spawn it and broadcasts its starting state. Since the server's state is the
|
||||
source of truth, the projectile's local state will be updated with the
|
||||
difference. This is called *reconciliation*.
|
||||
|
||||
If the client requests a projectile with an unlikely state, it will be
|
||||
rejected. This is to avoid players cheating, for example by requesting
|
||||
projectiles at a more advantageous position than they're at.
|
||||
|
||||
If the server is too strict with what difference is considered acceptable and
|
||||
what not, legitimate players may get cases where they fire a projectile which
|
||||
disappears after a short time period.
|
||||
|
||||
## Implementing a weapon
|
||||
|
||||
*NetworkWeapon* provides multiple functions to override. Make sure that all
|
||||
these methods work the same way on every player's game, otherwise players will
|
||||
experience glitches.
|
||||
|
||||
*_can_fire* returns a bool, indicating whether the weapon can be fired. For
|
||||
example, this method can return false if the weapon was fired recently and is
|
||||
still on cooldown. **Do not** update state here. Use *_after_fire* instead.
|
||||
|
||||
*_can_peer_use* indicates whether a given peer can fire the weapon. Due to the
|
||||
way RPCs are set up under the hood, any of the players can try to fire a
|
||||
weapon. Use this method to check if the player trying to fire has permission,
|
||||
e.g. a player is not trying to use someone else's weapon.
|
||||
|
||||
*_after_fire* is called after the weapon is successfully fired. Can be used to
|
||||
update state ( e.g. last time the weapon was fired ) and play sound effects.
|
||||
|
||||
*_spawn* creates the projectile. Make sure to return the created node.
|
||||
|
||||
*_get_data* must return the projectile's starting state in a dictionary. This
|
||||
can contain any property that is relevant to the projectile and must be
|
||||
synchronized. For example, *global_transform* is important to ensure that the
|
||||
projectile starts from the right position. On the other hand, projectile speed
|
||||
does not need to be captured if it's the same for every projectile.
|
||||
|
||||
*_apply_data* must apply the captured properties to a projectile.
|
||||
|
||||
*_is_reconcilable* checks if the difference between two projectile states ( as
|
||||
captured by *_get_data* ) is close enough to be allowed. Can be used to reject
|
||||
cheating.
|
||||
|
||||
*_reconcile* adjusts the projectile based on the difference between the local
|
||||
and server state.
|
||||
|
||||
## Specializations
|
||||
|
||||
*NetworkWeapon* extends [Node]. This also means that anything extending
|
||||
*NetworkWeapon* is also a node, and thus can't have a position for example.
|
||||
|
||||
Two specialized classes are provided - *NetworkWeapon3D*, and *NetworkWeapon2D*
|
||||
- extending Node3D and Node2D respectively.
|
||||
|
||||
This way, weapons can have transforms and have a presence in the game world.
|
||||
They also take care of reconciliation, implementing *_get_data*, *_apply_data*,
|
||||
*_is_reconcilable*, and *_reconcile*. These can be overridden, but make sure to
|
||||
to call the base class with *super(...)*.
|
||||
|
||||
Reconciliation is based on distance, and can be configured with the
|
||||
*distance_threshold* property.
|
||||
|
||||
Under the hood, these specializations create a special *NetworkWeapon* node,
|
||||
that proxies all the method calls back to the specialization. This is a
|
||||
workaround to build multiple inheritance in a single inheritance language.
|
||||
|
||||
## Compensating latency
|
||||
|
||||
Whenever the weapon is fired, it takes time for that event to arrive at the
|
||||
host. To combat this, along with the weapon being fired, the firing's tick is
|
||||
also sent. This way, the host doesn't only know that the weapon was fired, but
|
||||
it also knows *when*.
|
||||
|
||||
To retrieve the exact tick, call *get_fired_tick()*.
|
||||
|
||||
This can be used to adjust the created projectile's simulation, e.g. by
|
||||
simulating it from its spawn to the current tick in `_after_fire()`:
|
||||
|
||||
```gdscript
|
||||
func _after_fire(projectile: Node3D):
|
||||
last_fire = get_fired_tick()
|
||||
|
||||
for t in range(get_fired_tick(), NetworkTime.tick):
|
||||
if projectile.is_queued_for_deletion():
|
||||
break
|
||||
projectile._tick(NetworkTime.ticktime, t)
|
||||
```
|
||||
|
||||
!!!note
|
||||
To track the tick the weapon was last fired ( e.g. for cooldowns ), make sure
|
||||
to use `get_fired_tick()`, instead of `NetworkTime.tick`.
|
||||
|
||||
## Hitscan weapons
|
||||
|
||||
Use *NetworkWeaponHitscan3D* to build networked hitscan weapons. It builds upon
|
||||
the same principle as *NetworkWeapon*, but modified for hitscan.
|
||||
|
||||
Most of the callbacks are the same, with the following differences:
|
||||
|
||||
*_spawn* is not used, as there's no projectiles involved.
|
||||
|
||||
*_on_fire* is called whenever the weapon is successfully fired. Can be used to
|
||||
implement effects on firing, such as sound or visual effects.
|
||||
|
||||
*_on_hit* is called whenever the weapon hits a target. Depending on
|
||||
configuration, this may be another player, a different character, or just level
|
||||
geometry. The raycast result is passed as a parameter to distinguish between
|
||||
hits.
|
||||
|
||||
Reconciliation is handled under the hood - *_get_data*, *_apply_data*,
|
||||
*_is_reconcilable*, and *_reconcile* do not need to be implemented.
|
||||
|
||||
Hitscan weapons don't implement *get_fired_tick()*, as there's no projectile to
|
||||
simulate.
|
||||
|
||||
|
||||
[MultiplayerSynchronizer]: https://docs.godotengine.org/en/stable/classes/class_multiplayersynchronizer.html
|
||||
[RollbackSynchronizer]: ../../netfox/nodes/rollback-synchronizer.md
|
||||
[Node]: https://docs.godotengine.org/en/stable/classes/class_node.html
|
||||
@@ -0,0 +1,86 @@
|
||||
# Physics
|
||||
|
||||
At the time of writing official Godot releases have no support for manually
|
||||
stepping physics simulations. As such if you want to use physics nodes with
|
||||
rollback such as RigidBody you will need to run either a fork that supports
|
||||
stepping or use an alternate physics addon that exposes stepping.
|
||||
|
||||
!!!tip
|
||||
An example game demonstrating physics and rollback is available: [Godot
|
||||
Rocket League].
|
||||
|
||||
## Known Options
|
||||
|
||||
- **Godot with Stepping PR**: Build the Godot editor and relevant export
|
||||
templates manually with the [physics stepping PR] applied. See the [Godot
|
||||
documentation] for compilation instructions.
|
||||
- **Rapier Physics Addon**: A third-party 2D/3D physics engine for Godot with
|
||||
stepping support. Visit [godot.rapier.rs] for details.
|
||||
- **Blazium Fork**: A Godot fork with enhanced physics features, including
|
||||
stepping. Learn more at [blazium.app].
|
||||
|
||||
!!!tip
|
||||
For using Godot with the Stepping Physics PR applied, a [community run
|
||||
repository] is available with a custom build. Note that this repository and
|
||||
its builds are provided as-is, and are not associated with Godot nor the PR's
|
||||
author.
|
||||
|
||||
!!!note
|
||||
The current version of the Rapier drivers were tested against Godot Rapier
|
||||
version **0.8.26**.
|
||||
|
||||
## Enabling Physics Engine Rollback
|
||||
|
||||
To enable physics rollback, add the appropriate physics driver node to the root
|
||||
of your scene tree based on your physics engine. Because stepping methods are
|
||||
unavailable in standard Godot the classes are hidden to avoid compile errors.
|
||||
You will need to enable them in the editor by going to *Project -> Tools ->
|
||||
Enable physics driver*
|
||||
|
||||
- PhysicsDriver2D or PhysicsDriver3D for Godot’s default physics or Blazium.
|
||||
- RapierPhysicsDriver2D or RapierPhysicsDriver3D for the Rapier Physics Addon.
|
||||
|
||||
These nodes disable Godot’s default physics processing and step the physics
|
||||
simulation at **netfox**’s network tick rate.
|
||||
|
||||

|
||||
|
||||
## Physics Driver Configuration
|
||||
|
||||
**Physics Factor** - Controls the number of physics steps per network tick.
|
||||
For example, if your network tick rate is 30 Hz (one tick every ~33ms) but you
|
||||
need a 60 Hz physics simulation for smoother collisions, set this to 2 to run
|
||||
two physics steps per tick.
|
||||
|
||||
**Rollback Physics Space** - When enabled, rolls back all physics objects in
|
||||
the scene tree. Depending on how complex your scene tree is you may wish to
|
||||
only rollback specific nodes for performance rather than the entire simulation
|
||||
space.
|
||||
|
||||
## NetworkRigidBody
|
||||
|
||||
NetworkRigidBody2D and NetworkRigidBody3D nodes enable RigidBody
|
||||
synchronization with RollbackSynchronizer, keeping clients in sync with the
|
||||
server’s physics simulation. These nodes can replace standard RigidBody nodes
|
||||
with some minor setup.
|
||||
|
||||
!!!note
|
||||
Avoid using StateSyncronizer with NetworkRigidBody, as it doesn't participate
|
||||
in rollback it will end up stepping faster than other nodes.
|
||||
|
||||
To make use of NetworkRigidBody you need to:
|
||||
|
||||
1. Configure your RollbackSynchronizer to include the NetworkRigidBody's
|
||||
`physics_state` as a state property.
|
||||
2. Move physics-related logic from `_physics_process` to
|
||||
`_physics_rollback_tick`.
|
||||
|
||||

|
||||
|
||||
|
||||
[Godot Rocket League]: https://github.com/albertok/godot-rocket-league
|
||||
[community run repository]: https://github.com/albertok/godot/releases
|
||||
[physics stepping PR]: https://github.com/godotengine/godot/pull/76462
|
||||
[Godot documentation]: https://docs.godotengine.org/en/stable/contributing/development/compiling/index.html
|
||||
[godot.rapier.rs]: https://godot.rapier.rs/
|
||||
[blazium.app]: https://blazium.app/
|
||||
@@ -0,0 +1,66 @@
|
||||
# RewindableRandomNumberGenerator
|
||||
|
||||
A random number generator that can be used inside the [rollback tick loop].
|
||||
|
||||
An important point for writing code that works well in rollback is that it
|
||||
behaves the same given the same circumstances, no matter how many times it's
|
||||
run. It also must behave the same no matter which peer is running it.
|
||||
|
||||
Godot's built-in [RandomNumberGenerator] is not aware of rollback, so it will
|
||||
consistently return different numbers for each tick, and potentially on each
|
||||
peer.
|
||||
|
||||
To fix that, the *RewindableRandomNumberGenerator* generates the same random
|
||||
sequences on each peer, and for each tick.
|
||||
|
||||
It implements most of the same methods, so it's close to a drop-in replacement.
|
||||
|
||||
## Creating the generator
|
||||
|
||||
The *RewindableRandomNumberGenerator* requires a seed upon initialization. This
|
||||
seed must be the same on all peers.
|
||||
|
||||
!!!warning
|
||||
If two *different* RNGs use the *same* seed, they will also generate the
|
||||
same random numbers. Make sure that you use different seeds for different
|
||||
objects.
|
||||
|
||||
For RNG's used by singletons, hard-coding different seed values is a very
|
||||
simple approach:
|
||||
|
||||
```gd
|
||||
var rng := RewindableRandomNumberGenerator.new(15)
|
||||
```
|
||||
|
||||
If these objects also have names or similar, consistent identifiers, a simple
|
||||
hash works well too:
|
||||
|
||||
```gd
|
||||
var rng := RewindableRandomNumberGenerator.new(hash("Exit beacon"))
|
||||
```
|
||||
|
||||
For dynamically spawned objects, or just to avoid the possibility of a human
|
||||
error, the node path can be hashed:
|
||||
|
||||
```gd
|
||||
var rng := RewindableRandomNumberGenerator.new(hash(get_path()))
|
||||
```
|
||||
|
||||
This assumes that the same node will be spawned under the same path on all
|
||||
peers, which is also a requirement for RPCs to work.
|
||||
|
||||
## Generating random numbers
|
||||
|
||||
The *RewindableRandomNumberGenerator* can be used in the same way as Godot's
|
||||
built-in [RandomNumberGenerator]. All the per-peer and per-tick consistency is
|
||||
ensured under the hood:
|
||||
|
||||
```gd
|
||||
var rng := RewindableRandomNumberGenerator.new(0)
|
||||
|
||||
var dice_roll := rng.randi_range(1, 6)
|
||||
```
|
||||
|
||||
|
||||
[rollback tick loop]: ../../netfox/guides/network-rollback.md
|
||||
[RandomNumberGenerator]: https://docs.godotengine.org/en/stable/classes/class_randomnumbergenerator.html
|
||||
@@ -0,0 +1,214 @@
|
||||
# RewindableStateMachine
|
||||
|
||||
Rollback-aware state machine implementation.
|
||||
|
||||
State machines are often used in games to implement different behaviors.
|
||||
However, most implementations are not prepared for rollbacks. This class
|
||||
provides an extensible implementation that can be used alongside a
|
||||
[RollbackSynchronizer].
|
||||
|
||||
For a full example, see [multiplayer-state-machine].
|
||||
|
||||
## Creating a state machine
|
||||
|
||||
The first step is to add the RewindableStateMachine to your scene. It also
|
||||
requires a RollbackSynchronizer that manages its `state` property. Unless these
|
||||
conditions are satisfied, an editor warning will be displayed.
|
||||
|
||||
!!!note
|
||||
Editor warnings are only updated when the node tree changes. Configuration
|
||||
changes don't trigger an update. You may need to reload the scene after
|
||||
fixing a warning, or make a tree change, like deleting and re-adding a node
|
||||
by cutting and pasting.
|
||||
|
||||

|
||||
|
||||
Notice the RollbackSynchronizer added as a sibling to the
|
||||
RewindableStateMachine, and having its `state` property configured.
|
||||
|
||||
## Implementing states
|
||||
|
||||
States are where the custom gameplay logic can be implemented. Each state must
|
||||
be an extension of the RewindableState class, and added as a child to the
|
||||
RewindableStateMachine.
|
||||
|
||||
States react to the game world using the following callbacks:
|
||||
|
||||
`tick(delta, tick, is_fresh)`
|
||||
: Called for every rollback tick the state is active.
|
||||
|
||||
`enter(previous_state, tick)`
|
||||
: Called when entering the state.
|
||||
|
||||
`exit(next_state, tick)`
|
||||
: Called when exiting the state.
|
||||
|
||||
`can_enter(previous_state)`
|
||||
: Called before entering the state. The state is only entered if this method
|
||||
returns true.
|
||||
|
||||
`display_enter(previous_state, tick)`
|
||||
: Called before displaying the state.
|
||||
|
||||
`display_exit(next_state, tick)`
|
||||
: Called before displaying a different state.
|
||||
|
||||
You can override any of these callbacks to implement your custom behaviors.
|
||||
|
||||
For example, the snippet below implements an idle state, that transitions to
|
||||
other states based on movement inputs:
|
||||
|
||||
```gdscript
|
||||
extends RewindableState
|
||||
|
||||
@export var input: PlayerInputStateMachine
|
||||
|
||||
func tick(delta, tick, is_fresh):
|
||||
if input.movement != Vector3.ZERO:
|
||||
state_machine.transition(&"Move")
|
||||
elif input.jump:
|
||||
state_machine.transition(&"Jump")
|
||||
```
|
||||
|
||||
Transitions are based on *node names*, i.e. calling `transition(&"Move")` will
|
||||
transition to a state node called *Move*.
|
||||
|
||||

|
||||
|
||||
States must be added as children under a RewindableStateMachine to work.
|
||||
|
||||
## Using signals instead of classes
|
||||
|
||||
*RewindableState* nodes also emit signals during their lifetime. This enables
|
||||
an alternate style of implementing states, by connecting handlers to different
|
||||
signals. This can be useful if you want to keep all your logic in a single
|
||||
script, among others.
|
||||
|
||||
Each of these signals correspond to a callback explained above:
|
||||
|
||||
* `on_enter()` → `enter()`
|
||||
* `on_tick()` → `tick()`
|
||||
* `on_exit()` → `exit()`
|
||||
* `on_display_enter()` → `display_enter()`
|
||||
* `on_display_exit()` → `display_exit()`
|
||||
|
||||
## Adding states
|
||||
|
||||
Once implemented, add the state nodes as children of the
|
||||
*RewindableStateMachine* in the Scene Tree. When doing this programmatically,
|
||||
make sure to set the state's `owner` to the target *RewindableStateMachine*.
|
||||
Without the owner set, the *RewindableStateMachine* won't recognize the state.
|
||||
|
||||
## Display State vs State
|
||||
|
||||
There's two sets of callbacks for state transition - `enter()`/`exit()` and
|
||||
`display_enter()`/`display_exit()`.
|
||||
|
||||
The `enter()`/`exit()` callbacks are intended for implementing game logic. The
|
||||
`display_enter()`/`display_exit()` are intended for implementing presentation
|
||||
logic - visuals, animations, sound effects, etc.
|
||||
|
||||
The same applies to `on_state_changed` vs. `on_display_state_changed`.
|
||||
|
||||
Let's take an example. The game is currently on tick @8. It needs to re-run
|
||||
ticks @0 to @8 during rollback. In these ticks, the player moves a bit,
|
||||
performs a jump, and then stops after moving a bit more:
|
||||
|
||||
```puml
|
||||
@startuml
|
||||
|
||||
concise "Player" as P
|
||||
|
||||
@0
|
||||
P is Idle
|
||||
|
||||
@1
|
||||
P is Moving
|
||||
|
||||
@3
|
||||
P is Jumping
|
||||
|
||||
@5
|
||||
P is Moving
|
||||
|
||||
@8
|
||||
P is Idle
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
This will trigger the following state changes:
|
||||
|
||||
* Tick@1: Idle → Moving
|
||||
* Tick@3: Moving → Jumping
|
||||
* Tick@5: Jumping → Moving
|
||||
* Tick@8: Moving → Idle
|
||||
|
||||
For each of the above, the `on_state_changed` signal will be emitted, and the
|
||||
`enter()`/`exit()` callbacks will be triggered.
|
||||
|
||||
This makes the above callbacks ideal for game logic, e.g. adding an upward
|
||||
velocity to the player when they enter the `Jumping` state.
|
||||
|
||||
Note that the *displayed* state does not change. Before the rollback loop, the
|
||||
player's state was `Idle`. After the rollback loop, the player's state is also
|
||||
`Idle`. Even though the player has ran and performed a jump, it wouldn't make
|
||||
sense to change their animation or play any sound effect.
|
||||
|
||||
Let's take a different rollback example:
|
||||
|
||||
```puml
|
||||
@startuml
|
||||
|
||||
concise "Player" as P
|
||||
|
||||
@5
|
||||
P is Moving
|
||||
|
||||
@8
|
||||
P is Idle
|
||||
|
||||
@9
|
||||
P is Jumping
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
In this case, the display state *did* change. Before the rollback loop, the
|
||||
player's state was `Moving`. After the rollback loop, the player's state is
|
||||
`Jumping`. It would make sense to change the player's animation and play a
|
||||
jumping sound effect.
|
||||
|
||||
This can be done by using the display state callbacks - the
|
||||
`on_display_state_changed` signal, and the `display_enter()`/`display_exit()`
|
||||
methods.
|
||||
|
||||
## Caveats
|
||||
|
||||
RewindableStateMachine runs in the [rollback tick loop], which means that all
|
||||
the [Rollback Caveats] apply.
|
||||
|
||||
In addition, rollback ticks are only ran for nodes that have known inputs for
|
||||
the given tick, and *need* to be simulated - either on the server to determine
|
||||
the new state, or on the client to predict. In practice, ticks are usually only
|
||||
ran on the host owning state and the client owning inputs. The rest of the
|
||||
peers use the state broadcast by the host.
|
||||
|
||||
**This means that transition callbacks are not always ran.** This is by design
|
||||
and expected ( see [#327] ).
|
||||
|
||||
As a best practice, in the `enter()`, `exit()` callbacks and the
|
||||
`on_state_changed` signal, only change game state - i.e. properties that are
|
||||
configured as state in [RollbackSynchronizer].
|
||||
|
||||
To update visuals - e.g. change animation, spawn effects, etc. -, use either
|
||||
the `on_display_state_changed` signal, or the `display_enter()` and
|
||||
`display_exit()` callbacks to react to state transitions.
|
||||
|
||||
[multiplayer-state-machine]: https://github.com/foxssake/netfox/tree/main/examples/multiplayer-state-machine
|
||||
[RollbackSynchronizer]: ../../netfox/nodes/rollback-synchronizer.md
|
||||
[rollback tick loop]: ../../netfox/guides/network-rollback.md#network-rollback-loop
|
||||
[Rollback Caveats]: ../../netfox/tutorials/rollback-caveats.md
|
||||
[#327]: https://github.com/foxssake/netfox/issues/327#issuecomment-2491251374
|
||||
@@ -0,0 +1,33 @@
|
||||
# WindowTiler
|
||||
|
||||
A developer convenience feature that automatically tiles the launched windows
|
||||
when working from the editor.
|
||||
|
||||

|
||||
|
||||
## Limitations
|
||||
|
||||
### Borderless mode on Linux
|
||||
|
||||
Setting window position and size works inconsistently under Linux at the time
|
||||
of writing. Your mileage may vary based on your desktop environment and
|
||||
distribution.
|
||||
|
||||
In case the windows don't tile properly with *Borderless* enabled, disabling it
|
||||
is a fallback.
|
||||
|
||||
### Window decorations
|
||||
|
||||
At the time of writing, there is no known and consistent way to compensate for
|
||||
window decoration size and offset. In practice, this means that windows may
|
||||
slightly overlap.
|
||||
|
||||
## Configuration
|
||||
|
||||
*Auto Tile Windows* Enables auto tiling from editor launches.
|
||||
|
||||
*Screen* Which screen number to move and tile the windows to.
|
||||
|
||||
*Borderless* Enable borderless mode to make the most out of the screen real
|
||||
estate.
|
||||
|
||||
Reference in New Issue
Block a user