Tutorial: a working multiplayer menu
Step 4: Start the match
Send everyone from the pre-match lobby into the actual game, together.
Both players are in a session. Nobody is playing anything yet. This step connects them.
What has to happen
Starting the match is three things in order:
- The host stops accepting new players, so nobody drops into a game already running.
- The host travels to
L_Matchand starts listening for connections. - Everyone else finds out where the host is listening, and connects.
SteamForge does the second and third. The first is one node, and it is yours to call because only you know when your match begins.
Wire the Start button
Add an On Clicked event for Button_Start, then:
- Add Get SteamForge Sessions. That is the subsystem holding your session.
- Drag off it and add Start Match.
Two nodes, and that is the whole button. Start Match reads the map from the session you already created, closes it to new players, travels you there as a listen server, and publishes the address so everyone in the session follows.
Compile and save.
Show who is in the lobby
Players gather before you press Start, and until you show them, nobody can tell whether their friend actually made it in.
Add the text block
- Open
WBP_MainMenuand click Designer. - From the Palette, drag a Text widget into your Vertical Box, underneath the buttons.
- With it selected, rename it
Text_Playersin the Details panel and tick Is Variable. - Still in Details, find Appearance and tick Auto Wrap Text.
Compile.
Add a variable to build the list in
- Click Graph.
- In the Variables panel on the left, click +.
- Name it
PlayerNames. - Set its type to String.
- Click the icon to the right of the type and change it from a single value to an Array.
Compile.
Make the event
- Right click in an empty part of the graph.
- Search for Add Custom Event and add it.
- Rename it
Refresh Players.
Everything in the next section hangs off that event's execution pin.
Build the list
- Drag
PlayerNamesinto the graph, choose Get, then drag off it and add Clear. ConnectRefresh Playersinto it. Without this the list grows every time the event runs. - Add Get SteamForge Lobbies, drag off it, and add Get Current Lobby.
- Drag off Get Current Lobby and add Get Lobby Members.
- Add a For Each Loop after Clear, and connect the members array to its Array pin.
- In the loop body, drag off Array Element and add Break Steam Lobby Member.
- Drag
PlayerNamesin again, add Add, and connect Persona Name to its New Item pin. Connect Loop Body into it.
Then from the loop's Completed pin:
- Drag
PlayerNamesin, drag off it, and add Join String Array. - Set its Separator pin to
,(a comma and a space). - Drag in
Text_Players, add Set Text (Text), and connect the joined string to In Text.
Compile and save.
\n, so there is no way to type a newline into one. That is why this uses a comma with Auto Wrap Text rather than one name per line. For one per line you need a Vertical Box and a row widget, which is a bigger build than this step.Mark the host
Each member arrives with everything you need, already resolved:
| Field | What it is |
|---|---|
| Player | Their Steam ID |
| Persona Name | Their Steam name, ready to show |
| Is Local Player | True for whoever is holding this copy |
| Is Owner | True for the host |
To label the host, put a Branch inside the loop on Is Owner. On True, Append (host) to Persona Name and add that. On False, add Persona Name unchanged.
There is no second lookup anywhere here. Steam gives the names back with the member list.
Keeping the list current
Refresh Players needs something to call it, and in this menu one thing does: the lobby changing.
- Find Event Construct in the graph. If it is not there, right click and add it.
- Add Get SteamForge Lobbies.
- Drag off it, search for On Lobby Member Changed, and choose Assign On Lobby Member Changed. That places two nodes: the assign call, and a red bound event node wired into it.
- Connect Event Construct into the assign node.
- From the red bound event node, right click, type
Refresh Players, and place the call node. Connect it.
Compile and save.
That is the whole thing. Entering a lobby is a member change, so hosting or joining fires the event and fills the list, for the player who did it as well as for everyone already there.
You do not need a second Refresh Players on Event Construct. The menu is the first thing that loads, so you are not in a lobby yet, and it would read an empty lobby and set the text to the empty string it already holds.
Compile and save.
The bound event hands you a Change value: Entered, Left, Disconnected, Kicked, Banned or Unknown. You can ignore it and rebuild the whole list every time, which is what most games do and what the steps above assume.
Check it
With two clients: host on one, join on the other, and both should show two names. Leave on the second and the host's list should drop back to one within a moment.
Show it only to the host
Every player has a Start button and only one of them can use it. Is Session Host fixes that, and it returns false both for a client and for someone not in a session at all, so one node covers every case.
Bind it to the button's visibility rather than setting it from events, and it keeps itself right without you remembering to update it:
- In Designer, select
Button_Start. - In Details, find Behavior > Visibility, click Bind, then Create Binding.
- In the function it creates, add Get SteamForge Sessions and drag off it to Is Session Host.
- Add a Select node, feed the bool into its Index pin, set True to
Visibleand False toCollapsed, and wire it to Return Value.
Collapsed gives up its space in the layout, Hidden keeps it. For a button in a list, Collapsed is usually what you want.
What happens when you click it
On the host: the level changes to L_Match. The menu widget is destroyed along with the old level, which is expected. The host is now a listen server.
As soon as the map finishes loading, SteamForge publishes the address the host is listening on into the Steam lobby.
On every other player: Steam tells them an address appeared, and they travel to it automatically, arriving in L_Match connected to the host.
You do not wire any of that second half. It is the reason the session layer exists.
Run it
With two players and mock mode still set up from the last page:
- Window 1: Host.
- Window 2: Find, select, Join.
- Window 1: Start.
Both windows should end up in L_Match.
Making players appear in the match
Arriving in the level and having a body there are two different things. Whether you see anything is decided by L_Match's Game Mode, which you have not set.
You did not give L_Match a GameMode Override, so it falls back to the project default in Project Settings > Maps & Modes. That is usually fine, and what it spawns depends on the project:
| Project default | What each player gets |
|---|---|
| Third Person or similar template | The template character. Both players visible. |
| An empty project | A Default Pawn, a floating sphere you can fly around. Correct, if plain. |
| A Game Mode with Default Pawn Class set to None | Nothing. You arrive with no body and see a black screen. |
The third row is the one that looks broken but is not. If both windows reach L_Match and you see nothing, give L_Match its own GameMode Override in World Settings, with a Default Pawn Class and a Player Start placed in the level.
Reading the log when it goes wrong
Filter the Output Log for LogSteamForge. The host prints two lines, in this order. First, that it accepted the request and where it is going:
LogSteamForge: Starting match for session 109775..., travelling to L_Match?listenThen, once the map is up and a port is bound, the address joiners need:
LogSteamForge: Published session 109775... at 192.168.1.33:7777. Joiners can travel now.The joiner prints one line when it acts on that, from whichever route reached it first:
LogSteamForge: Host started the match; following them to the server.
LogSteamForge: Found the host's server at 192.168.1.33:7777 while waiting. Following them.Read them in order. The first one missing is the step that failed:
| Missing line | What it means |
|---|---|
| No Starting match line | Start Match refused. Not in a session, not the host, or the session published no map name. |
| Starting match but no Published | The listen server never came up. Wrong Map Name, or the map is missing from a packaged build. After about ten seconds the host logs that it gave up. |
| Published but nothing on the joiner | That client was not in the session, or joined with Travel unticked. |
| Both sides logged, but the joiner never arrives | A network problem rather than a Steam one. See below. |
About the address
The host publishes the address of the machine it is running on. Two windows on one computer, or two computers on one network, connect directly and need no setup.
Over the internet it is a direct connection like any other, so the host needs their port reachable. That is the same constraint every listen server game has, and a Steam lobby does not change it.
When the match ends
Call Destroy Session on the SteamForge Sessions subsystem, then travel back to L_Menu.
A session you never destroy keeps your slot occupied and keeps appearing in searches until Steam times it out.
You are done
You have a working host, a working browser, a working join, and a match both players end up in. Everything else in these docs builds on this shape.
Where to go next
- Sessions for what the session layer does underneath, and the parts this tutorial skipped.
- Lobby players and the host to show who is in the room before the match starts.
- Why a join failed to turn the diagnosis into something better than one status line.