Files
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

170 lines
10 KiB
Markdown

# Network Schemas
By default, *netfox* uses Godot's [Binary serialization API] to serialize data
before transmitting it over the network. This is designed to work under various
circumstances, with various data types, without knowing anything about them in
advance.
However, during development, developers often have knowledge about the
individual properties, such as their type and possible range of values. In
addition, some values may be less important as others, and thus can accept some
loss of precision.
Schemas enable developers to specify how each property should be serialized,
allowing them to use this knowledge to reduce packet sizes, and thus bandwidth
usage.
## Lossless vs. lossy
Most serializers are either lossless or lossy. This section gives a short
theoretical introduction on what each means and when are they useful.
### Lossless compression
When the same amount of information can be represented with less data ( bytes
), it is *lossless compression*.
For example, to represent a 2D normal vector, we do not need to serialize both
of its component ( x, y ). Since we know the vector's length to be 1 by
definition, we can store the vector's angle compared to predetermined reference
vector. From that, we can completely reconstruct the original vector on
deserialization.
Another example is when the range of values the vector can take on is much
smaller than its underlying datatype supports. For example, an inventory where
items can't stack beyond 99. Instead of defaulting to a 64 bit integer, it is
sufficient to serialize this data as a 8 bit integer. That is 1/8th of the
original data, while still perfectly representing the range of values needed.
Lossless compression is an excellent tool, since the same information is kept,
but with less data usage. Unfortunately, lossless compression is not feasible
for every property.
### Lossy compression
If some information is lost when using less data ( bytes ) to represent a
value, it is *lossy compression*. This can be useful in cases where the benefit
of reduced packet size outweighs the drawbacks of lost information.
For example, movement vectors for NPCs may be serialized as half precision
floats, instead of the default single precision. Since players don't directly
control NPC's, they won't notice any difference between their original input
and what was serialized.
While lossy compression can be a useful tool, it is important to judge whether
the loss of information or precision does not detract too much from the game
experience.
## Registering a schema
Both [RollbackSynchronizer] and [StateSynchronizer] expose a `set_schema()`
method, that can be used to register the schema used for transmitting
properties over the network. This method takes a dictionary, with the keys
being property path strings, and the values being serializers:
```gdscript
rollback_synchronizer.set_schema({
":transform": NetworkSchemas.transform3f32(),
":velocity": NetworkSchemas.vec3f32(),
":speed": NetworkSchemas.float32(),
":mass": NetworkSchemas.float32(),
"Input:movement": NetworkSchemas.vec3f32(),
"Input:aim": NetworkSchemas.vec3f32()
})
```
## Built-in serializers
`NetworkSchemas` provides many built-in serializers in the form of static
methods. Each supported type has multiple serializers for different sizes.
While many serializers are usable as-is, there are some generic ones that take
other serializers as arguments. For example, `vec3t()` serializes a Vector3,
and using the serializer passed to it to save each component of the vector.
This way, `vec3t(float16())` will save 3 half-precision floats, ending up with
6 bytes of data, while `vec3t(float32())` will save 3 single-precision floats,
ending up with 12 bytes.
!!!note
Many built-in serializers use half-precision floats. These are only
supported in Godot 4.4 and up. Earlier versions fall back to
single-precision floats.
For example, `float16()` may fall back to `float32()`, `vec2f16()` to
`vec2f32()`, etc.
### Algebraic types
| Type | Methods | Size |
|-----------------------|---------------------------------------------------------|--------------------------------------------------------------------|
| Booleans | `bool8()` | 1 byte |
| Signed integers | `int8()`, `int16()`, `int32()`, `int64()` | 1, 2, 4, or 8 bytes |
| Unsigned integers | `uint8()`, `uint16()`, `uint32()`, `uint64()` | 1, 2, 4, or 8 bytes |
| Floats | `float16()`, `float32()`, `float64()` | 2, 4, or 8 bytes |
| Vector2 | `vec2f16()`, `vec2f32()`, `vec2f64()` | 4, 8, or 16 bytes |
| Vector3 | `vec3f16()`, `vec3f32()`, `vec3f64()` | 6, 8, or 24 bytes |
| Vector4 | `vec4f16()`, `vec4f32()`, `vec4f64()` | 8, 16, or 32 bytes |
| Quaternion | `quatf16()`, `quatf32()`, `quatf64()` | 8, 16, or 32 bytes |
| Transform2D | `transform2f16()`, `transform2f32()`, `transform2f64()` | 12, 24, or 48 bytes |
| Transform3D | `transform3f16()`, `transform3f32()`, `transform3f64()` | 24, 48, or 96 bytes |
### Compressed types
| Type | Methods | Size |
|-----------------------|---------------------------------------------------------|--------------------------------------------------------------------|
| Numbers in `[0, 1]` | `ufrac8()`, `ufrac16()`, `ufrac32()` | 1, 2, or 4 bytes |
| Numbers in `[-1, +1]` | `sfrac8()`, `sfrac16()`, `sfrac32()` | 1, 2, or 4 bytes |
| Degrees | `degrees8()`, `degrees16()`, `degrees32()` | 1, 2, or 4 bytes |
| Radians | `radians8()`, `radians16()`, `radians32()` | 1, 2, or 4 bytes |
| Normalized 2D vectors | `normal2f16()`, `normal2f32()`, `normal2f64()` | 2, 4, or 8 bytes |
| Normalized 3D vectors | `normal3f16()`, `normal3f32()`, `normal3f64()` | 4, 8, or 16 bytes |
### Generic types
| Type | Methods | Size |
|-----------------------|---------------------------------------------------------|--------------------------------------------------------------------|
| Vector2 | `vec2t()` | `2 * sizeof(component)` |
| Vector3 | `vec3t()` | `3 * sizeof(component)` |
| Vector4 | `vec4t()` | `4 * sizeof(component)` |
| Quaternion | `quatt()` | `4 * sizeof(component)` |
| Transform2D | `transform2t()` | `6 * sizeof(component)` |
| Transform3D | `transform3t()` | `12 * sizeof(component)` |
| Normalized Vector2 | `normal2t()` | `sizeof(component)` |
| Normalized Vector3 | `normal3t()` | `2 * sizeof(component)` |
### Collections and others
| Type | Methods | Size |
|-----------------------|---------------------------------------------------------|--------------------------------------------------------------------|
| Arrays | `array_of()` | `sizeof(size) + array.size() * sizeof(item)` |
| Dictionaries | `dictionary()` | `sizeof(size) + dictionary.size() * (sizeof(key) + sizeof(value))` |
| Strings | `string()` | Size in UTF-8 + null-terminator at the end |
| Variant | `variant()` | Same as [var_to_bytes()] |
## Implementing a custom serializer
Custom serializers are also supported. To implement one, extend the
`NetworkSchemaSerializer` class, and implement the `encode()` and `decode()`
methods.
For example, consider a `Node` serializer that encodes the node's path:
```gdscript
--8<-- "examples/snippets/network-schemas/example-node-serializer.gd"
```
This custom serializer can now be used in schemas:
```gdscript
rollback_synchronizer.set_schema({
"Input:target": ExampleNodeSerializer.new()
})
```
[Binary serialization API]: https://docs.godotengine.org/en/stable/tutorials/io/binary_serialization_api.html
[RollbackSynchronizer]: ../nodes/rollback-synchronizer.md
[StateSynchronizer]: ../nodes/state-synchronizer.md
[var_to_bytes()]: https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html#class-globalscope-method-var-to-bytes