Building a Multiplayer System in Unreal Engine with Steam & LAN
WinterTurtle23

WinterTurtle23 @winterturtle23

About: Unreal Engine Game Developer | C++ & Blueprint | VR, Multiplayer & Android | I build simulations, shooters, runners, & sci-fi games

Location:
Prayagraj, India
Joined:
Oct 20, 2021

Building a Multiplayer System in Unreal Engine with Steam & LAN

Publish Date: May 21
1 0

🎮 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!


Comments 0 total

    Add comment