└── addons └── easy_peasy_multiplayer ├── plugin.gd.uid ├── steam_info.gd.uid ├── networking ├── network.gd.uid ├── network_enet.gd.uid ├── network_steam.gd.uid ├── network_enet.gd ├── network_steam.gd └── network.gd ├── plugin.cfg ├── LICENSE ├── plugin.gd ├── README.md └── steam_info.gd /addons/easy_peasy_multiplayer/plugin.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dfwt1pxbq6ai 2 | -------------------------------------------------------------------------------- /addons/easy_peasy_multiplayer/steam_info.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bnrrf8q027js4 2 | -------------------------------------------------------------------------------- /addons/easy_peasy_multiplayer/networking/network.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cq3b8bnp6no5g 2 | -------------------------------------------------------------------------------- /addons/easy_peasy_multiplayer/networking/network_enet.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c7oabpiovjxnx 2 | -------------------------------------------------------------------------------- /addons/easy_peasy_multiplayer/networking/network_steam.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b04p2yklefwat 2 | -------------------------------------------------------------------------------- /addons/easy_peasy_multiplayer/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Easy Peasy Multiplayer" 4 | description="A dead simple drag-and-drop networking solution for Godot" 5 | author="Kiki Mumme" 6 | version="1.1.0" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/easy_peasy_multiplayer/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Kj Mumme 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /addons/easy_peasy_multiplayer/networking/network_enet.gd: -------------------------------------------------------------------------------- 1 | class_name NetworkEnet 2 | extends Node 3 | 4 | ## The port number to use for Enet servers 5 | const DEFAULT_PORT = 7000 6 | 7 | ## The [MultiplayerPeer] for the Enet server. We can define this on initialization because this script should only run if we are going to be networking using ENet 8 | var peer : ENetMultiplayerPeer = ENetMultiplayerPeer.new() 9 | 10 | #region Network-Specific Functions 11 | ## Creates a game server as the host. See [Network.become_host] for more information 12 | func become_host(connection_info : Dictionary = { "port" : DEFAULT_PORT }): 13 | var error = peer.create_server(connection_info.port, Network.room_size) 14 | if error: 15 | if Network._is_verbose: 16 | print("Error creating host: %s" % error_string(error)) 17 | return error 18 | peer.get_host().compress(ENetConnection.COMPRESS_RANGE_CODER) 19 | 20 | multiplayer.multiplayer_peer = peer 21 | 22 | Network.connected_players[1] = Network.player_info 23 | Network.server_started.emit() 24 | Network.player_connected.emit(1, Network.player_info) 25 | Network.is_host = true 26 | if Network._is_verbose: 27 | print("ENet Server hosted on port %d" % connection_info.port) 28 | 29 | ## Joins a game using an id in [Network]. See [Network.join_as_client] for more information 30 | func join_as_client(): 31 | var ip = Network.ip_address 32 | var port = DEFAULT_PORT 33 | 34 | # Check if the ip_address contains a port (e.g., "192.168.1.1:8080") 35 | # This snippet was written by https://github.com/SimonMcCallum. Thank you for forking my plugin, your project is so cool! 36 | if ":" in ip: 37 | var parts = ip.split(":") 38 | ip = parts[0] 39 | port = int(parts[1]) 40 | 41 | var error = peer.create_client(ip, port) 42 | if error: 43 | if Network._is_verbose: 44 | print("ENet client failed to connect to server %s:%d with error: %s" % [ip, port, error_string(error)]) 45 | return error 46 | peer.get_host().compress(ENetConnection.COMPRESS_RANGE_CODER) 47 | 48 | multiplayer.multiplayer_peer = peer 49 | Network.is_host = false 50 | 51 | if Network._is_verbose: 52 | print("ENet client connecting to %s:%d" % [ip, port]) 53 | 54 | ## This does nothing as Enet does not have a lobby implementation. It is only here to prevent errors. 55 | func list_lobbies(): 56 | pass 57 | #endregion 58 | -------------------------------------------------------------------------------- /addons/easy_peasy_multiplayer/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | const PLUGIN_NAME = "easy_peasy_multiplayer" 5 | const AUTOLOADS = { 6 | "SteamInfo" : "res://addons/%s/steam_info.gd" % PLUGIN_NAME, 7 | "Network" : "res://addons/%s/networking/network.gd" % PLUGIN_NAME 8 | } 9 | 10 | const SETTINGS: Dictionary = { 11 | "general" : { 12 | "verbose_network_logging" : { 13 | "type" : TYPE_BOOL, 14 | "default_value" : false 15 | }, 16 | } 17 | } 18 | 19 | func _enter_tree() -> void: 20 | var godotsteam_exists := DirAccess.dir_exists_absolute("res://addons/godotsteam/") 21 | var multiplayer_peer_exists := DirAccess.dir_exists_absolute("res://addons/steam-multiplayer-peer") 22 | 23 | if godotsteam_exists and multiplayer_peer_exists: 24 | # Registers autoloads 25 | for autoload in AUTOLOADS: 26 | add_autoload_singleton(autoload, AUTOLOADS[autoload]) 27 | _add_project_settings() 28 | else: 29 | var dialog := AcceptDialog.new() 30 | dialog.title = "Missing Required Dependencies" 31 | dialog.dialog_text = "You are missing the following dependencies required for this addon to function: \n" 32 | if not godotsteam_exists: 33 | dialog.dialog_text += "GodotSteam" 34 | if not multiplayer_peer_exists: 35 | dialog.dialog_text += "Steam Multiplayer Peer" 36 | 37 | EditorInterface.popup_dialog_centered(dialog) 38 | 39 | func _disable_plugin() -> void: 40 | _remove_project_settings() 41 | 42 | func _exit_tree() -> void: 43 | # Removes autoloads 44 | for autoload in AUTOLOADS: 45 | remove_autoload_singleton(autoload) 46 | 47 | func _add_project_settings() -> void: 48 | for section : String in SETTINGS: 49 | for setting : String in SETTINGS[section]: 50 | var setting_name : String = "%s/%s/%s" % [PLUGIN_NAME, section, setting] 51 | if not ProjectSettings.has_setting(setting_name): 52 | ProjectSettings.set_setting(setting_name, \ 53 | SETTINGS[section][setting]["default_value"]) 54 | 55 | ProjectSettings.set_initial_value(setting_name, SETTINGS[section][setting]["default_value"]) 56 | ProjectSettings.set_as_basic(setting_name, true) 57 | 58 | var error : int = ProjectSettings.save() 59 | if not error == OK: 60 | push_error("Dev Tools - error %s while saving project settings." % error_string(error)) 61 | 62 | 63 | func _remove_project_settings() -> void: 64 | for section : String in SETTINGS: 65 | for setting : String in SETTINGS[section]: 66 | var setting_name : String = "%s/%s/%s" % [PLUGIN_NAME, section, setting] 67 | if ProjectSettings.has_setting(setting_name): 68 | ProjectSettings.set_setting(setting_name, null) 69 | 70 | var error : int = ProjectSettings.save() 71 | if not error == OK: 72 | push_error("Dev Tools - error %s while saving project settings." % error_string(error)) 73 | -------------------------------------------------------------------------------- /addons/easy_peasy_multiplayer/README.md: -------------------------------------------------------------------------------- 1 | # Easy Peasy Multiplayer 2 | ### Multiplayer in Godot has never been easier 3 | This plugin provides all of the backend tools you need to quickly start making a networked game in Godot! This plugin handles setting up MultiplayerPeers, lobby creation, network switching, and of course, connecting to servers. You will only need to: 4 | - Create a way to interface with the plugin (Or you can use a prebuilt UI that I have already created as a starting point [Here](https://github.com/Skeats/easy-peasy-multiplayer/tree/main/prefabs/ui/network_ui)) 5 | - Add Godot's [MultiplayerSpawners](https://docs.godotengine.org/en/stable/classes/class_multiplayerspawner.html) and [MultiplayerSynchronizers](https://docs.godotengine.org/en/stable/classes/class_multiplayersynchronizer.html#class-multiplayersynchronizer) (I will not cover that as it is a default Godot feature, however I have linked the documentation to each node.) 6 | 7 | This plugin also allows you to switch easily between Enet/traditional IP networking, as well as Steam networking powered by GodotSteam and SteamMultiplayerPeer (both are required dependencies). 8 | 9 | ## Required Dependencies 10 | These are all required for this plugin to work. 11 | - [GodotSteam](https://godotengine.org/asset-library/asset/2445) 12 | - [SteamMultiplayerPeer](https://godotengine.org/asset-library/asset/2258) 13 | 14 | ## Planned Features 15 | - Dedicated servers 16 | - Host Migration 17 | - More GodotSteam stuff (There is SO much to that plugin) 18 | - Steam User Authorization 19 | - Allowing the game to be run without steam in the case of cross-platform/non Steam builds 20 | 21 | ## Installation 22 | ### Option 1 (easiest): 23 | - Download the plugin through the Godot Asset Library [Here](). 24 | - Go to `Project > Project Settings > Plugins` and enable `Easy Peasy Multiplayer` 25 | ### Option 2: 26 | - Go to the [releases page](https://github.com/Skeats/easy-peasy-multiplayer/releases) and download the latest release. 27 | - Extract the zip file. 28 | - Take the `addons` folder and move it into your Godot project directory 29 | - Alternatively, if you already have addons installed, you can just move the folder named `easy_peasy_multiplayer` into your addons folder. 30 | - Go to `Project > Project Settings > Plugins` and enable `Easy Peasy Multiplayer` 31 | 32 | ## Getting Started 33 | For help getting started with this plugin, go to the wiki page [Here](https://github.com/Skeats/easy-peasy-multiplayer/wiki/Getting-Started). 34 | 35 | ## Additional Resources 36 | - [High Level Multiplayer | Godot](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html) 37 | - [Getting Started | GodotSteam](https://godotsteam.com/getting_started/introduction/) 38 | - [Remote Procedure Calls/RPCs | Godot](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html#remote-procedure-calls) 39 | -------------------------------------------------------------------------------- /addons/easy_peasy_multiplayer/steam_info.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # Steam Variables 4 | var is_on_steam_deck: bool = false 5 | var is_online: bool = false 6 | var is_owned: bool = false 7 | var is_family_shared: bool = false 8 | var is_free_weekend: bool = false 9 | var timed_trial_stats: Dictionary = {} 10 | var app_owner: int = 0 11 | var steam_id: int = 0 12 | var steam_username: String = "" 13 | 14 | ## DEPRECATED 15 | var steam_app_id: int = 480 16 | 17 | # User authentication variables [NOT IN USE] 18 | var auth_ticket: Dictionary ## Your auth ticket 19 | var client_auth_tickets: Array ## Array of tickets from other clients 20 | 21 | func _init() -> void: 22 | # Im going to leave these here just in case, but otherwise the appID should be changed in [ProjectSettings] 23 | #OS.set_environment("SteamAppId", str(steam_app_id)) 24 | #OS.set_environment("SteamGameId", str(steam_app_id)) 25 | pass 26 | 27 | func _ready() -> void: 28 | #Steam.get_auth_session_ticket_response.connect(_on_get_auth_session_ticket_response) 29 | #Steam.validate_auth_ticket_response.connect(_on_validate_auth_ticket_response) 30 | initialize_steam() 31 | 32 | func _process(_delta: float) -> void: 33 | Steam.run_callbacks() 34 | 35 | func initialize_steam() -> void: 36 | var initialize_response: Dictionary = Steam.steamInitEx() 37 | 38 | if initialize_response['status'] > 0: 39 | print("Failed to initialize Steam, shutting down: %s" % initialize_response) 40 | get_tree().quit() 41 | 42 | # Gather additional data 43 | is_on_steam_deck = Steam.isSteamRunningOnSteamDeck() 44 | is_online = Steam.loggedOn() 45 | is_owned = Steam.isSubscribed() 46 | is_family_shared = Steam.isSubscribedFromFamilySharing() 47 | is_free_weekend = Steam.isSubscribedFromFreeWeekend() 48 | timed_trial_stats = Steam.isTimedTrial() 49 | app_owner = Steam.getAppOwner() 50 | steam_id = Steam.getSteamID() 51 | steam_username = Steam.getPersonaName() 52 | auth_ticket = Steam.getAuthSessionTicket() 53 | 54 | if not is_owned or is_family_shared or is_free_weekend: 55 | print("User does not own this game") 56 | get_tree().quit() 57 | 58 | #region User Authentication [WIP, NOT FUNCTIONING] 59 | # https://godotsteam.com/tutorials/authentication/#__tabbed_1_2 60 | 61 | # Callback from getting the auth ticket from Steam 62 | func _on_get_auth_session_ticket_response(this_auth_ticket: int, result: int) -> void: 63 | print("Auth session result: %s" % result) 64 | print("Auth session ticket handle: %s" % this_auth_ticket) 65 | 66 | # Callback from attempting to validate the auth ticket 67 | func _on_validate_auth_ticket_response(auth_id: int, response: int, owner_id: int) -> void: 68 | print("Ticket Owner: %s" % auth_id) 69 | 70 | # Make the response more verbose, highly unnecessary but good for this example 71 | var verbose_response: String 72 | match response: 73 | 0: verbose_response = "Steam has verified the user is online, the ticket is valid and ticket has not been reused." 74 | 1: verbose_response = "The user in question is not connected to Steam." 75 | 2: verbose_response = "The user doesn't have a license for this App ID or the ticket has expired." 76 | 3: verbose_response = "The user is VAC banned for this game." 77 | 4: verbose_response = "The user account has logged in elsewhere and the session containing the game instance has been disconnected." 78 | 5: verbose_response = "VAC has been unable to perform anti-cheat checks on this user." 79 | 6: verbose_response = "The ticket has been canceled by the issuer." 80 | 7: verbose_response = "This ticket has already been used, it is not valid." 81 | 8: verbose_response = "This ticket is not from a user instance currently connected to steam." 82 | 9: verbose_response = "The user is banned for this game. The ban came via the Web API and not VAC." 83 | print("Auth response: %s" % verbose_response) 84 | print("Game owner ID: %s" % owner_id) 85 | 86 | func validate_auth_session(ticket: Dictionary, steam_id: int) -> void: 87 | var auth_response: int = Steam.beginAuthSession(ticket.buffer, ticket.size, steam_id) 88 | 89 | # Get a verbose response; unnecessary but useful in this example 90 | var verbose_response: String 91 | match auth_response: 92 | 0: verbose_response = "Ticket is valid for this game and this Steam ID." 93 | 1: verbose_response = "The ticket is invalid." 94 | 2: verbose_response = "A ticket has already been submitted for this Steam ID." 95 | 3: verbose_response = "Ticket is from an incompatible interface version." 96 | 4: verbose_response = "Ticket is not for this game." 97 | 5: verbose_response = "Ticket has expired." 98 | print("Auth verifcation response: %s" % verbose_response) 99 | 100 | if auth_response == 0: 101 | print("Validation successful, adding user to client_auth_tickets") 102 | client_auth_tickets.append({"id": steam_id, "ticket": ticket.id}) 103 | 104 | # You can now add the client to the game 105 | #endregion 106 | -------------------------------------------------------------------------------- /addons/easy_peasy_multiplayer/networking/network_steam.gd: -------------------------------------------------------------------------------- 1 | class_name NetworkSteam 2 | extends Node 3 | 4 | ## This is a network module for connecting to lobbies using Steam. It utilizes GodotSteam lobbies and SteamMultiplayerPeer for interfacing with the MultiplayerAPI 5 | 6 | ## I have no idea what this does I won't lie 7 | const PACKET_READ_LIMIT: int = 32 8 | 9 | ## The [MultiplayerPeer] for the Steam server. We can define this on initialization because this script should only run if we are going to be networking using Steam 10 | var peer : SteamMultiplayerPeer = SteamMultiplayerPeer.new() 11 | 12 | func _ready() -> void: 13 | Steam.lobby_created.connect(_on_lobby_created) 14 | Steam.lobby_joined.connect(_on_lobby_joined) 15 | #Steam.join_requested.connect(_on_lobby_join_requested) # I don't remember what this was for... 16 | Steam.lobby_match_list.connect(_on_lobby_match_list) 17 | 18 | #region Main Network Function 19 | 20 | ## Creates a game server as the host. Passing in a lobby type to [param lobby_type] allows you to change the lobby visibility 21 | func become_host(connection_info : Dictionary = { "steam_lobby_type" : Steam.LobbyType.LOBBY_TYPE_PUBLIC }): 22 | if Network.steam_lobby_id == 0: # Prevents you from creating a lobby if you are currently connected to a different lobby 23 | Steam.createLobby(connection_info.steam_lobby_type, Network.room_size) 24 | 25 | ## Joins a game server using the lobby id in [Network.steam_lobby_id] 26 | func join_as_client(): 27 | Steam.joinLobby(Network.steam_lobby_id) 28 | 29 | ## Lists lobbies using the Steam.addRequestLobby... functions. Call any of these filters before calling this function to refine the lobby search 30 | func list_lobbies(): 31 | Steam.addRequestLobbyListDistanceFilter(Steam.LOBBY_DISTANCE_FILTER_WORLDWIDE) 32 | 33 | if ProjectSettings.get_setting("steam/initialization/app_id", 0) == 480: 34 | Steam.addRequestLobbyListStringFilter("name", Network.steam_lobby_data["name"], Steam.LOBBY_COMPARISON_EQUAL) 35 | Steam.requestLobbyList() 36 | #endregion 37 | 38 | #region Steam Functions 39 | 40 | ## Checks for command line args that would tell the game to connect to a specific server on startup NOTE: This function is still Work-In-Progress. I have not had a chance to test this with an actual game as I do not have a Steam AppID of my own. 41 | func check_command_line() -> void: 42 | var command_args: Array = OS.get_cmdline_args() 43 | 44 | # There are arguments to process 45 | if command_args.size() > 0: 46 | 47 | # A Steam connection argument exists 48 | if command_args[0] == "+connect_lobby": 49 | 50 | # Lobby invite exists so try to connect to it 51 | if int(command_args[1]) > 0: 52 | 53 | # At this point, you'll probably want to change scenes 54 | # Something like a loading into lobby screen 55 | if Network._is_verbose: 56 | print("Command line lobby ID: %d" % command_args[1]) 57 | Network.steam_lobby_id = int(command_args[1]) 58 | Network.is_host = false 59 | 60 | #region Lobby/Host Startup 61 | 62 | ## Callback that runs when the [Steam] API finishes creating a lobby 63 | func _on_lobby_created(response : int, lobby_id : int): 64 | if response == Steam.CHAT_ROOM_ENTER_RESPONSE_SUCCESS: # On connected OK 65 | Network.steam_lobby_id = lobby_id 66 | print("Created lobby: %d" % lobby_id) 67 | 68 | Steam.setLobbyJoinable(Network.steam_lobby_id, true) 69 | 70 | for entry in Network.steam_lobby_data: 71 | Steam.setLobbyData(Network.steam_lobby_id, entry, Network.steam_lobby_data[entry]) 72 | 73 | _create_host() 74 | 75 | ## Creates a host, I guess. idk im tired I just finished documenting all of the [Network] class and this is a private function so I can come back to this later. 76 | func _create_host(): 77 | var error = peer.create_host(0) 78 | if error != OK: 79 | if Network._is_verbose: 80 | print("Error creating host: %s" %error_string(error)) 81 | return error 82 | 83 | multiplayer.multiplayer_peer = peer 84 | Network.connected_players[1] = Network.player_info 85 | Network.server_started.emit() 86 | Network.player_connected.emit(1, Network.player_info) 87 | Network.is_host = true 88 | if Network._is_verbose: 89 | print("Steam lobby hosted with id %d" % Network.steam_lobby_id) 90 | 91 | ## Callback function that runs when Steam responds to a [Network.list_lobbies] query with... a list of lobbies :O 92 | func _on_lobby_match_list(lobbies: Array) -> void: 93 | Network.lobbies_fetched.emit(lobbies) 94 | #endregion 95 | 96 | #region Lobby Joining 97 | ## I am not entirely clear on what this does, something to do with friend invites, but I can't test it because I don't have a Steam AppID 98 | #func _on_lobby_join_requested(this_lobby_id: int, friend_id: int) -> void: 99 | ## Get the lobby owner's name 100 | #var owner_name: String = Steam.getFriendPersonaName(friend_id) 101 | # 102 | #print("Joining %s's lobby..." % owner_name) 103 | # 104 | ## Attempt to join the lobby 105 | #join_as_client(this_lobby_id) 106 | 107 | ## Callback function that runs once Steam tells the client that it has either connected or failed to connect 108 | ## to the lobby 109 | func _on_lobby_joined(lobby_id : int, _permissions : int, _locked : bool, response : int): 110 | if response == Steam.CHAT_ROOM_ENTER_RESPONSE_SUCCESS: 111 | var id = Steam.getLobbyOwner(lobby_id) 112 | 113 | if id != Steam.getSteamID(): 114 | connect_socket(id) 115 | if Network._is_verbose: 116 | print("Connecting client to socket...") 117 | else: 118 | # Get the failure reason 119 | var FAIL_REASON : String 120 | match response: 121 | 2: FAIL_REASON = "This lobby no longer exists." 122 | 3: FAIL_REASON = "You don't have permission to join this lobby." 123 | 4: FAIL_REASON = "The lobby is now full." 124 | 5: FAIL_REASON = "Uh... something unexpected happened!" 125 | 6: FAIL_REASON = "You are banned from this lobby." 126 | 7: FAIL_REASON = "You cannot join due to having a limited account." 127 | 8: FAIL_REASON = "This lobby is locked or disabled." 128 | 9: FAIL_REASON = "This lobby is community locked." 129 | 10: FAIL_REASON = "A user in the lobby has blocked you from joining." 130 | 11: FAIL_REASON = "A user you have blocked is in the lobby." 131 | if FAIL_REASON: 132 | if Network._is_verbose: 133 | print("Steam lobby connection failed with error: %s" % FAIL_REASON) 134 | 135 | ## Creates a SteamMultiplayerPeer client 136 | func connect_socket(steam_id : int): 137 | var error = peer.create_client(steam_id, 0) 138 | if error: 139 | if Network._is_verbose: 140 | print("Error creating client: %s" % error_string(error)) 141 | return error 142 | 143 | if Network._is_verbose: 144 | print("Connecting peer to host...") 145 | multiplayer.multiplayer_peer = peer 146 | Network.is_host = false 147 | #endregion 148 | #endregion 149 | -------------------------------------------------------------------------------- /addons/easy_peasy_multiplayer/networking/network.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # These signals can be connected to by a UI lobby scene or the game scene. 4 | signal network_type_changed(network_type) ## Emitted when the [Network.active_network_type] is changed 5 | signal player_connected(peer_id, player_info) ## Emitted when a new player connects to the local client 6 | signal player_disconnected(peer_id) ## Emitted when a player disconnects from the local client 7 | signal server_disconnected ## Emitted when the client is forcefully disconnected from the server 8 | signal connection_fail ## Emitted when the local client fails to connect to the server 9 | signal player_ready ## Emitted when a player has readied or unreadied 10 | signal server_started ## Emitted when the server has been created 11 | signal lobbies_fetched(lobbies) ## Emitted when [Network.list_lobbies] has a response 12 | 13 | ## An enum for the network types that can be used 14 | enum MultiplayerNetworkType { DISABLED, ENET, STEAM } 15 | 16 | ## The currently active network type 17 | var active_network_type : MultiplayerNetworkType = MultiplayerNetworkType.DISABLED : 18 | set(value): 19 | active_network_type = value 20 | network_type_changed.emit(active_network_type) 21 | 22 | ## The physical node for the active network, which is what makes using multiple networks so easy 23 | var active_network : Node 24 | 25 | # General Variables 26 | ## The player info that the local client will send to other clients on connection to a server 27 | var player_info = { 28 | "name": "Name" 29 | } 30 | 31 | ## The number of players that can connect to the server. 32 | var room_size: int = 4 33 | 34 | ## Whether the local client is the host of a server or not. 35 | var is_host : bool 36 | 37 | ## A [Dictionary] containing all of the currently connected players, their network ids, and any info defined in [Network.player_info] 38 | var connected_players = {} 39 | 40 | ## An array containing network ids of all the players that are ready 41 | var players_ready : Array[int] 42 | 43 | ## The ip address that the network manager should use to try and connect to a server. Used for Enet only by default 44 | var ip_address : String = "127.0.0.1" # IPv4 localhost 45 | 46 | # Steam Variables 47 | ## The lobby data that a Steam lobby should be created with. 48 | var steam_lobby_data = { 49 | "name": "MOVEMENTSHOOTER_TEST_LOBBY", 50 | "game": "DEFAULTSCENE" 51 | } 52 | 53 | ## The lobby id of the Steam lobby 54 | var steam_lobby_id: int = 0 55 | 56 | ## Whether the network manager should print its actions to standard output 57 | var _is_verbose: bool = false 58 | 59 | func _ready(): 60 | _update_settings() 61 | 62 | # So many signals :O 63 | multiplayer.peer_connected.connect(_on_player_connected) 64 | multiplayer.peer_disconnected.connect(_on_player_disconnected) 65 | multiplayer.connected_to_server.connect(_on_connected_ok) 66 | multiplayer.connection_failed.connect(_on_connected_fail) 67 | multiplayer.server_disconnected.connect(_on_server_disconnected) 68 | 69 | # Sets the default username to the users Steam name, or if that doesnt exist, the OS name 70 | if SteamInfo.steam_username: 71 | player_info["name"] = SteamInfo.steam_username 72 | elif OS.has_environment("USERNAME"): 73 | player_info["name"] = OS.get_environment("USERNAME") 74 | else: 75 | var desktop_path := OS.get_system_dir(OS.SYSTEM_DIR_DESKTOP).replace("\\", "/").split("/") 76 | player_info["name"] = desktop_path[desktop_path.size() - 2] 77 | 78 | # This is specifically for interfacing with my custom dev tools, which can be found here: https://godotengine.org/asset-library/asset/4028) 79 | #DevTools.create_command("set_network", dev_set_network, "Sets the network type. Call without arguments to list available networks") 80 | #DevTools.create_command("host_lobby", dev_host_lobby, "Hosts a lobby using the current adtive network") 81 | #DevTools.create_command("connect", dev_join_lobby, "Connects to the given lobby") 82 | #DevTools.create_command("disconnect", dev_disconnect, "Disconnnects from the current lobby") 83 | 84 | ## These are designed for my dev tools, but they should be usable in code and in other console plugins, you just might need to adjust the arguments. 85 | #region Dev Commands 86 | ## Sets the current network 87 | func dev_set_network(network: String): 88 | if network: 89 | match network: 90 | "Steam": 91 | active_network_type = MultiplayerNetworkType.STEAM 92 | "Enet": 93 | active_network_type = MultiplayerNetworkType.ENET 94 | "None": 95 | active_network_type = MultiplayerNetworkType.DISABLED 96 | _: 97 | return "[color=red]ERROR: No network type named " + network + "[/color]" 98 | 99 | _build_multiplayer_network(true) 100 | return "Network type changed to " + network 101 | else: 102 | return "Available Arguments: [color=green]\nSteam\nEnet\nNone[/color]" 103 | 104 | ## Hosts a lobby using the currently selected network type 105 | func dev_host_lobby(): 106 | become_host() 107 | 108 | ## Joins a lobby using the argument passed in as either a Steam lobbyID or an IP address, depending on the type of network used 109 | func dev_join_lobby(connector: String): 110 | if active_network_type == MultiplayerNetworkType.STEAM: 111 | steam_lobby_id = connector.to_int() 112 | else: 113 | if connector: 114 | ip_address =connector 115 | 116 | join_as_client() 117 | 118 | ## Disconnects from a lobby, if connected 119 | func dev_disconnect(): 120 | disconnect_from_server() 121 | #endregion 122 | 123 | ## This is for updating the values from the [ProjectSettings] 124 | func _update_settings() -> void: 125 | if ProjectSettings.has_setting("easy_peasy_multiplayer/general/verbose_network_logging"): 126 | _is_verbose = ProjectSettings.get_setting("easy_peasy_multiplayer/general/verbose_network_logging", false) 127 | 128 | #region Private Network Setup Functions 129 | ## Sets the active network to the active network type 130 | func _build_multiplayer_network(destroy_previous_network : bool = false): 131 | if not active_network or destroy_previous_network: 132 | match active_network_type: 133 | MultiplayerNetworkType.ENET: 134 | if _is_verbose: 135 | print("Setting network type to ENet") 136 | _set_active_network(NetworkEnet) 137 | MultiplayerNetworkType.STEAM: 138 | if _is_verbose: 139 | print("Setting network type to Steam") 140 | _set_active_network(NetworkSteam) 141 | MultiplayerNetworkType.DISABLED: 142 | if _is_verbose: 143 | print("Disabled networking") 144 | _remove_active_network() 145 | _: 146 | push_warning("No match for network type") 147 | 148 | ## Builds a network scene based on the passed parameters 149 | func _set_active_network(new_network_type : Object): 150 | _remove_active_network() 151 | active_network = new_network_type.new() 152 | add_child(active_network, true) 153 | 154 | ## Removes the current active network, if one exists 155 | func _remove_active_network(): 156 | if is_instance_valid(active_network): 157 | active_network.queue_free() 158 | #endregion 159 | 160 | #region Network-Specific Functions 161 | 162 | ## Creates a new server using the currently selected [Steam.active_network_type]. Additional information regarding the connection can be passed through [param connection_info]. For [Network.MultiplayerNetworkType.STEAM] 163 | func become_host(connection_info : Dictionary = { 164 | "steam_lobby_type" : Steam.LobbyType.LOBBY_TYPE_PUBLIC, 165 | "port" : NetworkEnet.DEFAULT_PORT 166 | }): 167 | _build_multiplayer_network() 168 | if active_network_type != MultiplayerNetworkType.DISABLED: 169 | active_network.become_host(connection_info) 170 | 171 | 172 | ## Joins a lobby as a client using either the [Network.ip_address] or [Network.steam_lobby_id], depending on the current [Network.active_network_type] 173 | func join_as_client(): 174 | _build_multiplayer_network() 175 | if active_network_type != MultiplayerNetworkType.DISABLED: 176 | active_network.join_as_client() 177 | 178 | ## Disconnects the current peer from any connected servers. A [enum Network.MultiplayerNetworkType] can optionally be passed to set the network type to use after disconnecting, which can be useful for instances like going back to the lobby browser after leaving a server. 179 | func disconnect_from_server(network_type : MultiplayerNetworkType = MultiplayerNetworkType.DISABLED): 180 | # This expression may not be necessary 181 | if steam_lobby_id != 0: 182 | Steam.leaveLobby(steam_lobby_id) 183 | 184 | active_network_type = network_type 185 | multiplayer.multiplayer_peer = null 186 | connected_players.clear() 187 | steam_lobby_id = 0 188 | is_host = false 189 | _build_multiplayer_network(true) 190 | 191 | ## Lists any lobbies that the current [Network.active_network_type] can find. NOTE: This function does nothing when using Enet as the network type, as there is no lobby system when using Enet. 192 | func list_lobbies(): 193 | _build_multiplayer_network() 194 | if active_network_type != MultiplayerNetworkType.DISABLED: 195 | active_network.list_lobbies() 196 | #endregion 197 | 198 | #region MultiplayerAPI Signals 199 | 200 | ## Callback function that runs whenever a new player connects to the local client (Not necessarily the server in general. This was a misconception I had which confused me). This function will send the new player the current client's information, so that the connecting player will be aware of the local client. 201 | func _on_player_connected(id : int): 202 | _register_player.rpc_id(id, player_info) 203 | 204 | ## Callback function that runs whenever a player disconnects from the server. This updates the the player lists on all clients that are still connected. 205 | func _on_player_disconnected(id : int): 206 | connected_players.erase(id) 207 | players_ready.erase(id) 208 | player_disconnected.emit(id) 209 | 210 | ## Callback function that runs when this client successfully connects to a server. Also emits the [signal player_connected] signal... I don't exactly get what this does 211 | func _on_connected_ok(): 212 | var peer_id = multiplayer.get_unique_id() 213 | connected_players[peer_id] = player_info 214 | if _is_verbose: 215 | print("[%s]: Joined server" % peer_id) 216 | player_connected.emit(peer_id, player_info) 217 | 218 | ## Callback function that runs on the local client when it fails to connect to a server. 219 | func _on_connected_fail(): 220 | disconnect_from_server() 221 | connection_fail.emit() 222 | 223 | ## Callback function that runs on the local client when it is disconnected from the server. This occurs when you are kicked, the server shuts down, or the local client is otherwise forcefully removed from the server. 224 | func _on_server_disconnected(): 225 | disconnect_from_server() 226 | server_disconnected.emit() 227 | if _is_verbose: 228 | print("Disconnected from server") 229 | #endregion 230 | 231 | #region Ready RPCs 232 | ## This rpc can be called on any client, and should be passed to the server to register that player's ready state. The local client's ready state should be passed into [param toggled_on] so that the server knows what ready state the client is on. 233 | ## 234 | ## [br][br] 235 | ## 236 | ## Example code to run on clients when they ready: [code] ready_state.rpc_id(1, is_ready) [/code] 237 | @rpc("any_peer", "call_local", "reliable") 238 | func ready_state(toggled_on : bool): 239 | if multiplayer.is_server(): 240 | var sender_id = multiplayer.get_remote_sender_id() # This function is like magic to me but it's so convenient 241 | 242 | # Keeps track of who has readied so that people can only ready once (I wonder why this exists :P) 243 | if toggled_on and !players_ready.has(sender_id): 244 | players_ready.append(sender_id) 245 | elif !toggled_on: 246 | players_ready.erase(sender_id) 247 | 248 | propagate_ready_states.rpc(players_ready) # Updates the ready states on all clients 249 | 250 | ## This rpc should only be called on the host, sending the ready states to all of the players. This sort of rpc is server-authoratative, which is more secure than clients having authority, so long as the host is not malicious (it would be most secure when the server is hosted seperately from any of the players, but that also means having to maintain dedicated servers, which I most definitely do not have the money for, so I have not thought about implementing it). 251 | ## 252 | ## [br][br] 253 | ## 254 | ## Example code for the host sending ready states to clients: [code] propagate_ready_states.rpc(ready_states) [/code] 255 | @rpc("authority", "call_local", "reliable") 256 | func propagate_ready_states(server_ready_states : Array[int]): 257 | players_ready = server_ready_states 258 | player_ready.emit() 259 | #endregion 260 | 261 | ## This rpc is called during [_on_player_connected] and is sent from local clients to a newly connected client, as well as vice versa. This essentially initiates a handshake between the player that just connected to the server and all other players. 262 | @rpc("any_peer", "reliable") 263 | func _register_player(new_player_info : Dictionary): 264 | var new_player_id = multiplayer.get_remote_sender_id() 265 | connected_players[new_player_id] = new_player_info 266 | if multiplayer.is_server(): 267 | propagate_ready_states.rpc_id(new_player_id, players_ready) 268 | player_connected.emit(new_player_id, new_player_info) 269 | --------------------------------------------------------------------------------