Phase 7: netfox + godot-jolt stack upgrade
Stack installed: - netfox v1.35.3 (core + extras + noray + internals) - godot-jolt v0.16.0-stable Architecture: - Server: ENet transport (works headless, no netfox deps) - Client/Editor: netfox rollback (RollbackSynchronizer, TickInterpolator) New/modified: - docs/migration-netfox-plan.md — migration architecture - scripts/network/network_manager.gd — netfox-aware ENet fallback - scripts/network/player.gd — clean base player - client/characters/player_netfox.gd — rollback player w/ WeaponManager - client/characters/input/player_net_input.gd — BaseNetInput subclass - client/characters/character/fps_character_controller.gd — netfox input feed - client/weapons/ — weapon data, registry, TacticalWeaponHitscan, WeaponManager - client/scripts/round_replicator.gd — client-side round state bridge - server/scripts/round_manager.gd — improved state machine - server/scripts/plugin_api/plugin_manager.gd — refined plugin system - config: enemy_tag, ally_tag for meatball targeting Removed: old C++ SimulationServer GDExtension (replaced by netfox rollback)
This commit is contained in:
@@ -51,8 +51,8 @@ extends Camera3D
|
||||
# Internal
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Reference to parent controller.
|
||||
var _controller: FPSCharacterController = null
|
||||
## Reference to parent controller (duck-typed — uses has_method instead of class_name).
|
||||
var _controller: Node = null
|
||||
|
||||
## Bob time accumulator.
|
||||
var _bob_time: float = 0.0
|
||||
@@ -82,9 +82,9 @@ var _base_eye_y: float = 0.0
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _ready() -> void:
|
||||
_controller = get_parent() as FPSCharacterController
|
||||
if _controller == null:
|
||||
push_warning("FpsCamera: Parent is not an FPSCharacterController. View features disabled.")
|
||||
_controller = get_parent()
|
||||
if _controller == null or not _controller.has_method(&"is_sprinting"):
|
||||
push_warning("FpsCamera: Parent has no is_sprinting() — view features disabled.")
|
||||
|
||||
_base_fov = fov
|
||||
|
||||
@@ -172,9 +172,9 @@ func _update_bob(delta: float) -> void:
|
||||
func _update_sway(delta: float) -> void:
|
||||
# Sway follows mouse movement indirectly via controller yaw/pitch changes
|
||||
# Use velocity for physics-based sway: recentre over time
|
||||
var vel: Vector3 = _controller.velocity
|
||||
var sway_h := clamp(vel.x * 0.003, -sway_max_rotation, sway_max_rotation)
|
||||
var sway_v := clamp(vel.z * 0.003, -sway_max_rotation, sway_max_rotation)
|
||||
var vel := _controller.velocity as Vector3
|
||||
var sway_h: float = clamp(vel.x * 0.003, -sway_max_rotation, sway_max_rotation)
|
||||
var sway_v: float = clamp(vel.z * 0.003, -sway_max_rotation, sway_max_rotation)
|
||||
# Add a tiny amount from pitch/yaw delta (not mouse, from movement direction change)
|
||||
_sway_target = Vector2(
|
||||
move_toward(_sway_target.x, sway_h, sway_response * delta),
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
## PlayerNetInput — netfox BaseNetInput subclass for the tactical shooter.
|
||||
##
|
||||
## Collects player input each network tick for the rollback system.
|
||||
## Referenced by RollbackSynchronizer's `input_properties`.
|
||||
##
|
||||
## This script is ONLY loaded from client scenes where netfox is active.
|
||||
|
||||
extends "res://addons/netfox.extras/base-net-input.gd"
|
||||
class_name PlayerNetInput
|
||||
|
||||
# Movement
|
||||
var move_direction: Vector3
|
||||
var look_yaw: float
|
||||
var look_pitch: float
|
||||
|
||||
# Actions
|
||||
var jump: bool
|
||||
var sprint: bool
|
||||
var crouch: bool
|
||||
var shoot: bool
|
||||
var aim: bool
|
||||
|
||||
# Weapon
|
||||
var weapon_slot: int = -1 # -1 = no change, 0-4 for slot index
|
||||
var reload: bool
|
||||
var use: bool # interact / plant / defuse
|
||||
|
||||
# Input sequence for reconciliation
|
||||
var input_sequence: int
|
||||
@@ -0,0 +1,210 @@
|
||||
## PlayerNetfox — netfox rollback-aware player for the tactical-shooter client.
|
||||
##
|
||||
## Extends the base Player with netfox RollbackSynchronizer, TickInterpolator,
|
||||
## BaseNetInput, and WeaponManager for deterministic first-person combat.
|
||||
##
|
||||
## This script is ONLY loaded from client scenes where netfox is active.
|
||||
## The headless server uses the base player.gd which has no netfox deps.
|
||||
|
||||
extends "res://scripts/network/player.gd"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Netfox node references
|
||||
# ---------------------------------------------------------------------------
|
||||
@onready var _tick_interpolator := $TickInterpolator as TickInterpolator
|
||||
@onready var _input := $PlayerNetInput as PlayerNetInput
|
||||
@onready var _weapon_manager := $WeaponManager as WeaponManager
|
||||
@onready var _weapon_anchor := $WeaponAnchor as Node3D
|
||||
|
||||
var _rs: RollbackSynchronizer
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State
|
||||
# ---------------------------------------------------------------------------
|
||||
var _gravity: float = ProjectSettings.get_setting(&"physics/3d/default_gravity", 9.8)
|
||||
var _is_local: bool = false
|
||||
var _camera: Camera3D
|
||||
var _mouse_captured: bool = true
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _ready() -> void:
|
||||
super._ready()
|
||||
|
||||
if Engine.has_singleton(&"NetworkTime") and Engine.has_singleton(&"NetworkRollback"):
|
||||
_setup_rollback()
|
||||
_connect_netfox_signals()
|
||||
print("[PlayerNetfox] Rollback initialised for %s" % name)
|
||||
else:
|
||||
push_warning("[PlayerNetfox] netfox not available — running without rollback")
|
||||
set_process(false)
|
||||
set_physics_process(false)
|
||||
|
||||
_camera = get_viewport().get_camera_3d() if get_viewport() else null
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rollback setup
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _setup_rollback() -> void:
|
||||
_rs = RollbackSynchronizer.new()
|
||||
_rs.name = "RollbackSynchronizer"
|
||||
_rs.state_properties = PackedStringArray([
|
||||
"global_position", "is_alive", "team_id",
|
||||
"is_spectating", "spectate_target_id",
|
||||
])
|
||||
_rs.input_properties = PackedStringArray([
|
||||
"PlayerNetInput/move_direction", "PlayerNetInput/look_yaw",
|
||||
"PlayerNetInput/look_pitch", "PlayerNetInput/jump",
|
||||
"PlayerNetInput/sprint", "PlayerNetInput/crouch",
|
||||
"PlayerNetInput/shoot", "PlayerNetInput/aim",
|
||||
"PlayerNetInput/input_sequence", "PlayerNetInput/weapon_slot",
|
||||
"PlayerNetInput/reload", "PlayerNetInput/use",
|
||||
])
|
||||
add_child(_rs, true)
|
||||
_rs.rollback_ticks.connect(_rollback_tick)
|
||||
|
||||
func _connect_netfox_signals() -> void:
|
||||
var nt = Engine.get_singleton(&"NetworkTime")
|
||||
if nt:
|
||||
nt.before_tick_loop.connect(_before_tick_loop)
|
||||
nt.after_tick_loop.connect(_after_tick_loop)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rollback hooks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _before_tick_loop() -> void:
|
||||
if _tick_interpolator and _tick_interpolator.enabled:
|
||||
_tick_interpolator.save_transform()
|
||||
|
||||
func _after_tick_loop() -> void:
|
||||
if _tick_interpolator and _tick_interpolator.enabled:
|
||||
_tick_interpolator.restore_transform()
|
||||
|
||||
func _rollback_tick(delta: float, tick: int, is_fresh: bool) -> void:
|
||||
if not is_alive:
|
||||
return
|
||||
|
||||
var input := _input
|
||||
|
||||
# Look rotation
|
||||
rotate_object_local(Vector3(0, 1, 0), input.look_yaw)
|
||||
|
||||
# Gravity + jump
|
||||
_force_update_is_on_floor()
|
||||
if is_on_floor():
|
||||
if input.jump:
|
||||
velocity.y = ServerConfig.movement_jump_velocity if ServerConfig else 6.0
|
||||
else:
|
||||
velocity.y -= _gravity * delta
|
||||
|
||||
# Speed
|
||||
var speed := 5.0
|
||||
if ServerConfig:
|
||||
speed = ServerConfig.movement_walk_speed
|
||||
if input.sprint:
|
||||
speed = ServerConfig.movement_sprint_speed if ServerConfig else 7.0
|
||||
if input.crouch:
|
||||
speed = ServerConfig.movement_crouch_speed if ServerConfig else 2.5
|
||||
|
||||
# Horizontal movement
|
||||
var dir := (transform.basis * input.move_direction).normalized()
|
||||
if dir.length() > 0.0:
|
||||
velocity.x = dir.x * speed
|
||||
velocity.z = dir.z * speed
|
||||
else:
|
||||
velocity.x = move_toward(velocity.x, 0.0, speed * 2.0)
|
||||
velocity.z = move_toward(velocity.z, 0.0, speed * 2.0)
|
||||
|
||||
move_and_slide()
|
||||
|
||||
func _force_update_is_on_floor() -> void:
|
||||
var old := velocity
|
||||
velocity = Vector3.ZERO
|
||||
move_and_slide()
|
||||
velocity = old
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Weapon system
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Give starting weapons (called by spawner).
|
||||
func give_default_weapons(team_id: int) -> void:
|
||||
if _weapon_manager == null:
|
||||
return
|
||||
|
||||
# Always give knife
|
||||
_weapon_manager.add_weapon(WeaponRegistry.get_by_id(WeaponRegistry.ID_KNIFE))
|
||||
|
||||
# Give team-default sidearm
|
||||
var default_wep := WeaponRegistry.get_default_weapon(team_id)
|
||||
_weapon_manager.add_weapon(default_wep)
|
||||
|
||||
## Handle weapon slot switching from input.
|
||||
func _handle_weapon_input(input: PlayerNetInput) -> void:
|
||||
if _weapon_manager == null:
|
||||
return
|
||||
|
||||
if input.reload:
|
||||
_weapon_manager.reload()
|
||||
|
||||
if input.weapon_slot >= 0:
|
||||
_weapon_manager.switch_to_slot(input.weapon_slot)
|
||||
# Reset so it doesn't keep switching
|
||||
input.weapon_slot = -1
|
||||
|
||||
if input.shoot:
|
||||
_weapon_manager.fire()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Input handling (frame-rate)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not _is_local or not _mouse_captured:
|
||||
return
|
||||
if _input == null:
|
||||
return
|
||||
|
||||
if event is InputEventMouseMotion:
|
||||
_input.look_yaw -= event.relative.x * 0.005
|
||||
_input.look_pitch = clamp(_input.look_pitch - event.relative.y * 0.005 * 0.8, -1.57, 1.57)
|
||||
|
||||
if event is InputEventMouseButton and event.pressed:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT:
|
||||
_input.shoot = true
|
||||
|
||||
func _unhandled_key_input(event: InputEvent) -> void:
|
||||
if not _is_local:
|
||||
return
|
||||
if event.is_action_pressed(&"reload"):
|
||||
_input.reload = true
|
||||
elif event.is_action_pressed(&"shoot"):
|
||||
_input.shoot = true
|
||||
|
||||
# Weapon slots 1-4
|
||||
for i in range(4):
|
||||
if event.is_action_pressed(&"weapon_slot_%d" % (i + 1)):
|
||||
_input.weapon_slot = i
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func mark_local() -> void:
|
||||
_is_local = true
|
||||
if _tick_interpolator:
|
||||
_tick_interpolator.enabled = false
|
||||
if _input:
|
||||
_input.set_process(true)
|
||||
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
||||
|
||||
func apply_damage(amount: int, attacker_id: int = -1, weapon_id: int = 0) -> void:
|
||||
health -= amount
|
||||
if health <= 0:
|
||||
health = 0
|
||||
is_alive = false
|
||||
print("[PlayerNetfox] %s killed by peer %d with weapon %d" % [name, attacker_id, weapon_id])
|
||||
Reference in New Issue
Block a user