t_p1_hitscan: Add lag-compensated hitscan weapon system
NEW: server/scripts/combat/lag_compensation.gd
- 128-entry ring buffer of player positions per server tick
- rewind_and_raycast(tick, origin, direction, range, exclude):
saves current positions, rewinds to tick-time positions,
performs PhysicsDirectSpaceState3D.intersect_ray(), restores
- Falls back to normal raycast if history missing for tick
- shot_processed signal
NEW: scripts/combat/damage_processor.gd
- Processes WeaponServer hit results: applies damage, tracks kills
- Optional distance-based damage falloff
- player_damaged / player_killed signals
- Health and kill-count queries
MODIFY: server/scripts/weapons/weapon_server.gd
- fire() now takes tick parameter: fire(tick, player_id, ...)
- _perform_hitscan() uses LagCompensation.rewind_and_raycast()
when lag_compensation reference is set and tick >= 0
- Non-compensated fallback path preserved
MODIFY: server/scripts/game_server.gd
- Adds _current_tick counter, incremented each simulation tick
- Creates WeaponServer + LagCompensation + DamageProcessor as children
- record_tick() called before each tick's simulation
- register/unregister_player_node() for player lifecycle
- _on_player_killed() relays kill events via player_damaged signal
MODIFY: scripts/network/server_main.gd
- _spawn_player() registers node with GameServer.register_player_node()
- _despawn_player() unregisters via unregister_player_node()
This commit is contained in:
@@ -40,6 +40,18 @@ signal player_damaged(victim_entity_id: int, shooter_entity_id: int, damage: flo
|
||||
var simulation_server: Object = null
|
||||
var is_running: bool = false
|
||||
|
||||
## WeaponServer — handles hit-scan raycasting and lag-compensated fire.
|
||||
var weapon_server: WeaponServer = null
|
||||
|
||||
## LagCompensation — records player positions per tick for rewind raycasts.
|
||||
var lag_compensation: LagCompensation = null
|
||||
|
||||
## DamageProcessor — applies hit results, tracks health and kills.
|
||||
var damage_processor: DamageProcessor = null
|
||||
|
||||
## Current server tick counter, incremented each time tick() is called.
|
||||
var _current_tick: int = 0
|
||||
|
||||
# Map entity_id → peer_id for broadcasting damage events
|
||||
var entity_to_peer: Dictionary = {} # entity_id (int) → peer_id (int)
|
||||
|
||||
@@ -74,6 +86,29 @@ func _ready() -> void:
|
||||
# Set history depth for lag compensation (64 ticks = ~500ms at 128Hz)
|
||||
simulation_server.set_history_depth(64)
|
||||
|
||||
# --- Combat subsystems (GDScript lag-compensated hit-scan) ---
|
||||
# Create and wire the WeaponServer with LagCompensation and DamageProcessor.
|
||||
# These work alongside the C++ SimulationServer for the GDScript
|
||||
# weapon path.
|
||||
weapon_server = WeaponServer.new()
|
||||
weapon_server.physics_world = get_world_3d()
|
||||
add_child(weapon_server)
|
||||
|
||||
lag_compensation = LagCompensation.new()
|
||||
add_child(lag_compensation)
|
||||
|
||||
damage_processor = DamageProcessor.new()
|
||||
add_child(damage_processor)
|
||||
|
||||
# Wire lag compensation into WeaponServer
|
||||
weapon_server.lag_compensation = lag_compensation
|
||||
|
||||
# Connect damage processor signals to GameServer signals for upstream relay
|
||||
damage_processor.player_killed.connect(_on_player_killed)
|
||||
|
||||
_current_tick = 0
|
||||
print("[GameServer] Combat subsystems ready: WeaponServer + LagCompensation + DamageProcessor")
|
||||
|
||||
# Register as singleton so FPSCharacterController can find us
|
||||
Engine.register_singleton("SimulationServer", simulation_server)
|
||||
|
||||
@@ -147,11 +182,48 @@ func get_peer_for_entity(entity_id: int) -> int:
|
||||
func get_simulation_server() -> Object:
|
||||
return simulation_server
|
||||
|
||||
## Get the current server tick counter.
|
||||
func get_current_tick() -> int:
|
||||
return _current_tick
|
||||
|
||||
## Configure weapon damage profile.
|
||||
func set_weapon(config: Dictionary) -> void:
|
||||
if simulation_server:
|
||||
simulation_server.set_weapon_config(config)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Combat subsystem — player registration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Register a player node with LagCompensation and DamageProcessor.
|
||||
## Call this after spawning a player entity and assigning its entity_id.
|
||||
## entity_id: the simulation entity ID assigned by spawn_player_entity()
|
||||
## node: the player's Node3D (CharacterBody3D) in the scene tree
|
||||
## max_health: starting/maximum health for damage tracking
|
||||
func register_player_node(entity_id: int, node: Node3D, max_health: float = 100.0) -> void:
|
||||
if lag_compensation:
|
||||
lag_compensation.register_player_node(entity_id, node)
|
||||
if damage_processor:
|
||||
damage_processor.register_player(entity_id, max_health)
|
||||
print("[GameServer] Registered player node entity=%d with lag compensation + damage processor" % entity_id)
|
||||
|
||||
## Unregister a player node from LagCompensation and DamageProcessor.
|
||||
func unregister_player_node(entity_id: int) -> void:
|
||||
if lag_compensation:
|
||||
lag_compensation.unregister_player_node(entity_id)
|
||||
if damage_processor:
|
||||
damage_processor.unregister_player(entity_id)
|
||||
print("[GameServer] Unregistered player node entity=%d" % entity_id)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Combat subsystem — kill handling
|
||||
# ---------------------------------------------------------------------------
|
||||
func _on_player_killed(victim_id: int, shooter_id: int) -> void:
|
||||
# Relay the kill event upstream with the shooter's peer_id if available
|
||||
var shooter_peer: int = entity_to_peer.get(shooter_id, -1)
|
||||
player_damaged.emit(victim_id, shooter_id, 0.0, true)
|
||||
print("[GameServer] Kill: victim_entity=%d, shooter_entity=%d (peer=%d)" % [victim_id, shooter_id, shooter_peer])
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main loop (128 Hz)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -162,6 +234,12 @@ func _physics_process(delta: float) -> void:
|
||||
|
||||
# Drive the fixed-timestep simulation loop
|
||||
while simulation_server.can_tick(delta):
|
||||
# Record positions for lag compensation BEFORE processing this tick's inputs.
|
||||
# This captures the state at the start of the tick, so rewind_and_raycast
|
||||
# can restore players to their pre-movement positions.
|
||||
if lag_compensation:
|
||||
lag_compensation.record_tick(_current_tick)
|
||||
|
||||
var snapshot: PackedByteArray = simulation_server.tick()
|
||||
# snapshot is the serialized state — send to network layer in Phase 2
|
||||
# For now, just emit the tick completed signal
|
||||
@@ -175,3 +253,5 @@ func _physics_process(delta: float) -> void:
|
||||
var killed: bool = hit_result.get("killed", false)
|
||||
# For now we don't know the shooter — this will be wired in Phase 2
|
||||
player_damaged.emit(victim_id, -1, damage, killed)
|
||||
|
||||
_current_tick += 1
|
||||
|
||||
Reference in New Issue
Block a user