OnlineBeacons Tutorial with Blueprint Access

Hey everyone,

I noticed that this is covered very little in the official docs and around the web. You can find tidbits here and there, but not a full example that works with Blueprints. Today, I am going to reveal the great mystery behind how they work and how to get them to work in C++ and have blueprint access to them.

First lets cover what Beacons are good for in an online game:

  1. They can be used to get a quick ping from the server.
  2. You can use it to reserve spots on the server.
  3. They can be used for sending RPC to the server without taking up a spot.
  4. You can use it for party invites and party formation.
  5. Your imagination is the limit as to how you can use them.

Beacons are broken down into three categories: Client Beacon, Host Beacon, and a Host Beacon Object. They all branch off of the AOnlineBeacon class, except for the Host Beacon Object. The Host Beacon Object simply inherits directly from AActor.

But before we get started with the code. You need to set some stuff up in the DefaultEngine.ini to get this to work properly.

In the DefaultEngine.ini in the Config folder add the following to support both Steam and basic IP:

We set up the port to use for the beacon host and some timeout stuff.

[/Script/OnlineSubsystemUtils.OnlineBeaconHost]
ListenPort=7787
BeaconConnectionInitialTimeout=48.0
BeaconConnectionTimeout=49.0

[/Script/Engine.Engine]
!NetDriverDefinitions=ClearArray
+NetDriverDefinitions=(DefName=ā€œGameNetDriverā€,DriverClassName="/Script/OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="/Script/OnlineSubsystemUtils.IpNetDriver")
+NetDriverDefinitions=(DefName=ā€œDemoNetDriverā€,DriverClassName="/Script/Engine.DemoNetDriver",DriverClassNameFallback="/Script/Engine.DemoNetDriver")
+NetDriverDefinitions=(DefName=ā€œBeaconNetDriverā€,DriverClassName="/Script/OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="/Script/OnlineSubsystemUtils.IpNetDriver")

You will notice the third one is the most important the BeaconNetDriver. Without this your server and client beacons will not work. Now, you will need to restart the editor for these new settings to take place.

Letā€™s start by creating our client beacon for a simple one shot ping request to get the milliseconds for a round trip.

First create your class via C++ in the Editor and pick the AOnlineBeaconClient as the parent class. Once that is done letā€™s look at a header file.

The header file for the client:



#include "CoreMinimal.h"
#include "Engine.h"
#include "OnlineBeaconClient.h"
#include "PingBeacon.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnBeaconPingComplete, int32, TimeMS);

DECLARE_LOG_CATEGORY_EXTERN(FBeaconLog, Log, All);

/**
 * Simple ping client beacon class
 */
 /** the config = engine is for if you want to use config based properties. Engine = Engine.ini */
UCLASS(Blueprintable, BlueprintType, transient, notplaceable, config = Engine)
class APingBeacon : public AOnlineBeaconClient
{    
    GENERATED_UCLASS_BODY()


    //~ Begin AOnlineBeaconClient Interface
    virtual void OnFailure() override;
    //~ End AOnlineBeaconClient Interface

    /** Send a ping RPC to the client */
    UFUNCTION(client, reliable)
    virtual void ClientPing();

    /** Let's us know the beacon is ready so we can prep the initial start time for ping round trip */
    UFUNCTION(client, reliable)
    virtual void Ready();

    /** Send a pong RPC to the host */
    UFUNCTION(server, reliable, WithValidation)
    virtual void ServerPong();

    /** Provide Blueprint Access to Start the Beacon
    UFUNCTION(BlueprintCallable, Category = "PingBeacon")
    bool Start(FString address, int32 port, const bool portOverride);

    /** Provide Blueprint access to disconnect and destroy the client beacon */
    UFUNCTION(BlueprintCallable, Category = "PingBeacon")
    void Disconnect();

public:
    /** Provide a Blueprint binding for the OnPingComplete event */
    UPROPERTY(BlueprintAssignable, Category = "PingBeacon")
    FOnBeaconPingComplete OnPingComplete;

    /** Need to add one also for a failure. That is up to you though! */

protected:
    /** Holds our initial start time in Ticks from FDateTime.Now().GetTicks() */
    int64 startTime;
};

There you have it, the header for a PingBeacon client.

Now lets look at the .cpp file for it.


#include "PingBeacon.h"

DEFINE_LOG_CATEGORY(FBeaconLog);

APingBeacon::APingBeacon(const FObjectInitializer& ObjectInitializer) :
    Super(ObjectInitializer)
{
}

void APingBeacon::OnFailure()
{
    Super::OnFailure();

    /** This is where you would call the delegate for failure if you had one */
    UE_LOG(FBeaconLog, Log, TEXT("Beacon Connection failure"));
}

/** The rpc client ping implementation */
void APingBeacon::ClientPing_Implementation()
{
    UE_LOG(FBeaconLog, Log, TEXT("Ping RPC Called"));

    //Get our end time in Ticks
    int64 endTime = FDateTime::Now().GetTicks();
    //Find the difference in ticks.
    int64 diff = endTime - startTime;

    //Divide diff by 10,000 to convert to Milliseconds
    //And cast to int32 while we are at
    int32 ms = (int32)diff / 10000;

    //Broadcast the ping complete
    OnPingComplete.Broadcast(ms);

   //For looping simply call Ready() again from right here
}

/** The rpc client ready implementation */
void APingBeacon::Ready_Implementation()
{
    UE_LOG(FBeaconLog, Log, TEXT("Ready RPC Called"));
    //Set our initial start time in ticks
    startTime = FDateTime::Now().GetTicks();
    //Call server pong rpc
    ServerPong();
}

bool APingBeacon::ServerPong_Validate()
{
    return true;
}

/** ServerPong rpc implementation **/
void APingBeacon::ServerPong_Implementation()
{
    UE_LOG(FBeaconLog, Log, TEXT("Pong RPC Called"));
    //Send ping rpc back to client
    ClientPing();
}

/** Our blueprint helper for stuff **/
bool APingBeacon::Start(FString address, int32 port, const bool portOverride)
{
    //Address must be an IP or valid domain name such as epicgames.com or 127.0.0.1
    //Do not include a port in the address! Beacons use a different port then the standard 7777 for connection
    FURL url(nullptr, *address, ETravelType::TRAVEL_Absolute);

    //overriding it with a user specified port?
    if (portOverride)
    {
        url.Port = port;
    }
    //if not overriding just pull the config for it based on the beacon host ListenPort
    else
    {
        int32 portConfig;
        GConfig->GetInt(TEXT("/Script/OnlineSubsystemUtils.OnlineBeaconHost"), TEXT("ListenPort"), portConfig, GEngineIni);
        url.Port = portConfig;
    }

    //Tell our beacon client to begin connection request to server address with our beacon port
    return InitClient(url);
}

/** Our blueprint helper for disconnecting and destroying the beacon */
void APingBeacon::Disconnect()
{
    DestroyBeacon();
}

And that is all there is to the Client PingBeacon.

Now this is where things get tricky and the provided official documentation is incorrect.

The official documentation says you should not have to override AOnlineBeaconHost. This is completely false. If you do not override it with your own then it will always have a BeaconState of DenyRequests. At least that is what it looks like after examining the code for it thoroughly. There is no place in the AOnlineBeacon or AOnlineBeaconHost that dynamically changes it to AllowRequests.

The BeaconState is a protected variable in the AOnlineBeacon class, and thus the only way to modify is to inherit the AOnlineBeaconHost and set it to AllowRequests.

A quick an dirty implementation of it to always allow requests. The header file:


#pragma once

#include "CoreMinimal.h"
#include "OnlineBeaconHost.h"
#include "PingBeaconHost.generated.h"

class AOnlineBeaconHostObject;

UCLASS(Blueprintable, BlueprintType, transient, notplaceable, config=Engine)
class APingBeaconHost : public AOnlineBeaconHost
{
    GENERATED_UCLASS_BODY()

public:
    /** Blueprint accessor to init the beacon host */
    UFUNCTION(BlueprintCallable, Category = "PingBeaconHost")
    bool Start();

    /** A blueprint helper to add our PingBeaconHostObject */
    UFUNCTION(BlueprintCallable, Category = "PingBeaconHost")
    void AddHost(AOnlineBeaconHostObject* HostObject)

    /** You can also remove a host if you so wish to as well */
    /** You can remove it with: UnregisterHost(const FString& BeaconType) */

protected:
    /** If we successfully started are not */
    bool IsReady;
};

The .cpp file for it:


#include "PingBeaconHost.h"
#include "OnlineBeaconHostObject.h"

APingBeaconHost::APingBeaconHost(const FObjectInitializer& ObjectInitializer) :
    Super(ObjectInitializer)
{
    //Set the beacon host state to allow requests
    BeaconState = EBeaconState::AllowRequests;
}

bool APingBeaconHost::Start()
{
    //Call our init to start up the network interface
    IsReady = InitHost();
    return IsReady;
}

void APingBeaconHost::AddHost(AOnlineBeaconHostObject* HostObject)
{
    /** Make sure we inited properly */
    if(IsReady)
    {
        RegisterHost(HostObject);
    }
}

Great that solves that problem. Now one more class to go!

We still need to create the PingBeaconHostObject to actually allow the PingBeacon to have RPC capabilities.

The header file for the PingBeaconHostObject:


#pragma once

#include "CoreMinimal.h"
#include "OnlineBeaconHostObject.h"
#include "PingBeaconHostObject.generated.h"

UCLASS(Blueprintable, BlueprintType, transient, notplaceable, config = Engine)
class APingBeaconHostObject : public AOnlineBeaconHostObject
{
    GENERATED_UCLASS_BODY()

    //~ Begin AOnlineBeaconHost Interface
    /** You can do stuff in this one if you want, but we just use the super for this example */
    virtual AOnlineBeaconClient* SpawnBeaconActor(class UNetConnection* ClientConnection) override;
    virtual void OnClientConnected(class AOnlineBeaconClient* NewClientActor, class UNetConnection* ClientConnection) override;
    //~ End AOnlineBeaconHost Interface

    /** In case you ever want to do other things */
    virtual bool Init();

    /**
        Other override functions of the AOnlineBeaconHostObject:

        /**
         * Disconnect a given client from the host
         *
         * @param ClientActor the beacon client to disconnect
         */
        virtual void DisconnectClient(AOnlineBeaconClient* ClientActor);

        /**
         * Notification that a client has been disconnected from the host in some way (timeout, client initiated, etc)
         *
         * @param LeavingClientActor actor that has disconnected
         */
        virtual void NotifyClientDisconnected(AOnlineBeaconClient* LeavingClientActor);

        /**
         * Called when this class is unregistered by the beacon host
         * Do any necessary cleanup.
         */
        virtual void Unregister();
    */
};

Now the .cpp file for it:


#include "PingBeaconHostObject.h"
#include "PingBeacon.h"

APingBeaconHostObject::APingBeaconHostObject(const FObjectInitializer& ObjectInitializer) :
    Super(ObjectInitializer)
{
    /** Set our actual actor client class this host object will handle */
    ClientBeaconActorClass = APingBeacon::StaticClass();
    /** Set the beacon type name **/
    BeaconTypeName = ClientBeaconActorClass->GetName();

    /** Make sure we can tick **/
    PrimaryActorTick.bCanEverTick = true;
    PrimaryActorTick.bAllowTickOnDedicatedServer = true;
    PrimaryActorTick.bStartWithTickEnabled = true;
}

bool APingBeaconHostObject::Init()
{
    //just returning true for now
    return true;
}

void APingBeaconHostObject::OnClientConnected(AOnlineBeaconClient* NewClientActor, UNetConnection* ClientConnection)
{
    //Call super
    Super::OnClientConnected(NewClientActor, ClientConnection);

    //Cast to our actual APingBeacon
    APingBeacon* BeaconClient = Cast<APingBeacon>(NewClientActor);
    if (BeaconClient != NULL)
    {
        //It's good, so lets rpc back to the client and tell it we are ready
        BeaconClient->Ready();
    }
}

AOnlineBeaconClient* APingBeaconHostObject::SpawnBeaconActor(UNetConnection* ClientConnection)
{
    //Just super for now, technically you can return NULL here as well to prevent spawning
    return Super::SpawnBeaconActor(ClientConnection);
}

Yay, all of our classes are ready to go! But, how do we use them? Obviously, the Host and HostObject classes should be server only and thus created in our custom GameMode class or Blueprint.

So, letā€™s go through the steps of setting up the server side in your game mode blueprint:
In BeginPlay, first create the PingBeaconHost via SpawnActor of Class node. Then next, call the Start, and if successful spawn a PingBeaconHostObject the same way as before and use AddHost on PingBeaconHost to add it.

I do not have a screenshot of that, since I implemented that part directly into my base C++ GameMode class in via BeginPlay() override;

Hit run and test your game mode real quick. In the log you should see a second Network Adapter come up like so: LogNet: IpNetDriver_1 IpNetDriver_1 IpNetDriver listening on port 7787. If you saw it, then your server beacon is working properly and is ready to receive connections. You can also look in the editor to see if there is a Host and HostObject actors in the scene view.

Now the client part!

The image example you see is from a UI widget that is being setup in the server listing. But, this is how you should handle it roughly. Ignore the port override, I was just testing to make sure the function worked as expected.

The most important parts of the screenshot are how to handle the Start of the client beacon and how to Handle the ping complete. This is a one shot ping example.

A little messy, but it works.

And thatā€™s it for a one shot PingBeacon.

You could also make it ping indefinitely if you wanted until you call Disconnect. You would have to call Ready() again from the ClientPing() in PingBeacon to create a looping effect.

I hope this helps someone else. Took me a bit to put all the pieces together from all over the net and my own research. Letā€™s just say, not much is out there on the subject at all currently. However, it is a really important aspect to have in a multiplayer game.

10 Likes

Hey thanks for the tutorial. I have to implement beacons in my current project so any enlightenment on the subject is very helpful. :slight_smile:

Hi **Metricton,
i am implementing your code C++ for my project in blueprint, but i have some problem. I add in engine.in the following to support both Steam and basic IP and restart the egnine, then i have create the **OnlineBeaconClient header and cpp adding your code, so also for OnlineBeaconHost. Then i have created a class PingBeaconHostObject. When in blueprint i want to create the PingBeaconHost via SpawnActor of Class node, there isnā€™t. Can you help me? My knowledge in C++ is very ridicoulous Thanks.

Hey Metricton, I wish I saw your post before last week as I painstakingly went through the same process to get it functional the last few days.
I have got it to work when the Host and Client Beacons are created on two game instances without any map options.
However, when the Host is made a listen server and the beacons are then created, client shows a connection failure message when trying to connect to a Host Beacon.
Is this an issue with the FURL being incorrect on the Client beacon due to additional options added to the server URL i.e. listen? Any suggestions as to how to actually set the client URL correctly as right now mine is just set to blank?

Also, you donā€™t have to override the OnlineBeaconHost. After the Host beacon is spawned, you can just call
BeaconHost->PauseBeaconRequests(false);
which sets the enum to Allow Requests.

This function is public and in the OnlineBeacon.h file.

There are few tutorial about OnlineBeacon. Itā€™s very helpful.Thanks!

Unfortunately you canā€™t use BP version of OnlineBeaconClient :frowning: Either OnBeaconSpawnedMapping is not populated or FOnBeaconSpawned->Execute(Connection) is failing

Is that Server structure in ā€˜VersesCardā€™ custom? How do you get the IP address the server?

This is still a very good tutorial, albeit incomplete. One thing though. In PingBeaconHost.cpp these function stubs are wrong. Donā€™t copy them or you will be in for a world of pain!

void APingBeaconHostObject::DisconnectClient(AOnlineBeaconClient* ClientActor) {}
void APingBeaconHostObject::NotifyClientDisconnected(AOnlineBeaconClient* LeavingClientActor) {}
void APingBeaconHostObject::Unregister() {}

Should be changed to call the supers:
void APingBeaconHostObject::DisconnectClient(AOnlineBeaconClient* ClientActor)
{
Super::DisconnectClient(ClientActor);
}

void APingBeaconHostObject::NotifyClientDisconnected(AOnlineBeaconClient* LeavingClientActor)
{
Super::NotifyClientDisconnected(LeavingClientActor);
}

void APingBeaconHostObject::Unregister()
{
Super::Unregister();
}

Otherwise youā€™ll be getting fatal errors when a client connects to the beacon hosts twice!

1 Like

Thanks for pointing that out @CinaedAnDuine. So, essentially, unless you intend for some alternate behaviour, donā€™t override and keep base class versions. Just implementing my own version.

Otherwise, great tutorial @Metricton ā€“ Needed this for our project. Had a ā€œworkingā€ communication setup using FMessageEndpoint and the Message Busā€¦ until I discovered thatā€™s not intended for shipping builds.

Cheers / Patric

Hi

Having trouble on getting even the simple ping function to work. It seems to get a BeaconWelcome message, but then fails, and disconnects before doing the ping. How can you properly debug this to get a clue whatā€™s wrong?!

[2021.08.26-09.04.46:980][459]LogBeacon: BeaconClient_C_0[SteamSocketsNetConnection_0] Client received: BeaconWelcome
[2021.08.26-09.04.47:126][468]LogBeacon: BeaconClient_C_0[SteamSocketsNetConnection_0] Client received: Failure
[2021.08.26-09.04.47:126][468]LogBeacon: Beacon close from NMT_Failure Join failure, Couldnā€™t spawn beacon.

Im hosting a game with listen? option on the host, whoā€™s running a map. This has the beaconhost spawned and registered hostobject. Then on the client using the same setup as mentioned above, on button press it spawns the client and starts the ping function.

For server address Iā€™m using Steam.XXXXX (x = Friends SteamID). When I use that session ID it fails instantly telling me theres no available session around. (or should that ID also have Steam. as prefix?)

any help welcome!

thank you

Frank

1 Like

Hey @Metricton, how to ā€œstartā€ client beacon if you are using online sessions (advanced steam sessions plugin)? You donā€™t have address there, and you still need to connect the client beacon. What would be the solution there? Only thing you have is FOnlineSessionSearchResult

I managed to get it working with EOS, spending about 17 hours of my life because people canā€™t make reproducible examples. Code in the tutorial above was a better start than nothing, but come on, it wonā€™t even compile.

I posted code for EOS there (it may be useful even for steam or raw ips, as I think I collected all possible errors with it and i mention them there):

https://eoshelp.epicgames.com/s/question/0D54z00009W99J1CAJ/how-can-i-get-online-beacons-working-with-eos-currently-failing-to-bind-to-a-socket-properly