Hi, Multiplayer Service SDK is a recent addition, but I found little information about its connection setup, like what is the difference between NetworkManager.Singleton.StartHost, that existed previously, and MultiplayerService.Instance.CreateSessionAsync (with relay network)?
I haven’t looked into it but I would expect the latter is a convenience feature since StartHost also requires you to call SetRelayServerData which is on the transport and perhaps some more things.
Assume the Services SDK is more tightly integrated with Unity Services.
Hi @Donkrokodil !
Thank you for jumping into our brand new Multiplayer Service SDK !
TL;DR: Indeed like @CodeSmile mentioned, the SDK (and by extension CreateSessionAsync) is a convenient wrapper around Unity Services and most of the setup (including the network connection).
NetworkManager.Singleton.StartHost is not deprecated and you can still use it when creating a game based on Netcode for GameObjects. It handles the network host listening and the connection from clients regardless of any service you might use. If you were to use Relay, you would have to use the Relay SDK and pass the relevant data to the UnityTransport component (see Use Relay with Netcode for GameObjects (NGO)). You would use StartHost over CreateSessionAsync to deeply customize your connection flow and the usage of all services involved and if you don’t mind writing the glue code between them.
The Multiplayer SDK introduces the concept of Session to encapsulate the glue complexity between various services (Relay, Matchmaker, Lobby, QoS and Multiplay Hosting, Distributed Authority) and handling the connection setup glue as well (both Netcode for GameObjects and Netcode for Entities). Please not that the Multiplayer SDK does include all the SDKs mentioned, so you can safely continue using them “raw” without any Session if you need to. You would use CreateSessionAsync over StartHost to easily get started and have all the connection glue code and setup handled for you with an easy customization of which services to include in the session lifecycle (direct connection, connection via code, connection via matchmaking, connection via session browsing, connection over Relay, …).
I hope it answered your question. The various documentation pages around this SDK as well as the standalone (Lobby, Relay, Matchmaker, Multiplay, …) SDKs should be updated in the following weeks. If you have any other questions or feedback on the Multiplayer SDK or the current documentation pages please feel free to voice them ! It will greatly help us identify weakpoints in our documentation that we might have missed.
I have also tried distributed authority topology with CreateSessionAsync. Suppose that one player starts the session and subscribes to the NetworkManager.Singleton.OnConnectionEvent callback to check if a new client is connected. When another client connects, the callback is triggered on the side of the session owner. session.Players.Count property correctly shows 2 players, but NetworkManager.Singleton.ConnectedClientsList.Count still shows only one player.
Piggy-backing on this thread: I have been using the old API to create a lobby, allow players to join the jobby and toggle their ready status, and then, after all players are ready allow the host to start the game using NetworkManager.Singleton.StartHost().
If I am understanding the new sdk correctly: When starting a session, not only is a lobby and relay allocation create but it also immediately starts the game - i.e. equivalent of calling NetworkManager.Singleton.StartHost(). Is that correct?
Are there any tutorials or samples of showing how to achieve the join/signal ready/start-game when all players are ready functionality using the new sdk?
Hi @MidniteOil ,
Thank you for your feedback ! When a session is created, we indeed usually go through the whole pipeline and start the NetworkManager. We are aware that using multiple steps is a very requested functionality and we have tasks aimed to introduce this feature. Unfortunately we don’t have any ETA for its release.
I see two workarounds
- Continue using the underlying SDKs (Lobby, Relay, …) within the Multiplayer SDK without the sessions API. Have a look at this documentation Migrate to the Multiplayer Services SDK.
- Use the Multiplayer SDK but do not use any
With*SessionOption (WithDirectNetwork,WithNetworkHandler,WithRelayNetwork, …), this will cause the SDK to create the session as a simple Lobby and not start it. This actually turns the session API into a simple Lobby wrapper. To actually start the session you will have to add the RelayServerData to UTP, manually start the NetworkManager, … the same as in the old API.
Thanks. That’s helpful. I definitely see the benefit of this new integrated approach. It will require some refactoring of how I create/join lobbies but I think it will be change for the better.
I do have a question around how to handle customizing the transport to use ‘wss’ to work in WebGL builds. Here’s how I did it with the old sdk:
async Task SetRelayClientData(string relayCode)
{
var transport = NetworkManager.Singleton.GetComponentInChildren<UnityTransport>();
var joinAllocation = await RelayService.Instance.JoinAllocationAsync(relayCode);
var endpoint = GetEndpointForAllocation(
joinAllocation.ServerEndpoints,
joinAllocation.RelayServer.IpV4,
joinAllocation.RelayServer.Port,
out var isSecure);
transport.SetClientRelayData(
AddressFromEndpoint(endpoint),
endpoint.Port,
joinAllocation.AllocationIdBytes,
joinAllocation.Key,
joinAllocation.ConnectionData,
joinAllocation.HostConnectionData,
isSecure);
#if UNITY_WEBGL
var relayClientData = joinAllocation.ToRelayServerData("wss");
transport.SetRelayServerData(relayClientData);
#else
var relayClientData = joinAllocation.ToRelayServerData("dtls");
transport.SetRelayServerData(relayClientData);
#endif
await PlayerConnectionsManager.Instance.StartClient();
}
async Task<string> GetRelayAllocation()
{
var regions = await RelayService.Instance.ListRegionsAsync();
var region = regions[0].Id;
var allocation = await RelayService.Instance.CreateAllocationAsync(CurrentLobby.MaxPlayers, region);
var relayCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId);
var endpoint = GetEndpointForAllocation(
allocation.ServerEndpoints,
allocation.RelayServer.IpV4,
allocation.RelayServer.Port,
out bool isSecure);
var transport = NetworkManager.Singleton.GetComponent<UnityTransport>();
transport.SetHostRelayData(AddressFromEndpoint(endpoint),
endpoint.Port,
allocation.AllocationIdBytes,
allocation.Key,
allocation.ConnectionData,
isSecure);
#if UNITY_WEBGL
SetRelayServerData(allocation, "wss");
#else
SetHostRelayData(allocation, "dtls");
#endif
return relayCode;
}
void SetRelayServerData(Allocation allocation, string connectionType)
{
var relayServerData = allocation.ToRelayServerData(connectionType);
Transport.SetRelayServerData(relayServerData);
}
async Task SetLobbyRelayCode(string relayCode)
{
var options = new UpdateLobbyOptions
{
Data = new Dictionary<string, DataObject>
{
[RelayCodeKey] = new(DataObject.VisibilityOptions.Public, relayCode)
}
};
CurrentLobby = await LobbyService.Instance.UpdateLobbyAsync(CurrentLobby.Id, options);
}
string AddressFromEndpoint(NetworkEndpoint endpoint)
{
return endpoint.Address.Split(':')[0];
}
/// <summary>
/// Determine the server endpoint for connecting to the Relay server, for either an Allocation or a JoinAllocation.
/// If DTLS encryption is available, and there's a secure server endpoint available, use that as a secure connection. Otherwise, just connect to the Relay IP unsecured.
/// </summary>
NetworkEndpoint GetEndpointForAllocation(
List<RelayServerEndpoint> endpoints,
string ip,
int port,
out bool isSecure)
{
#if ENABLE_MANAGED_UNITYTLS && !UNITY_WEBGL
foreach (RelayServerEndpoint endpoint in endpoints)
{
if (!endpoint.Secure || endpoint.Network != RelayServerEndpoint.NetworkOptions.Udp) continue;
isSecure = true;
return NetworkEndpoint.Parse(endpoint.Host, (ushort)endpoint.Port);
}
#endif
isSecure = false;
return NetworkEndpoint.Parse(ip, (ushort)port);
}
Hi @Donkrokodil ,
Sorry for the late reply. This is most probably due to how we connect players under the hood: when a player joins a “session”, he first joins the Lobby which contains the ip/port that we use under the hood to connect Netcode for Gameobjects.
What you are probably seeing is the player having joined the Lobby but not connect (or not registered) in Netcode for Gameobjects yet.
Hi @MidniteOil ,
What is your question ? Do you have a specific error or concern ?
The Migrate to the Multiplayer Services SDK page I linked in my previous message has code snippets showing how to create a RelayServerData based on a connection type which you can pass to the transport as usual.
Thanks, I’ll review the link you posted and get back to you if I still have questions.
I looked at the link and I see how you would create RelayServerData based on a connection type.
void CreateHostAllocation() {
// Set connection type to UDP in this example.
var connectionType = "udp";
// Set max connections to 4 in this example. Note the more players connected, the higher the bandwidth/latency impact.
int maxConnections = 4;
// [...]
// Use the first region as an example and create the Relay allocation
List<Region> regions = await RelayService.Instance.ListRegionsAsync();
string region = regions[0].Id;
var hostAllocation = await RelayService.Instance.CreateAllocationAsync(maxConnections, region);
var relayServerData = hostAllocation.ToRelayServerData(connectionType);
// Use the RelayServerData here
}
My question is using the new integrated session manager, which creates a lobby, relay allocation and starts a host connection with a single call, how would I create custom RelayServerData.
async void StartSessionAsHost() {
var playerProperties = await GetPlayerProperties();
var options = new SessionOptions {
MaxPlayers = 2,
IsLocked = false,
IsPrivate = false,
PlayerProperties = playerProperties
}.WithRelayNetwork(); // or WithDistributedAuthorityNetwork() to use Distributed Authority instead of Relay
ActiveSession = await MultiplayerService.Instance.CreateSessionAsync(options);
Debug.Log($"Session {ActiveSession.Id} created! Join code: {ActiveSession.Code}");
}
Hi @MidniteOil ,
I apologize, I might have had a misunderstanding in my previous response. I want to make sure we are on the same page. When I suggested the workarounds, I meant that both use the multiplayer SDK, both use Relay and Netcode for Gameobject and both are mutually exclusive.
The main difference between the two approaches is whether you want to use a part (more on this later) of the Session API or keep using the Lobby and Relay APIs.
If you want to keep using the Lobby and Relay APIs, the migration guide helps you transitioning to the Multiplayer SDK with a minimal impact on your code base. Here is what it looks like for the host (I omitted the try/catch for simplicity)
// Here is the main function
async Task CreateHostGame() {
await Authenticate();
var lobby = await CreateLobby();
// Later, when all players are ready
var hostAllocation = await CreateHostAllocation(lobby);
}
// Authenticate before using the services.
async Task Authenticate() {
await UnityServices.InitializeAsync();
if (!AuthenticationService.Instance.IsSignedIn)
{
await AuthenticationService.Instance.SignInAnonymouslyAsync();
}
}
// Create the lobby
async Task<Lobby> CreateLobby() {
string lobbyName = "new lobby";
int maxPlayers = 4;
CreateLobbyOptions options = new CreateLobbyOptions();
Lobby lobby = await LobbyService.Instance.CreateLobbyAsync(lobbyName, maxPlayers, options);
return lobby;
}
// Create the Relay allocation and start the netcode
async Task<string> CreateHostAllocation(Lobby lobby) {
// Set connection type to UDP in this example.
var connectionType = "wss";
// Use the first region as an example and create the Relay allocation
List<Region> regions = await RelayService.Instance.ListRegionsAsync();
string region = regions[0].Id;
var hostAllocation = await RelayService.Instance.CreateAllocationAsync(lobby.maxPlayers, region);
var relayServerData = hostAllocation.ToRelayServerData(connectionType);
var currentPlayer = lobby.Players.Find(p => p.Id == lobby.HostId);
currentPlayer.AllocationId = hostAllocation.AllocationId;
NetworkManager.Singleton.GetComponent<UnityTransport>().SetRelayServerData(relayServerData);
return hostAllocation;
}
If you want to use the Session APIs instead, notice most of the integrations (with Relay and NGO) will be disabled. The Session API will only work as a Lobby wrapper. Here is what it would look like
// Here is the main function
async Task CreateHostGame() {
await Authenticate();
var session = await CreateSession();
// Later, when all players are ready
var hostAllocation = await CreateHostAllocation(session );
}
// Authenticate before using the services.
async Task Authenticate() {
await UnityServices.InitializeAsync();
if (!AuthenticationService.Instance.IsSignedIn)
{
await AuthenticationService.Instance.SignInAnonymouslyAsync();
}
}
// Create the lobby
async Task<Session> CreateSession() {
var options = new SessionOptions
{
MaxPlayers = 2
};
// Notice we do not use any "With" option because we want to manage the Relay allocation and the Netcode start on our own
// this will disable the automated integrations for this specific session
var session = await MultiplayerService.Instance.CreateSessionAsync(options);
return session;
}
// Create the Relay allocation and start the netcode
async Task<string> CreateHostAllocation(Session session) {
// Set connection type to UDP in this example.
var connectionType = "wss";
// Use the first region as an example and create the Relay allocation
List<Region> regions = await RelayService.Instance.ListRegionsAsync();
string region = regions[0].Id;
var hostAllocation = await RelayService.Instance.CreateAllocationAsync(session.MaxPlayers, region);
var relayServerData = hostAllocation.ToRelayServerData(connectionType);
session.CurrentPlayer.SetAllocationId(hostAllocation.AllocationId);
await session.SaveCurrentPlayerDataAsync();
NetworkManager.Singleton.GetComponent<UnityTransport>().SetRelayServerData(relayServerData);
return hostAllocation;
}