🎮 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
ReplicatedandReplicatedUsingproperties in C++ to sync data. -
RPCs:
-
Serverfunctions execute logic on the server. -
Multicastfunctions replicate to all clients. -
Clientfunctions execute logic on a specific client.
-
UFUNCTION(Server, Reliable) void Server_Fire(); UFUNCTION(NetMulticast, Reliable) void Multicast_PlayMuzzleFlash(); 🧪 Testing Locally
- For LAN, use
Play → Standalonewith 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=480until 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)