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)