Cross-Play support
Cross-play is the ability of players on different platforms to play together in the same game session. This is normally not an issue when all players use the same connection type, but it can be when different players must use different means of connecting to a server. For example, players on desktop platforms might use a UDP-based connection, whereas players on WebGL would use WebSockets.
Cross-play with Unity Relay
When using Unity Relay, then cross-play support comes for free, without you having to do anything to enable it. You could have a host connecting to the Relay server with DTLS, a client connecting with WebSocket, and another connecting with UDP, and both clients will be able to communicate with the host without any issue.
Refer to the Unity Relay documentation for details on how to select a connection type for your host and client connections.
Cross-play with direct connections
When connecting NetworkDriver
instances directly to each other without using Unity Relay as the intermediary, the situation is more complicated. NetworkDriver
can only accept connections on a single connection type. For example, a driver that is using the WebSocketNetworkInterface
and is bound to an IPv4 endpoint will only be able to accept connections made from WebSocket clients with IPv4 addresses.
A solution to this problem is simply to use multiple NetworkDriver
instances. For example, one driver for accepting UDP connections, and another to accept WebSocket connections. But that can be bothersome to work with. To simplify this workflow, Unity Transport provides a handy MultiNetworkDriver
component.
Overview of MultiNetworkDriver
A MultiNetworkDriver
is a container for multiple NetworkDriver
instances. Its API is mostly the same as NetworkDriver
, so pretty much everywhere you would use NetworkDriver
, you can use MultiNetworkDriver
instead.
On creation, a MultiNetworkDriver
is empty. You can add NetworkDriver
instances to it with the AddDriver
method. There are a few limitations to what drivers can be added:
- The added driver must already be in the
Listening
state. That is, one must already have calledListen
on theNetworkDriver
instance before adding it. - No connections must have been already made to the added driver.
- The added driver must have the same number of pipelines configured as previously-added drivers.
A MultiNetworkDriver
can have a maximum of 4 NetworkDriver
instances added to it. How these drivers are defined is up to you. They could use different network interfaces (e.g. UDP vs WebSocket), listen on different endpoints, use different configuration values, etc.
Below you'll find examples for two common cases: listening to both UDP and WebSocket connections, and listening on an IPv4 and an IPv6 endpoint. Note that for brevity's sake, error handling is omitted from these examples. In real production code you should check the values returned by Bind
and Listen
.
Example: UDP and WebSockets
This example shows how to create a MultiNetworkDriver
that will accept connections from both UDP and WebSocket clients. A full working version of that example (including client code) is provided with the Unity Transport package in the "CrossPlay" sample.
First, we need to create a NetworkDriver
that will accept the UDP connections:
var udpDriver = NetworkDriver.Create(new UDPNetworkInterface());
udpDriver.Bind(NetworkEndpoint.AnyIpv4.WithPort(7777));
udpDriver.Listen();
Next, we create a NetworkDriver
that will accept the WebSocket connections:
var wsDriver = NetworkDriver.Create(new WebSocketNetworkInterface());
wsDriver.Bind(NetworkEndpoint.AnyIpv4.WithPort(7778));
wsDriver.Listen();
Finally, we can create the MultiNetworkDriver
and add both of our drivers to it:
var multiDriver = MultiNetworkDriver.Create();
multiDriver.AddDriver(udpDriver);
multiDriver.AddDriver(wsDriver);
At this point, multiDriver
can be used as you would a normal NetworkDriver
. You can Accept
new connections, pop events with PopEvent
or PopEventForConnection
, send messages with BeginSend
and EndSend
, schedule updates with ScheduleUpdate
, etc. Clients will be able to reach the server on UDP port 7777 (for UDP connections) and TCP port 7778 (for WebSocket connections).
While it is possible to keep references to udpDriver and wsDriver around and operate on them individually, it is not recommended to do so. Once a driver is added to a MultiNetworkDriver, only that MultiNetworkDriver should be used to operate on the NetworkDriver. For example, do not call ScheduleUpdate on udpDriver or wsDriver. Instead, call the ScheduleUpdate method of multiDriver.
Example: IPv4 and IPv6
This example shows how to create a MultiNetworkDriver
that will accept connections from both IPv4 and IPv6 addresses (using the default UDP-based protocol). This can be very useful for servers for mobile games as cellular networks are increasingly IPv6-based.
First, we create the NetworkDriver
that will listen to IPv4 addresses:
var ipv4Driver = NetworkDriver.Create();
ipv4Driver.Bind(NetworkEndpoint.AnyIpv4.WithPort(7777));
ipv4Driver.Listen();
Next, we create the NetworkDriver
that will listen to IPv6 addresses:
var ipv6Driver = NetworkDriver.Create();
ipv6Driver.Bind(NetworkEndpoint.AnyIpv6.WithPort(7777));
ipv6Driver.Listen();
Finally, we add both drivers to a MultiNetworkDriver
:
var multiDriver = MultiNetworkDriver.Create();
multiDriver.AddDriver(ipv4Driver);
multiDriver.AddDriver(ipv6Driver);
With this setup, calling multiDriver.Accept
can now return connections made by IPv4 and IPv6 clients in a transparent manner. If you wish to know if any given connection was made from an IPv4 or IPv6 client, you can use GetRemoteEndpoint
to get the address of a client.
Advanced usage: per-driver pipeline definitions
All the drivers in a MultiNetworkDriver
are normally expected to have matching pipelines. If all your drivers are expected to have the same pipeline configurations, then you can simply add the pipelines directly with the CreatePipeline
method of MultiNetworkDriver
. For example:
// Add a simulator pipeline to all drivers in multiDriver.
multiDriver.CreatePipeline(typeof(SimulatorPipelineStage));
But there might be occasions where you might not want all pipelines to be configured the same way. For example, if you have a driver configured for WebSocket connections, there is no need to ever use ReliableSequencedPipelineStage
since WebSocket is built on top of TCP which already provides all the guarantees of the reliable pipeline stage.
In such situations, we can create the pipelines before adding the drivers to the MultiNetworkDriver
. Here's an example:
// Create a UDP driver with a reliable pipeline.
var udpDriver = NetworkDriver.Create(new UDPNetworkInterface());
var reliablePipeline = udpDriver.CreatePipeline(typeof(ReliableSequencedPipelineStage));
udpDriver.Bind(NetworkEndpoint.AnyIpv4.WithPort(7777));
udpDriver.Listen();
// Create a WebSocket driver with a "dummy" reliable pipeline.
var wsDriver = NetworkDriver.Create(new WebSocketNetworkInterface());
var dummyPipeline = wsDriver.CreatePipeline(typeof(NullPipelineStage));
wsDriver.Bind(NetworkEndpoint.AnyIpv4.WithPort(7778));
wsDriver.Listen();
// Add both drivers to a new MultiNetworkDriver.
var multiDriver = MultiNetworkDriver.Create();
multiDriver.AddDriver(udpDriver);
multiDriver.AddDriver(wsDriver);
In the above example, reliablePipeline
and dummyPipeline
are actually equal. Either one could be used with our MultiNetworkDriver
to send traffic reliably:
// Will send the value 42 reliably. If connection is UDP-based, then this will send
// the packet through the ReliableSequencedPipelineStage. If it is WebSocket-based,
// then this will send the packet through the NullPipelineStage, which does nothing.
multiDriver.BeginSend(reliablePipeline, connection, out var writer);
writer.WriteInt(42);
multiDriver.EndSend(writer);
Pipelines in all drivers added to a MultiNetworkDriver are expected to have symmetric functions. That is, if the first pipeline is a reliable one, then it must be so for all drivers in the MultiNetworkDriver. This is why we create a "dummy" pipeline for the WebSocket driver in the example above. We still need some pipeline to act as the reliable one, even if this pipeline does nothing.
Sharing code between server and client
You may have noticed that all examples above use MultiNetworkDriver
in a server role. While this is indeed its main intended usage, using it only as a server can be problematic if you have code shared between your server and client builds. For example, if you had common networking code that used NetworkDriver
, porting it to MultiNetworkDriver
could be difficult since that code would then need to use a MultiNetworkDriver
on the server, and a NetworkDriver
on clients.
To address this problem, MultiNetworkDriver
can also act as a container for client drivers. A non-listening driver can be added to a MultiNetworkDriver
and connected to a server in the manner below:
var clientDriver = NetworkDriver.Create();
var multiDriver = MultiNetworkDriver.Create();
var driverId = multiDriver.AddDriver(clientDriver);
var connection = multiDriver.Connect(driverId, serverEndpoint);
There is no real performance penalty from using a MultiNetworkDriver
that contains a single NetworkDriver
. So having both server and client builds rely on MultiNetworkDriver
is a good way of writing code that can be shared between them. In a way, MultiNetworkDriver
then serves the purpose of an hypothetical INetworkDriver
interface for the functionality common between NetworkDriver and MultiNetworkDriver
(the transport package does not actually provide such an interface because it would be impractical to use in Burst-compiled code).