feat: Implement core player movement, input, and player scene setup, including grid-based movement, rotation, push mechanics, and multiplayer synchronization.
This commit is contained in:
+110
-29
@@ -1,4 +1,4 @@
|
||||
extends Node3D
|
||||
extends CharacterBody3D
|
||||
|
||||
# Managers
|
||||
var movement_manager
|
||||
@@ -133,6 +133,10 @@ var finish_locations: Array:
|
||||
get: return race_manager.finish_locations if race_manager else []
|
||||
|
||||
var target_visual_position: Vector3 = Vector3.ZERO # For client-side smoothing
|
||||
var snapshot_buffer: Array = [] # Array of {time: int, pos: Vector3}
|
||||
const INTERPOLATION_OFFSET: int = 150 # Increased to 150ms for better jitter buffering
|
||||
var _last_received_timestamp: int = 0
|
||||
var _last_movement_finish_time: int = 0
|
||||
|
||||
var spawn_point_selected = false
|
||||
|
||||
@@ -176,6 +180,11 @@ const AVAILABLE_CHARACTERS: Array[String] = ["Bob", "Masbro", "Gatot", "Oldpop"]
|
||||
|
||||
func _ready():
|
||||
# Ensure name is set first (node name = authority ID for multiplayer identification)
|
||||
# 3. SET PHYSICS MODE TO FLOATING
|
||||
# This prevents "grounding" friction and gravity from fighting our Tweens
|
||||
motion_mode = CharacterBody3D.MOTION_MODE_FLOATING
|
||||
|
||||
print("[Player] initialized: ", name, " Authority: ", get_multiplayer_authority())
|
||||
# name = str(get_multiplayer_authority()) # CRITICAL FIX: Do NOT overwrite name. Bots have authority 1 but unique names (IDs).
|
||||
# Look up player's display name from LobbyManager
|
||||
var my_id = get_multiplayer_authority()
|
||||
@@ -769,14 +778,13 @@ func apply_slow_effect(duration: float = 3.0):
|
||||
|
||||
# Restore after duration
|
||||
# Note: If they stand in the zone, this will be re-applied constantly, resetting the visual
|
||||
await get_tree().create_timer(duration).timeout
|
||||
|
||||
# Only restore if not re-applied recently (simple check: if still tinted?)
|
||||
# A better way is managing a "slow_timer" but for now let's just reset if timer expires.
|
||||
# The persistent zone logic reapplies every frame, so we want this timer to be short
|
||||
# OR we rely on the zone logic.
|
||||
# The RPC call says "apply_slow_effect(0.5)", so it expires quickly.
|
||||
|
||||
await get_tree().create_timer(duration).timeout
|
||||
|
||||
if movement_manager:
|
||||
movement_manager.set_speed_multiplier(1.0)
|
||||
|
||||
@@ -1020,18 +1028,10 @@ func _process(delta):
|
||||
if can_rpc():
|
||||
rpc("ping_existence")
|
||||
else:
|
||||
# Client-side visual smoothing
|
||||
# Only interpolate if NOT running a movement tween, OR if the drift is large (teleport/snap)
|
||||
# Client-side visual smoothing with snapshot interpolation
|
||||
# Only interpolate if NOT running a movement tween
|
||||
if not is_player_moving:
|
||||
var dist_sq = global_position.distance_squared_to(target_visual_position)
|
||||
|
||||
if dist_sq > 4.0: # If distance > 2.0 units (teleport/spawn), snap immediately
|
||||
global_position = target_visual_position
|
||||
elif dist_sq < 0.001: # Prevent micro-jitter
|
||||
global_position = target_visual_position
|
||||
else:
|
||||
# Interpolate towards the target position received from authority
|
||||
global_position = global_position.lerp(target_visual_position, delta * 15.0)
|
||||
_process_remote_interpolation(delta)
|
||||
|
||||
# Delegate rotation to movement manager
|
||||
if movement_manager:
|
||||
@@ -1044,6 +1044,48 @@ func _process(delta):
|
||||
immunity_timer = 0
|
||||
_apply_tint_recursive(self , Color.WHITE) # Remove immunity tint
|
||||
|
||||
func _process_remote_interpolation(_delta):
|
||||
if snapshot_buffer.size() < 2:
|
||||
# Fallback to simple lerp if not enough snapshots
|
||||
# Keep this very soft to smooth out transitions between tween and interpolation
|
||||
if global_position.distance_squared_to(target_visual_position) > 0.001:
|
||||
global_position = global_position.lerp(target_visual_position, _delta * 10.0)
|
||||
return
|
||||
|
||||
var render_time = Time.get_ticks_msec() - INTERPOLATION_OFFSET
|
||||
|
||||
# Find the two snapshots to interpolate between
|
||||
var older = snapshot_buffer[0]
|
||||
var newer = null
|
||||
|
||||
for i in range(1, snapshot_buffer.size()):
|
||||
if snapshot_buffer[i].time > render_time:
|
||||
newer = snapshot_buffer[i]
|
||||
older = snapshot_buffer[i-1]
|
||||
break
|
||||
|
||||
if newer:
|
||||
# Interpolate
|
||||
var total_time = newer.time - older.time
|
||||
var elapsed_time = render_time - older.time
|
||||
var t = float(elapsed_time) / float(total_time)
|
||||
|
||||
var target_pos = older.pos.lerp(newer.pos, t)
|
||||
|
||||
# If drift is massive (teleport), snap
|
||||
var dist_sq = global_position.distance_squared_to(target_pos)
|
||||
if dist_sq > 16.0:
|
||||
global_position = target_pos
|
||||
elif dist_sq > 0.0001:
|
||||
# Use a lower lerp weight for much softer tracking
|
||||
global_position = global_position.lerp(target_pos, _delta * 30.0)
|
||||
else:
|
||||
# Extrapolate or just stay at latest if render_time is ahead of buffer
|
||||
var latest = snapshot_buffer[-1]
|
||||
if global_position.distance_squared_to(latest.pos) > 0.001:
|
||||
global_position = global_position.lerp(latest.pos, _delta * 20.0) # Slower catchup to latest
|
||||
|
||||
|
||||
@rpc("any_peer", "call_local")
|
||||
func ping_existence():
|
||||
# This just lets other clients know this player exists
|
||||
@@ -1053,11 +1095,19 @@ func ping_existence():
|
||||
var last_sent_position: Vector3
|
||||
|
||||
func _physics_process(delta):
|
||||
# Sync position periodically (Heartbeat / Smoothing)
|
||||
if is_multiplayer_authority():
|
||||
if global_position.distance_squared_to(last_sent_position) > 0.001:
|
||||
if can_rpc():
|
||||
rpc("remote_set_position", global_position)
|
||||
last_sent_position = global_position
|
||||
# OPTIMIZATION: Only send smoothing updates if we ARE NOT currently mid-tween
|
||||
# The start/end of paths are already synced via start_movement_along_path.
|
||||
# Sending packets during a tween just adds noise/jitter for the client.
|
||||
if not is_player_moving:
|
||||
var current_world_pos = global_position
|
||||
var last_sent_pos = get_meta("_last_sent_pos", Vector3.ZERO)
|
||||
|
||||
# Use a slightly larger threshold (0.05) to ignore micro-vibrations
|
||||
if current_world_pos.distance_to(last_sent_pos) > 0.05:
|
||||
set_meta("_last_sent_pos", current_world_pos)
|
||||
rpc("remote_set_position", current_world_pos, Time.get_ticks_msec())
|
||||
|
||||
# NOTE: Finish line checking removed - game uses cycle-based goals system now
|
||||
|
||||
@@ -1286,17 +1336,19 @@ func simple_move_to(grid_position: Vector2i):
|
||||
func move_player_to_clicked_position(grid_position: Vector2i):
|
||||
movement_manager.move_to_clicked_position(grid_position)
|
||||
|
||||
@rpc("any_peer", "call_local")
|
||||
@rpc("any_peer", "call_remote", "unreliable")
|
||||
func start_movement_along_path(path: Array, clear_visual: bool = true):
|
||||
if is_player_moving:
|
||||
return # ALREADY MOVING. Guard against redundant RPCs or interruptions.
|
||||
|
||||
print("[Player] %s starting move along path: %s" % [name, path])
|
||||
|
||||
# SERVER-SIDE VIOLATION CHECK (for Stop n Go)
|
||||
if multiplayer.is_server() and LobbyManager.game_mode == "Stop n Go":
|
||||
var main = get_tree().root.get_node_or_null("Main")
|
||||
if main:
|
||||
var sng_manager = main.get_node_or_null("StopNGoManager")
|
||||
if sng_manager and sng_manager.has_method("check_movement_violation"):
|
||||
# Check if the START of this movement is a violation
|
||||
# We use current_position as the 'from' point.
|
||||
var target_pos = Vector2i(path[0].x, path[0].y) if path.size() > 0 else current_position
|
||||
sng_manager.check_movement_violation(name.to_int(), current_position, target_pos)
|
||||
|
||||
@@ -1304,10 +1356,10 @@ func start_movement_along_path(path: Array, clear_visual: bool = true):
|
||||
if path.size() > 0:
|
||||
target_position = Vector2i(path[-1].x, path[-1].y)
|
||||
|
||||
# FORCE SNAP START: Ensure we start animating from our actual grid position
|
||||
# This prevents "jumps" if the visual node drifted or was set via global_position vs position mismatch
|
||||
# RELAXED SNAP START: Only snap if the visual node is far from where it should be (> 0.5 units)
|
||||
# This prevents "twitching" when a move starts if the node was still lerping slowly to its final spot.
|
||||
var start_world_pos = grid_to_world(current_position)
|
||||
if global_position.distance_squared_to(start_world_pos) > 0.001:
|
||||
if global_position.distance_to(start_world_pos) > 0.5:
|
||||
global_position = start_world_pos
|
||||
|
||||
if _movement_tween:
|
||||
@@ -1330,7 +1382,7 @@ func start_movement_along_path(path: Array, clear_visual: bool = true):
|
||||
is_player_moving = false
|
||||
_movement_tween = null
|
||||
target_position = Vector2i(-1, -1)
|
||||
|
||||
_last_movement_finish_time = Time.get_ticks_msec()
|
||||
print("[Player] %s finished move. %s -> %s" % [name, old_pos, current_position])
|
||||
|
||||
if is_carrying_tekton and is_instance_valid(carried_tekton):
|
||||
@@ -1369,6 +1421,10 @@ func start_movement_along_path(path: Array, clear_visual: bool = true):
|
||||
if enhanced_gridmap:
|
||||
enhanced_gridmap.clear_path_visualization()
|
||||
|
||||
# Reset interpolation buffer to prevent "jumping back" 150ms after tween ends
|
||||
snapshot_buffer.clear()
|
||||
target_visual_position = grid_to_world(current_position)
|
||||
|
||||
# Check for buffered input
|
||||
if movement_manager and movement_manager.has_method("_on_movement_finished"):
|
||||
movement_manager._on_movement_finished()
|
||||
@@ -1470,9 +1526,34 @@ static func reset_race_stats():
|
||||
race_mgr.lap1_finishers = 0
|
||||
race_mgr.lap2_finishers = 0
|
||||
|
||||
@rpc("any_peer", "call_local", "unreliable")
|
||||
func remote_set_position(authority_position):
|
||||
# Don't snap directly, update target for interpolation
|
||||
@rpc("any_peer", "call_remote", "unreliable")
|
||||
func remote_set_position(authority_position: Vector3, timestamp: int = -1):
|
||||
if timestamp == -1:
|
||||
timestamp = Time.get_ticks_msec()
|
||||
|
||||
# DISCARD STALE PACKETS:
|
||||
# 1. If we've already seen a newer packet
|
||||
if timestamp <= _last_received_timestamp:
|
||||
return
|
||||
|
||||
# 2. If this packet was sent before we finished our last authoritative movement step
|
||||
# (Preventing "past" packets from overriding the "present" destination)
|
||||
if timestamp <= _last_movement_finish_time:
|
||||
return
|
||||
|
||||
_last_received_timestamp = timestamp
|
||||
|
||||
# Add to snapshot buffer
|
||||
snapshot_buffer.append({"time": timestamp, "pos": authority_position})
|
||||
|
||||
# Ensure chronological order (in case of network jitter/reordering)
|
||||
snapshot_buffer.sort_custom(func(a, b): return a.time < b.time)
|
||||
|
||||
# Keep buffer size manageable
|
||||
if snapshot_buffer.size() > 30:
|
||||
snapshot_buffer.pop_front()
|
||||
|
||||
# Update target visual position as fallback for simpler lerping
|
||||
target_visual_position = authority_position
|
||||
|
||||
@rpc("any_peer", "call_local")
|
||||
|
||||
@@ -18,24 +18,20 @@ func _process(delta):
|
||||
return
|
||||
|
||||
# Continuous movement input
|
||||
var target_position = player.current_position
|
||||
var move_vec = Vector2i.ZERO
|
||||
if Input.is_action_pressed("move_north"): move_vec.y -= 1
|
||||
if Input.is_action_pressed("move_south"): move_vec.y += 1
|
||||
if Input.is_action_pressed("move_east"): move_vec.x += 1
|
||||
if Input.is_action_pressed("move_west"): move_vec.x -= 1
|
||||
|
||||
if Input.is_action_pressed("move_north"):
|
||||
target_position += Vector2i(0, -1)
|
||||
elif Input.is_action_pressed("move_northeast"):
|
||||
target_position += Vector2i(1, -1)
|
||||
elif Input.is_action_pressed("move_east"):
|
||||
target_position += Vector2i(1, 0)
|
||||
elif Input.is_action_pressed("move_southeast"):
|
||||
target_position += Vector2i(1, 1)
|
||||
elif Input.is_action_pressed("move_south"):
|
||||
target_position += Vector2i(0, 1)
|
||||
elif Input.is_action_pressed("move_southwest"):
|
||||
target_position += Vector2i(-1, 1)
|
||||
elif Input.is_action_pressed("move_west"):
|
||||
target_position += Vector2i(-1, 0)
|
||||
elif Input.is_action_pressed("move_northwest"):
|
||||
target_position += Vector2i(-1, -1)
|
||||
# Fallback for explicit diagonal actions (if mapped)
|
||||
if move_vec == Vector2i.ZERO:
|
||||
if Input.is_action_pressed("move_northeast"): move_vec = Vector2i(1, -1)
|
||||
elif Input.is_action_pressed("move_southeast"): move_vec = Vector2i(1, 1)
|
||||
elif Input.is_action_pressed("move_southwest"): move_vec = Vector2i(-1, 1)
|
||||
elif Input.is_action_pressed("move_northwest"): move_vec = Vector2i(-1, -1)
|
||||
|
||||
|
||||
|
||||
# Action inputs (still momentary)
|
||||
if Input.is_action_just_pressed("action_grab"):
|
||||
@@ -43,7 +39,16 @@ func _process(delta):
|
||||
elif Input.is_action_just_pressed("action_put"):
|
||||
player.auto_put_item()
|
||||
|
||||
if target_position != player.current_position:
|
||||
if move_vec != Vector2i.ZERO:
|
||||
# Calculate target relative to intent (future position) to prevent zigzagging
|
||||
var reference_pos = player.current_position
|
||||
if movement_manager.is_moving:
|
||||
if not movement_manager.movement_queue.is_empty():
|
||||
reference_pos = movement_manager.movement_queue[-1]
|
||||
elif player.target_position != Vector2i(-1, -1):
|
||||
reference_pos = player.target_position
|
||||
|
||||
var target_position = reference_pos + move_vec
|
||||
movement_manager.simple_move_to(target_position)
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ func initialize(p_player: Node3D, p_gridmap: Node):
|
||||
use_diagonal_movement = player.use_diagonal_movement
|
||||
|
||||
signal movement_finished
|
||||
var buffered_direction: Vector2i = Vector2i.ZERO
|
||||
var movement_queue: Array[Vector2i] = [] # Queue of target grid positions
|
||||
var current_move_direction: Vector2i = Vector2i.ZERO
|
||||
|
||||
func _process(delta):
|
||||
@@ -49,10 +49,17 @@ func _can_rpc() -> bool:
|
||||
|
||||
func simple_move_to(grid_position: Vector2i) -> bool:
|
||||
if is_moving:
|
||||
var direction = grid_position - player.current_position
|
||||
if direction != current_move_direction:
|
||||
buffer_move_input(direction)
|
||||
return false
|
||||
# Check if we are already moving to this position or if it's already queued
|
||||
var current_target = player.target_position
|
||||
if movement_queue.is_empty():
|
||||
if current_target == grid_position:
|
||||
return false
|
||||
elif movement_queue[-1] == grid_position:
|
||||
return false
|
||||
|
||||
if movement_queue.size() < 1: # Buffer at most 1 future move
|
||||
movement_queue.append(grid_position)
|
||||
return true
|
||||
|
||||
if not player.is_multiplayer_authority():
|
||||
print("[Move] Failed: Not authority for %s (Authority: %d, My Peer: %d)" % [player.name, player.get_multiplayer_authority(), player.multiplayer.get_unique_id()])
|
||||
@@ -67,13 +74,15 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
||||
if main and main.goals_cycle_manager and not main.goals_cycle_manager.is_match_active:
|
||||
return false
|
||||
|
||||
|
||||
var distance: int
|
||||
if use_diagonal_movement:
|
||||
distance = max(abs(grid_position.x - player.current_position.x), abs(grid_position.y - player.current_position.y))
|
||||
else:
|
||||
distance = abs(grid_position.x - player.current_position.x) + abs(grid_position.y - player.current_position.y)
|
||||
|
||||
if distance > movement_range:
|
||||
return false
|
||||
|
||||
if not enhanced_gridmap.is_position_valid(grid_position):
|
||||
# print("[Move] Failed: Position not valid on GridMap %s" % grid_position)
|
||||
return false
|
||||
@@ -130,11 +139,13 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
||||
current_move_direction = grid_position - player.current_position
|
||||
|
||||
if player.is_multiplayer_authority():
|
||||
# Authority starts their own tween locally
|
||||
var is_bot = player.is_bot or player.is_in_group("Bots")
|
||||
player.start_movement_along_path(path, not is_bot)
|
||||
|
||||
# Authority sends RPC to others (call_remote) to start their tweens
|
||||
if _can_rpc():
|
||||
player.rpc("start_movement_along_path", path, not is_bot)
|
||||
else:
|
||||
player.start_movement_along_path(path, not is_bot)
|
||||
|
||||
return true
|
||||
|
||||
@@ -250,14 +261,17 @@ func set_speed_multiplier(multiplier: float):
|
||||
# Let's check how 'start_movement_along_path' is implemented in player.gd.
|
||||
|
||||
|
||||
func buffer_move_input(direction: Vector2i):
|
||||
buffered_direction = direction
|
||||
|
||||
func _on_movement_finished():
|
||||
if buffered_direction != Vector2i.ZERO:
|
||||
var target = player.current_position + buffered_direction
|
||||
buffered_direction = Vector2i.ZERO
|
||||
simple_move_to(target)
|
||||
if not movement_queue.is_empty():
|
||||
var next_target = movement_queue.pop_front()
|
||||
# Use a small delay or call_deferred to avoid recursion issues,
|
||||
# but keep it snappy by executing immediately if possible.
|
||||
if not simple_move_to(next_target):
|
||||
# If next move failed, clear queue and signal finished
|
||||
movement_queue.clear()
|
||||
current_move_direction = Vector2i.ZERO
|
||||
emit_signal("movement_finished")
|
||||
else:
|
||||
current_move_direction = Vector2i.ZERO
|
||||
emit_signal("movement_finished")
|
||||
|
||||
Reference in New Issue
Block a user