WIP Low level network handling

This commit is contained in:
olof.pettersson
2025-11-03 17:27:36 +01:00
parent 5e851049b5
commit 2f5a88345f
21 changed files with 406 additions and 38 deletions

View File

@ -20,6 +20,9 @@ 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"
[dotnet]

View File

@ -76,4 +76,4 @@ func _generate_brachistochrone_plan(rendezvous_point: DataTypes.PathPoint) -> Ar
# The key difference is that all calculations are now based on a confirmed possible intercept.
var plan: Array[DataTypes.ImpulsiveBurnPlan] = []
# ... logic to build the plan ...
return plan
return plan

View File

@ -2,6 +2,8 @@
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
@ -17,6 +19,7 @@ var _pitch_yaw_input: Vector2 = Vector2.ZERO
## Rotation Variables
@onready var camera_pivot: Node3D = $CameraPivot
@onready var camera: Camera3D = $CameraPivot/SpringArm/Camera3D
@export_range(0.1, PI / 2.0) var max_yaw_rad: float = deg_to_rad(80.0)
@export_range(-PI / 2.0 + 0.01, 0) var min_pitch_rad: float = deg_to_rad(-75.0)
@export_range(0, PI / 2.0 - 0.01) var max_pitch_rad: float = deg_to_rad(60.0)

View File

@ -2,8 +2,12 @@
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
var player_id: int = -1
# --- Mouse Sensitivity ---
@export var mouse_sensitivity: float = 0.002 # Radians per pixel motion
@ -19,6 +23,9 @@ class KeyInput:
released = _r
func _unhandled_input(event: InputEvent):
if not is_multiplayer_authority() or not is_instance_valid(possessed_pawn):
return
# Handle mouse motion input directly here
if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
# Calculate yaw and pitch based on mouse movement
@ -28,13 +35,12 @@ func _unhandled_input(event: InputEvent):
var sensitivity_modified_mouse_input = Vector2(yaw_input, pitch_input) * mouse_sensitivity
# Send rotation input via RPC immediately
server_process_rotation_input.rpc_id(multiplayer.multiplayer_peer.get_unique_id(), sensitivity_modified_mouse_input)
server_process_rotation_input.rpc_id(multiplayer.get_unique_id(), sensitivity_modified_mouse_input)
func _physics_process(_delta):
if not is_multiplayer_authority() or not is_instance_valid(possessed_pawn):
return
# 1. Gather Input
var move_vec = Input.get_vector("move_left_3d", "move_right_3d", "move_backward_3d", "move_forward_3d")
var roll_input = Input.get_action_strength("roll_right_3d") - Input.get_action_strength("roll_left_3d")
var vertical_input = Input.get_action_strength("move_up_3d") - Input.get_action_strength("move_down_3d")
@ -43,9 +49,9 @@ func _physics_process(_delta):
var l_input = KeyInput.new(Input.is_action_just_pressed("left_click"), Input.is_action_pressed("left_click"), Input.is_action_just_released("left_click"))
var r_input = KeyInput.new(Input.is_action_just_pressed("right_click"), Input.is_action_pressed("right_click"), Input.is_action_just_released("right_click"))
server_process_movement_input.rpc_id(multiplayer.multiplayer_peer.get_unique_id(), move_vec, roll_input, vertical_input)
server_process_interaction_input.rpc_id(multiplayer.multiplayer_peer.get_unique_id(), interact_input)
server_process_clicks.rpc_id(multiplayer.multiplayer_peer.get_unique_id(), l_input, r_input)
server_process_movement_input.rpc_id(multiplayer.get_unique_id(), move_vec, roll_input, vertical_input)
server_process_interaction_input.rpc_id(multiplayer.get_unique_id(), interact_input)
server_process_clicks.rpc_id(multiplayer.get_unique_id(), l_input, r_input)
@rpc("any_peer", "call_local")
func server_process_movement_input(move: Vector2, roll: float, vertical: float):
@ -69,7 +75,8 @@ func server_process_clicks(l_action: KeyInput, r_action: KeyInput):
func possess(pawn_to_control: CharacterPawn3D):
possessed_pawn = pawn_to_control
print("PlayerController3D possessed: ", possessed_pawn.name)
possessed_pawn.camera.make_current()
print("PlayerController3D possessed: ", possessed_pawn.name)
# Optional: Release mouse when losing focus
func _notification(what):

View File

@ -0,0 +1,40 @@
extends Node
signal handle_local_id_assignment(local_id: int)
signal handle_remote_id_assignment(remote_id: int)
signal handle_player_position(player_position: PlayerPosition)
var id: int = -1
var remote_ids: Array[int]
func _ready() -> void:
LowLevelNetworkHandler.on_client_packet.connect(on_client_packet)
func on_client_packet(data: PackedByteArray) -> void:
var packet_type: int = data.decode_u8(0)
match packet_type:
PacketInfo.PACKET_TYPE.ID_ASSIGNMENT:
manage_ids(IDAssignment.create_from_data(data))
PacketInfo.PACKET_TYPE.PLAYER_POSITION:
handle_player_position.emit(PlayerPosition.create_from_data(data))
_:
push_error("Packet type with index ", data[0], " unhandled!")
func manage_ids(id_assignment: IDAssignment) -> void:
# TODO: This will cause issues if two clients believe they are the target for the ID assignment
if id == -1: # When id == -1, the id sent by the server is for us
id = id_assignment.id
handle_local_id_assignment.emit(id_assignment.id)
remote_ids = id_assignment.remote_ids
for remote_id in remote_ids:
if remote_id == id: continue
handle_remote_id_assignment.emit(remote_id)
else: # When id != -1, we already own an id, and just append the remote ids by the sent id
remote_ids.append(id_assignment.id)
handle_remote_id_assignment.emit(id_assignment.id)

View File

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

View File

@ -0,0 +1,122 @@
extends Node
# Server signals
signal on_peer_connected(peer_id: int)
signal on_peer_disconnected(peer_id: int)
signal on_server_packet(peer_id: int, data: PackedByteArray)
# Client signals
signal on_connected_to_server()
signal on_disconnected_from_server()
signal on_client_packet(data: PackedByteArray)
# Server variables
var available_peer_ids: Array = range(255, -1, -1)
var client_peers: Dictionary[int, ENetPacketPeer] # key: peer_id (int), value: peer (ENetPacketPeer). The peer_id should hold "id" meta data. Use get_meta("id")
# Client variables
var server_peer: ENetPacketPeer
# General variables
var connection: ENetConnection
var is_server: bool = false
func _process(delta: float) -> void:
if connection == null: return
handle_events()
func handle_events() -> void:
var packet_event: Array = connection.service()
var event_type: ENetConnection.EventType = packet_event[0]
while event_type != ENetConnection.EVENT_NONE:
var peer: ENetPacketPeer = packet_event[1]
match (event_type):
ENetConnection.EVENT_ERROR:
push_warning("Package resulted in an unknown error!")
return
ENetConnection.EVENT_CONNECT:
if is_server:
peer_connected(peer)
else:
connected_to_server()
ENetConnection.EVENT_DISCONNECT:
if is_server:
peer_disconnected(peer)
else:
disconnected_from_server()
return # Return because connection was set to null
ENetConnection.EVENT_RECEIVE:
if is_server:
on_server_packet.emit(peer.get_meta("id"), peer.get_packet())
else:
on_client_packet.emit(peer.get_packet())
# Call service() again to handle remaining packets in current while loop
packet_event = connection.service()
event_type = packet_event[0]
func start_server(ip_address: String = "127.0.0.1", port: int = 42069) -> void:
connection = ENetConnection.new()
var error: Error = connection.create_host_bound(ip_address, port)
if error:
print("Server starting failed: ", error_string(error))
connection = null
return
print("Server started")
is_server = true
func peer_connected(peer: ENetPacketPeer) -> void:
var peer_id: int = available_peer_ids.pop_back()
peer.set_meta("id", peer_id)
client_peers[peer_id] = peer
print("Peer connected with assigned id: ", peer_id)
on_peer_connected.emit(peer_id)
func peer_disconnected(peer: ENetPacketPeer) -> void:
var peer_id: int = peer.get_meta("id")
available_peer_ids.push_back(peer_id)
client_peers.erase(peer_id)
print("Succesfully disconnected: ", peer_id, " from server!")
on_peer_disconnected.emit(peer_id)
func start_client(ip_address: String = "127.0.0.1", port: int = 42069) -> void:
connection = ENetConnection.new()
var error: Error = connection.create_host(1)
if error:
print("Client starting failed: ", error_string(error))
connection = null
return
print("Client started")
server_peer = connection.connect_to_host(ip_address, port)
func disconnect_client() -> void:
if is_server: return
server_peer.peer_disconnect()
func connected_to_server() -> void:
print("Successfully connected to server!")
on_connected_to_server.emit()
func disconnected_from_server() -> void:
print("Successfully disconnected from server!")
on_disconnected_from_server.emit()
connection = null

View File

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

View File

@ -0,0 +1,41 @@
extends CharacterBody2D
const SPEED: float = 500.0
var is_authority: bool:
get: return !LowLevelNetworkHandler.is_server && owner_id == ClientNetworkGlobals.id
var owner_id: int
func _enter_tree() -> void:
ServerNetworkGlobals.handle_player_position.connect(server_handle_player_position)
ClientNetworkGlobals.handle_player_position.connect(client_handle_player_position)
func _exit_tree() -> void:
ServerNetworkGlobals.handle_player_position.disconnect(server_handle_player_position)
ClientNetworkGlobals.handle_player_position.disconnect(client_handle_player_position)
func _physics_process(delta: float) -> void:
if !is_authority: return
velocity = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") * SPEED
move_and_slide()
PlayerPosition.create(owner_id, global_position).send(LowLevelNetworkHandler.server_peer)
func server_handle_player_position(peer_id: int, player_position: PlayerPosition) -> void:
if owner_id != peer_id: return
global_position = player_position.position
PlayerPosition.create(owner_id, global_position).broadcast(LowLevelNetworkHandler.connection)
func client_handle_player_position(player_position: PlayerPosition) -> void:
if is_authority || owner_id != player_position.id: return
global_position = player_position.position

View File

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

View File

@ -0,0 +1,17 @@
extends Node
# const LOW_LEVEL_NETWORK_PLAYER = preload("res://low_level_example/scenes/low_level_network_player.tscn")
func _ready() -> void:
LowLevelNetworkHandler.on_peer_connected.connect(spawn_player)
ClientNetworkGlobals.handle_local_id_assignment.connect(spawn_player)
ClientNetworkGlobals.handle_remote_id_assignment.connect(spawn_player)
func spawn_player(id: int) -> void:
pass
# var player = LOW_LEVEL_NETWORK_PLAYER.instantiate()
# player.owner_id = id
# player.name = str(id) # Optional, but it beats the name "@CharacterBody2D@2/3/4..."
# call_deferred("add_child", player)

View File

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

View File

@ -0,0 +1,35 @@
class_name IDAssignment extends PacketInfo
var id: int
var remote_ids: Array[int]
static func create(id: int, remote_ids: Array[int]) -> IDAssignment:
var info: IDAssignment = IDAssignment.new()
info.packet_type = PACKET_TYPE.ID_ASSIGNMENT
info.flag = ENetPacketPeer.FLAG_RELIABLE
info.id = id
info.remote_ids = remote_ids
return info
static func create_from_data(data: PackedByteArray) -> IDAssignment:
var info: IDAssignment = IDAssignment.new()
info.decode(data)
return info
func encode() -> PackedByteArray:
var data: PackedByteArray = super.encode()
data.resize(2 + remote_ids.size())
data.encode_u8(1, id)
for i in remote_ids.size():
var id: int = remote_ids[i]
data.encode_u8(2 + i, id)
return data
func decode(data: PackedByteArray) -> void:
super.decode(data)
id = data.decode_u8(1)
for i in range(2, data.size()):
remote_ids.append(data.decode_u8(i))

View File

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

View File

@ -0,0 +1,32 @@
## BASECLASS ##
class_name PacketInfo
# Don't make values above 255, since we send "packet_type" as a single byte
enum PACKET_TYPE {
ID_ASSIGNMENT = 0,
PLAYER_POSITION = 10,
}
var packet_type: PACKET_TYPE
var flag: int
# Override function in derived classes
func encode() -> PackedByteArray:
var data: PackedByteArray
data.resize(1)
data.encode_u8(0, packet_type)
return data
# Override function in derived classes
func decode(data: PackedByteArray) -> void:
packet_type = data.decode_u8(0)
func send(target: ENetPacketPeer) -> void:
target.send(0, encode(), flag)
func broadcast(server: ENetConnection) -> void:
server.broadcast(0, encode(), flag)

View File

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

View File

@ -0,0 +1,33 @@
class_name PlayerPosition extends PacketInfo
var id: int
var position: Vector2
static func create(id: int, position: Vector2) -> PlayerPosition:
var info: PlayerPosition = PlayerPosition.new()
info.packet_type = PACKET_TYPE.PLAYER_POSITION
info.flag = ENetPacketPeer.FLAG_UNSEQUENCED
info.id = id
info.position = position
return info
static func create_from_data(data: PackedByteArray) -> PlayerPosition:
var info: PlayerPosition = PlayerPosition.new()
info.decode(data)
return info
func encode() -> PackedByteArray:
var data: PackedByteArray = super.encode()
data.resize(10)
data.encode_u8(1, id)
data.encode_float(2, position.x)
data.encode_float(6, position.y)
return data
func decode(data: PackedByteArray) -> void:
super.decode(data)
id = data.decode_u8(1)
position = Vector2(data.decode_float(2), data.decode_float(6))

View File

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

View File

@ -0,0 +1,30 @@
extends Node
signal handle_player_position(peer_id: int, player_position: PlayerPosition)
var peer_ids: Array[int]
func _ready() -> void:
LowLevelNetworkHandler.on_peer_connected.connect(on_peer_connected)
LowLevelNetworkHandler.on_peer_disconnected.connect(on_peer_disconnected)
LowLevelNetworkHandler.on_server_packet.connect(on_server_packet)
func on_peer_connected(peer_id: int) -> void:
peer_ids.append(peer_id)
IDAssignment.create(peer_id, peer_ids).broadcast(LowLevelNetworkHandler.connection)
func on_peer_disconnected(peer_id: int) -> void:
peer_ids.erase(peer_id)
# Create IDUnassignment to broadcast to all still connected peers
func on_server_packet(peer_id: int, data: PackedByteArray) -> void:
match data[0]:
PacketInfo.PACKET_TYPE.PLAYER_POSITION:
handle_player_position.emit(peer_id, PlayerPosition.create_from_data(data))
_:
push_error("Packet type with index ", data[0], " unhandled!")

View File

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

View File

@ -28,32 +28,24 @@ 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():
# This instance is the server.
var peer = ENetMultiplayerPeer.new()
var error = peer.create_server(5999)
if error != OK:
printerr("Failed to create ENet server. Error code: ", error)
return
multiplayer.multiplayer_peer = peer
print("GameManager: ENet server created on port 5999.")
# The server's own connection is handled by the peer_connected signal for ID 1.
LowLevelNetworkHandler.start_server()
else:
# This instance is a client.
var peer = ENetMultiplayerPeer.new()
# The address "localhost" is used for local testing.
peer.create_client("localhost", 5999)
multiplayer.multiplayer_peer = peer
print("GameManager: ENet client created, attempting to connect to localhost:5999.")
# Connect signals for all peers (server and clients) to handle new players.
multiplayer.peer_connected.connect(on_player_connected)
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()
# 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
# if not multiplayer.is_server():
# multiplayer.get_remote_sender_id() # This is a placeholder to illustrate client-side setup
# Called when the game starts (e.g., from _ready() in StarSystemGenerator)
func start_game():
@ -62,8 +54,8 @@ func start_game():
# on_player_connected(1)
# This would be connected to a network signal in a multiplayer game.
func on_player_connected(player_id: int):
print("GameManager: Player %d connected." % player_id)
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():
@ -72,24 +64,28 @@ func on_player_connected(player_id: int):
# 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.player_id = player_id
controller.name = "PlayerController_%d" % player_id
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(player_id)
controller.set_multiplayer_authority(id)
add_child(controller)
player_controllers[player_id] = 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 'player_id'.
client_set_controller.rpc_id(player_id, controller.get_path())
# 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(player_id)
_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):
@ -136,6 +132,7 @@ func server_spawn_player_pawn(player_id: int):
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