b0c83af092
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>
155 lines
5.1 KiB
GDScript
155 lines
5.1 KiB
GDScript
@tool
|
|
extends Node
|
|
class_name StateSynchronizer
|
|
|
|
## Synchronizes state from authority.
|
|
##
|
|
## Similar to Godot's [MultiplayerSynchronizer], but is tied to the network tick loop. Works well
|
|
## with [TickInterpolator].
|
|
## [br][br]
|
|
## @tutorial(StateSynchronizer Guide): https://foxssake.github.io/netfox/latest/netfox/nodes/state-synchronizer/
|
|
|
|
## The root node for resolving node paths in properties.
|
|
@export var root: Node
|
|
|
|
## Properties to record and broadcast.
|
|
@export var properties: Array[String]
|
|
|
|
## Ticks to wait between sending full states.
|
|
## [br][br]
|
|
## If set to 0, full states will never be sent. If set to 1, only full states
|
|
## will be sent. If set higher, full states will be sent regularly, but not
|
|
## for every tick.
|
|
## [br][br]
|
|
## Only considered if [member _NetworkRollback.enable_diff_states] is true.
|
|
## @deprecated: This can now be configured in the project settings.
|
|
@export_range(0, 128, 1, "or_greater")
|
|
var full_state_interval: int = 24
|
|
|
|
## @deprecated: This is no longer used.
|
|
@export_range(0, 128, 1, "or_greater")
|
|
var diff_ack_interval: int = 0
|
|
|
|
## Decides which peers will receive updates
|
|
var visibility_filter := PeerVisibilityFilter.new()
|
|
|
|
var _properties_dirty: bool = false
|
|
var _properties := _PropertyPool.new()
|
|
var _schema_nodes := _Set.new()
|
|
|
|
var _is_initialized: bool = false
|
|
|
|
static var _logger := NetfoxLogger._for_netfox("StateSynchronizer")
|
|
|
|
## Process settings.
|
|
## [br][br]
|
|
## Call this after any change to configuration.
|
|
func process_settings() -> void:
|
|
# Remove old configuration
|
|
for node in _properties.get_subjects():
|
|
for property in _properties.get_properties_of(node):
|
|
NetworkHistoryServer.deregister_sync_state(node, property)
|
|
NetworkSynchronizationServer.deregister_sync_state(node, property)
|
|
|
|
# Register new configuration
|
|
_properties.set_from_paths(root, properties)
|
|
for node in _properties.get_subjects():
|
|
NetworkSynchronizationServer.register_visibility_filter(node, visibility_filter)
|
|
NetworkIdentityServer.register_node(node)
|
|
|
|
for property in _properties.get_properties_of(node):
|
|
NetworkHistoryServer.register_sync_state(node, property)
|
|
NetworkSynchronizationServer.register_sync_state(node, property)
|
|
|
|
_is_initialized = true
|
|
|
|
## Add a state property.
|
|
## [br][br]
|
|
## Settings will be automatically updated. The [param node] may be a string or
|
|
## [NodePath] pointing to a node, or an actual [Node] instance. If the given
|
|
## property is already tracked, this method does nothing.
|
|
func add_state(node: Variant, property: String) -> void:
|
|
var property_path := PropertyEntry.make_path(root, node, property)
|
|
if not property_path or properties.has(property_path):
|
|
return
|
|
|
|
properties.push_back(property_path)
|
|
_properties_dirty = true
|
|
_reprocess_settings.call_deferred()
|
|
|
|
## Set the schema for transmitting properties over the network.
|
|
## [br][br]
|
|
## The [param schema] must be a dictionary, with the keys being property path
|
|
## strings, and the values are the associated [NetworkSchemaSerializer] objects.
|
|
## Properties are interpreted relative to the [member root] node. Properties not
|
|
## specified in the schema will use a generic fallback serializer. By using the
|
|
## right serializer for the right property, bandwidth usage can be lowered.
|
|
## [br][br]
|
|
## See [NetworkSchemas] for many common serializers.
|
|
## [br][br]
|
|
## Example:
|
|
## [codeblock]
|
|
## state_synchronizer.set_schema({
|
|
## ":transform": NetworkSchemas.transform3f32(),
|
|
## ":velocity": NetworkSchemas.vec3f32()
|
|
## })
|
|
## [/codeblock]
|
|
func set_schema(schema: Dictionary) -> void:
|
|
# Remove previous schema
|
|
for node in _schema_nodes:
|
|
NetworkSynchronizationServer.deregister_schema_for(node)
|
|
_schema_nodes.clear()
|
|
|
|
# Register new schema
|
|
for prop in schema:
|
|
var prop_entry := PropertyEntry.parse(root, prop)
|
|
var serializer := schema[prop] as NetworkSchemaSerializer
|
|
NetworkSynchronizationServer.register_schema(prop_entry.node, prop_entry.property, serializer)
|
|
_schema_nodes.add(prop_entry.node)
|
|
|
|
func _notification(what) -> void:
|
|
if what == NOTIFICATION_EDITOR_PRE_SAVE:
|
|
update_configuration_warnings()
|
|
elif what == NOTIFICATION_PREDELETE:
|
|
for node in _properties.get_subjects():
|
|
NetworkSynchronizationServer.deregister(node)
|
|
NetworkHistoryServer.deregister(node)
|
|
NetworkIdentityServer.deregister(node)
|
|
|
|
func _get_configuration_warnings() -> PackedStringArray:
|
|
if not root:
|
|
root = get_parent()
|
|
|
|
# Explore state properties
|
|
if not root:
|
|
return ["No valid root node found!"]
|
|
|
|
return _NetfoxEditorUtils.gather_properties(root, "_get_synchronized_state_properties",
|
|
func(node, prop):
|
|
add_state(node, prop)
|
|
)
|
|
|
|
func _enter_tree() -> void:
|
|
if Engine.is_editor_hint():
|
|
return
|
|
|
|
if not visibility_filter:
|
|
visibility_filter = PeerVisibilityFilter.new()
|
|
if not visibility_filter.get_parent():
|
|
add_child(visibility_filter)
|
|
|
|
process_settings.call_deferred()
|
|
|
|
func _ready():
|
|
# Reprocess authority
|
|
# Important if nodes are pre-placed in the scene - node starts as owned by
|
|
# us ( offline peer is 1 ), but once we connect, we no longer own the node
|
|
multiplayer.connected_to_server.connect(process_settings)
|
|
|
|
func _reprocess_settings() -> void:
|
|
if not _properties_dirty:
|
|
return
|
|
|
|
_properties_dirty = false
|
|
process_settings()
|