UE4: How to set up LAN-Multiplayer for Windows and Android
10 minute read.
The past three months I have been fighting OnlineSubsystemNull in UE4. I want to make sure you can skip this phase of your life and buy me coffee instead.
This blog post is about how to get LAN-Multiplayer or LAN sessions to work between Android and Windows. While this is theoretically supported out of the box in Unreal Engine, it turns out that a set of bugs prevents this from working in practice.
Let’s have a look at how LAN sessions work in Unreal Engine, then the issues and finally the solutions.
OnlineSubsystemNull LAN Sessions
When a device hosts a LAN multiplayer session, it uses the FLanBeacon class to advertise the session to other devices in the network. Underneat it uses Datagram Sockets to broadcast packets to all devices in the network.
A broadcast works by creating a socket and binding it to a special broadcast IP address.
With individual IP addresses, where each device has an IP address (e.g. 192.168.172.34
),
there is a localhost address (127.0.0.1
) that refers to “this device”.
The analog in UDP broadcasts is 255.255.255.255
, which translates to “this network”.
A more specific braodcast address (analog to the actual IP addres of the device in the local network)
would be 192.168.172.255
for example.
For a session search, OnlineSubsystemNull now uses LAN beacon to send a broadcast query packet. That packet is received by other devices and they respond with the session(s) that they are currently hosting. Every session contains an FOnlineSessionInfo, which for OnlineSubsystemNull contains the IP address (“Local host address”) of the device to connect to.
Those responses can then be used to list sessions for the user to choose and connect to one.
Issues and Solutions
A couple of issues prevent this working for Android LAN sessions:
Wifi Blocks Broadcast Traffic
Many Android devices block UDP broadcast traffic to save performance (see documentation of android.net.wifi.WifiManager.MulticastLock). As do Oculus GO, Samsung Galaxy S8 (and similar), Samsung Galaxy Tab A and the Google Pixel (first gen) for example. Therefore broadcast query packets are not received on hosting Android devices.
Solution is to acquire the afforementioned MulticastLock, telling the Wifi Stack to let through broadcast/multicast traffic.
Local Host Address is Wrong
SocketSubsystemAndroid tries to get the address of the devices through the BSD sockets API.
While this has been “fixed”,
on UE 4.21 it currently just returns 127.0.0.1
. That works fine for hosting, but if this were
to be sent as a broadcast query response, the other devices would try to connect to itself rather
than to the Android device.
Solution is to retrieve the address via the Java Android APIs.
Broadcast 255.255.255.255
Apparently connecting to 255.255.255.255
(physical layer broadcast address) is not sufficient and
we instead need to use the more explicit network layer broadcast address, e.g. 192.168.172.255
(More info here).
While, yes, you can just change the last byte of your IP adress to 255
, but that’s a very ugly
hack and we should rather just ask the Java APIs again.
Solution is to also retrieve the broadcast address via the Java Android APIs.
But, apparently, if you send to 255.255.255.255
from Windows, packets still don’t reach Android,
we therefore need to use the more specific address there, too!
Solution is to retrieve the more specific broadcast address via WinSock APIs.
In Practice
The most valuable part shouldn’t be missing, of course. Step for step, you “just” need to 1. add the following Java 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 handles acquiring the multicast lock, getting the IP addresses properly and allows us to next 2. call these via JNI.
JNI
Thanks to the wrappers in UE4, calling these Java functions is rather straight forward:
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); } }
Finally for Android, 3. make sure you have the following permissions:
android.permission.INTERNET android.permission.ACCESS_WIFI_STATE android.permission.ACCESS_NETWORK_STATE android.permission.CHANGE_WIFI_MULTICAST_STATE
How do you inject the IP addresses into the OnlineSubsystemNull? You can either compile the engine
yourself from source and change the source, or you try to wrap it somehow.
For a while I had a FSocketSubsystemAndroidFixed
class, which would use it’s original to
“manually subclass” it (since private sources of the engine, you cannot subclass trivially).
Windows
Now we have to merely have to 4. fix the broadcast address on Windows:
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; }
Summary
All this is a lot of work, so here is a branch on my UnreanEngine fork.
If you create an installed build, you can deploy the binaries that changes (UE4Game on all Platforms and UE4Editor-Socekts for your Editor platform).
My hope is to get the attention of UE4 devs with this blog post which hopefully get this fixed
by UE 4.23. I currently don’t have the time to create a pullrequest myself, as this will likely
require changes in iOS, Linux (to broadcast to the more specific broadcast address, instead of
255.255.255.255
) and others aswell to allow the cross-play on LAN without breaking Linux-Windows
connections for example.
I hope I as able to spare you some working hours. If it did and you are in a position to do so, here is a way to say thanks, otherwise, I’m glad I could help and apprechiate any tweets you send my way!