Created
July 27, 2024 07:33
-
-
Save apples/2cdafa0dd2d7e5a400ff8d2e204471ed to your computer and use it in GitHub Desktop.
Godot Lobby system with simple client verification.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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