The multiplayer introduction got the same door opening on two screens. Next you want to join from another PC. That requires building the game's entry points: "host a room" and "find a room".
This article builds a menu where, on two PCs on the same LAN, one hosts a room that appears in the other's list to select and join. A LAN is a network limited to a home, classroom, or similar. Let's connect finding someone and playing together there first.
What You'll Learn
- The difference between Sessions and the mechanism syncing in-game state
- The order in which you use Create, Find, and Join Session
- How to pass search results into rows with a join button
- Handling zero results, failures, and leaving so you can test again
- What roles Steam and EOS play when extending to the internet
The exercise assumes a Blueprint Third Person project and basic UMG work. Steam and EOS setup is covered as an overview later; the hands-on stays on LAN.
- Finding a room versus playing in the same world
- Null here, versus Steam and EOS
- Preparation: separate the join menu from the game level
- Build the menu and the result row
- Host: create a room and wait for connections
- Joining side: search for rooms and build the list
- Join the room from the selected row
- Confirm creation through joining on two PCs
- Leaving, and cleanup when the connection drops
- What to add when extending to the internet
- Summary
Finding a room versus playing in the same world
A Session is the mechanism managing information about the room you play in together. It handles "who is the host", "how many can join", and "where to join". It is not a room placed in a level or the menu shown on screen.
In PIE (Play In Editor) from the Replication introduction, the editor prepared the connection destination. With separately launched games, you build that entry point yourself.

| Role | What it handles | Example here |
|---|---|---|
| Room information | Find others and choose where to join | Session |
| Game connection | Establish a path for communicating | Connecting to a LAN host |
| In-game syncing | Align character and door states | Replication and RPCs |
Joining a Session does not automatically sync arbitrary doors. Conversely, having door syncing working still needs a separate menu for finding where to join.
Split three operations between host and joiner
The host registers the room with Create Session . After it succeeds, it opens the game level with the listen option and waits for other players to connect. That is a Listen Server setup where the host also plays.
The joiner searches with Find Sessions and passes the chosen result to Join Session . The host's Create and the joiner's Find and Join run on their respective PCs.

The wiring diagrams below excerpt the part each section explains. Connect surrounding logic and omitted inputs per the text and tables.
These are asynchronous nodes. The result is not out yet right after calling them. Wire result-dependent logic into On Success / On Failure . Do not mix up the exec output right after starting with the outputs on completion.
Null here, versus Steam and EOS
An Online Subsystem is the entry point for calling online features from UE. Search methods and available features change with the service used underneath. Even with identically named nodes, which service you use matters.
| Option | Its position here | Main preparation |
|---|---|---|
| Null | The first experiment searching LAN rooms | Choose Null in the project and align LAN settings |
| Steam | Extending to rooms, invites, and connection features for Steam users | Steamworks, an app ID, UE-side plugin and networking settings |
| EOS | Considering online features spanning multiple stores | Product setup on Epic's side, authentication, the features you use and networking settings |
Null works for experiments finding rooms on a LAN. Turning Use LAN off does not by itself let you search rooms on the internet. Also, "LAN search does not reach" and "whether a direct connection by IP address is possible" are different matters.

Even on the same Wi-Fi name, guest networks and the like can isolate PCs from each other. We test with two PCs on a LAN that can communicate mutually. Mixing wired and wireless is fine as long as that holds.
Preparation: separate the join menu from the game level
State the LAN settings explicitly
We proceed in a UE5 Blueprint Third Person project. For projects already configured for Steam or EOS, using a copy or a separate prototype project makes settings easier to follow.
Confirm Online Subsystem Null is enabled in "Edit → Plugins". Set the following in the project's Config/DefaultEngine.ini and restart the editor. If the same entry exists, change its value.
[OnlineSubsystem]
DefaultPlatformService=Null
We use the standard Online Subsystem Blueprint nodes here. Do not mix in setup steps for the similarly named "Online Services" or nodes from additional plugins.

Prepare two levels
| Level | Purpose | Settings |
|---|---|---|
L_Menu | The screen for hosting and finding | A new empty level. The menu appears only here |
L_Game | Where two people play | A duplicate of the Third Person level. Place two Player Starts |
L_Game uses the Third Person GameMode. If you have BP_SharedDoor (the door shared by everyone) from the Replication introduction, placing it here also lets you test syncing after joining. For L_Menu, create BP_MenuGameMode with GameMode Base as the parent and set Default Pawn Class to None. Specify it in GameMode Override in L_Menu's World Settings.
Set Game Default Map to L_Menu in Project Settings' "Maps & Modes". Also add L_Menu and L_Game to Packaging's List of maps to include in a packaged build . Levels opened by name with Open Level still need including in the distributed game. The packaging steps are also a reference.
Build the menu and the result row
We build two Widgets. The room list creates a small Widget per entry and stacks them vertically.

WBP_SessionMenu: the whole menu
Place the following in a Widget Blueprint WBP_SessionMenu . Turn "Is Variable" on for the parts used from the graph.
| Name | Part | Role |
|---|---|---|
ActionsBox | Vertical Box | Groups the two buttons below and ResultsBox |
HostButton | Button + Text | "Host a room" |
FindButton | Button + Text | "Find rooms" |
ResultsBox | Vertical Box | Holds rows for found rooms. Empty at first |
StatusText | Text | "Host or find a room." Placed outside ActionsBox |
"Room A" and "Room B" in the diagram are display examples. Actual names come from the search results later.
Placing StatusText and ActionsBox under a Canvas Panel, with the two buttons and ResultsBox as ActionsBox's children, is enough.
In L_Menu's Level Blueprint, wire BeginPlay into Create Widget (Class = WBP_SessionMenu) → Add to Viewport . Pass Get Player Controller (Player Index = 0) into Create Widget's Owning Player and its Return Value into Add to Viewport's Target.
Then call Set Input Mode UI Only . Player Controller is the same Get Player Controller and In Widget to Focus is Create Widget's Return Value. Set that Controller's Show Mouse Cursor to true . Play L_Menu, and seeing the menu and mouse means you are ready.
WBP_SessionRow: one search result
Create a Widget Blueprint WBP_SessionRow and place a Text RoomText inside a Button JoinButton . Make both usable as variables and add these variables.
| Variable | Type | Settings |
|---|---|---|
SessionResult | Blueprint Session Result | Instance Editable and Expose on Spawn on |
MenuRef | WBP_SessionMenu Object Reference | Instance Editable and Expose on Spawn on |
A Blueprint Session Result is a struct bundling the room information from a search. Join takes this value. A display name alone cannot specify where to join.
Expose on Spawn lets you pass values when creating the Widget. After compiling, SessionResult and MenuRef inputs appear on the Create Widget you place later. MenuRef serves as a reference to "the menu that listed this row".

In WBP_SessionRow's Event Construct, connect SessionResult's Get into Get Server Name 's Result. Convert the Return Value to Text, pass it into RoomText's Set Text , and wire the exec line from Construct into Set Text. That is the display name from the service, not a room name we authored.
Disable the menu while working
Create a function SetMenuBusy in WBP_SessionMenu with a Boolean input Busy . Call Set Is Enabled from the entry with Target ActionsBox and In Is Enabled taking Busy through a NOT Boolean .

NOT Boolean flips true and false. Setting "working" to true makes "operable" false.
With Busy true, the buttons inside ActionsBox become unpressable. That prevents re-searching mid-search or choosing another room mid-join. StatusText sits outside it, so the explanation stays visible while working.
Create another function FinishMenuAction with a Text input Message . Go from the entry into Set Text with StatusText's Get as Target and Message as In Text. After that call SetMenuBusy (Busy = false, Target = Self). From here, this function bundles the result explanation and re-enabling controls.
Host: create a room and wait for connections
From here we work in WBP_SessionMenu's Event Graph. Wire HostButton's On Clicked into SetMenuBusy (true) → StatusText's Set Text ("Creating room…") → Create Session .
| Create Session input | Value |
|---|---|
| Player Controller | This Widget's Get Owning Player |
| Public Connections | 2 (capacity including the host) |
| Use LAN | On |
| Use Lobbies if Available | Off where present. We use LAN Sessions here |
Get Owning Player returns the Controller of the player set on this Widget. Specifying Create Widget's Owning Player earlier was for this.
Wire Create Session's On Success into Open Level (by Name) . Level Name is L_Game , Absolute is on, and the expanded Options field takes listen . Do not type quotes or a leading ? .

Wire On Failure into FinishMenuAction (Message = "Could not create a room"). On failure, keep the menu and return it to an operable state.
Create Session alone does not start listening in the play level. Registering the room and then opening L_Game as a Listen Server is the host's full preparation.
In L_Game's Level Blueprint, use Get Player Controller (0) from BeginPlay to call Set Input Mode Game Only and set Show Mouse Cursor to false. That returns each PC's controls to the game. We do not create the menu Widget in L_Game.
Joining side: search for rooms and build the list
Wire FindButton's On Clicked into SetMenuBusy (true) → ResultsBox's Clear Children → StatusText's Set Text ("Searching…") → Find Sessions . Clear Children removes rows displayed previously.
| Find Sessions input | Value |
|---|---|
| Player Controller | Get Owning Player |
| Max Results | 20 |
| Use LAN | On |
| Use Lobbies | Off where present |
A successful search can still return zero
Go from On Success into a Branch with the Condition being whether Results' Length is greater than 0 . Length is the array's count. Create Length from Results, feed its output into an integer > 's A, and set B to 0.

Connect the comparison's Return Value into the Branch's Condition.

False goes to FinishMenuAction ("No rooms found"). Wire On Failure into a separate FinishMenuAction ("Search failed"). Separate not being able to search from searching and finding nothing.
Pass results into row Widgets one at a time
Wire the Branch's True into For Each Loop and pass Find Sessions' Results into Array. Call Create Widget (Class = WBP_SessionRow) from Loop Body and connect as follows.
| Create Widget input | Connected from |
|---|---|
| Owning Player | Get Owning Player |
| SessionResult | For Each Loop's Array Element |
| MenuRef | Self (WBP_SessionMenu itself) |

Then call Add Child to Vertical Box . Target is ResultsBox and Content is Create Widget's Return Value.
| Add Child to Vertical Box input | Connected from |
|---|---|
| Exec input | Create Widget's exec output |
| Target (where to add) | ResultsBox's Get |
| Content (what to add) | Create Widget's Return Value |
Array Element is the one room currently being pulled out. Saving it into the row establishes "pressing this row goes to this room".
Call FinishMenuAction ("Choose a room to join") from For Each Loop's Completed . Controls come back after every row is built. There is no need to call Join Session inside the loop.
Join the room from the selected row
Create a Custom Event JoinSelectedSession in WBP_SessionMenu's Event Graph with an input SelectedResult of type Blueprint Session Result. Because it calls the asynchronous Join Session, it is a Custom Event rather than a normal function. Compile it.
Back in WBP_SessionRow, call JoinSelectedSession from JoinButton's On Clicked. Target is MenuRef and SelectedResult is that row's SessionResult.

In WBP_SessionMenu's JoinSelectedSession, wire SetMenuBusy (true) → StatusText's Set Text ("Joining…") → Join Session . Pass Get Owning Player into Player Controller and the event's SelectedResult into Search Result.

On Success, set StatusText to "Traveling to the host…". The standard Join Session node performs the travel too. Do not re-open L_Game with Open Level on the joining side. That would open a same-named level separately in your own environment instead of the host's.
On Failure, Clear Children on ResultsBox and call FinishMenuAction ("Could not join. Search again."). The host may quit after your search or the room may fill up, so we avoid reusing stale rows. If a Session lingers after a failure, the Destroy Session cleanup below is needed. For first-pass isolation, quitting and relaunching the app also works.
Confirm creation through joining on two PCs
Compile and save every Blueprint and package in a Development configuration. Copy the same output folder set to two PCs on the same LAN.
- Launch the game on both and confirm L_Menu's menu appears.
- Press "Host a room" on PC A. Moving to L_Game and controlling the character means the host is ready.
- Press "Find rooms" on PC B. Select the result row and confirm it travels to L_Game.
- Confirm two characters appear on both screens and you can see the other's movement. If you placed the shared door, try opening it from PC B and see both change.
- Quit both apps and start over. This time search before hosting and confirm the zero-results display lets you search again.

PIE and multiple processes on one PC are useful during development. Those alone cannot confirm searching and networking conditions between separate PCs, though. This article's goal is joining the same room from separate PCs .
Change where you look based on which stage failed
| Symptom | Where to check first |
|---|---|
| Buttons unpressable or no screen | Widget creation in L_Menu, Owning Player, the UI input settings |
| Create or Find itself fails | An active Online Subsystem, the Controller input, errors in the Output Log |
| Zero search results | The host's creation success, Use LAN on both, whether the LAN allows mutual communication |
| No result rows appear | The Length branch, For Each Loop, Add Child into ResultsBox |
| Rows appear but joining fails | Whether that row's SessionResult was passed, whether the host is listening, whether it is full |
| Level travel fails in the packaged build | Whether L_Menu and L_Game are in the package, whether Level Name is correct |
| Joined but only the door does not sync | Replication and RPCs beyond Sessions |
When another PC cannot find it, check whether the OS firewall permits this game's executable to communicate on the network in use. There is no need to disable it entirely. Also look at guest Wi-Fi client isolation and whether a VPN changed the route.
Leaving, and cleanup when the connection drops
Our minimal test quit the app and started over. When you add a "leave the room" button in game, pair returning the screen with cleaning up the Session .
Use Destroy Session . Called by the host it closes the room; called by a joiner it is the logic for that joiner to leave the Session. A joiner leaving does not delete the host's whole room.

From the leave button's On Clicked, disable the button, call Destroy Session, and pass that Widget's Get Owning Player into Player Controller. From On Success, Open Level L_Menu (Absolute on, Options empty). On Failure, display the failure and restore controls so it can be retried.
When the host quits, joiners lose the connection. A successful Join does not guarantee that later communication and travel keep succeeding. GameInstance's Event Network Error and Event Travel Error are entry points for handling disconnections and travel failures. Connect them to logic that displays the error, cleans up the Session, and returns to the menu.
As leaving and error handling grow, gathering them in the GameInstance makes it easier to continue cleanup across level travel. Confirm "you can join" first, then add "you can act again after a disconnect".
Bonus: what to add when extending to the internet
Connecting to a PC in another household means thinking about the network path in addition to room search.
NAT , common on home routers, maps addresses inside the home to an outside address. An unsolicited connection from outside has no mapping saying which PC to deliver to and may not arrive. The techniques for establishing a connection through that are called NAT traversal.
Relay is a method that communicates through an intermediary server. You can consider setups that leave direct connection or relaying to Steam's or EOS's networking features. Confirm separately, though, that rooms becoming searchable and the game's traffic using that path are different things.

Steam: the entry point where Steam users gather
Online Subsystem Steam integrates with Steam's rooms, friends, invites, and similar features. You align an AppID identifying the app, settings on the Steamworks side, and UE's plugin and networking method settings.
Turning the plugin on does not complete the friend invite screen and join handling. Session and Lobby settings also differ. Based on the official setup material for your UE version, confirm with a separate account and connection.
EOS: features that can span stores
EOS (Epic Online Services) is a set of services providing room and player management, authentication, P2P networking, and more. Epic's terms allow free use, but that does not mean a full game-server hosting setup is provided free.
You configure a product on Epic's side and build authentication identifying players plus the features you need into the game. It is a candidate for setups spanning multiple stores, but also confirm each platform's terms and the corresponding UE-side integration.
A relay server and a game server are different
Relay forwards traffic. A Dedicated Server , meanwhile, does not play as a player and handles game progression and state computation. It is a setup to consider when you do not want the host's PC at the center of game progression.
Both are "servers" with different roles. Using a dedicated server means preparing its runtime environment and operation too. Choose to fit the join method, player count, and session length your project needs. For projects played on one screen, split screen is another option.
Summary
The host created a room with Create Session and opened L_Game with listen. The joiner passed Find Sessions' results into row Widgets and sent the chosen result to Join Session. Doors and characters after connecting are shared through Replication.
Start by confirming on two PCs on the same LAN that the other's room appears in the list and you can walk together. Being able to trace search, joining, and in-game syncing separately makes it clearer what to add when you later move to Steam or EOS.
Reference: Create Session, Find Sessions, Join Session, Online Session nodes and error handling, Steam networking features.