DEV Community

WinterTurtle23
WinterTurtle23

Posted on

1

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!


AWS GenAI LIVE image

How is generative AI increasing efficiency?

Join AWS GenAI LIVE! to find out how gen AI is reshaping productivity, streamlining processes, and driving innovation.

Learn more

Top comments (0)

Tiger Data image

🐯 🚀 Timescale is now TigerData: Building the Modern PostgreSQL for the Analytical and Agentic Era

We’ve quietly evolved from a time-series database into the modern PostgreSQL for today’s and tomorrow’s computing, built for performance, scale, and the agentic future.

So we’re changing our name: from Timescale to TigerData. Not to change who we are, but to reflect who we’ve become. TigerData is bold, fast, and built to power the next era of software.

Read more

👋 Kindness is contagious

Explore this insightful piece, celebrated by the caring DEV Community. Programmers from all walks of life are invited to contribute and expand our shared wisdom.

A simple "thank you" can make someone’s day—leave your kudos in the comments below!

On DEV, spreading knowledge paves the way and fortifies our camaraderie. Found this helpful? A brief note of appreciation to the author truly matters.

Let’s Go!