🎮 Building a Multiplayer System in Unreal Engine with Steam & LAN
Multiplayer game development can seem intimidating, but Unreal Engine makes it surprisingly approachable. Whether you're aiming to create a simple LAN party game or a full-fledged Steam multiplayer shooter, Unreal’s built-in networking systems have you covered.
In this blog, I’ll walk you through how I implemented multiplayer using Steam and LAN subsystems in Unreal Engine — from project setup to handling replication.
⚙️ Why Use Steam and LAN?
Steam Subsystem:
Used for online multiplayer across the internet. It supports matchmaking, achievements, leaderboards, and more.
LAN Subsystem:
Best for local play or internal testing. It’s fast, requires no internet connection, and works great for prototypes and offline setups.
In my game Offensive Warfare, I implemented both to allow players flexibility: testing on LAN and releasing over Steam.
🛠 Project Setup
1. Enable Required Plugins
-
Go to
Edit > Plugins
, and enable:- Online Subsystem
- Online Subsystem Steam
- (Optional) Online Subsystem Null for fallback
2. Configure DefaultEngine.ini
[/Script/Engine.GameEngine]
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")
[OnlineSubsystem]
DefaultPlatformService=Steam
[OnlineSubsystemSteam]
bEnabled=true
SteamDevAppId=480
; If using Sessions
; bInitServerOnClient=true
[/Script/OnlineSubsystemSteam.SteamNetDriver]
NetConnectionClassName="OnlineSubsystemSteam.SteamNetConnection"
For LAN testing, change DefaultPlatformService=Null
.
🎮 Creating Multiplayer Logic
Creating the Game Instance Subsystem
Instead of cluttering the GameInstance class, I created a custom UGameInstanceSubsystem to keep the session logic modular and reusable across the project.
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "OnlineSubsystem.h"
#include "Interfaces/OnlineSessionInterface.h"
#include "OnlineSessionSettings.h"
#include "Online/OnlineSessionNames.h"
#include "MultiplayerSessionSubsystem.generated.h"
/**
*
*/
UCLASS()
class Game_API UMultiplayerSessionSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
UMultiplayerSessionSubsystem();
void Initialize(FSubsystemCollectionBase& Collection) override;
void Deinitialize() override;
IOnlineSessionPtr SessionInterface;
UFUNCTION(BlueprintCallable)
void CreateServer(FString ServerName);
UFUNCTION(BlueprintCallable)
void FindServer(FString ServerName);
void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful);
void OnDestroySessionComplete(FName SessionName, bool bWasSuccessful);
bool CreateServerAfterDestroy;
FString DestroyServerName;
FString ServerNameToFind;
FName MySessionName;
TSharedPtr<FOnlineSessionSearch> SessionSearch;
void OnFindSessionComplete(bool bWasSuccessful);
void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result);
UPROPERTY(BlueprintReadWrite)
FString GameMapPath;
UFUNCTION(BlueprintCallable)
void TravelToNewLevel(FString NewLevelPath);
};
#include "MultiplayerSessionSubsystem.h"
void PrintString(const FString & String)
{
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red,String);
}
UMultiplayerSessionSubsystem::UMultiplayerSessionSubsystem()
{
//PrintString("Subsystem Constructor");
CreateServerAfterDestroy=false;
DestroyServerName="";
ServerNameToFind="";
MySessionName="MultiplayerSubsystem";
}
void UMultiplayerSessionSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
//PrintString("UMultiplayerSessionSubsystem::Initialize");
IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get();
if (OnlineSubsystem)
{
FString SubsystemName= OnlineSubsystem->GetSubsystemName().ToString();
PrintString(SubsystemName);
SessionInterface= OnlineSubsystem->GetSessionInterface();
if (SessionInterface.IsValid())
{
SessionInterface->OnCreateSessionCompleteDelegates.AddUObject(this,&UMultiplayerSessionSubsystem::OnCreateSessionComplete);
SessionInterface->OnDestroySessionCompleteDelegates.AddUObject(this,&UMultiplayerSessionSubsystem::OnDestroySessionComplete);
SessionInterface->OnFindSessionsCompleteDelegates.AddUObject(this,&UMultiplayerSessionSubsystem::OnFindSessionComplete);
SessionInterface->OnJoinSessionCompleteDelegates.AddUObject(this,&UMultiplayerSessionSubsystem::OnJoinSessionComplete);
}
}
}
void UMultiplayerSessionSubsystem::Deinitialize()
{
//UE_LOG(LogTemp, Warning,TEXT("UMultiplayerSessionSubsystem::Deinitialize") );
}
void UMultiplayerSessionSubsystem::CreateServer(FString ServerName)
{
PrintString(ServerName);
FOnlineSessionSettings SessionSettings;
SessionSettings.bAllowJoinInProgress = true;
SessionSettings.bIsDedicated = false;
SessionSettings.bShouldAdvertise = true;
SessionSettings.bUseLobbiesIfAvailable = true;
SessionSettings.NumPublicConnections=2;
SessionSettings.bUsesPresence = true;
SessionSettings.bAllowJoinViaPresence = true;
if(IOnlineSubsystem::Get()->GetSubsystemName()=="NULL")
SessionSettings.bIsLANMatch=true;
else
{
SessionSettings.bIsLANMatch=false;
}
FNamedOnlineSession* ExistingSession= SessionInterface->GetNamedSession(MySessionName);
if (ExistingSession)
{
CreateServerAfterDestroy=true;
DestroyServerName=ServerName;
SessionInterface->DestroySession(MySessionName);
return;
}
SessionSettings.Set(FName("SERVER_NAME"),ServerName,EOnlineDataAdvertisementType::ViaOnlineServiceAndPing);
SessionInterface->CreateSession(0,MySessionName, SessionSettings);
}
void UMultiplayerSessionSubsystem::FindServer(FString ServerName)
{
PrintString(ServerName);
SessionSearch= MakeShareable(new FOnlineSessionSearch());
if(IOnlineSubsystem::Get()->GetSubsystemName()=="NULL")
SessionSearch->bIsLanQuery=true;
else
{
SessionSearch->bIsLanQuery=false;
}
SessionSearch->MaxSearchResults=100;
SessionSearch->QuerySettings.Set(SEARCH_PRESENCE,true, EOnlineComparisonOp::Equals);
ServerNameToFind=ServerName;
SessionInterface->FindSessions(0,SessionSearch.ToSharedRef());
}
void UMultiplayerSessionSubsystem::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful)
{
PrintString(FString::Printf(TEXT("OnCreateSessionComplete: %d"), bWasSuccessful));
if(bWasSuccessful)
{
FString DefaultGameMapPath="/Game/ThirdPerson/Maps/ThirdPersonMap?listen";
if(!GameMapPath.IsEmpty())
{
GetWorld()->ServerTravel(GameMapPath+"?listen");
}
else
{
GetWorld()->ServerTravel(DefaultGameMapPath);
}
}
}
void UMultiplayerSessionSubsystem::OnDestroySessionComplete(FName SessionName, bool bWasSuccessful)
{
if(CreateServerAfterDestroy)
{
CreateServerAfterDestroy=false;
CreateServer(DestroyServerName);
}
}
void UMultiplayerSessionSubsystem::OnFindSessionComplete(bool bWasSuccessful)
{
if(!bWasSuccessful)
return;
if(ServerNameToFind.IsEmpty())
return;
TArray<FOnlineSessionSearchResult> Results=SessionSearch->SearchResults;
FOnlineSessionSearchResult* CorrectResult= 0;
if(Results.Num()>0)
{
for(FOnlineSessionSearchResult Result:Results)
{
if(Result.IsValid())
{
FString ServerName="No-Name";
Result.Session.SessionSettings.Get(FName("SERVER_NAME"),ServerName);
if(ServerName.Equals(ServerNameToFind))
{
CorrectResult=&Result;
break;
}
}
}
if(CorrectResult)
{
SessionInterface->JoinSession(0,MySessionName,*CorrectResult);
}
}
else
{
PrintString("OnFindSessionComplete: No sessions found");
}
}
void UMultiplayerSessionSubsystem::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
{
if(Result==EOnJoinSessionCompleteResult::Success)
{
PrintString("OnJoinSessionComplete: Success");
FString Address= "";
bool Success= SessionInterface->GetResolvedConnectString(MySessionName,Address);
if(Success)
{
PrintString(FString::Printf(TEXT("Address: %s"), *Address));
APlayerController* PlayerController= GetGameInstance()->GetFirstLocalPlayerController();
if(PlayerController)
{
PrintString("ClientTravelCalled");
PlayerController->ClientTravel(Address,TRAVEL_Absolute);
}
}
else
{
PrintString("OnJoinSessionComplete: Failed");
}
}
}
void UMultiplayerSessionSubsystem::TravelToNewLevel(FString NewLevelPath)
{
//Travel to new level with the connected client
GetWorld()->ServerTravel(NewLevelPath+"?listen",true);
}
Blueprint Bindings (if using)
- Use BlueprintImplementableEvents to trigger session create/join from UI.
- Bind session delegates to handle success/failure states.
🔁 Handling Replication
Unreal Engine uses server-authoritative networking. Here are the basics to keep in mind:
- Use
Replicated
andReplicatedUsing
properties in C++ to sync data. -
RPCs:
-
Server
functions execute logic on the server. -
Multicast
functions replicate to all clients. -
Client
functions execute logic on a specific client.
-
UFUNCTION(Server, Reliable)
void Server_Fire();
UFUNCTION(NetMulticast, Reliable)
void Multicast_PlayMuzzleFlash();
🧪 Testing Locally
- For LAN, use
Play → Standalone
with multiple clients and ensurebIsLANMatch = true
. - For Steam, launch separate builds and test using the Steam Overlay (
Shift+Tab
) and App ID 480 (Spacewar test app).
🧠 Pro Tips
- Always use
SteamDevAppId=480
until your game is approved on Steam. - Use logging extensively to debug session creation, joining, and replication issues.
- Firewall/Antivirus can block Steam connections — test on clean setups.
- Test LAN and Steam in shipping builds, not just editor.
📌 Final Thoughts
Implementing multiplayer using Unreal Engine's Steam and LAN systems gives you flexibility during development and release. Whether you’re building a local co-op game or an online competitive shooter, the workflow stays largely the same — just swap the subsystem and fine-tune your logic.
If you’re working on a multiplayer game or have questions about Steam setup, feel free to connect with me in the comments!
Top comments (0)