Skip to content

Instantly share code, notes, and snippets.

@apples
Created July 27, 2024 07:33
Show Gist options
  • Save apples/2cdafa0dd2d7e5a400ff8d2e204471ed to your computer and use it in GitHub Desktop.
Save apples/2cdafa0dd2d7e5a400ff8d2e204471ed to your computer and use it in GitHub Desktop.
Godot Lobby system with simple client verification.
extends Node
##
## Manages multiplayer connections.
##
## Emitted when a client player joins the session. Only emitted on the server.
signal player_joined(unique_id: int)
## Emitted when a client player leaves the session (or is kicked). Only emitted on the server.
signal player_disconnected(unique_id: int, reason: LeaveReason)
## Emitted when this client is connected to the server. Only emitted on the client.
signal connected_to_server()
## Emitted when this client is disconnected from the server. Only emitted on the client
signal disconnected_from_server()
## Emitted when this client fails to join the server. Only emitted on the client
signal failed_to_join()
## Reason that a player left the session.
enum LeaveReason {
## The player intentionally quit the game.
PLAYER_QUIT,
## The player was unexpectedly disconnected for an unknown reason.
SUDDEN_DISCONNECT,
## The host kicked the player.
KICKED,
}
## Default port number.
const PORT = 23456
## The maximum number of remote players allowed.
const MAX_PLAYERS = 4
## The time in seconds that a new player has to complete the handshake.
const AUTH_TIMEOUT = 10.0
## An array of all players.
var players: Array[PlayerSlot] = []
@onready var scene_multiplayer: SceneMultiplayer = multiplayer as SceneMultiplayer
## TODO: These need to be populated from some game-specific source, such as Steam ID or just asking the player.
var my_name: String
var my_profile: String
func _ready() -> void:
my_name = OS.get_cmdline_user_args()[0] if OS.get_cmdline_user_args().size() > 0 else "Nobody"
my_profile = str(hash(Time.get_unix_time_from_system()))
scene_multiplayer.peer_connected.connect(_on_peer_connected)
scene_multiplayer.peer_disconnected.connect(_on_peer_disconnected)
scene_multiplayer.server_disconnected.connect(_on_server_disconnected)
scene_multiplayer.connected_to_server.connect(_on_connected_to_server)
scene_multiplayer.connection_failed.connect(_on_connection_failed)
scene_multiplayer.peer_authenticating.connect(_on_peer_authenticating)
scene_multiplayer.auth_timeout = AUTH_TIMEOUT
scene_multiplayer.auth_callback = _auth_callback
func start_server():
var peer = ENetMultiplayerPeer.new()
peer.create_server(PORT)
multiplayer.multiplayer_peer = peer
var player_slot := PlayerSlot.new()
player_slot.multiplayer_id = 1
player_slot.player_profile = my_profile
player_slot.player_name = my_name
players.append(player_slot)
func start_client(server_address: String):
var peer = ENetMultiplayerPeer.new()
peer.create_client(server_address, PORT)
multiplayer.multiplayer_peer = peer
## Returns the player slot for the given multiplayer unique id.
func get_player_by_multiplayer_id(multiplayer_id: int) -> PlayerSlot:
for p in players:
if p.multiplayer_id == multiplayer_id:
return p
return null
## Forcibly disconnects a player.
func kick_player(multiplayer_id: int, emit_signal: bool = true):
multiplayer.multiplayer_peer.disconnect_peer(multiplayer_id, true)
var slot := get_player_by_multiplayer_id(multiplayer_id)
if not slot:
return
if emit_signal:
player_disconnected.emit(multiplayer_id, LeaveReason.KICKED)
players.erase(slot)
## (async) Disconnects this peer from the network or shuts down the server.
func close():
if not multiplayer.is_server():
_client_quit_rpc.rpc_id(1)
await get_tree().create_timer(0.05).timeout
multiplayer.multiplayer_peer.close()
multiplayer.multiplayer_peer = null
func _on_peer_authenticating(id: int):
if scene_multiplayer.is_server():
return
assert(id == 1)
var client_info = {
player_profile = my_profile,
player_name = my_name,
}
scene_multiplayer.send_auth(1, var_to_bytes(client_info))
scene_multiplayer.complete_auth(1)
func _on_peer_connected(id: int):
if not scene_multiplayer.is_server():
return
player_joined.emit(id)
func _on_peer_disconnected(id: int):
if not scene_multiplayer.is_server():
return
var slot := get_player_by_multiplayer_id(id)
if slot:
player_disconnected.emit(id, LeaveReason.SUDDEN_DISCONNECT)
players.erase(slot)
func _on_connected_to_server():
connected_to_server.emit()
func _on_server_disconnected():
disconnected_from_server.emit()
func _on_connection_failed():
failed_to_join.emit()
func _auth_callback(multiplayer_id: int, data: PackedByteArray):
if players.size() >= MAX_PLAYERS:
push_error("Player limit reached.")
scene_multiplayer.disconnect_peer(multiplayer_id)
return
var client_info = bytes_to_var(data)
if not client_info:
push_error("Client client_info is empty.")
scene_multiplayer.disconnect_peer(multiplayer_id)
return
if not "player_profile" in client_info or client_info.player_profile is not String or client_info.player_profile == "":
push_error("Client client_info is missing fields.")
scene_multiplayer.disconnect_peer(multiplayer_id)
return
if not "player_name" in client_info or client_info.player_name is not String or client_info.player_name == "":
push_error("Client client_info is missing fields.")
scene_multiplayer.disconnect_peer(multiplayer_id)
return
for slot in players:
if slot.player_profile == client_info.player_profile:
push_error("Client with player_profile = \"%s\" tried to join, but another player is already using that profile!" % [client_info.player_profile])
scene_multiplayer.disconnect_peer(multiplayer_id)
return
var player_slot := PlayerSlot.new()
player_slot.multiplayer_id = multiplayer_id
player_slot.player_profile = client_info.player_profile
player_slot.player_name = client_info.player_name
players.append(player_slot)
scene_multiplayer.complete_auth(multiplayer_id)
@rpc("any_peer", "call_remote", "reliable")
func _client_quit_rpc():
if not multiplayer.is_server():
push_error("_client_quit_rpc received, but I am not the server.")
return
var multiplayer_id := multiplayer.get_remote_sender_id()
var slot := get_player_by_multiplayer_id(multiplayer_id)
if not slot:
return
player_disconnected.emit(multiplayer_id, LeaveReason.PLAYER_QUIT)
players.erase(slot)
## Represents a client player connected to the server.
class PlayerSlot extends RefCounted:
## The client's multiplayer unique id.
var multiplayer_id: int
## The player's unique profile id (e.g. Steam id).
var player_profile: String
## The player's chosen nickname or Steam name.
var player_name: String
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment