Networked player pawns

This commit is contained in:
olof.pettersson
2025-11-05 12:15:32 +01:00
parent 2f5a88345f
commit 71ad2f09ff
10 changed files with 163 additions and 142 deletions

View File

@ -20,9 +20,7 @@ config/icon="res://icon.svg"
OrbitalMechanics="*res://scripts/singletons/orbital_mechanics.gd"
GameManager="*res://scripts/singletons/game_manager.gd"
Constants="*res://scripts/singletons/constants.gd"
ClientNetworkGlobals="*res://scripts/network/client_network_globals.gd"
LowLevelNetworkHandler="*res://scripts/network/low_level_network_handler.gd"
ServerNetworkGlobals="*res://scripts/network/server_network_globals.gd"
NetworkHandler="*res://scripts/network/network_handler.gd"
[dotnet]

View File

@ -1,6 +1,8 @@
extends Node3D
extends Area3D
class_name Spawner
@onready var mp_spawner: MultiplayerSpawner = $MultiplayerSpawner
# This spawner will register itself with the GameManager when it enters the scene.
func _ready():
# super()
@ -8,5 +10,7 @@ func _ready():
await get_tree().process_frame
GameManager.register_spawner(self)
func can_spawn() -> bool:
return get_overlapping_bodies().is_empty()
# We can add properties to the spawner later, like which faction it belongs to,
# or a reference to the body it's orbiting for initial velocity calculation.

View File

@ -1,7 +1,16 @@
[gd_scene load_steps=2 format=3 uid="uid://dvpy3urgtm62n"]
[gd_scene load_steps=3 format=3 uid="uid://dvpy3urgtm62n"]
[ext_resource type="Script" uid="uid://db1u2qqihhnq4" path="res://scenes/ship/components/hardware/spawner.gd" id="1_lldyu"]
[node name="Spawner" type="Node2D"]
[sub_resource type="SphereShape3D" id="SphereShape3D_lldyu"]
radius = 1.0
[node name="Spawner" type="Area3D"]
script = ExtResource("1_lldyu")
metadata/_custom_type_script = "uid://calosd13bkakg"
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("SphereShape3D_lldyu")
[node name="MultiplayerSpawner" type="MultiplayerSpawner" parent="."]
_spawnable_scenes = PackedStringArray("uid://7yc6a07xoccy")
spawn_path = NodePath("..")

View File

@ -2,8 +2,6 @@
extends CharacterBody3D
class_name CharacterPawn3D
var owner_id: int
## Core Parameters
@export var collision_energy_loss: float = 0.3
@export var base_inertia: float = 1.0 # Pawn's inertia without suit
@ -54,6 +52,9 @@ func _ready():
else:
printerr("GripDetector Area3D node not found on CharacterPawn!")
if is_multiplayer_authority():
camera.make_current()
func _physics_process(delta: float):
# 1. Apply Mouse Rotation (Universal head look)
@ -149,3 +150,8 @@ func _on_ladder_area_exited(area: Area3D): if area == overlapping_ladder_area: o
func _reset_head_yaw(delta: float):
# Smoothly apply the reset target to the actual pivot rotation
camera_pivot.rotation.y = lerpf(camera_pivot.rotation.y, 0.0, delta * head_turn_lerp_speed)
func _notification(what: int) -> void:
match what:
NOTIFICATION_ENTER_TREE:
set_multiplayer_authority(int(name))

View File

@ -1,9 +1,9 @@
[gd_scene load_steps=9 format=3 uid="uid://7yc6a07xoccy"]
[ext_resource type="Script" uid="uid://cdmmiixa75f3x" path="res://scenes/tests/3d/character_pawn_3d.gd" id="1_4frsu"]
[ext_resource type="Script" uid="uid://vjfk3xnapfti" path="res://scenes/tests/3d/player_controller_3d.gd" id="2_r62el"]
[ext_resource type="PackedScene" uid="uid://bm1rbv4tuppbc" path="res://eva_suit_controller.tscn" id="3_gnddn"]
[ext_resource type="Script" uid="uid://y3vo40i16ek3" path="res://scenes/tests/3d/zero_g_movement_component.gd" id="4_8jhjh"]
[ext_resource type="PackedScene" uid="uid://ba3ijdstp2bvt" path="res://scenes/tests/3d/player_controller_3d.tscn" id="4_bcy3l"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_6vm80"]
@ -12,10 +12,25 @@
[sub_resource type="SphereShape3D" id="SphereShape3D_gnddn"]
radius = 1.0
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_8jhjh"]
properties/0/path = NodePath("CharacterPawn3d:global_transform")
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_gnddn"]
properties/0/path = NodePath(".:position")
properties/0/spawn = true
properties/0/replication_mode = 1
properties/1/path = NodePath(".:rotation")
properties/1/spawn = true
properties/1/replication_mode = 1
properties/2/path = NodePath("CameraPivot/SpringArm/Camera3D:position")
properties/2/spawn = true
properties/2/replication_mode = 1
properties/3/path = NodePath("CameraPivot/SpringArm/Camera3D:rotation")
properties/3/spawn = true
properties/3/replication_mode = 1
properties/4/path = NodePath("CameraPivot:position")
properties/4/spawn = true
properties/4/replication_mode = 1
properties/5/path = NodePath("CameraPivot:rotation")
properties/5/spawn = true
properties/5/replication_mode = 1
[node name="CharacterPawn3D" type="CharacterBody3D"]
script = ExtResource("1_4frsu")
@ -27,10 +42,6 @@ shape = SubResource("CapsuleShape3D_6vm80")
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
mesh = SubResource("CapsuleMesh_6vm80")
[node name="PlayerController3D" type="Node" parent="."]
script = ExtResource("2_r62el")
metadata/_custom_type_script = "uid://vjfk3xnapfti"
[node name="CameraPivot" type="Node3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.75, 0)
@ -38,7 +49,6 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.75, 0)
spring_length = 3.0
[node name="Camera3D" type="Camera3D" parent="CameraPivot/SpringArm"]
current = true
[node name="GripDetector" type="Area3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -1)
@ -56,4 +66,6 @@ metadata/_custom_type_script = "uid://y3vo40i16ek3"
[node name="EVAMovementComponent" parent="." instance=ExtResource("3_gnddn")]
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
replication_config = SubResource("SceneReplicationConfig_8jhjh")
replication_config = SubResource("SceneReplicationConfig_gnddn")
[node name="PlayerController3d" parent="." instance=ExtResource("4_bcy3l")]

View File

@ -2,12 +2,7 @@
extends Node
class_name PlayerController3D
var is_authority: bool:
get: return !LowLevelNetworkHandler.is_server && owner_id == ClientNetworkGlobals.id
var owner_id: int = -1
@onready var possessed_pawn: CharacterPawn3D
@onready var possessed_pawn: CharacterPawn3D = get_parent()
# --- Mouse Sensitivity ---
@export var mouse_sensitivity: float = 0.002 # Radians per pixel motion
@ -24,6 +19,7 @@ class KeyInput:
func _unhandled_input(event: InputEvent):
if not is_multiplayer_authority() or not is_instance_valid(possessed_pawn):
# print("Peer ID: %s, Node Authority: %s" % [multiplayer.get_unique_id(), get_multiplayer_authority()])
return
# Handle mouse motion input directly here
@ -75,12 +71,18 @@ func server_process_clicks(l_action: KeyInput, r_action: KeyInput):
func possess(pawn_to_control: CharacterPawn3D):
possessed_pawn = pawn_to_control
possessed_pawn.camera.make_current()
print("PlayerController3D possessed: ", possessed_pawn.name)
#print("PlayerController3D %d possessed: %s" % [multiplayer.get_unique_id(), possessed_pawn.name])
# Optional: Release mouse when losing focus
func _notification(what):
if what == NOTIFICATION_WM_WINDOW_FOCUS_OUT:
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
elif what == NOTIFICATION_WM_WINDOW_FOCUS_IN:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
match what:
NOTIFICATION_WM_WINDOW_FOCUS_OUT:
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
NOTIFICATION_WM_WINDOW_FOCUS_IN:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
NOTIFICATION_EXIT_TREE:
print("PlayerController %s exited tree" % multiplayer.get_unique_id())
NOTIFICATION_ENTER_TREE:
print("PlayerController %s entered tree" % multiplayer.get_unique_id())

View File

@ -1,7 +1,7 @@
[gd_scene load_steps=7 format=3 uid="uid://ddfsn0rtdnfda"]
[ext_resource type="PackedScene" uid="uid://5noqmp8b267n" path="res://scenes/tests/3d/grips/single_handhold.tscn" id="1_jlvj7"]
[ext_resource type="Script" uid="uid://db1u2qqihhnq4" path="res://scenes/ship/components/hardware/spawner.gd" id="2_jlvj7"]
[ext_resource type="PackedScene" uid="uid://dvpy3urgtm62n" path="res://scenes/ship/components/hardware/spawner.tscn" id="2_jlvj7"]
[sub_resource type="BoxMesh" id="BoxMesh_kateb"]
size = Vector3(50, 1, 50)
@ -47,6 +47,7 @@ shape = SubResource("BoxShape3D_25xtv")
transform = Transform3D(-4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0, 1, 25, 0, 0)
[node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody3D3"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.050821304, 0.029743195, -0.014732361)
mesh = SubResource("BoxMesh_kateb")
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D3"]
@ -158,10 +159,5 @@ mesh = SubResource("CylinderMesh_nvgim")
transform = Transform3D(4.33065e-08, 0.00434584, -0.999991, 4.38977e-08, -0.999991, -0.00434584, -1, -4.35214e-08, -4.37722e-08, 0, 0, 0)
shape = SubResource("CylinderShape3D_nvgim")
[node name="Spawner" type="Node3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 21.143803, 0, -10.62656)
script = ExtResource("2_jlvj7")
metadata/_custom_type_script = "uid://db1u2qqihhnq4"
[node name="MultiplayerSpawner" type="MultiplayerSpawner" parent="."]
spawn_path = NodePath("../Spawner")
[node name="Spawner" parent="." instance=ExtResource("2_jlvj7")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12.309784, 0, -12.84836)

View File

@ -0,0 +1,50 @@
extends Node
var port = 42069
func create_server() -> void:
print(multiplayer.multiplayer_peer)
var peer = ENetMultiplayerPeer.new()
setup_connections()
var error = peer.create_server(port)
if error:
push_error(error)
return
multiplayer.multiplayer_peer = peer
print("Server Unique ID: ", multiplayer.get_unique_id())
func create_client() -> void:
setup_connections()
var peer = ENetMultiplayerPeer.new()
var error = peer.create_client("127.0.0.1", port)
if error:
push_error(error)
return
multiplayer.multiplayer_peer = peer
print("Client Unique ID: ", multiplayer.get_unique_id())
func setup_connections():
multiplayer.peer_connected.connect(on_peer_connected)
multiplayer.peer_disconnected.connect(on_peer_disconnected)
multiplayer.connected_to_server.connect(on_connected_to_server)
func on_peer_connected(peer_id: int) -> void:
print("Peer %s recieved connection: %s" % [multiplayer.get_unique_id(), peer_id])
# For each peer that connects, we put them in the queue to spawn
if multiplayer.is_server():
GameManager.queue_spawn_player(peer_id)
func on_peer_disconnected(peer_id: int) -> void:
print("Peer %s lost connection to: %s" % [multiplayer.get_unique_id(), peer_id])
print(multiplayer.get_peers())
func on_connected_to_server() -> void:
print("%s connected to server!" % multiplayer.get_unique_id())

View File

@ -0,0 +1 @@
uid://dw3hm45dog1sk

View File

@ -28,121 +28,70 @@ func _ready():
# Check command-line arguments to determine if this instance should be a server or client.
# Godot's "Run Multiple Instances" feature adds "--server" to the main instance.
if "--server" in OS.get_cmdline_args():
LowLevelNetworkHandler.start_server()
NetworkHandler.create_server()
elif "--host" in OS.get_cmdline_args():
NetworkHandler.create_server()
# Host also acts as a player, so we need to handle its own connection.
NetworkHandler.on_peer_connected.call_deferred(1)
else:
LowLevelNetworkHandler.on_peer_connected.connect(on_player_connected)
ClientNetworkGlobals.handle_local_id_assignment.connect(on_player_connected)
ClientNetworkGlobals.handle_remote_id_assignment.connect(on_player_connected)
LowLevelNetworkHandler.start_client()
print("GameManager: Starting as CLIENT.")
NetworkHandler.create_client()
# LowLevelNetworkHandler.on_peer_connected.connect(on_player_connected)
# LowLevelNetworkHandler.on_peer_disconnected.connect(on_player_disconnected)
# If this is a client, we create a proxy node that allows us to easily
# call RPCs on the server's GameManager (which always has node path /root/GameManager).
# if not multiplayer.is_server():
# multiplayer.get_remote_sender_id() # This is a placeholder to illustrate client-side setup
func _process(_delta):
if find_available_spawner():
_try_spawn_waiting_player()
# Called when the game starts (e.g., from _ready() in StarSystemGenerator)
func start_game():
pass
# For a single-player game, we simulate a player connecting with ID 1.
# on_player_connected(1)
# This would be connected to a network signal in a multiplayer game.
func on_player_connected(id: int):
print("GameManager: Player %d connected." % id)
# The server is responsible for all spawning logic.
if not multiplayer.is_server():
return
# This function runs on the SERVER when a client connects.
# 1. Spawn a controller for the new player on the server.
var controller: PlayerController3D = player_controller_3d_scene.instantiate()
controller.owner_id = id
controller.name = "PlayerController_%d" % id
# Set the authority for this controller to the player who just connected.
controller.set_multiplayer_authority(id)
add_child(controller)
player_controllers[id] = controller
# 2. Tell the newly connected client which controller is theirs.
# This RPC will only be executed on the machine of the player with 'id'.
# client_set_controller.rpc_id(id, controller.get_path())
# 3. Attempt to spawn a pawn for them immediately.
_attempt_to_spawn_player(id)
func on_player_disconnected(player_id: int):
print("GameManager: Player %d disconnected." % player_id)
@rpc("call_local")
func client_set_controller(controller_path: NodePath):
# This function runs on the CLIENT. It finds the controller node that the
# server created for it and can be used for client-side setup if needed.
print("Client received controller path: ", controller_path)
func _attempt_to_spawn_player(player_id: int):
if registered_spawners.is_empty():
# No spawners available, add the player to the waiting queue.
if not player_id in waiting_players:
waiting_players.append(player_id)
print("GameManager: No spawners available. Player %d is now waiting." % player_id)
# You could show a "Waiting for available spawner..." UI here.
else:
# Spawners are available, proceed with spawning.
server_spawn_player_pawn.rpc(player_id)
func queue_spawn_player(player_id: int):
waiting_players.append(player_id)
print("GameManager: Player %d queued for spawn." % player_id)
# function to process the waiting queue.
func _try_spawn_waiting_player():
if not waiting_players.is_empty() and not registered_spawners.is_empty():
var player_to_spawn = waiting_players.pop_front()
print("GameManager: Spawner is now available. Spawning waiting player %d." % player_to_spawn)
server_spawn_player_pawn.rpc(player_to_spawn)
@rpc("call_local")
func server_spawn_player_pawn(player_id: int):
# This function now runs on ALL clients, ensuring everyone sees the new pawn.
if not player_controllers.has(player_id):
push_error("Cannot spawn pawn for non-existent player %d" % player_id)
return
var player_id = waiting_players.pop_back()
# --- NEW SPAWNING LOGIC ---
if registered_spawners.is_empty():
push_error("GameManager: No spawners available to create pawn!")
return
print("GameManager: Spawner is now available. Spawning waiting player %d." % player_id)
var spawn_point = find_available_spawner()
# For now, we'll just pick the first available spawner.
# Later, you could present a UI for the player to choose.
var spawn_point: Spawner = registered_spawners[0]
if not is_instance_valid(spawn_point):
push_error("GameManager: Spawn point not found!")
return
# 1. Instantiate the pawn and add it to the scene.
if spawn_point:
_spawn_player_pawn(player_id)
var pawn = player_pawns[player_id]
pawn.set_multiplayer_authority(player_id)
spawn_point.add_child(pawn)
print("GameManager peer %s: Player %d spawned successfully." % [multiplayer.get_unique_id(), player_id])
else:
waiting_players.append(player_id)
print("GameManager peer %s: Failed to spawn player %d." % [multiplayer.get_unique_id(), player_id])
func find_available_spawner() -> Spawner:
var idx = registered_spawners.find_custom(func(spawner: Spawner) -> bool:
return spawner.can_spawn()
)
return registered_spawners[idx] if idx != -1 else null
# @rpc("call_local")
func _spawn_player_pawn(player_id: int):
var pawn = character_pawn_3d_scene.instantiate()
# Add the pawn as a child of the main scene root, not a module.
pawn.name = "CharacterPawn_%d" % player_id
get_tree().root.add_child(pawn)
pawn.owner = get_tree().root
pawn.set_multiplayer_authority(player_id)
# 2. Set its position and initial velocity from the spawner.
pawn.global_position = spawn_point.global_position
player_pawns[player_id] = pawn
# 3. Possess the pawn with the player's controller.
player_controllers[player_id].possess(pawn)
print("GameManager: Spawned 3D Pawn for player %d" % player_id)
pawn.name = str(player_id)
player_pawns[player_id] = pawn
pawn.set_multiplayer_authority(player_id)
print("GameManager: Spawned 3D Pawn for player %d" % player_id)
# Any scene that generates a star system will call this function to register itself.
func register_star_system(system_node):
@ -162,7 +111,7 @@ func register_spawner(spawner_node: Spawner):
print("GameManager: Spawner '%s' registered." % spawner_node.name)
# NEW: If a player is waiting, try to spawn them now.
_try_spawn_waiting_player()
# _try_spawn_waiting_player()
# A helper function for easily accessing the system's data.
func get_system_data() -> SystemData:
@ -188,9 +137,3 @@ func get_all_trackable_bodies() -> Array[OrbitalBody2D]:
func request_server_action(action_name: String, args: Array = []):
# This function's body only runs on the SERVER.
print("Server received request: ", action_name, " with args: ", args)
# Here, the server would validate and execute the action.
# match action_name:
# "start_match":
# _server_start_match()
# "player_chose_loadout":
# _server_process_loadout(multiplayer.get_remote_sender_id(), args)