UE4: How to set up LAN-Multiplayer for Windows and Android

10 minute read.

The past three months I have been fight­ing On­li­ne­Sub­sys­tem­Null in UE4. I want to make sure you can skip this phase of your life and buy me cof­fee in­stead.

This blog post is about how to get LAN-Mul­ti­play­er or LAN ses­sions to work be­tween An­droid and Win­dows. While this is the­o­ret­i­cal­ly sup­port­ed out of the box in Un­re­al En­gine, it turns out that a set of bugs pre­vents this from work­ing in prac­tice.

Let’s have a look at how LAN ses­sions work in Un­re­al En­gine, then the is­sues and fi­nal­ly the so­lu­tions.

On­li­ne­Sub­sys­tem­Null LAN Ses­sions

When a de­vice hosts a LAN mul­ti­play­er ses­sion, it us­es the FLan­Bea­con class to ad­ver­tise the ses­sion to oth­er de­vices in the net­work. Un­der­neat it us­es Da­ta­gram Sock­ets to broad­cast pack­ets to all de­vices in the net­work.

A broad­cast works by cre­at­ing a sock­et and bind­ing it to a spe­cial broad­cast IP ad­dress. With in­di­vid­u­al IP ad­dress­es, where each de­vice has an IP ad­dress (e.g. 192.168.172.34), there is a lo­cal­host ad­dress (127.0.0.1) that refers to “this de­vice”.

The ana­log in UDP broad­casts is 255.255.255.255, which trans­lates to “this net­work”. A more spe­cif­ic braod­cast ad­dress (ana­log to the ac­tu­al IP ad­dres of the de­vice in the lo­cal net­work) would be 192.168.172.255 for ex­am­ple.

For a ses­sion search, On­li­ne­Sub­sys­tem­Null now us­es LAN bea­con to send a broad­cast query pack­et. That pack­et is re­ceived by oth­er de­vices and they re­spond with the ses­sion(s) that they are cur­rent­ly host­ing. Ev­ery ses­sion con­tains an FOn­li­ne­Ses­sion­In­fo, which for On­li­ne­Sub­sys­tem­Null con­tains the IP ad­dress (“Lo­cal host ad­dress”) of the de­vice to con­nect to.

Those re­spons­es can then be used to list ses­sions for the us­er to choose and con­nect to one.

Is­sues and So­lu­tions

A cou­ple of is­sues pre­vent this work­ing for An­droid LAN ses­sions:

Wifi Blocks Broad­cast Traf­fic

Many An­droid de­vices block UDP broad­cast traf­fic to save per­for­mance (see doc­u­men­ta­tion of an­droid.net.wifi.Wifi­Man­ag­er.Mul­ti­cas­t­Lock). As do Ocu­lus GO, Sam­sung Gal­axy S8 (and sim­i­lar), Sam­sung Gal­axy Tab A and the Google Pix­el (first gen) for ex­am­ple. There­fore broad­cast query pack­ets are not re­ceived on host­ing An­droid de­vices.

So­lu­tion is to ac­quire the af­fore­men­tioned Mul­ti­cas­t­Lock, telling the Wifi Stack to let through broad­cast/mul­ti­cast traf­fic.

Lo­cal Host Ad­dress is Wrong

Sock­et­Sub­sys­te­mAn­droid tries to get the ad­dress of the de­vices through the BSD sock­ets API. While this has been “fixed”, on UE 4.21 it cur­rent­ly just re­turns 127.0.0.1. That works fine for host­ing, but if this were to be sent as a broad­cast query re­sponse, the oth­er de­vices would try to con­nect to it­self rather than to the An­droid de­vice.

So­lu­tion is to re­trieve the ad­dress via the Ja­va An­droid APIs.

Broad­cast 255.255.255.255

Ap­par­ent­ly con­nect­ing to 255.255.255.255 (phys­i­cal lay­er broad­cast ad­dress) is not suf­fi­cient and we in­stead need to use the more ex­plic­it net­work lay­er broad­cast ad­dress, e.g. 192.168.172.255 (More in­fo here). While, yes, you can just change the last byte of your IP adress to 255, but that’s a very ug­ly hack and we should rather just ask the Ja­va APIs again.

So­lu­tion is to al­so re­trieve the broad­cast ad­dress via the Ja­va An­droid APIs.

But, ap­par­ent­ly, if you send to 255.255.255.255 from Win­dows, pack­ets still don’t reach An­droid, we there­fore need to use the more spe­cif­ic ad­dress there, too!

So­lu­tion is to re­trieve the more spe­cif­ic broad­cast ad­dress via WinSock APIs.

In Prac­tice

The most valu­able part shouldn’t be miss­ing, of course. Step for step, you “just” need to 1. add the fol­low­ing Ja­va code via an APL XML:

<gameActivityImportAdditions>
     <insert>
         import android.net.wifi.WifiManager;
         import android.net.wifi.WifiInfo;
         import android.net.DhcpInfo;
         import android.net.wifi.WifiManager.MulticastLock;
     </insert>
 </gameActivityImportAdditions>


 <!-- optional additions to the GameActivity class in GameActivity.java -->
 <gameActivityClassAdditions>
     <insert>
         <![CDATA[
         /**
         * Get IP address from wifi adapter
         */
         public int Sockets_GetIP() {
             final WifiManager wm = (WifiManager) getSystemService("wifi");
             return wm.getConnectionInfo().getIpAddress();
         }

         /**
         * Get Broadcast IP address from wifi adapter
         */
         public int Sockets_GetBroadcastIP() {
             final WifiManager wm = (WifiManager) getSystemService("wifi");

             DhcpInfo dhcp = wm.getDhcpInfo();
             if(dhcp == null) return 0;

             final int broadcast = (dhcp.ipAddress & dhcp.netmask) | ~dhcp.netmask;
             return broadcast;
         }

         static MulticastLock multicastLock = null;

         /**
         * Aquire a MulticastLock
         */
         public void Sockets_AcquireMulticastLock() {
             if(multicastLock == null) {
                 /* First time initialization of lock */
                 final WifiManager wm = (WifiManager) getSystemService("wifi");
                 multicastLock = wm.createMulticastLock("UE4-multicastlock");
                 multicastLock.setReferenceCounted(true);
             }

             /* Increment reference count */
             multicastLock.acquire();
         }

         /**
         * Release a MulticastLock
         */
         public void Sockets_ReleaseMulticastLock() {
             if(multicastLock == null || !multicastLock.isHeld()) return;

             /* Decrements reference count */
             multicastLock.release();
         }

         ]]>
     </insert>
 </gameActivityClassAdditions>

That han­dles ac­quir­ing the mul­ti­cast lock, get­ting the IP ad­dress­es prop­er­ly and al­lows us to next 2. call these via JNI.

JNI

Thanks to the wrap­pers in UE4, call­ing these Ja­va func­tions is rather straight for­ward:

static jmethodID Sockets_GetIP = NULL;
static jmethodID Sockets_GetBroadcastIP = NULL;
static jmethodID Sockets_AcquireMulticastLock = NULL;
static jmethodID Sockets_ReleaseMulticastLock = NULL;

/* Init java functions */

 if (JNIEnv* Env = FAndroidApplication::GetJavaEnv(true)) {
     Sockets_GetIP = FJavaWrapper::FindMethod(Env,
             FJavaWrapper::GameActivityClassID,
             "Sockets_GetIP", "()I", false);
     check(Sockets_GetIP != NULL);

     Sockets_GetBroadcastIP = FJavaWrapper::FindMethod(Env,
             FJavaWrapper::GameActivityClassID,
             "Sockets_GetBroadcastIP", "()I", false);
     check(Sockets_GetBroadcastIP != NULL);

     Sockets_AcquireMulticastLock = FJavaWrapper::FindMethod(Env,
             FJavaWrapper::GameActivityClassID,
             "Sockets_AcquireMulticastLock", "()V", false);
     check(Sockets_AcquireMulticastLock != NULL);

     Sockets_AcquireMulticastLock = FJavaWrapper::FindMethod(Env,
             FJavaWrapper::GameActivityClassID,
             "Sockets_ReleaseMulticastLock", "()V", false);
     check(Sockets_AcquireMulticastLock != NULL);
 }

/* Call java functions */

void GetLocalHostAddrFixed(TSharedRef<FInternetAddr> localIp) {
   if (JNIEnv* Env = FAndroidApplication::GetJavaEnv(true)) {
      const int ip = FJavaWrapper::CallIntMethod(Env, FJavaWrapper::GameActivityThis,
               Sockets_GetIP);
      localIp->SetRawIp({
            uint8(ip & 0xff),
            uint8(ip >> 8 & 0xff),
            uint8(ip >> 16 & 0xff),
            uint8(ip >> 24 & 0xff)});
   }
}

void GetBroadcastAddrFixed(FInternetAddr& broadcastAddr) {
   if (JNIEnv* Env = FAndroidApplication::GetJavaEnv(true)) {
      const int ip = FJavaWrapper::CallIntMethod(Env, FJavaWrapper::GameActivityThis,
               Sockets_GetBroadcastIP);
      broadcastAddr.SetRawIp({
            uint8(ip & 0xff),
            uint8(ip >> 8 & 0xff),
            uint8(ip >> 16 & 0xff),
            uint8(ip >> 24 & 0xff)});
   }
}

void AcquireMulticastLock() {
   if (JNIEnv* Env = FAndroidApplication::GetJavaEnv(true)) {
      FJavaWrapper::CallVoidMethod(Env, FJavaWrapper::GameActivityThis,
               Sockets_AcquireMulticastLock);
   }
}

void ReleaseMulticastLock() {
   if (JNIEnv* Env = FAndroidApplication::GetJavaEnv(true)) {
      FJavaWrapper::CallVoidMethod(Env, FJavaWrapper::GameActivityThis,
               Sockets_ReleaseMulticastLock);
   }
}

Fi­nal­ly for An­droid, 3. make sure you have the fol­low­ing per­mis­sions:

android.permission.INTERNET
android.permission.ACCESS_WIFI_STATE
android.permission.ACCESS_NETWORK_STATE
android.permission.CHANGE_WIFI_MULTICAST_STATE

How do you in­ject the IP ad­dress­es in­to the On­li­ne­Sub­sys­tem­Null? You can ei­ther com­pile the en­gine your­self from source and change the source, or you try to wrap it some­how. For a while I had a FSocketSubsystemAndroidFixed class, which would use it’s orig­i­nal to “man­u­al­ly sub­class” it (since pri­vate sources of the en­gine, you can­not sub­class triv­ial­ly).

Win­dows

Now we have to mere­ly have to 4. fix the broad­cast ad­dress on Win­dows:

bool FSocketSubsystemWindows::GetLocalAdapterBroadcastAddresses( TArray<TSharedPtr<FInternetAddr> >& OutAdresses )
{
   ULONG Flags = GAA_FLAG_INCLUDE_PREFIX | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_SKIP_FRIENDLY_NAME;
   ULONG Result;
   ULONG Size = 0;
   ULONG Family = (PLATFORM_HAS_BSD_IPV6_SOCKETS) ? AF_UNSPEC : AF_INET;

   Result = GetAdaptersAddresses(Family, Flags, NULL, NULL, &Size);
   if (Result != ERROR_BUFFER_OVERFLOW)
   {
      return false;
   }

   PIP_ADAPTER_ADDRESSES AdapterAddresses = (PIP_ADAPTER_ADDRESSES)FMemory::Malloc(Size);

   Result = GetAdaptersAddresses(Family, Flags, NULL, AdapterAddresses, &Size);
   if (Result != ERROR_SUCCESS)
   {
      FMemory::Free(AdapterAddresses);

      return false;
   }

   // extract the list of physical addresses from each adapter
   for (PIP_ADAPTER_ADDRESSES AdapterAddress = AdapterAddresses; AdapterAddress != NULL; AdapterAddress = AdapterAddress->Next)
   {
      if ((AdapterAddress->IfType == IF_TYPE_ETHERNET_CSMACD) ||
            (AdapterAddress->IfType == IF_TYPE_IEEE80211))
      {
         for (PIP_ADAPTER_UNICAST_ADDRESS UnicastAddress = AdapterAddress->FirstUnicastAddress; UnicastAddress != NULL; UnicastAddress = UnicastAddress->Next)
         {
            if ((UnicastAddress->Flags & IP_ADAPTER_ADDRESS_DNS_ELIGIBLE) != 0)
            {
               ULONG mask;
               ConvertLengthToIpv4Mask(UnicastAddress->OnLinkPrefixLength, &mask);

               const sockaddr_storage* RawAddress = (const sockaddr_storage*)(UnicastAddress->Address.lpSockaddr);
               TSharedRef<FInternetAddrBSD> NewAddress = MakeShareable(new FInternetAddrBSD(this));
               const int32 broadcastAddr = ~mask | ((sockaddr_in*)RawAddress)->sin_addr.s_addr;
               NewAddress->SetRawIp({
                     uint8(broadcastAddr & 0xff),
                     uint8(broadcastAddr >> 8 & 0xff),
                     uint8(broadcastAddr >> 16 & 0xff),
                     uint8(broadcastAddr >> 24 & 0xff)});
               OutAdresses.Add(NewAddress);
            }
         }
      }
   }

   FMemory::Free(AdapterAddresses);

   return true;
}

Sum­ma­ry

All this is a lot of work, so here is a branch on my Un­re­a­nEngine fork.

If you cre­ate an in­stalled build, you can de­ploy the bi­na­ries that changes (UE4Game on all Plat­forms and UE4Ed­i­tor-Socek­ts for your Ed­i­tor plat­form).

My hope is to get the at­ten­tion of UE4 de­vs with this blog post which hope­ful­ly get this fixed by UE 4.23. I cur­rent­ly don’t have the time to cre­ate a pull­re­quest my­self, as this will like­ly re­quire changes in iOS, Lin­ux (to broad­cast to the more spe­cif­ic broad­cast ad­dress, in­stead of 255.255.255.255) and oth­ers aswell to al­low the cross-play on LAN with­out break­ing Lin­ux-Win­dows con­nec­tions for ex­am­ple.

I hope I as able to spare you some work­ing hours. If it did and you are in a po­si­tion to do so, here is a way to say thanks, oth­er­wise, I’m glad I could help and ap­prechi­ate any tweets you send my way!