The Godot Barn
Sign in
Sign in
Home
News & updates
Explore
Articles
Snippets
Shaders
Themes
Submit content
Sign in to submit content
Give or get help
Tutorials
Questions
Conversations
Coming soon!
GodotSteam Tutorials - Lobbies
0
Description
"One of the more requested tutorials is multiplayer lobbies and P2P networking through Steam; this tutorial specifically covers the lobby portion and any of our networking tutorials should cover the other half. This is somewhat based on the [GodotSteamKit lobby starter kit](https:\/\/godotsteam.com\/projects\/godotsteamkit).\r\n\r\nI'd also like to suggest you [check out the Additional Resources section of this tutorial](#additional-resources) before continuing on.\r\n\r\n??? guide \"Relevant GodotSteam classes and functions\"\r\n* [Matchmaking class](..\/classes\/matchmaking.md)\r\n\t* [addRequestLobbyListFilterSlotsAvailable()](..\/classes\/matchmaking.md#addrequestlobbylistfilterslotsavailable)\r\n\t* [addRequestLobbyListNearValueFilter()](..\/classes\/matchmaking.md#addrequestlobbylistnearvaluefilter)\r\n\t* [addRequestLobbyListNumericalFilter()](..\/classes\/matchmaking.md#addrequestlobbylistnumericalfilter)\r\n\t* [addRequestLobbyListResultCountFilter()](..\/classes\/matchmaking.md#addrequestlobbylistresultcountfilter)\r\n\t* [addRequestLobbyListStringFilter()](..\/classes\/matchmaking.md#addrequestlobbyliststringfilter)\r\n\t* [createLobby()](..\/classes\/matchmaking.md#createlobby)\r\n\t* [getLobbyData()](..\/classes\/matchmaking.md#getlobbydata)\r\n\t* [getLobbyMemberByIndex()](..\/classes\/matchmaking.md#getlobbymemberbyindex)\r\n\t* [getNumLobbyMembers()](..\/classes\/matchmaking.md#getnumlobbymembers)\r\n\t* [joinLobby()](..\/classes\/matchmaking.md#joinlobby)\r\n\t* [leaveLobby()](..\/classes\/matchmaking.md#leavelobby)\r\n\t* [requestLobbyList()](..\/classes\/matchmaking.md#requestlobbylist)\r\n\t* [sendLobbyChatMsg()](..\/classes\/matchmaking.md#sendlobbychatmsg)\r\n\t* [setLobbyData()](..\/classes\/matchmaking.md#setlobbydata)\r\n\t* [setLobbyJoinable()](..\/classes\/matchmaking.md#setlobbyjoinable)\r\n* [Friends class](..\/classes\/friends.md)\r\n\t* [getFriendPersonaName()](..\/classes\/friends.md#getfriendpersonaname)\r\n???\r\n\r\n!!! warning Note\r\nYou may want to [double-check our Initialization tutorial](https:\/\/godotsteam.com\/tutorials\/initializing) to set up initialization and callbacks functionality if you haven't done so already.\r\n!!!\r\n\r\n## Preparations{.block}\r\n\r\nWe will want to keep track of our lobby ID if we keep the lobby going during matches \/ games. This can be a good way of using the lobby's chat system and also keeping track of when players join or leave mid-game. You can set the lobby to prevent people from joining after the match \/ game starts if you want too; [which we will cover in the Modifying Lobbies section](#modifying-lobbies). In our **steamworks.gd** file (or whatever global Steam file you are using), you will want to simply add a variable to track it:\r\n\r\n```gdscript\r\nvar lobby_id: int = 0\r\n\r\n# Used to track a lobby ID from Steam invites or joining via Friends List\r\nvar invite_lobby_id: int = 0\r\n```\r\n\r\nThe **invite_lobby_id** is optional but helps with other methods of joining lobbies as noted above.\r\n\r\nIf you plan on closing the lobby after the match \/ game starts, then this **lobby_id** can actually just be kept in your **lobby manager** scene's script as it will not be needed anywhere else. However, you will still probably want to keep the **invite_lobby_id** globally.\r\n\r\n### Lobby Manager\r\n\r\nFor this tutorial, we will want to break this system up into three scenes: host, lobby list \/ join, and the lobby itself. These are best controlled by a lobby manager scene so the player can switch between them and also helps with player's joining from a Steam invite without the game already running.\r\n\r\n\r\n\r\nWe will hook up the **lobby_joined** signal here as this will swap our host or join scene to the actual lobby scene when a lobby is created or selected from our lobby list.\r\n\r\n```gdscript\r\nfunc _ready() -> void:\r\n\tSteam.lobby_joined.connect(\"_on_lobby_joined\")\r\n```\r\n\r\nWe will come back to this in our [**Into The Lobby** section](#into-the-lobby).\r\n\r\n## Hosting A Lobby{.block}\r\n\r\nFirst we will set up our lobby host scene. Here you see we give the host options on maximum number of players, the lobby visibility, and setting different lobby data \"tags\" so players searching for lobbies can filter for them. This last one is more useful if you are testing on app ID 480 since there are _tons_ of other test and junk lobbies.\r\n\r\n\r\n\r\nBefore we get into that, we will connect the only Steam signal \/ callback we need for hosting and make some shortcuts for our settings fields:\r\n\r\n```gdscript\r\n@onready var create: Button = %Create\r\n@onready var lobby_data: LineEdit = %LobbyData\r\n@onready var max_players: SpinBox = %MaxPlayers\r\n@onready var visibility: OptionButton = %Visibility\r\n\r\n\r\nfunc _ready() -> void:\r\n\tcreate.pressed.connect(_on_create_pressed)\r\n\tSteam.lobby_created.connect(_on_lobby_created)\r\n```\r\n\r\nBefore putting together our **\\_on_create_pressed** and **\\_on_lobby_created** functions, we will sort out the three host settings that lead us there.\r\n\r\n### Maximum Players\r\n\r\nThe maximum players is just a **SpinBox** we will name MaxPlayers. The maximum number of players per lobby is 250 so you may want to cap this appropriately. The node does not need to be connected to anything since we will grab the value during lobby creation.\r\n\r\n### Lobby Visibility\r\n\r\nOur lobby visibility is an OptionButton that will have four available options: private, friends only, public, and invisible. You can read more about what those do in the [Steam LobbyType enums section.](https:\/\/godotsteam.com\/classes\/matchmaking\/#lobbytype) Much like our maximum players value, we will get it when we actually create the lobby.\r\n\r\n### Lobby Data\r\n\r\nThe lobby data is just a LineEdit that must contain a common-separate list of key:value pairs which our code will translate. And, you guessed it, we will just get these later on.\r\n\r\nThis data can be queried at any point and used to store all manner of information. As mentioned at the beginning of this section, you can use this to set a search term for testing when using app ID 480 which will often be flooded with lots of other lobbies that are not yours. For example, you could set the tag as Game:My Cool Game and then users can search for that key:value pair specifically to return only your lobby in the lobby list.\r\n\r\n### Creating The Lobby\r\n\r\nNow we will take all those options and write our function that is triggered by the **Create** button from earlier:\r\n\r\n```gdscript\r\nfunc _on_create_pressed() -> void:\r\n\tvar lobby_type: Steam.LobbyType = visibility.selected as Steam.LobbyType\r\n\tSteam.createLobby(lobby_type, int(max_players.value))\r\n```\r\n\r\nWe will use the **selected** property from our OptionButton and cast it as a **Steam.LobbyType** then pass that and the maximum players **value**, cast as an integer, to our **createLobby()** function. This in turn will trigger our **lobby_created** callback:\r\n\r\n```gdscript\r\nfunc _on_lobby_created(connect_status: Steam.Result, lobby_id: int) -> void:\r\n\tif connect_status == Steam.Result.RESULT_OK:\r\n\t\t# Set the lobby ID in our global script (if using one)\r\n\t\tprint(\"Successfully created lobby %s\" % lobby_id)\r\n\t\tSteamworks.lobby_id = lobby_id\r\n\t\t\r\n\t\t# Set the lobby name for displaying in searches, the lobby scene, etc.\r\n\t\tif not Steam.setLobbyData(Steamworks.lobby_id, \"lobby_name\", \"My Cool Lobby\"):\r\n\t\t\tprinterr(\"Failed to set lobby name\")\r\n\t\t\r\n\t\t# We will grab and set all lobby data tags\r\n\t\t# These must have been both comma-separated and in key:value pairs\r\n\t\tvar data_sets: PackedStringArray = lobby_data.text.split(\",\", false)\r\n\t\tfor this_data in data_sets:\r\n\t\t\tvar data_key_value: PackedStringArray = this_data.split(\":\", false, 1)\r\n\t\t\tif data_key_value.size() == 2:\r\n\t\t\t\tif not Steam.setLobbyData(Steamworks.lobby_id, data_key_value[0], data_key_value[1]):\r\n\t\t\t\t\tprinterr(\"Failed to set lobby %s data [%s : %s]\" % [Steamworks.lobby_id, data_key_value[0], data_key_value[1]])\r\n\r\n\t# Our lobby creation failed\r\n\telse:\r\n\t\tprinterr(\"Failed to create a lobby: %s\" % connect_status)\r\n```\r\n\r\nA second Steam callback should trigger if the lobby is successfully created, which would be **lobby_joined**; meaning the lobby is created **and** the host successfully joined it.\r\n\r\nObviously, you can set up whatever naming scheme you want for lobbies above. I usually name it after the host and append some kind of randomized noun like: lobby, game, etc.\r\n\r\n## Joining Lobbies{.block}\r\n\r\nNow that we can create lobbies, it is time to query and pull a list of lobbies so we can join one. As you can see below, we have a panel with two main buttons: filters and refresh.\r\n\r\n\r\n\r\nIn our script we will attach some signals and buttons used for for refreshing the list of lobbies, setting filters to get the lobbies we want, and joining one in particular:\r\n\r\n```gdscript\r\n# A scene for displaying and joining a returned lobby. The location will vary on your setup.\r\nconst LOBBY_ENTRY = preload(\"\")\r\n\r\n@onready var _distance: OptionButton = %Distance\r\n@onready var _filters: Button = %Filters\r\n@onready var _lobby_filters: Control = %LobbyFilters\r\n@onready var _max_lobbies: SpinBox = %MaxLobbies\r\n@onready var _open_slots: SpinBox = %OpenSlots\r\n@onready var _refresh: Button = %Refresh\r\n@onready var _search_terms: LineEdit = %SearchTerms\r\n\r\n\r\nfunc _ready() -> void:\r\n\t_close_filters.pressed.connect(_on_close_filters_pressed)\r\n\t_filters.pressed.connect(_on_filters_pressed)\r\n\t_refresh.pressed.connect(_on_refresh_pressed)\r\n\tSteam.lobby_match_list.connect(_on_lobby_match_list)\r\n```\r\n\r\n### Search Filters\r\n\r\nRight now, nothing will show up until we press **Refresh** but we should set some filters first. Clicking on our **Filters** button will bring up the below panel where we can toggle: search distance, open player slots, max lobbies returned, and search terms which should link to our lobby data tags from our earlier hosting section.\r\n\r\n\r\n\r\nMuch like our host settings, these fields have no connected signals and get queries when we press **Refresh**.\r\n\r\n#### Search Distance\r\n\r\nThis will effect how far from us to pull available lobbies; the options are [the Steam lobby distance filter enums](https:\/\/godotsteam.com\/classes\/matchmaking\/#lobbydistancefilter): close, default, far, and worldwide.\r\n\r\n#### Open Player Slots\r\n\r\nThis will look for lobbies with the specified amount of open slots available, in case you are looking for space for you and some friends.\r\n\r\n#### Max Lobbies Returned\r\n\r\nThe current maximum is 50 lobbies but you can narrow that down with this option.\r\n\r\n#### Search Terms\r\n\r\nThis is the matching side of our hosts lobby data tags from the previous section. Much like that one, this must be a comma-separated list of key:value pairs. Best for finding specific lobbies in a sea of possibilities.\r\n\r\n#### Related Signals\r\n\r\nJust so we do not forget them, our button signals **\\_on_close_filters_pressed()** and **\\_on_filters_pressed()** literally just control the visbility of our filters panel:\r\n\r\n```gdscript\r\nfunc _on_close_filters_pressed() -> void:\r\n\t_lobby_filters.visible = false\r\n\r\n\r\nfunc _on_filters_pressed() -> void:\r\n\t_lobby_filters.visible = true\r\n```\r\n\r\nThey could probably also just be merged into one function with a boolean toggle.\r\n\r\n### Refresh The List\r\n\r\nNow that we have some filters set, we can press the **Refresh** button and get some results. Below you have our **\\_on_refresh_pressed()** function which also calls a **\\_add_request_lobby_filters()** function to process all our search options:\r\n\r\n```gdscript\r\nfunc _on_refresh_pressed() -> void:\r\n\t_add_request_lobby_filters()\r\n\tSteam.requestLobbyList()\r\n\r\n\r\nfunc _add_request_lobby_filters() -> void:\r\n\tSteam.addRequestLobbyListDistanceFilter(_distance.selected as Steam.LobbyDistanceFilter)\r\n\tSteam.addRequestLobbyListFilterSlotsAvailable(_open_slots.value)\r\n\tSteam.addRequestLobbyListResultCountFilter(_max_lobbies.value)\r\n\r\n\tvar these_terms: PackedStringArray = _search_terms.text.split(\",\", false)\r\n\tif these_terms.size() > 0:\r\n\t\tfor this_term in these_terms:\r\n\t\t\tvar data_key_value: PackedStringArray = this_term.split(\":\", false, 1)\r\n\t\t\tif data_key_value.size() == 2:\r\n\t\t\t\tif data_key_value[0].length() > Steam.MAX_LOBBY_KEY_LENGTH:\r\n\t\t\t\t\tprinterr(\"Invalid term passed, too long: %s\" % this_term)\r\n\t\t\t\t\treturn\r\n\t\t\t\tSteam.addRequestLobbyListStringFilter(data_key_value[0], data_key_value[1], Steam.LOBBY_COMPARISON_EQUAL)\r\n```\r\n\r\nAs you can see, it queries each of our fields and applies the values to the right Steam filters. There are two extra filters we do not use but you may find handy in your game:\r\n\r\n- [addRequestLobbyListNearValueFilter](https:\/\/godotsteam.com\/classes\/matchmaking#addrequestlobbylistnearvaluefilter)\r\n- [addRequestLobbyListNumericalFilter](https:\/\/godotsteam.com\/classes\/matchmaking#addrequestlobbylistnumericalfilter)\r\n\r\nAfter all that is processed our call to Steam's **requestLobbyList()** function will get us a list of lobbies by way of a **lobby_match_list** callback which triggers our **\\_on_lobby_match_list()** function:\r\n\r\n```gdscript\r\nfunc _on_lobby_match_list(these_lobbies: Array) -> void:\r\n\tif these_lobbies.size() == 0:\r\n\t\tprint(\"No lobbies were found\")\r\n\t\treturn\r\n\r\n\tfor this_lobby in these_lobbies:\r\n\t\tvar lobby_object := LOBBY_ENTRY.instantiate()\r\n\t\tlobby_object.name = \"Lobby%s\" % this_lobby\r\n\t\tlobby_object.set_lobby_id(this_lobby)\r\n\t\tlobby_object.joining_lobby.connect(_on_joining_lobby)\r\n\t\t_lobby_list.call_deferred(\"add_child\", lobby_object)\r\n```\r\n\r\nThis function will instantiate our **LOBBY_ENTRY** scene and add it to our **LobbyList** VBoxContainer for each lobby. You will notice it passes the lobby ID to it as well which we use to get more data and joining said lobby.\r\n\r\n#### Lobby Entries\r\n\r\nEach lobby in our now-refreshed list is just a tiny instanced scene as you can see below. Pretty much just a label and button.\r\n\r\n\r\n\r\nAs mentioned in the section above, the only data passed to this scene is the lobby ID which we use to get the lobby's name we set in the hosting section and use for the join signal we pass when the player clicks it.\r\n\r\n```gdscript\r\nsignal joining_lobby\r\n\r\nvar lobby_id: int = 0 : set = set_lobby_id\r\nvar lobby_name: String = \"\" : set = set_lobby_name\r\n\r\n@onready var join: Button = %Join\r\n@onready var name_label: Label = %Name\r\n\r\n\r\nfunc _ready() -> void:\r\n\t_connect_signals()\r\n\r\n\r\n#region Signals\r\nfunc _connect_signals() -> void:\r\n\tjoin.pressed.connect(_on_pressed)\r\n\r\n\r\nfunc _on_pressed() -> void:\r\n\tjoining_lobby.emit(lobby_id)\r\n\r\n\r\n# The setter for lobby ID which attempts to get the lobby name when set.\r\nfunc set_lobby_id(new_lobby_id: int) -> void:\r\n\tlobby_id = new_lobby_id\r\n\tlobby_name = Steam.getLobbyData(lobby_id, \"lobby_name\")\r\n\r\n\r\n# The setter for lobby name which will default to just the lobby ID if no valid\r\n# name is passed to it.\r\nfunc set_lobby_name(new_name: String) -> void:\r\n\tlobby_name = new_name\r\n\tif not is_node_ready(): await ready\r\n\tname_label.text = \"Lobby %s\" % lobby_id if lobby_name.is_empty() else lobby_name\r\n#endregion\r\n```\r\n\r\nOur tiny script will grab the lobby's name with **getLobbyData()** and then display it. You can change that to be whatever fits your use-case.\r\n\r\nClicking the **Join** button on any of these instanced lobby scenes will now trigger our **\\_on_joining_lobby()** function back in our lobby list scene:\r\n\r\n```gdscript\r\nfunc _on_joining_lobby(lobby_id: int) -> void:\r\n\tprint(\"Attempting to join lobby %s from the lobby list\" % lobby_id)\r\n\tSteam.joinLobby(lobby_id)\r\n```\r\n\r\nWe should then receive a **lobby_joined** callback in our lobby manager scene and finally move on to the lobby itself. First, we will talk about two other methods for joining lobbies.\r\n\r\n### Join Without The Lobby List\r\n\r\nThere are situations where the player will join a lobby without selecting one from the lobby list: accepting a Steam invite or joining a friend from the friends list, with or without the game running.\r\n\r\n#### From Invite With Game Running\r\n\r\nIf the player is already in-game and accepts a Steam invite or right-clicks on a friend in their friends list then selects **Join Game** or **Join Lobby** from there, it will trigger the **join_requested** callback. This function will handle that:\r\n\r\n```gdscript\r\nfunc _ready() -> void:\r\n\tSteam.join_requested.connect(_on_lobby_join_requested)\r\n\r\n\r\nfunc _on_lobby_join_requested(lobby_id: int, friend_id: int) -> void:\r\n\t# Get name of the friend who invited the player or who they joined via the friends list\r\n\tvar friend_joining: String = Steam.getFriendPersonaName(friend_id)\r\n\r\n\tprint(\"Joining lobby with...\" % friend_joining)\r\n\r\n\tSteam.joinLobby(lobby_id)\r\n\r\n\t# Alternatively, change scenes to the lobby manager and set this lobby ID in our global\r\n\t# script and remove the above call to Steam.joinLobby()\r\n\tSteamworks.invite_lobby_id = lobby_id\r\n```\r\n\r\nSince this can happen at any time, this should probably be in a global script like our **steamworks.gd** or some networking script.\r\n\r\n#### From Invite Without Game Running\r\n\r\nIf a player **does not have the game running** and accepts a Steam invite or joins a lobby by right-clicking on a friend's name then selecting **Join Game** or **Join Lobby**, Steam will launch the game with an additional command line attached which will look something like: `+connect_lobby
`. So we will want to check for this when the game boots up.\r\n\r\nDepending on how your game starts, you will want to add this **check_command_line()** function in so it triggers after all the important start-up processes but before your menu scene since you will actually want to direct the player to your lobby scene itself.\r\n\r\n```gdscript\r\nfunc check_command_line() -> void:\r\n\tvar command_line_args: Array = OS.get_cmdline_args()\r\n\tif these_arguments.size() == 0:\r\n\t\treturn\r\n\t\r\n\tprint(\"Command line arguments: %s\" % [command_line_args])\r\n\tif these_arguments[0] != \"+connect_lobby\":\r\n\t\treturn\r\n\r\n\tif int(these_arguments[1]) > 0:\r\n\t\tSteam.joinLobby(int(these_arguments[1]))\r\n\r\n\t\t# Alternatively, change scenes to the lobby manager and set this lobby ID in our global\r\n\t\t# script and remove the above call to Steam.joinLobby()\r\n\t\tSteamworks.invite_lobby_id = lobby_id\r\n```\r\n\r\n#### Getting To The Lobby\r\n\r\nIn both cases above, you will want to direct the player to your **lobby manager** as it has the function for the **lobby_joined** callback we will get from calling **joinLobby**. This could actually be handled a few different ways but, for this tutorial, we will add a bit to our global script and **lobby manager** script.\r\n\r\nReplace the **joinLobby** calls in **\\_on_lobby_join_requested** and **check_command_line** with a scene change to the **lobby manager** after passing the lobby ID of the invite to our **steamworks.gd** global to store it. Then we can have the **lobby manager** check for this ID after it loads and just join it:\r\n\r\n```gdscript\r\n# Add this variable to our global script to use in the lobby manager\r\nvar invite_lobby_id: int = 0\r\n\r\n\r\n# Add this to our lobby manager script\r\nfunc _ready() -> void:\r\n\tif Steamworks.invite_lobby_id > 0:\r\n\t\tSteam.joinLobby(Steamworks.invite_lobby_id)\r\n\t\t# Make sure to reset this lobby ID\r\n\t\tSteamworks.invite_lobby_id = 0\r\n```\r\n\r\nAlternatively, you can set the appropriate scene name in your Steamworks launch options in the Steamworks back-end. You would want to add the full scene path (res:\/\/your-lobby-manager-scene.tscn) on the **Arguments** line in your launch option. However, this only works when we join while not having the game running. Big thanks to **Antokolos** for providing this example.\r\n\r\nNow we should be able to join lobbies from any available method.\r\n\r\n### Into The Lobby\r\n\r\nRegardless of which option to join is used, back in our [**lobby manager** scene](#lobby-manager) we will receive a **lobby_joined** callback from Steam. This will trigger our **\\_on_lobby_joined()** function:\r\n\r\n```gdscript\r\nfunc _on_lobby_joined(lobby_id: int, _permissions: int, _locked: int, response: Steam.ChatRoomEnterResponse) -> void:\r\n\tif response == Steam.ChatRoomEnterResponse.CHAT_ROOM_ENTER_RESPONSE_SUCCESS:\r\n\t\tprint(\"Lobby %s joined successfully\" % lobby_id)\r\n\t\t# Passed to our Steam global or if not needing to store the lobby ID, just saved locally in this script\r\n\t\t# _lobby_id = lobby_id\r\n\t\tSteamworks.lobby_id = lobby_id\r\n\r\n\t\t# Swap to the lobby scene here\r\n\r\n\telse:\r\n\t\tmatch response:\r\n\t\t\tSteam.ChatRoomEnterResponse.CHAT_ROOM_ENTER_RESPONSE_DOESNT_EXIST:\r\n\t\t\t\tprinterr(\"Failed joining lobby %s, this lobby no longer exists.\")\r\n\t\t\tSteam.ChatRoomEnterResponse.CHAT_ROOM_ENTER_RESPONSE_NOT_ALLOWED:\r\n\t\t\t\tprinterr(\"Failed joining lobby %s, you don't have permission to join this Lobbies.\")\r\n\t\t\tSteam.ChatRoomEnterResponse.CHAT_ROOM_ENTER_RESPONSE_FULL:\r\n\t\t\t\tprinterr(\"Failed joining lobby %s, the lobby is now full.\")\r\n\t\t\tSteam.ChatRoomEnterResponse.CHAT_ROOM_ENTER_RESPONSE_ERROR:\r\n\t\t\t\tprinterr(\"Failed joining lobby %s, something unexpected happened!\")\r\n\t\t\tSteam.ChatRoomEnterResponse.CHAT_ROOM_ENTER_RESPONSE_BANNED:\r\n\t\t\t\tprinterr(\"Failed joining lobby %s, you are banned from this lobby.\")\r\n\t\t\tSteam.ChatRoomEnterResponse.CHAT_ROOM_ENTER_RESPONSE_LIMITED:\r\n\t\t\t\tprinterr(\"Failed joining lobby %s, you cannot join due to having a limited account.\")\r\n\t\t\tSteam.ChatRoomEnterResponse.CHAT_ROOM_ENTER_RESPONSE_CLAN_DISABLED:\r\n\t\t\t\tprinterr(\"Failed joining lobby %s, this lobby is locked or disabled.\")\r\n\t\t\tSteam.ChatRoomEnterResponse.CHAT_ROOM_ENTER_RESPONSE_COMMUNITY_BAN:\r\n\t\t\t\tprinterr(\"Failed joining lobby %s, this lobby is community locked.\")\r\n\t\t\tSteam.ChatRoomEnterResponse.CHAT_ROOM_ENTER_RESPONSE_MEMBER_BLOCKED_YOU:\r\n\t\t\t\tprinterr(\"Failed joining lobby %s, a user in the lobby has blocked you from joining.\")\r\n\t\t\tSteam.ChatRoomEnterResponse.CHAT_ROOM_ENTER_RESPONSE_YOU_BLOCKED_MEMBER:\r\n\t\t\t\tprinterr(\"Failed joining lobby %s, a user you have blocked is in the lobby.\")\r\n\t\t\tSteam.ChatRoomEnterResponse.CHAT_ROOM_ENTER_RESPONSE_RATE_LIMIT_EXCEEDED:\r\n\t\t\t\tprinterr(\"Failed joining lobby %s, you have exceeded the rate limit.\")\r\n```\r\n\r\nFor a more clear explanation of these chat room responses, [check out the enums listings in the Main class](https:\/\/godotsteam.com\/classes\/main\/#chatroomenterresponse).\r\n\r\nOn success, we will now move to the lobby scene itself as well as setting up any networking connections. This will vary if depending on what options you want to use: [Classic Networking](https:\/\/godotsteam.com\/tutorials\/networking), [Networking Messages](https:\/\/godotsteam.com\/tutorials\/networking_messages), [Networking Sockets](https:\/\/godotsteam.com\/tutorials\/networking_sockets), or [MultiplayerPeer](https:\/\/godotsteam.com\/tutorials\/multiplayer_peer#lobby-and-multiplayerpeer).\r\n\r\nIf the join attempt fails for some reason, it is best to print the error and move the player back to the **lobby list** scene; you could also just give them the option to host or join a lobby.\r\n\r\n## The Lobby{.block}\r\n\r\nFinally we get to our actual lobby scene. From here we list all our current lobby members, have a chat log for messaging while we wait, etc.\r\n\r\n\r\n\r\nWe will set up some constants, node shortcuts, and signals to start:\r\n\r\n```gdscript\r\n# A custom scene for the lobby player.\r\nconst LOBBY_PLAYER = preload()\r\n\r\n@onready var _chat: Control = %Chat\r\n@onready var _invite: Button = %Invite\r\n@onready var _leave: Button = %Leave\r\n@onready var _player_list: VBoxContainer = %PlayerList\r\n@onready var _start: Button = %Start\r\n@onready var _title: Label = %Title\r\n\r\n\r\nfunc _ready() -> void:\r\n\t# Just in case\r\n\tif Steamworks.lobby_id == 0:\r\n\t\tprinterr(\"You are not in a lobby currently\")\r\n\t\t# We may also want to swap back to our lobby list scene or just give the player\r\n\t\t# options to host or join a lobby\r\n\t\treturn\r\n\t_get_lobby_name()\r\n\t_get_lobby_members()\r\n\r\n\t_chat.close_panel.connect(_on_leave_pressed)\r\n\t_invite.pressed.connect(_on_invite_pressed)\r\n\t_leave.pressed.connect(_on_leave_pressed)\r\n\t_start.pressed.connect(_on_start_pressed)\r\n\r\n\tSteam.lobby_chat_update.connect(\"_on_lobby_chat_update\")\r\n\tSteam.lobby_data_update.connect(\"_on_lobby_data_update\")\r\n```\r\n\r\nIn our **\\_ready()** function we will check to make sure there is an actual lobby to be in and, if not, move the player back to the lobby list or just give them the options to host or join a lobby instead. Otherwise, we will set up the lobby's title and the list of current members with our **\\_get_lobby_name()** and **\\_get_lobby_members()** functions:\r\n\r\n```gdscript\r\nfunc _get_lobby_members() -> void:\r\n\tprint(\"Getting lobby members for lobby %s\" % Steamworks.lobby_id)\r\n\tfor this_player in _player_list.get_children():\r\n\t\tthis_player.visible = false\r\n\t\tthis_player.queue_free()\r\n\r\n\tvar num_lobby_members: int = Steam.getNumLobbyMembers(Steamworks.lobby_id)\r\n\tfor this_player in range(0, num_lobby_members):\r\n\t\tvar player_object := LOBBY_PLAYER.instantiate()\r\n\t\tplayer_object.steam_id = Steam.getLobbyMemberByIndex(Steamworks.lobby_id, this_player)\r\n\t\t_player_list.call_deferred(\"add_child\", player_object)\r\n\r\n\r\nfunc _get_lobby_name() -> void:\r\n\tvar lobby_name: String = Steam.getLobbyData(Steamworks.lobby_id, \"lobby_name\")\r\n\t_title.text = lobby_name\r\n```\r\n\r\nThe **\\_get_lobby_name()** function is pretty simple, it just uses **getLobbyData()** to pull the name we set during in the **Lobby Host** section from lobby data.\r\n\r\nThe **\\_get_lobby_members()** is a little more involved. Each time we update the list, we remove all existing member objects then instantiate a whole new batch.\r\n\r\n### Lobby Player\r\n\r\nOur **LOBBY_PLAYER** scene is just a simple scene that contains an avatar and a bunch of buttons.\r\n\r\n\r\n\r\nWhen it is instantiated, we pass the user's Steam ID to it which will then grab their avatar and username via [our GodotSteamKit custom nodes](https:\/\/godotsteam.com\/projects\/godotsteamkit). However, you can duplicate this by checking out our [Avatar tutorial](https:\/\/godotsteam.com\/tutorials\/avatars) to see how to retrieve and display those; then calling **getFriendPersonaName()** for the username and passing it to our name label.\r\n\r\nOther than that, we have a few buttons to do things like check out the user's profile, list of achievements for this game, promote them to host, or kick them from the lobby if you are the host:\r\n\r\n```gdscript\r\nvar steam_id: int = 0 :\r\n\tset = set_steam_id\r\n\r\n@onready var _achievements: Button = %Achievements\r\n@onready var _avatar: SteamAvatarRect = %Avatar\r\n@onready var _host: TextureRect = %Host\r\n@onready var _kick: Button = %Kick\r\n@onready var _options: Button = %Options\r\n@onready var _options_list: HBoxContainer = %OptionList\r\n@onready var _profile: Button = %Profile\r\n@onready var _promote: Button = %Promote\r\n@onready var _username: SteamUsername = %Name\r\n\r\n\r\nfunc _ready() -> void:\r\n\t_achievements.pressed.connect(_on_achievements_pressed)\r\n\t_kick.pressed.connect(_on_kick_pressed)\r\n\t_options.toggled.connect(_on_options_toggled)\r\n\t_profile.pressed.connect(_on_profile_pressed)\r\n\t_promote.pressed.connect(_on_promote_pressed)\r\n\r\n\t_set_defaults()\r\n\r\n\r\nfunc _set_defaults() -> void:\r\n\tprint(\"Steam IDs: %s (instance) \/ %s (local)\" % [steam_id, Steamworks.steam_id])\r\n\t# This little icon is visible for the host player\r\n\t_host.visible = is_lobby_host(steam_id)\r\n\t# These buttons should not be visible if you are not the host\r\n\t_kick.visible = is_lobby_host() and steam_id != Steamworks.steam_id\r\n\t_promote.visible = is_lobby_host() and steam_id != Steamworks.steam_id\r\n\r\n\r\n# Set the player's Steam ID. This will in turn set the avatar and username for this player if using\r\n# GodotSteamKit custom nodes; they should also automatically update with any changes.\r\nfunc set_steam_id(new_id: int) -> void:\r\n\tsteam_id = new_id\r\n\r\n\t# If not using GodotSteamKit's nodes, you will want to replace the following with code to fetch\r\n\t# and display the avtar and a call to getFriendPersonaName to display the username.\r\n\tif not is_node_ready(): await ready\r\n\t_avatar.steam_id = steam_id\r\n\t_username.steam_id = steam_id\r\n\r\n\t_set_defaults()\r\n\r\n\r\nfunc _on_achievements_pressed() -> void:\r\n\tSteam.activateGameOverlayToUser(\"achievements\", steam_id)\r\n\r\n\r\nfunc _on_kick_pressed() -> void:\r\n\tif is_lobby_host() and steam_id != Steamworks.steam_id:\r\n\t\tprint(\"Sending kick command for %s\" % steam_id)\r\n\t\tif not Steam.sendLobbyChatMsg(Steamworks.lobby_id, \"\/kick %s\" % steam_id):\r\n\t\t\tprinterr(\"Failed to send kick command for %s\" % steam_id)\r\n\r\n\r\nfunc _on_add_friend_pressed() -> void:\r\n\tSteam.activateGameOverlayToUser(\"friendadd\", steam_id)\r\n\r\n\r\nfunc _on_options_toggled(show_options: bool) -> void:\r\n\t_username.visible = not show_options\r\n\t_options_list.visible = show_options\r\n\r\n\r\nfunc _on_profile_pressed() -> void:\r\n\tSteam.activateGameOverlayToUser(\"steamid\", steam_id)\r\n\r\n\r\nfunc _on_promote_pressed() -> void:\r\n\tif is_lobby_host() and steam_id != Steamworks.steam_id:\r\n\t\tprint(\"Promoting %s to lobby owner\" % steam_id)\r\n\t\tif not Steam.setLobbyOwner(Steamworks.lobby_id, steam_id):\r\n\t\t\tprinterr(\"Failed to promote %s to lobby owner\" % steam_id)\r\n\t# Update the lobby player interface afterwards\r\n\t_set_defaults()\r\n\r\n\r\n# Check to see if this player is the lobby host.\r\nfunc is_lobby_host(check_id: int = Steamworks.steam_id) -> bool:\r\n\treturn Steam.getLobbyOwner(Steamworks.lobby_id) == check_id\r\n```\r\n\r\nMost of these are pretty self-explanatory but we will talk a little bit about kicking the player. The kick button will send a lobby message command which boots them from the lobby. Oddly, Steamworks does not have any kind of official kick command and this method is what Valve uses in their SpaceWar example. We will talk about it a bit more in the [Kicking Players](#kicking-players) section.\r\n\r\n### Chat\r\n\r\nA huge component of the lobby scene is chat which for us is just a RichTextLabel for the chat log, LineEdit for the message field, and a Button to send it.\r\n\r\n\r\n\r\nWe connect a few simple signals and set up some node shortcuts:\r\n\r\n```gdscript\r\n@onready var _log: RichTextLabel = %Log\r\n@onready var _message: LineEdit = %Message\r\n@onready var _send: Button = %Send\r\n\r\n\r\nfunc _ready() -> void:\r\n\t_message.text_changed.connect(_on_message_text_changed)\r\n\t_message.text_submitted.connect(_on_message_text_submitted)\r\n\t_send.pressed.connect(_on_send_pressed)\r\n\r\n\tSteam.lobby_chat_update.connect(\"_on_lobby_chat_update\")\r\n\tSteam.lobby_message.connect(\"_on_lobby_message\")\r\n```\r\n\r\nOur **Send** button will be disabled if there is no text to send. We toggle this with our **text_changed** and **text_submitted** functions; also pressing the **Send** button will do so:\r\n\r\n```gdscript\r\nfunc _on_message_text_changed(new_text: String) -> void:\r\n\t_send.disabled = true if new_text.length() == 0 else false\r\n\r\n\r\n# Ignore the text here because we will just query the LineEdit when it sends\r\nfunc _on_message_text_submitted(_new_text: String) -> void:\r\n\t_on_send_pressed()\r\n\r\n\r\nfunc _on_send_pressed() -> void:\r\n\tSteam.sendLobbyChatMsg(Steamworks.lobby_id, _message.text)\r\n\t_message.clear()\r\n```\r\n\r\nNext we will deal with our two Steam signals: **lobby_chat_update** and **lobby_message**.\r\n\r\n```gdscript\r\nfunc _on_lobby_chat_update(lobby_id: int, user_changed_id: int, making_change_id: int, chat_state: Steam.ChatMemberStateChange) -> void:\r\n\tif lobby_id != Steamworks.lobby_id:\r\n\t\treturn\r\n\r\n\tvar changed_user: String = Steam.getFriendPersonaName(user_changed_id)\r\n\tif chat_state == Steam.ChatMemberStateChange.CHAT_MEMBER_STATE_CHANGE_ENTERED:\r\n\t\t_log.add_text(\"%s joined the lobby\\n\" % changed_user)\r\n\telse:\r\n\t\t_log.add_text(\"%s left the lobby\\n\" % changed_user)\r\n\r\n\r\nfunc _on_lobby_message(lobby_id: int, sender: int, message: String, chat_type: Steam.ChatEntryType) -> void:\r\n\tif lobby_id != Steamworks.lobby_id:\r\n\t\treturn\r\n\r\n\tvar sender_name: String = Steam.getFriendPersonaName(sender)\r\n\tif chat_type == Steam.ChatEntryType.CHAT_ENTRY_TYPE_CHAT_MSG:\r\n\t\tif message.begins_with(\"\/\") and sender == Steam.getLobbyOwner(Steamworks.lobby_id):\r\n\t\t\t_process_chat_commands(message)\r\n\t\telse:\r\n\t\t\t_log.add_text(\"%s: %s\\n\" % [sender_name, message])\r\n```\r\n\r\nAny time a player leaves or joins the lobby, the **lobby_chat_update** callback will fire. There are a few different reasons why the player is leaving but we will lump them all under the same message.\r\n\r\nWhen someone sends a message to the lobby, the **lobby_message** callback fires. For the most part, these will just be people chatting. However, the host can send specific commands here that we process in our **\\_process_chat_commands()** function. If you want you could also add some commands for the non-host players too.\r\n\r\n```gdscript\r\nfunc _process_chat_commands(message: String) -> void:\r\n\tprint(\"Command message: %s\" % message)\r\n\tvar command: PackedStringArray = message.split(\" \", false)\r\n\tprint(\"Command array: %s\" % [command])\r\n\tif command.size() == 1:\r\n\t\tprinterr(\"Could not process chat command %s, missing value\" % command[0])\r\n\tif command[0] == \"\/kick\":\r\n\t\tprint(\"Steam ID \/ kicked: %s \/ %s\" % [Steamworks.steam_id, command[1]])\r\n\t\tif Steamworks.steam_id == int(command[1]):\r\n\t\t\tprint(\"You are being kicked\")\r\n\t\t\t\r\n\t\t\t# Cause player to leave the lobby and reset the scene back to either the lobby list\r\n\t\t\t# or the option to host or join a lobby\r\n```\r\n\r\nCurrently all we are using this for is kicking players. As you can see like typical console commands, the command starts with a forward slash followed by the command name and then the Steam ID to affect. When the player gets kicked, this will cause them to leave the lobby and reset their interface.\r\n\r\nThis whole scene can be reused in-game if you are using persistent lobbies so you can have an easy chat system.\r\n\r\n### Modifying Lobbies\r\n\r\nAfter you are in the lobby, you may want to close it to new members at some point. If so, you will want to make a call to **setLobbyJoinable()**:\r\n\r\n```gdscript\r\nSteam.setLobbyJoinable(false)\r\n```\r\n\r\nThis may be a new button to add for the host specifically or perhaps a chat command. Additionally, you can still set more lobby data too; either for the whole lobby or individual players like their ready status:\r\n\r\n```gdscript\r\n# For the whole lobby\r\nSteam.setLobbyData(Steamworks.lobby_id, key, value)\r\n\r\n# For a specific player\r\nSteam.setLobbyMemberData(Steamworks.lobby_id, key, value)\r\n```\r\n\r\nNote that the **setLobbyMemberData()** function only sets it for the local user so it must be set on their end. Both of these functions will cause a **lobby_data_update** callback for all lobby members which we cover down in the [**Lobby Updates** section](#lobby-updates).\r\n\r\n### Inviting Others\r\n\r\nOur **Invite** button just opens the Steam overlay to an invite dialog panel:\r\n\r\n```gdscript\r\nfunc _on_invite_pressed() -> void:\r\n\tSteam.activateGameOverlayInviteDialog(Steamworks.lobby_id)\r\n\r\n```\r\n\r\nSince Steam overlay has been wonky since Vulkan was added to Godot, this may not work correctly for you during testing unless you run the game from the Steam client itself. You can read more about this in the [**Common Issues**](https:\/\/godotsteam.com\/issues\/common_issues\/#steam-overlay).\r\n\r\n### Leaving The Lobby\r\n\r\nIf the player clicks our **Leave** button we will very simply call the **leaveLobby()** function in Steamworks and reset the lobby ID to zero. We should also then change scenes back to our host or join options.\r\n\r\n```gdscript\r\nfunc _on_leave_pressed() -> void:\r\n\tSteam.leaveLobby(Steamworks.lobby_id)\r\n\tSteamworks.lobby_id = 0\r\n```\r\n\r\nThere are other situations where the player might leave like getting disconnected or being kicked by the host which everyone will be notified of by way of a lobby update.\r\n\r\n### Lobby Updates\r\n\r\nIf a player leaves the lobby for any reason, or joins, we will get notified by **lobby_chat_update** which calls our **\\_on_lobby_chat_update()** function that we set up at the beginning of the [**Lobby** section](#the-lobby):\r\n\r\n```gdscript\r\nfunc _on_lobby_chat_update(lobby_id: int, _changed_id: int, _making_change_id: int, _chat_state: int) -> void:\r\n\tif Steamworks.lobby_id != lobby_id:\r\n\t\treturn\r\n\t_get_lobby_members()\r\n\r\n\r\nfunc _on_lobby_data_update(success: int, lobby_id: int, member_id: int) -> void:\r\n\tprint(\"Lobby update: %s\" % lobby_id)\r\n\tif Steamworks.lobby_id != lobby_id:\r\n\t\treturn\r\n\t_get_lobby_members()\r\n```\r\n\r\nAlso if a player's metadata has changed, we will get a **lobby_data_update** callback which calls our **\\_on_lobby_data_update()** function. In both cases, we will just do a full lobby member update via **\\_get_lobby_members()** to display the newest status regardless of what specifically happened.\r\n\r\n### Kicking Players\r\n\r\nAs mentioned earlier in the [**Lobby Player**](#lobby-player) section, kicking players is basically a chat command. This command can also be passed by the host as a chat message directly: `\/kick
`. This will trigger the same process as our [**Leaving The Lobby** section](#leaving-the-lobby) by calling the **leaveLobby()** function and resetting the lobby ID.\r\n\r\nIf you keep a persistent lobby during the match, you can keep using this method to boot players.\r\n\r\n### Starting A Match\r\n\r\nMost games will have a ready \/ not ready toggle which we currently do not cover but will be added later on. This tutorial currently just starts the match when the host hits the **Start** button. There also is not much code to share here because this process will vary greatly on how your game is actually set up and what networking methods it uses. But in short:\r\n\r\n- The host should send some signal, packet, RPC, etc. to all clients to change to the game scene.\r\n- If not using persistent lobbies, all players should wipe the lobby data; specifically the lobby ID.\r\n\r\n## Up Next{.block}\r\n\r\nThat concludes the lobby tutorial. At this point you may want to check out one of the networking tutorials:\r\n\r\n- [Networking](https:\/\/godotsteam.com\/tutorials\/networking)\r\n- [Networking Messages](https:\/\/godotsteam.com\/tutorials\/networking_messages)\r\n- [Networking Sockets](https:\/\/godotsteam.com\/tutorials\/networking_sockets)\r\n- [MultiplayerPeer](https:\/\/godotsteam.com\/tutorials\/multiplayer_peer)\r\n\r\n## Additional Resources{.block}\r\n\r\n### Video Tutorials\r\n\r\nPrefer video tutorials? Feast your eyes and ears!\r\n\r\n[youtube: Godot Tutorial: GodotSteam Lobby System](https:\/\/youtu.be\/si50G3S1XGU){ data-author=\"DawnsCrow Games\" }\r\n\r\n### Related Projects\r\n\r\n['GodotSteamHL' by JDare](https:\/\/github.com\/JDare\/GodotSteamHL)\r\n\r\n### Extra Tools\r\n\r\nWant to skip all of this? [GodotSteamKit contains a lobby starter kit that contains all the custom scenes and framework to build off of.](https:\/\/godotsteam.com\/projects\/godotsteamkit)\r\n\r\n### Example Project\r\n\r\n[Later this year you can see this tutorial in action with more in-depth information by checking out our upcoming free-to-play game Skillet on Codeberg.](https:\/\/codeberg.org\/godotsteam\/skillet) There you will be able to view of the code used which can serve as a starting point for you to branch out from."
Comments
Log in to post a comment
Licensed under the CC-BY license
See the full license details
Submitted by
Gramps
Table of contents
Compatibility
Works in Godot
3.x / 4.x
Tags
SteamĀ®
steamworks
GodotSteam
lobbies
Share this tutorial
Share on Bluesky
Share on X / Twitter
or share this direct link:
https://thegodotbarn.com/contributions/tutorial/912/godotsteam-tutorials-lobbies
Please wait ...
Okay
Okay
No
Yes
Okay