您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
421 行
15 KiB
421 行
15 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading.Tasks;
|
|
using Unity.Multiplayer.Samples.BossRoom.Shared.Infrastructure;
|
|
using Unity.Multiplayer.Samples.BossRoom.Shared.Net.UnityServices.Infrastructure;
|
|
using Unity.Services.Authentication;
|
|
using Unity.Services.Lobbies;
|
|
using Unity.Services.Lobbies.Models;
|
|
using UnityEngine;
|
|
using VContainer;
|
|
using VContainer.Unity;
|
|
|
|
namespace Unity.Multiplayer.Samples.BossRoom.Shared.Net.UnityServices.Lobbies
|
|
{
|
|
/// <summary>
|
|
/// An abstraction layer between the direct calls into the Lobby API and the outcomes you actually want.
|
|
/// </summary>
|
|
public class LobbyServiceFacade : IDisposable, IStartable
|
|
{
|
|
[Inject] LifetimeScope m_ParentScope;
|
|
[Inject] UpdateRunner m_UpdateRunner;
|
|
[Inject] LocalLobby m_LocalLobby;
|
|
[Inject] LocalLobbyUser m_LocalUser;
|
|
[Inject] IPublisher<UnityServiceErrorMessage> m_UnityServiceErrorMessagePub;
|
|
[Inject] IPublisher<LobbyListFetchedMessage> m_LobbyListFetchedPub;
|
|
|
|
const float k_HeartbeatPeriod = 8; // The heartbeat must be rate-limited to 5 calls per 30 seconds. We'll aim for longer in case periods don't align.
|
|
float m_HeartbeatTime = 0;
|
|
|
|
LifetimeScope m_ServiceScope;
|
|
LobbyAPIInterface m_LobbyApiInterface;
|
|
JoinedLobbyContentHeartbeat m_JoinedLobbyContentHeartbeat;
|
|
|
|
RateLimitCooldown m_RateLimitQuery;
|
|
RateLimitCooldown m_RateLimitJoin;
|
|
RateLimitCooldown m_RateLimitQuickJoin;
|
|
RateLimitCooldown m_RateLimitHost;
|
|
|
|
public Lobby CurrentUnityLobby { get; private set; }
|
|
|
|
bool m_IsTracking = false;
|
|
|
|
public void Start()
|
|
{
|
|
m_ServiceScope = m_ParentScope.CreateChild(builder =>
|
|
{
|
|
builder.Register<JoinedLobbyContentHeartbeat>(Lifetime.Singleton);
|
|
builder.Register<LobbyAPIInterface>(Lifetime.Singleton);
|
|
});
|
|
|
|
m_LobbyApiInterface = m_ServiceScope.Container.Resolve<LobbyAPIInterface>();
|
|
m_JoinedLobbyContentHeartbeat = m_ServiceScope.Container.Resolve<JoinedLobbyContentHeartbeat>();
|
|
|
|
//See https://docs.unity.com/lobby/rate-limits.html
|
|
m_RateLimitQuery = new RateLimitCooldown(1f);
|
|
m_RateLimitJoin = new RateLimitCooldown(3f);
|
|
m_RateLimitQuickJoin = new RateLimitCooldown(10f);
|
|
m_RateLimitHost = new RateLimitCooldown(3f);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
EndTracking();
|
|
if (m_ServiceScope != null)
|
|
{
|
|
m_ServiceScope.Dispose();
|
|
}
|
|
}
|
|
|
|
public void SetRemoteLobby(Lobby lobby)
|
|
{
|
|
CurrentUnityLobby = lobby;
|
|
m_LocalLobby.ApplyRemoteData(lobby);
|
|
}
|
|
|
|
public void BeginTracking()
|
|
{
|
|
if (!m_IsTracking)
|
|
{
|
|
m_IsTracking = true;
|
|
// 2s update cadence is arbitrary and is here to demonstrate the fact that this update can be rather infrequent
|
|
// the actual rate limits are tracked via the RateLimitCooldown objects defined above
|
|
m_UpdateRunner.Subscribe(UpdateLobby, 2f);
|
|
m_JoinedLobbyContentHeartbeat.BeginTracking();
|
|
}
|
|
}
|
|
|
|
public Task EndTracking()
|
|
{
|
|
var task = Task.CompletedTask;
|
|
if (CurrentUnityLobby != null)
|
|
{
|
|
CurrentUnityLobby = null;
|
|
|
|
if (!string.IsNullOrEmpty(m_LocalLobby?.LobbyID))
|
|
{
|
|
task = LeaveLobbyAsync(m_LocalLobby?.LobbyID);
|
|
}
|
|
|
|
m_LocalUser.ResetState();
|
|
m_LocalLobby?.Reset(m_LocalUser);
|
|
}
|
|
|
|
if (m_IsTracking)
|
|
{
|
|
m_UpdateRunner.Unsubscribe(UpdateLobby);
|
|
m_IsTracking = false;
|
|
m_HeartbeatTime = 0;
|
|
m_JoinedLobbyContentHeartbeat.EndTracking();
|
|
}
|
|
|
|
return task;
|
|
}
|
|
|
|
async void UpdateLobby(float unused)
|
|
{
|
|
if (!m_RateLimitQuery.CanCall)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var lobby = await m_LobbyApiInterface.GetLobby(m_LocalLobby.LobbyID);
|
|
|
|
CurrentUnityLobby = lobby;
|
|
m_LocalLobby.ApplyRemoteData(lobby);
|
|
|
|
// as client, check if host is still in lobby
|
|
if (!m_LocalUser.IsHost)
|
|
{
|
|
foreach (var lobbyUser in m_LocalLobby.LobbyUsers)
|
|
{
|
|
if (lobbyUser.Value.IsHost)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
m_UnityServiceErrorMessagePub.Publish(new UnityServiceErrorMessage("Host left the lobby", "Disconnecting.", UnityServiceErrorMessage.Service.Lobby));
|
|
await EndTracking();
|
|
// no need to disconnect Netcode, it should already be handled by Netcode's callback to disconnect
|
|
}
|
|
}
|
|
catch (LobbyServiceException e)
|
|
{
|
|
if (e.Reason == LobbyExceptionReason.RateLimited)
|
|
{
|
|
m_RateLimitQuery.PutOnCooldown();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempt to create a new lobby and then join it.
|
|
/// </summary>
|
|
public async Task<(bool Success, Lobby Lobby)> TryCreateLobbyAsync(string lobbyName, int maxPlayers, bool isPrivate)
|
|
{
|
|
if (!m_RateLimitHost.CanCall)
|
|
{
|
|
Debug.LogWarning("Create Lobby hit the rate limit.");
|
|
return (false, null);
|
|
}
|
|
|
|
try
|
|
{
|
|
var lobby = await m_LobbyApiInterface.CreateLobby(AuthenticationService.Instance.PlayerId, lobbyName, maxPlayers, isPrivate, m_LocalUser.GetDataForUnityServices(), null);
|
|
return (true, lobby);
|
|
}
|
|
catch (LobbyServiceException e)
|
|
{
|
|
if (e.Reason == LobbyExceptionReason.RateLimited)
|
|
{
|
|
m_RateLimitHost.PutOnCooldown();
|
|
}
|
|
}
|
|
|
|
return (false, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempt to join an existing lobby. Will try to join via code, if code is null - will try to join via ID.
|
|
/// </summary>
|
|
public async Task<(bool Success, Lobby Lobby)> TryJoinLobbyAsync(string lobbyId, string lobbyCode)
|
|
{
|
|
if (!m_RateLimitJoin.CanCall ||
|
|
(lobbyId == null && lobbyCode == null))
|
|
{
|
|
Debug.LogWarning("Join Lobby hit the rate limit.");
|
|
return (false, null);
|
|
}
|
|
|
|
try
|
|
{
|
|
if (!string.IsNullOrEmpty(lobbyCode))
|
|
{
|
|
var lobby = await m_LobbyApiInterface.JoinLobbyByCode(AuthenticationService.Instance.PlayerId, lobbyCode, m_LocalUser.GetDataForUnityServices());
|
|
return (true, lobby);
|
|
}
|
|
else
|
|
{
|
|
var lobby = await m_LobbyApiInterface.JoinLobbyById(AuthenticationService.Instance.PlayerId, lobbyId, m_LocalUser.GetDataForUnityServices());
|
|
return (true, lobby);
|
|
}
|
|
}
|
|
catch (LobbyServiceException e)
|
|
{
|
|
if (e.Reason == LobbyExceptionReason.RateLimited)
|
|
{
|
|
m_RateLimitJoin.PutOnCooldown();
|
|
}
|
|
}
|
|
|
|
return (false, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempt to join the first lobby among the available lobbies that match the filtered onlineMode.
|
|
/// </summary>
|
|
public async Task<(bool Success, Lobby Lobby)> TryQuickJoinLobbyAsync()
|
|
{
|
|
if (!m_RateLimitQuickJoin.CanCall)
|
|
{
|
|
Debug.LogWarning("Quick Join Lobby hit the rate limit.");
|
|
return (false, null);
|
|
}
|
|
|
|
try
|
|
{
|
|
var lobby = await m_LobbyApiInterface.QuickJoinLobby(AuthenticationService.Instance.PlayerId, m_LocalUser.GetDataForUnityServices());
|
|
return (true, lobby);
|
|
}
|
|
catch (LobbyServiceException e)
|
|
{
|
|
if (e.Reason == LobbyExceptionReason.RateLimited)
|
|
{
|
|
m_RateLimitQuickJoin.PutOnCooldown();
|
|
}
|
|
}
|
|
|
|
return (false, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used for getting the list of all active lobbies, without needing full info for each.
|
|
/// </summary>
|
|
public async Task RetrieveAndPublishLobbyListAsync()
|
|
{
|
|
if (!m_RateLimitQuery.CanCall)
|
|
{
|
|
Debug.LogWarning("Retrieve Lobby list hit the rate limit. Will try again soon...");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var response = await m_LobbyApiInterface.QueryAllLobbies();
|
|
m_LobbyListFetchedPub.Publish(new LobbyListFetchedMessage(LocalLobby.CreateLocalLobbies(response)));
|
|
}
|
|
catch (LobbyServiceException e)
|
|
{
|
|
if (e.Reason == LobbyExceptionReason.RateLimited)
|
|
{
|
|
m_RateLimitQuery.PutOnCooldown();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempt to leave a lobby
|
|
/// </summary>
|
|
public async Task LeaveLobbyAsync(string lobbyId)
|
|
{
|
|
string uasId = AuthenticationService.Instance.PlayerId;
|
|
try
|
|
{
|
|
await m_LobbyApiInterface.RemovePlayerFromLobby(uasId, lobbyId);
|
|
}
|
|
catch (LobbyServiceException e)
|
|
when (e is { Reason: LobbyExceptionReason.LobbyNotFound })
|
|
{
|
|
// If Lobby is not found, it has already been deleted. No need to throw here.
|
|
}
|
|
|
|
}
|
|
|
|
public async void RemovePlayerFromLobbyAsync(string uasId, string lobbyId)
|
|
{
|
|
if (m_LocalUser.IsHost)
|
|
{
|
|
await m_LobbyApiInterface.RemovePlayerFromLobby(uasId, lobbyId);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("Only the host can remove other players from the lobby.");
|
|
}
|
|
}
|
|
|
|
public async void DeleteLobbyAsync(string lobbyId)
|
|
{
|
|
if (m_LocalUser.IsHost)
|
|
{
|
|
await m_LobbyApiInterface.DeleteLobby(lobbyId);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("Only the host can delete a lobby.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempt to push a set of key-value pairs associated with the local player which will overwrite any existing data for these keys.
|
|
/// </summary>
|
|
public async Task UpdatePlayerDataAsync(Dictionary<string, PlayerDataObject> data)
|
|
{
|
|
if (!m_RateLimitQuery.CanCall)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var result = await m_LobbyApiInterface.UpdatePlayer(CurrentUnityLobby.Id, AuthenticationService.Instance.PlayerId, data, null, null);
|
|
|
|
if (result != null)
|
|
{
|
|
CurrentUnityLobby = result; // Store the most up-to-date lobby now since we have it, instead of waiting for the next heartbeat.
|
|
}
|
|
}
|
|
catch (LobbyServiceException e)
|
|
{
|
|
if (e.Reason == LobbyExceptionReason.RateLimited)
|
|
{
|
|
m_RateLimitQuery.PutOnCooldown();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lobby can be provided info about Relay (or any other remote allocation) so it can add automatic disconnect handling.
|
|
/// </summary>
|
|
public async Task UpdatePlayerRelayInfoAsync(string allocationId, string connectionInfo)
|
|
{
|
|
if (!m_RateLimitQuery.CanCall)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
await m_LobbyApiInterface.UpdatePlayer(CurrentUnityLobby.Id, AuthenticationService.Instance.PlayerId, new Dictionary<string, PlayerDataObject>(), allocationId, connectionInfo);
|
|
}
|
|
catch (LobbyServiceException e)
|
|
{
|
|
if (e.Reason == LobbyExceptionReason.RateLimited)
|
|
{
|
|
m_RateLimitQuery.PutOnCooldown();
|
|
}
|
|
|
|
//todo - retry logic? SDK is supposed to handle this eventually
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempt to update a set of key-value pairs associated with a given lobby.
|
|
/// </summary>
|
|
public async Task UpdateLobbyDataAsync(Dictionary<string, DataObject> data)
|
|
{
|
|
if (!m_RateLimitQuery.CanCall)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var dataCurr = CurrentUnityLobby.Data ?? new Dictionary<string, DataObject>();
|
|
|
|
foreach (var dataNew in data)
|
|
{
|
|
if (dataCurr.ContainsKey(dataNew.Key))
|
|
{
|
|
dataCurr[dataNew.Key] = dataNew.Value;
|
|
}
|
|
else
|
|
{
|
|
dataCurr.Add(dataNew.Key, dataNew.Value);
|
|
}
|
|
}
|
|
|
|
//we would want to lock lobbies from appearing in queries if we're in relay mode and the relay isn't fully set up yet
|
|
var shouldLock = string.IsNullOrEmpty(m_LocalLobby.RelayJoinCode);
|
|
|
|
try
|
|
{
|
|
var result = await m_LobbyApiInterface.UpdateLobby(CurrentUnityLobby.Id, dataCurr, shouldLock);
|
|
|
|
if (result != null)
|
|
{
|
|
CurrentUnityLobby = result;
|
|
}
|
|
}
|
|
catch (LobbyServiceException e)
|
|
{
|
|
if (e.Reason == LobbyExceptionReason.RateLimited)
|
|
{
|
|
m_RateLimitQuery.PutOnCooldown();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lobby requires a periodic ping to detect rooms that are still active, in order to mitigate "zombie" lobbies.
|
|
/// </summary>
|
|
public void DoLobbyHeartbeat(float dt)
|
|
{
|
|
m_HeartbeatTime += dt;
|
|
if (m_HeartbeatTime > k_HeartbeatPeriod)
|
|
{
|
|
m_HeartbeatTime -= k_HeartbeatPeriod;
|
|
m_LobbyApiInterface.SendHeartbeatPing(CurrentUnityLobby.Id);
|
|
}
|
|
}
|
|
}
|
|
}
|