DEV Community

WinterTurtle23
WinterTurtle23

Posted on

Building a Multiplayer System in Unreal Engine with Steam & LAN

🎮 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" 
Enter fullscreen mode Exit fullscreen mode

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); }; 
Enter fullscreen mode Exit fullscreen mode
#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); } 
Enter fullscreen mode Exit fullscreen mode

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 and ReplicatedUsing 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(); 
Enter fullscreen mode Exit fullscreen mode

🧪 Testing Locally

  • For LAN, use Play → Standalone with multiple clients and ensure bIsLANMatch = 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)