浏览代码

Merging the Open_beta_update branch. The behavior for error pop-ups isn't quite done yet, but we've gotten a few other updates and fixes wrapped up with that so we'll just merge this all together now and branch of completion of that task next.

/main/staging
nathaniel.buck@unity3d.com 3 年前
当前提交
ea4a1137
共有 39 个文件被更改,包括 1609 次插入297 次删除
  1. 2
      Assets/Prefabs/UI/RenamePopup.prefab
  2. 131
      Assets/Scenes/mainScene.unity
  3. 2
      Assets/Scripts/Auth/SubIdentity_Authentication.cs
  4. 14
      Assets/Scripts/Game/GameManager.cs
  5. 1
      Assets/Scripts/Game/LobbyServiceData.cs
  6. 42
      Assets/Scripts/Infrastructure/LogHandler.cs
  7. 125
      Assets/Scripts/Lobby/LobbyAPIInterface.cs
  8. 34
      Assets/Scripts/Lobby/LobbyAsyncRequests.cs
  9. 33
      Assets/Scripts/Relay/RelayAPIInterface.cs
  10. 45
      Assets/Scripts/Tests/PlayMode/LobbyRoundtripTests.cs
  11. 12
      Assets/Scripts/UI/SpinnerUI.cs
  12. 20
      Packages/manifest.json
  13. 44
      Packages/packages-lock.json
  14. 44
      ProjectSettings/PackageManagerSettings.asset
  15. 6
      ProjectSettings/ProjectSettings.asset
  16. 161
      README.md
  17. 1001
      Assets/Prefabs/UI/PopUpUI.prefab
  18. 7
      Assets/Prefabs/UI/PopUpUI.prefab.meta
  19. 44
      Assets/Scripts/Infrastructure/AsyncRequest.cs
  20. 11
      Assets/Scripts/Infrastructure/AsyncRequest.cs.meta
  21. 32
      Assets/Scripts/Infrastructure/LogHandlerSettings.cs
  22. 11
      Assets/Scripts/Infrastructure/LogHandlerSettings.cs.meta
  23. 25
      Assets/Scripts/UI/PopUpUI.cs
  24. 11
      Assets/Scripts/UI/PopUpUI.cs.meta
  25. 8
      Assets/StreamingAssets.meta
  26. 3
      Assets/TextMesh Pro/Fonts.meta.private.0
  27. 7
      Assets/TextMesh Pro/Fonts.meta.private.0.meta
  28. 3
      Assets/TextMesh Pro/Resources.meta.private.0
  29. 7
      Assets/TextMesh Pro/Resources.meta.private.0.meta
  30. 3
      Assets/TextMesh Pro/Shaders.meta.private.0
  31. 7
      Assets/TextMesh Pro/Shaders.meta.private.0.meta
  32. 3
      Assets/TextMesh Pro/Sprites.meta.private.0
  33. 7
      Assets/TextMesh Pro/Sprites.meta.private.0.meta

2
Assets/Prefabs/UI/RenamePopup.prefab


m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 1
m_PresetInfoIsWorld: 0
--- !u!225 &7663265756039998349
CanvasGroup:
m_ObjectHideFlags: 0

131
Assets/Scenes/mainScene.unity


m_Script: {fileID: 11500000, guid: 5b3b588e7ae40ec4ca35fdb9404513ab, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1 &1095306254
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1095306259}
- component: {fileID: 1095306258}
- component: {fileID: 1095306257}
- component: {fileID: 1095306256}
- component: {fileID: 1095306255}
m_Layer: 5
m_Name: LogManager
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1095306255
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1095306254}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 089231d71bcfb8d479b4f8b778b1026f, type: 3}
m_Name:
m_EditorClassIdentifier:
m_editorLogVerbosity: 0
m_popUpPrefab: {fileID: 2974111728825125460, guid: 79d6084439b78bb4eaf5232cb953fd87,
type: 3}
m_reaction:
m_logMessageCallback:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 1095306255}
m_TargetAssemblyTypeName: LobbyRelaySample.LogHandlerSettings, LobbyRelaySample
m_MethodName: SpawnErrorPopup
m_Mode: 0
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!114 &1095306256
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1095306254}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreReversedGraphics: 1
m_BlockingObjects: 0
m_BlockingMask:
serializedVersion: 2
m_Bits: 4294967295
--- !u!114 &1095306257
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1095306254}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3}
m_Name:
m_EditorClassIdentifier:
m_UiScaleMode: 1
m_ReferencePixelsPerUnit: 100
m_ScaleFactor: 1
m_ReferenceResolution: {x: 800, y: 600}
m_ScreenMatchMode: 0
m_MatchWidthOrHeight: 0
m_PhysicalUnit: 3
m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 0
--- !u!223 &1095306258
Canvas:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1095306254}
m_Enabled: 1
serializedVersion: 3
m_RenderMode: 0
m_Camera: {fileID: 0}
m_PlaneDistance: 100
m_PixelPerfect: 0
m_ReceivesEvents: 1
m_OverrideSorting: 0
m_OverridePixelPerfect: 0
m_SortingBucketNormalizedSize: 0
m_AdditionalShaderChannelsFlag: 25
m_SortingLayerID: 0
m_SortingOrder: 1
m_TargetDisplay: 0
--- !u!224 &1095306259
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1095306254}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0, y: 0, z: 0}
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 4
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0, y: 0}
--- !u!114 &1217229506 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 354886978664675623, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}

2
Assets/Scripts/Auth/SubIdentity_Authentication.cs


private async void DoSignIn(Action onSigninComplete)
{
await UnityServices.Initialize();
await UnityServices.InitializeAsync();
AuthenticationService.Instance.SignedIn += OnSignInChange;
AuthenticationService.Instance.SignedOut += OnSignInChange;

14
Assets/Scripts/Game/GameManager.cs


/// </summary>
public class GameManager : MonoBehaviour, IReceiveMessages
{
[SerializeField]
[Tooltip("Only logs of this level or higher will appear in the console.")]
private LogMode m_logMode = LogMode.Critical;
/// <summary>
/// All these should be assigned the observers in the scene at the start.
/// </summary>

private LobbyColor m_lobbyColorFilter;
#region Setup
private void Awake()
{
// Do some arbitrary operations to instantiate singletons.

LogHandler.Get().mode = m_logMode;
Locator.Get.Provide(new Auth.Identity(OnAuthSignIn));
Application.wantsToQuit += OnWantToQuit;
}

foreach (var userObs in m_LocalUserObservers)
userObs.BeginObserving(m_localUser);
}
#endregion
/// <summary>

OnLobbiesQueried(lobby.ToLocalLobby.Convert(qr));
},
er => {
long errorLong = 0;
if (er != null)
errorLong = er.Status;
OnLobbyQueryFailed(errorLong);
OnLobbyQueryFailed();
},
m_lobbyColorFilter);
}

m_lobbyServiceData.CurrentLobbies = newLobbyDict;
}
private void OnLobbyQueryFailed(long errorCode)
private void OnLobbyQueryFailed()
m_lobbyServiceData.lastErrorCode = errorCode;
m_lobbyServiceData.State = LobbyQueryState.Error;
}

1
Assets/Scripts/Game/LobbyServiceData.cs


{
LobbyQueryState m_CurrentState = LobbyQueryState.Empty;
public long lastErrorCode;
public LobbyQueryState State
{
get { return m_CurrentState; }

42
Assets/Scripts/Infrastructure/LogHandler.cs


using System;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;
namespace LobbyRelaySample

static LogHandler s_instance;
ILogHandler m_DefaultLogHandler = Debug.unityLogger.logHandler; // Store the default logger that prints to console.
ErrorReaction m_reaction;
public static LogHandler Get()
{

return s_instance;
}
public void SetLogReactions(ErrorReaction reactions)
{
m_reaction = reactions;
}
public void LogFormat(LogType logType, Object context, string format, params object[] args)
{
if (logType == LogType.Exception) // Exceptions are captured by LogException and should always be logged.

public void LogException(Exception exception, Object context)
{
LogReaction(exception);
}
private void LogReaction(Exception exception)
{
m_reaction?.Filter(exception);
}
}
/// <summary>
/// The idea here is to present the most relevant error first.
/// </summary>
[Serializable]
public class ErrorReaction
{
public UnityEvent<string> m_logMessageCallback;
public void Filter(Exception exception)
{
string message = "";
var rawExceptionMessage = "";
// We want to ensure the most relevant error message is on top.
if (exception.InnerException != null)
rawExceptionMessage = exception.InnerException.ToString();
else
rawExceptionMessage = exception.ToString();
var firstLineIndex = rawExceptionMessage.IndexOf("\n");
var firstRelayString = rawExceptionMessage.Substring(0, firstLineIndex);
message = firstRelayString;
if (string.IsNullOrEmpty(message))
return;
m_logMessageCallback?.Invoke(message);
}
}
}

125
Assets/Scripts/Lobby/LobbyAPIInterface.cs


using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Services.Lobbies.Lobby;
using Unity.Services.Lobbies.Models;
namespace LobbyRelaySample.lobby

/// </summary>
public static class LobbyAPIInterface
{
/// <summary>
/// API calls are asynchronous, but for debugging and other reasons we want to reify them as objects so that they can be monitored.
/// </summary>
private class InProgressRequest<T>
{
public InProgressRequest(Task<T> task, Action<T> onComplete)
{
DoRequest(task, onComplete);
}
private async void DoRequest(Task<T> task, Action<T> onComplete)
{
T result = default;
string currentTrace = System.Environment.StackTrace; // If we don't get the calling context here, it's lost once the async operation begins.
try {
result = await task;
} catch (Exception e) {
Exception eFull = new Exception($"Call stack before async call:\n{currentTrace}\n", e);
throw eFull;
} finally {
onComplete?.Invoke(result);
}
}
}
public static void CreateLobbyAsync(string requesterUASId, string lobbyName, int maxPlayers, bool isPrivate, Dictionary<string, PlayerDataObject> localUserData, Action<Response<Lobby>> onComplete)
public static void CreateLobbyAsync(string requesterUASId, string lobbyName, int maxPlayers, bool isPrivate, Dictionary<string, PlayerDataObject> localUserData, Action<Lobby> onComplete)
CreateLobbyRequest createRequest = new CreateLobbyRequest(new CreateRequest(
name: lobbyName,
player: new Player(id: requesterUASId, data: localUserData),
maxPlayers: maxPlayers,
isPrivate: isPrivate
));
var task = LobbyService.LobbyApiClient.CreateLobbyAsync(createRequest);
new InProgressRequest<Response<Lobby>>(task, onComplete);
CreateLobbyOptions createOptions = new CreateLobbyOptions
{
IsPrivate = isPrivate,
Player = new Player(id: requesterUASId, data: localUserData)
};
var task = Lobbies.Instance.CreateLobbyAsync(lobbyName, maxPlayers, createOptions);
AsyncRequest.DoRequest(task, onComplete);
public static void DeleteLobbyAsync(string lobbyId, Action<Response> onComplete)
public static void DeleteLobbyAsync(string lobbyId, Action onComplete)
DeleteLobbyRequest deleteRequest = new DeleteLobbyRequest(lobbyId);
var task = LobbyService.LobbyApiClient.DeleteLobbyAsync(deleteRequest);
new InProgressRequest<Response>(task, onComplete);
var task = Lobbies.Instance.DeleteLobbyAsync(lobbyId);
AsyncRequest.DoRequest(task, onComplete);
public static void JoinLobbyAsync_ByCode(string requesterUASId, string lobbyCode, Dictionary<string, PlayerDataObject> localUserData, Action<Response<Lobby>> onComplete)
public static void JoinLobbyAsync_ByCode(string requesterUASId, string lobbyCode, Dictionary<string, PlayerDataObject> localUserData, Action<Lobby> onComplete)
JoinLobbyByCodeRequest joinRequest = new JoinLobbyByCodeRequest(new JoinByCodeRequest(lobbyCode, new Player(id: requesterUASId, data: localUserData)));
var task = LobbyService.LobbyApiClient.JoinLobbyByCodeAsync(joinRequest);
new InProgressRequest<Response<Lobby>>(task, onComplete);
JoinLobbyByCodeOptions joinOptions = new JoinLobbyByCodeOptions { Player = new Player(id: requesterUASId, data: localUserData) };
var task = Lobbies.Instance.JoinLobbyByCodeAsync(lobbyCode, joinOptions);
AsyncRequest.DoRequest(task, onComplete);
public static void JoinLobbyAsync_ById(string requesterUASId, string lobbyId, Dictionary<string, PlayerDataObject> localUserData, Action<Response<Lobby>> onComplete)
public static void JoinLobbyAsync_ById(string requesterUASId, string lobbyId, Dictionary<string, PlayerDataObject> localUserData, Action<Lobby> onComplete)
JoinLobbyByIdRequest joinRequest = new JoinLobbyByIdRequest(lobbyId, new Player(id: requesterUASId, data: localUserData));
var task = LobbyService.LobbyApiClient.JoinLobbyByIdAsync(joinRequest);
new InProgressRequest<Response<Lobby>>(task, onComplete);
JoinLobbyByIdOptions joinOptions = new JoinLobbyByIdOptions { Player = new Player(id: requesterUASId, data: localUserData) };
var task = Lobbies.Instance.JoinLobbyByIdAsync(lobbyId, joinOptions);
AsyncRequest.DoRequest(task, onComplete);
public static void LeaveLobbyAsync(string requesterUASId, string lobbyId, Action<Response> onComplete)
public static void LeaveLobbyAsync(string requesterUASId, string lobbyId, Action onComplete)
RemovePlayerRequest leaveRequest = new RemovePlayerRequest(lobbyId, requesterUASId);
var task = LobbyService.LobbyApiClient.RemovePlayerAsync(leaveRequest);
new InProgressRequest<Response>(task, onComplete);
var task = Lobbies.Instance.RemovePlayerAsync(lobbyId, requesterUASId);
AsyncRequest.DoRequest(task, onComplete);
public static void QueryAllLobbiesAsync(List<QueryFilter> filters, Action<Response<QueryResponse>> onComplete)
public static void QueryAllLobbiesAsync(List<QueryFilter> filters, Action<QueryResponse> onComplete)
QueryLobbiesRequest queryRequest = new QueryLobbiesRequest(new QueryRequest(count: k_maxLobbiesToShow, filter: filters));
var task = LobbyService.LobbyApiClient.QueryLobbiesAsync(queryRequest);
new InProgressRequest<Response<QueryResponse>>(task, onComplete);
QueryLobbiesOptions queryOptions = new QueryLobbiesOptions
{
Count = k_maxLobbiesToShow,
Filters = filters
};
var task = Lobbies.Instance.QueryLobbiesAsync(queryOptions);
AsyncRequest.DoRequest(task, onComplete);
public static void GetLobbyAsync(string lobbyId, Action<Response<Lobby>> onComplete)
public static void GetLobbyAsync(string lobbyId, Action<Lobby> onComplete)
GetLobbyRequest getRequest = new GetLobbyRequest(lobbyId);
var task = LobbyService.LobbyApiClient.GetLobbyAsync(getRequest);
new InProgressRequest<Response<Lobby>>(task, onComplete);
var task = Lobbies.Instance.GetLobbyAsync(lobbyId);
AsyncRequest.DoRequest(task, onComplete);
public static void UpdateLobbyAsync(string lobbyId, Dictionary<string, DataObject> data, Action<Response<Lobby>> onComplete)
public static void UpdateLobbyAsync(string lobbyId, Dictionary<string, DataObject> data, Action<Lobby> onComplete)
UpdateLobbyRequest updateRequest = new UpdateLobbyRequest(lobbyId, new UpdateRequest(
data: data
));
var task = LobbyService.LobbyApiClient.UpdateLobbyAsync(updateRequest);
new InProgressRequest<Response<Lobby>>(task, onComplete);
UpdateLobbyOptions updateOptions = new UpdateLobbyOptions { Data = data };
var task = Lobbies.Instance.UpdateLobbyAsync(lobbyId, updateOptions);
AsyncRequest.DoRequest(task, onComplete);
public static void UpdatePlayerAsync(string lobbyId, string playerId, Dictionary<string, PlayerDataObject> data, Action<Response<Lobby>> onComplete, string allocationId, string connectionInfo)
public static void UpdatePlayerAsync(string lobbyId, string playerId, Dictionary<string, PlayerDataObject> data, Action<Lobby> onComplete, string allocationId, string connectionInfo)
UpdatePlayerRequest updateRequest = new UpdatePlayerRequest(lobbyId, playerId, new PlayerUpdateRequest(
data: data,
allocationId: allocationId,
connectionInfo: connectionInfo
));
var task = LobbyService.LobbyApiClient.UpdatePlayerAsync(updateRequest);
new InProgressRequest<Response<Lobby>>(task, onComplete);
UpdatePlayerOptions updateOptions = new UpdatePlayerOptions
{
Data = data,
AllocationId = allocationId,
ConnectionInfo = connectionInfo
};
var task = Lobbies.Instance.UpdatePlayerAsync(lobbyId, playerId, updateOptions);
AsyncRequest.DoRequest(task, onComplete);
HeartbeatRequest request = new HeartbeatRequest(lobbyId);
var task = LobbyService.LobbyApiClient.HeartbeatAsync(request);
new InProgressRequest<Response>(task, null);
var task = Lobbies.Instance.SendHeartbeatPingAsync(lobbyId);
AsyncRequest.DoRequest(task, null);
}
}
}

34
Assets/Scripts/Lobby/LobbyAsyncRequests.cs


Locator.Get.UpdateSlow.Subscribe(UpdateLobby, 0.5f); // Shouldn't need to unsubscribe since this instance won't be replaced. 0.5s is arbitrary; the rate limits are tracked later.
}
private static bool IsSuccessful(Response response)
{
return response != null && response.Status >= 200 && response.Status < 300; // Uses HTTP status codes, so 2xx is a success.
}
#region Once connected to a lobby, cache the local lobby object so we don't query for it for every lobby operation.
// (This assumes that the player will be actively in just one lobby at a time, though they could passively be in more.)
private string m_currentLobbyId = null;

string uasId = AuthenticationService.Instance.PlayerId;
LobbyAPIInterface.CreateLobbyAsync(uasId, lobbyName, maxPlayers, isPrivate, CreateInitialPlayerData(localUser), OnLobbyCreated);
void OnLobbyCreated(Response<Lobby> response)
void OnLobbyCreated(Lobby response)
if (!IsSuccessful(response))
if (response == null)
{
var pendingLobby = response.Result;
onSuccess?.Invoke(pendingLobby); // The Create request automatically joins the lobby, so we need not take further action.
}
onSuccess?.Invoke(response); // The Create request automatically joins the lobby, so we need not take further action.
}
}

else
LobbyAPIInterface.JoinLobbyAsync_ByCode(uasId, lobbyCode, CreateInitialPlayerData(localUser), OnLobbyJoined);
void OnLobbyJoined(Response<Lobby> response)
void OnLobbyJoined(Lobby response)
if (!IsSuccessful(response))
if (response == null)
onSuccess?.Invoke(response?.Result);
onSuccess?.Invoke(response);
}
}

/// <param name="onListRetrieved">If called with null, retrieval was unsuccessful. Else, this will be given a list of contents to display, as pairs of a lobby code and a display string for that lobby.</param>
public void RetrieveLobbyListAsync(Action<QueryResponse> onListRetrieved, Action<Response<QueryResponse>> onError = null, LobbyColor limitToColor = LobbyColor.None)
public void RetrieveLobbyListAsync(Action<QueryResponse> onListRetrieved, Action<QueryResponse> onError = null, LobbyColor limitToColor = LobbyColor.None)
{
if (!m_rateLimitQuery.CanCall())
{

LobbyAPIInterface.QueryAllLobbiesAsync(filters, OnLobbyListRetrieved);
void OnLobbyListRetrieved(Response<QueryResponse> response)
void OnLobbyListRetrieved(QueryResponse response)
if (IsSuccessful(response))
onListRetrieved?.Invoke(response?.Result);
if (response != null)
onListRetrieved?.Invoke(response);
else
onError?.Invoke(response);
}

}
LobbyAPIInterface.GetLobbyAsync(lobbyId, OnGet);
void OnGet(Response<Lobby> response)
void OnGet(Lobby response)
onComplete?.Invoke(response?.Result);
onComplete?.Invoke(response);
}
}

string uasId = AuthenticationService.Instance.PlayerId;
LobbyAPIInterface.LeaveLobbyAsync(uasId, lobbyId, OnLeftLobby);
void OnLeftLobby(Response response)
void OnLeftLobby()
{
onComplete?.Invoke();
// Lobbies will automatically delete the lobby if unoccupied, so we don't need to take further action.

33
Assets/Scripts/Relay/RelayAPIInterface.cs


public static class RelayAPIInterface
{
/// <summary>
/// API calls are asynchronous, but for debugging and other reasons we want to reify them as objects so that they can be monitored.
/// </summary>
private class InProgressRequest<T>
{
public InProgressRequest(Task<T> task, Action<T> onComplete)
{
DoRequest(task, onComplete);
}
private async void DoRequest(Task<T> task, Action<T> onComplete)
{
T result = default;
string currentTrace = System.Environment.StackTrace; // If we don't get the calling context here, it's lost once the async operation begins.
try {
result = await task;
} catch (Exception e) {
Exception eFull = new Exception($"Call stack before async call:\n{currentTrace}\n", e);
throw eFull;
} finally {
onComplete?.Invoke(result);
}
}
}
/// <summary>
/// A Relay Allocation represents a "server" for a new host.
/// </summary>
public static void AllocateAsync(int maxConnections, Action<Allocation> onComplete)

new InProgressRequest<Response<AllocateResponseBody>>(task, OnResponse);
AsyncRequest.DoRequest(task, OnResponse);
Debug.LogError("Relay returned a null Allocation. It's possible the Relay service is currently down.");
Debug.LogError("Relay returned a null Allocation. This might occur if the Relay service has an outage, if your cloud project ID isn't linked, or if your Relay package version is outdated.");
else if (response.Status >= 200 && response.Status < 300)
onComplete?.Invoke(response.Result.Data.Allocation);
else

{
CreateJoincodeRequest joinCodeRequest = new CreateJoincodeRequest(new JoinCodeRequest(hostAllocationId));
var task = RelayService.AllocationsApiClient.CreateJoincodeAsync(joinCodeRequest);
new InProgressRequest<Response<JoinCodeResponseBody>>(task, onComplete);
AsyncRequest.DoRequest(task, onComplete);
}
/// <summary>

{
JoinRelayRequest joinRequest = new JoinRelayRequest(new JoinRequest(joinCode));
var task = RelayService.AllocationsApiClient.JoinRelayAsync(joinRequest);
new InProgressRequest<Response<JoinResponseBody>>(task, onComplete);
AsyncRequest.DoRequest(task, onComplete);
}
}
}

45
Assets/Scripts/Tests/PlayMode/LobbyRoundtripTests.cs


// Since we're signed in through the same pathway as the actual game, the list of lobbies will include any that have been made in the game itself, so we should account for those.
// If you want to get around this, consider having a secondary project using the same assets with its own credentials.
yield return new WaitForSeconds(1); // To prevent a possible 429 with the upcoming Query request, in case a previous test had one; Query requests can only occur at a rate of 1 per second.
Response<QueryResponse> queryResponse = null;
QueryResponse queryResponse = null;
float timeout = 5;
LobbyAPIInterface.QueryAllLobbiesAsync(new List<QueryFilter>(), (qr) => { queryResponse = qr; });
while (queryResponse == null && timeout > 0)

Assert.Greater(timeout, 0, "Timeout check (query #0)");
Assert.IsTrue(queryResponse.Status >= 200 && queryResponse.Status < 300, "QueryAllLobbiesAsync should return a success code. (#0)");
int numLobbiesIni = queryResponse.Result.Results?.Count ?? 0;
Assert.IsNotNull(queryResponse, "QueryAllLobbiesAsync should return a non-null result. (#0)");
int numLobbiesIni = queryResponse.Results?.Count ?? 0;
Response<Lobby> createResponse = null;
Lobby createResponse = null;
timeout = 5;
string lobbyName = "TestLobby-JustATest-123";
LobbyAPIInterface.CreateLobbyAsync(m_auth.GetContent("id"), lobbyName, 100, false, m_mockUserData, (r) => { createResponse = r; });

}
Assert.Greater(timeout, 0, "Timeout check (create)");
Assert.IsTrue(createResponse.Status >= 200 && createResponse.Status < 300, "CreateLobbyAsync should return a success code.");
m_workingLobbyId = createResponse.Result.Id;
Assert.AreEqual(lobbyName, createResponse.Result.Name, "Created lobby should match the provided name.");
Assert.IsNotNull(createResponse, "CreateLobbyAsync should return a non-null result.");
m_workingLobbyId = createResponse.Id;
Assert.AreEqual(lobbyName, createResponse.Name, "Created lobby should match the provided name.");
// Query for the test lobby via QueryAllLobbies.
yield return new WaitForSeconds(1); // To prevent a possible 429 with the upcoming Query request.

timeout -= 0.25f;
}
Assert.Greater(timeout, 0, "Timeout check (query #1)");
Assert.IsTrue(queryResponse.Status >= 200 && queryResponse.Status < 300, "QueryAllLobbiesAsync should return a success code. (#1)");
Assert.AreEqual(1 + numLobbiesIni, queryResponse.Result.Results.Count, "Queried lobbies list should contain the test lobby.");
Assert.IsTrue(queryResponse.Result.Results.Where(r => r.Name == lobbyName).Count() == 1, "Checking queried lobby for name.");
Assert.IsTrue(queryResponse.Result.Results.Where(r => r.Id == m_workingLobbyId).Count() == 1, "Checking queried lobby for ID.");
Assert.IsNotNull(queryResponse, "QueryAllLobbiesAsync should return a non-null result. (#1)");
Assert.AreEqual(1 + numLobbiesIni, queryResponse.Results.Count, "Queried lobbies list should contain the test lobby.");
Assert.IsTrue(queryResponse.Results.Where(r => r.Name == lobbyName).Count() == 1, "Checking queried lobby for name.");
Assert.IsTrue(queryResponse.Results.Where(r => r.Id == m_workingLobbyId).Count() == 1, "Checking queried lobby for ID.");
Response<Lobby> getResponse = null;
Lobby getResponse = null;
LobbyAPIInterface.GetLobbyAsync(createResponse.Result.Id, (r) => { getResponse = r; });
LobbyAPIInterface.GetLobbyAsync(createResponse.Id, (r) => { getResponse = r; });
Assert.IsTrue(getResponse.Status >= 200 && getResponse.Status < 300, "GetLobbyAsync should return a success code.");
Assert.AreEqual(lobbyName, getResponse.Result.Name, "Checking the lobby we got for name.");
Assert.AreEqual(m_workingLobbyId, getResponse.Result.Id, "Checking the lobby we got for ID.");
Assert.IsNotNull(getResponse, "GetLobbyAsync should return a non-null result.");
Assert.AreEqual(lobbyName, getResponse.Name, "Checking the lobby we got for name.");
Assert.AreEqual(m_workingLobbyId, getResponse.Id, "Checking the lobby we got for ID.");
Response deleteResponse = null;
bool didDeleteFinish = false;
LobbyAPIInterface.DeleteLobbyAsync(m_workingLobbyId, (r) => { deleteResponse = r; });
while (deleteResponse == null && timeout > 0)
LobbyAPIInterface.DeleteLobbyAsync(m_workingLobbyId, () => { didDeleteFinish = true; });
while (timeout > 0 && !didDeleteFinish)
Assert.IsTrue(deleteResponse.Status >= 200 && deleteResponse.Status < 300, "DeleteLobbyAsync should return a success code.");
Response<QueryResponse> queryResponseTwo = null;
QueryResponse queryResponseTwo = null;
timeout = 5;
LobbyAPIInterface.QueryAllLobbiesAsync(new List<QueryFilter>(), (qr) => { queryResponseTwo = qr; });
while (queryResponseTwo == null && timeout > 0)

Assert.Greater(timeout, 0, "Timeout check (query #2)");
Assert.IsTrue(queryResponseTwo.Status >= 200 && queryResponseTwo.Status < 300, "QueryAllLobbiesAsync should return a success code. (#2)");
Assert.AreEqual(numLobbiesIni, queryResponseTwo.Result.Results.Count, "Queried lobbies list should be empty.");
Assert.IsNotNull(queryResponse, "QueryAllLobbiesAsync should return a non-null result. (#2)");
Assert.AreEqual(numLobbiesIni, queryResponseTwo.Results.Count, "Queried lobbies list should be empty.");
// Some error messages might be asynchronous, so to reduce spillover into other tests, just wait here for a bit before proceeding.
yield return new WaitForSeconds(3);

12
Assets/Scripts/UI/SpinnerUI.cs


{
spinnerImage.Hide();
errorTextVisibility.Show();
var errorString = new StringBuilder();
errorString.Append("Error");
errorString.Append(": ");
if (observed.lastErrorCode > 0)
{
errorString.Append(observed.lastErrorCode);
errorString.Append(", ");
}
errorString.Append("Check Unity Console Log for Details.");
errorText.SetText(errorString.ToString());
errorText.SetText("Error. See Unity Console log for details.");
}
else if (observed.State == LobbyQueryState.Fetched)
{

20
Packages/manifest.json


"com.unity.ide.visualstudio": "2.0.9",
"com.unity.ide.vscode": "1.2.3",
"com.unity.nuget.newtonsoft-json": "2.0.0",
"com.unity.services.authentication": "1.0.0-pre.4",
"com.unity.services.core": "1.0.0",
"com.unity.services.lobby": "1.0.0-pre.3",
"com.unity.services.relay": "1.0.0-pre.3",
"com.unity.transport": "1.0.0-pre.1",
"com.unity.ugui": "1.0.0",
"com.unity.modules.ai": "1.0.0",
"com.unity.modules.androidjni": "1.0.0",

"com.unity.modules.vr": "1.0.0",
"com.unity.modules.wind": "1.0.0",
"com.unity.modules.xr": "1.0.0"
}
},
"scopedRegistries": [
{
"name": "Candidates",
"url": "https://artifactory.prd.cds.internal.unity3d.com/artifactory/api/npm/upm-candidates",
"scopes": [
"com.unity.services.lobby",
"com.unity.services.relay",
"com.unity.services.authentication",
"com.unity.services.core",
"com.unity.transport"
]
}
]
}

44
Packages/packages-lock.json


"url": "https://packages.unity.com"
},
"com.unity.services.authentication": {
"version": "file:com.unity.services.authentication",
"version": "1.0.0-pre.4",
"source": "embedded",
"source": "registry",
"com.unity.services.core": "1.1.0-pre.4",
"com.unity.services.core": "1.1.0-pre.8",
}
},
"url": "https://artifactory.prd.cds.internal.unity3d.com/artifactory/api/npm/upm-candidates"
"version": "file:com.unity.services.core",
"depth": 0,
"source": "embedded",
"version": "1.1.0-pre.8",
"depth": 1,
"source": "registry",
}
},
"url": "https://artifactory.prd.cds.internal.unity3d.com/artifactory/api/npm/upm-candidates"
"version": "file:com.unity.services.lobby",
"version": "1.0.0-pre.3",
"source": "embedded",
"source": "registry",
"com.unity.services.core": "1.1.0-pre.4",
"com.unity.services.core": "1.1.0-pre.8",
"com.unity.nuget.newtonsoft-json": "2.0.0"
}
"com.unity.nuget.newtonsoft-json": "2.0.0",
"com.unity.services.authentication": "1.0.0-pre.3"
},
"url": "https://artifactory.prd.cds.internal.unity3d.com/artifactory/api/npm/upm-candidates"
"version": "file:com.unity.services.relay",
"version": "1.0.0-pre.3",
"source": "embedded",
"source": "registry",
"dependencies": {
"com.unity.services.core": "1.1.0-pre.4",
"com.unity.modules.unitywebrequest": "1.0.0",

"com.unity.modules.unitywebrequestwww": "1.0.0",
"com.unity.nuget.newtonsoft-json": "2.0.0"
}
},
"url": "https://artifactory.prd.cds.internal.unity3d.com/artifactory/api/npm/upm-candidates"
},
"com.unity.sysroot": {
"version": "0.1.19-preview",

"url": "https://packages.unity.com"
},
"com.unity.transport": {
"version": "file:com.unity.transport",
"version": "1.0.0-pre.1",
"source": "embedded",
"source": "registry",
}
},
"url": "https://artifactory.prd.cds.internal.unity3d.com/artifactory/api/npm/upm-candidates"
},
"com.unity.ugui": {
"version": "1.0.0",

44
ProjectSettings/PackageManagerSettings.asset


m_Script: {fileID: 13964, guid: 0000000000000000e000000000000000, type: 0}
m_Name:
m_EditorClassIdentifier:
m_EnablePreReleasePackages: 0
m_EnablePackageDependencies: 0
m_EnablePreviewPackages: 1
m_EnablePackageDependencies: 1
m_SeeAllPackageVersions: 0
oneTimeWarningShown: 1
m_Registries:
- m_Id: main

m_IsDefault: 1
m_Capabilities: 7
m_UserSelectedRegistryName:
- m_Id: scoped:Candidates
m_Name: Candidates
m_Url: https://artifactory.prd.cds.internal.unity3d.com/artifactory/api/npm/upm-candidates
m_Scopes:
- com.unity.services.lobby
- com.unity.services.relay
- com.unity.services.authentication
- com.unity.services.core
- com.unity.transport
m_IsDefault: 0
m_Capabilities: 0
m_UserSelectedRegistryName: Candidates
m_Modified: 0
m_UserModificationsInstanceId: -822
m_OriginalInstanceId: -824
m_LoadAssets: 0
m_Original:
m_Id: scoped:Candidates
m_Name: Candidates
m_Url: https://artifactory.prd.cds.internal.unity3d.com/artifactory/api/npm/upm-candidates
m_Scopes:
- com.unity.services.lobby
- com.unity.services.relay
- com.unity.services.authentication
- com.unity.services.core
- com.unity.transport
m_IsDefault: 0
m_Capabilities: 0
m_Modified: 0
m_Name: Candidates
m_Url: https://artifactory.prd.cds.internal.unity3d.com/artifactory/api/npm/upm-candidates
m_Scopes:
- com.unity.services.lobby
- com.unity.services.relay
- com.unity.services.authentication
- com.unity.services.core
- com.unity.transport
m_SelectedScopeIndex: 0

6
ProjectSettings/ProjectSettings.asset


m_VersionName:
apiCompatibilityLevel: 6
activeInputHandler: 0
cloudProjectId:
cloudProjectId: 0bf0426b-e1fd-4251-82d0-3eea033ef1ad
projectName:
organizationId:
projectName: com.unity.services.samples.lobby-rooms
organizationId: operate-samples
cloudEnabled: 0
legacyClampBlendShapeWeights: 0
virtualTexturingSupportEnabled: 0

161
README.md


### Closed Beta - 7/14/21
Lobby and Relay are **only** available in closed beta at the moment, to use these services you will need to have signed up here for the services to show in your Organization: https://create.unity3d.com/relay-lobby-beta-signup
# Game Lobby Sample
_Tested with Unity 2020.3 for PC and Mac._
This sample demonstrates how to use the Lobby and Relay packages to create a typical game lobby experience. Players can host lobbies that other players can join via a public list or private code, and the Unity Transport (“UTP”) handles basic real time communication between them. With Relay and Authentication (“Auth”), all players are kept anonymous from each other, without persistent accounts. \
This is not a “drag-and-drop” solution; the Game Lobby Sample is not a minimal code sample intended to be completely copied into a full-scale project. Rather, it demonstrates how to use multiple services in a vertical slice with some basic game logic and infrastructure. Use it as a reference to learn how Lobby and Relay work together and how to integrate them into your project!
#### Features Demonstrated:
* **Anonymous Auth login**: Track player credentials without a persistent account.
* **Lobby creation**: Players host lobbies for others to join.
* **Lobby query**: Find a list of lobbies with filters, or use private codes.
* **Relay obfuscation**: Players in a lobby are connected through an anonymous IP.
* **UTP communication**: Players transmit basic data to lobby members in real time.
* **Lobby + Relay connection management**: The services together automatically handle new connections and disconnections.
### Service Organization Setup
In order to use Unity’s multiplayer services, you need a cloud organization ID for your project. If you do not currently have one, follow this guide to set up your cloud organization:
[https://support.unity.com/hc/en-us/articles/208592876-How-do-I-create-a-new-Organization-](https://support.unity.com/hc/en-us/articles/208592876-How-do-I-create-a-new-Organization-)
Once you have an ID, link it to your project under **Edit > Project Settings > Services**. You can now use the Unity Dashboard to manage your project’s services.
### Service Overview
#### **Lobby**
The Lobby service allows developers to create lobbies and share data between players before a real time network connection needs to be established. It simplifies the first step in connecting users to other services such as Relay and provides tools to allow players to find other lobbies.
The Lobby documentation contains code samples and additional information about the service. It includes comprehensive details for using Lobby along with additional code samples, and it might help you better understand the Game Lobby Sample: [http://documentation.cloud.unity3d.com/en/articles/5371715-unity-lobby-service](http://documentation.cloud.unity3d.com/en/articles/5371715-unity-lobby-service)
The Lobby service can be managed in the Unity Dashboard:
# Game Lobby Sample
## *Unity 2021.2 0b1*
[https://dashboard.unity3d.com/lobby](http://documentation.cloud.unity3d.com/en/articles/5371715-unity-lobby-service)
This is a Unity Project Sample showing how to integrate Lobby and Relay into a typical Game Lobby experience.
Features Covered:
- Lobby Creation
- Lobby Query
- Lobby Data Sync
- Emotes
- Player Names
- Player Ready Check State
- Lobby Join
- Relay Server Creation
- Relay Code Generation
- Relay Server Join
#### **Relay**
## Service Organization Setup
**Create an organization**
The Relay service connects players in a host-client model with an obfuscated host IP. This allows them to host networked experiences as though players connected directly while only sharing private information with Relay itself. \
Follow the guide to set up your cloud organization:
[Organization Tutorial](https://support.unity.com/hc/en-us/articles/208592876-How-do-I-create-a-new-Organization-)
The Relay documentation contains code samples and additional information about the service. It includes comprehensive details for using Relay along with additional code samples, and it might help you better understand the Game Lobby Sample:
[http://documentation.cloud.unity3d.com/en/articles/5371723-relay-overview](http://documentation.cloud.unity3d.com/en/articles/5371723-relay-overview)
## Lobby & Relay
The Relay service can be managed in the Unity Dashboard:
We use the lobby service to create a space that our users can join and share data through.
[https://dashboard.unity3d.com/relay](http://documentation.cloud.unity3d.com/en/articles/5371723-relay-overview)
[Lobby Overview](http://documentation.cloud.unity3d.com/en/articles/5371715-unity-lobby-service)
In this sample, once players are connected to a lobby, they are connected via Relay to set up real time data transfer over UTP. Lobby and Relay both depend on Auth for credentials. This sample uses Auth’s anonymous login feature to create semi-permanent credentials that are unique to each player but do not require developers to maintain a persistent account for them.
[Lobby Dashboard](https://dashboard.unity3d.com/lobby)
#### **Setup **
The Lobby and Relay sections of the Unity Dashboard contain their own setup instructions. Select **About & Support > Get Started** and follow the provided steps to integrate the services into your project.
We use the Relay service to obfuscate the Hosts' IP, while still allowing them to locally host strangers.
With those services set up and your project linked to your cloud organization, you can open the mainScene scene in the Editor and begin using the Game Lobby Sample.
### Running the Sample
You will need two “players” to demonstrate the full sample functionality. Create a standalone build to run alongside the Editor in Play mode. Although Auth creates anonymous credentials using your machine’s registry, your Editor and your build have different credentials since they create different registry entries.
[Relay Overview](http://documentation.cloud.unity3d.com/en/articles/5371723-relay-overview)
[Relay Dashboard]( https://dashboard.unity3d.com/relay)
#### **Lobby Join Menu**
### Setup
For either one, select "About & Support => Get Started"
![alt_text](~Documentation/3_lobby_list.PNG "Lobby List")
**Closed Beta Only**
Follow the steps, downloading your packaged folders to the Sample Project Package\Packages
The Lobby Join Menu contains the Lobby List UI, which acts as a hub for players to connect to each other via the public list or via a Lobby Code.
*If you open the project and you get the "Enter Safe Mode" dialogue, it means you are missing your packages.*
*If you still cannot find the package namespaces, ensure the Assets/Scripts/LobbyRelaySample.asmdef is referencing the packages.*
Follow the steps until you hit "Lobby/Relay On"
1. **Public Lobby List**: Shows all lobbies not set to private. Lobbies contain developer-defined data which can be set to public and non-public visibility. The Lobby service cleans up any “zombie” rooms so they don’t appear in this list. For this sample, lobby names and player counts are shown, and lobbies in the “in-game” state are not shown. You can select a lobby and then select Join.
2. **Refresh Button**: Refreshes the Lobby List. The Lobby service imposes rate limits on all API calls to prevent spamming. Refresh attempts within the rate limit will do nothing (approximately every 1.5 seconds, see Lobby documentation for details).
3. **Lobby Code Field**: Enter a Lobby Code for an existing lobby. In addition to the public list, all lobbies may be joined using their codes. This allows players to privately share access to lobbies.
4. **Filters**: Sets the Lobby List to only show servers of a certain color. The Lobby service can filter any queries by data set to public visibility. For this sample, players may optionally filter by color, which hosts may have chosen for their lobbies.
5. **Join Button**:** **Requests to join via Lobby List selection or Lobby Code. Failed requests are also rate limited to prevent spam, if the player presses the button repeatedly.
6. **Create Tab**: Allows creation of a new lobby. Players select a lobby name and whether to make a private lobby, and they then connect to the new lobby as its host.
7. **Player Name**: Displays the player name and allows renaming. By default, players are assigned a name based on their anonymous Auth credentials, but name changes follow their credentials so that all players see the new name.
## Solo Testing
#### **Lobby View**
Create a new Unity Build of the project in the OS of your choice.
Because the Authentication service creates a unique ID for builds, you will need to host a lobby in Build and join in Editor or vice versa.
1. Start the game, and hit start to enter the Room List. This Queries the rooms service for available Lobbies, there wont be any right now.
![alt_text](~Documentation/4_lobby.PNG "Lobby List")
![Join Menu](~Documentation/Images/tutorial_1_lobbyList.png?raw=true "Join Menu")
2. The Create Menu Lets you make a new Lobby.
The Lobby View UI displays information from Lobby and Relay for all players in a lobby. When a new player joins, they will immediately begin connecting to the host, after which they will synchronize emotes and state changes with the other lobby members.
![Create Menu](~Documentation/Images/tutorial_2_createMenu.png?raw=true)
3. This is the Lobby, It has a Room code for you to share with your friends to allow them to join.
For demonstration purposes we also show the Relay Code, which will be passed to all users in the Lobby.
![Lobby View](~Documentation/Images/tutorial_3_HostGame.png?raw=true)
1. **Lobby Name**: Set when the lobby was created. Cannot be changed.
2. **Lobby Code**: Shareable code generated by the Lobby service. This may be provided externally to other players to allow them to join this lobby.
3. **Lobby User**: A player in this lobby. The player’s name, state, and emote are displayed; these data are synchronized via Relay + UTP, so any changes that a player makes will appear immediately for all connected players. Incoming players will be sent the current data once they have connected.
4. **Relay Server IP**:** **The anonymous IP that Relay generated. This does not need to be shown to players and is displayed here simply to indicate that Relay is functioning.
5. **Relay Code**: An internal join code generated by Relay, used during Relay connection. This does not need to be shown to players and is displayed here simply to indicate that Relay is functioning.
6. **Emote Buttons**: Sets the player’s emote, synchronized via UTP.
7. **Lobby Color**: (Host only) Sets the lobby color for filtering in the Lobby List. This is synchronized via Lobby, so changes won’t appear immediately for all players since Lobby queries are rate-limited.
8. **Ready Button**: Sets a ready state on the player. When all players are ready, the host initiates a countdown to an “in-game” state, and the lobby becomes hidden from the public Lobby List.
4. Open the second game instance in Editor or in Build, you should now see your Lobby in the list.
### Architecture
![Populated Join View](~Documentation/Images/tutorial_4_newLobby.png?raw=true)
The Game Lobby Sample is designed as a vertical slice of a multiplayer lobby, so it has additional infrastructure that might be expected in full game production, as well as some components to allow multiple of our services to work together. As such, not all of the codebase will be relevant depending on your needs. Most contents are self-documenting, but some high-level points follow:
5. The Lobby holds up to 4 players and will pass the Relay code once all the players are ready.
![Relay Ready!](~Documentation/Images/tutorial_5_editorCow.png?raw=true)
* Logic for using the Authentication (Auth), Lobby, and Relay services are encapsulated in their own directories. All API usage is abstracted behind a buffer for more convenient access;
* For example, LobbyAPIInterface contains the actual calls into the Lobby API, but LobbyAsyncRequests has additional processing on the results of those calls as well as some structures necessary for timing the API calls properly.
* The Relay directory also contains logic for using the Unity Transport (UTP), since it requires a transport to function.
* The Game directory contains core “glue” classes for running the sample itself, representing a simple framework for a game.
* GameManager has all the core logic for running the sample. It sets up the services and UI, manages game states, and fields messages from other components.
* Various other classes exist here to maintain the states of multiple components of the sample and to interface between our sample’s needs for Lobby and Relay data and the structure of that data remotely in the services.
* The Infrastructure directory contains classes used for essential tasks related to overall function but not specifically to any service.
* Locator mimics a Service Locator pattern, allowing for behaviors that might otherwise be Singletons to easily be swapped in and out.
* Messenger creates a simple messaging system used to keep unrelated classes decoupled, letting them instead message arbitrary listeners when interesting things happen.
* An Observer pattern is used for all UI elements and for local copies of remote Lobby and Relay data. An Observer is alerted whenever its observed data changes, and the owner of that data doesn’t need to know who is observing.
* The UI directory strictly contains logic for the sample’s UI and observing relevant data. Viewing these files should not be necessary to understand how to use the services themselves, though they do demonstrate the use of the Observer pattern.
* Several files exist with classes that simply implement ObserverBehaviour. This is because Unity requires MonoBehaviours to exist in files of the same names.
* Note that much of the UI is driven by CanvasGroup alpha for visibility, which means that some behaviors continue to run even when invisible to the player.
* Multiple Tests directories are included to demonstrate core behavior and edge cases for some of the code. In particular, the Play mode tests for Lobby and Relay can be used to ensure your connection to the services is functioning correctly.
* In the Editor, the project assets are broken into nested prefabs for convenience when making changes during sample development. Their details should not be considered vital, although there are UI elements that depend on event handlers that are serialized.
6. The countdown will start after the rooms data synch has completed. (It is a little slow due to our refresh rate being low at the moment)
### Considerations
![Countdown!](~Documentation/Images/tutorial_6_countDown.png?raw=true)
While the Game Lobby Sample represents more than just a minimal implementation of the Lobby and Relay services, it is not comprehensive, and some design decisions were made for faster or more readable development.
7. The relay service IP gets passed to all users in the lobby, and this is where you would connect to a server, if you had one.
![InGame!](~Documentation/Images/tutorial_7_ingame.png?raw=true)
* All operations using Lobby and Relay rely on asynchronous API calls. The sample code has some logic for handling issues that can result from receiving the results at arbitrary times, but it doesn’t have logic for enqueuing calls that the user initiates during setup and cleanup. Rapid operations when entering and exiting lobbies can result in unexpected behavior.
* Relay does not support host migration, but Lobby does. If a lobby host disconnects, the lobby might seem to clients to continue to operate until Lobby detects the disconnect. In practice, you might want to implement an additional periodic handshake between hosts and clients in cases where data could get out of sync quickly.
* The sample sets up heartbeat pings with Lobby and Relay to keep the connections alive, but they do not impose any limitations on a lobby’s duration. Consider a maximum duration in actual use, such as a maximum game length.
* HTTP errors will appear in the console. These are returned by Lobby and Relay API calls for various reasons. In general, they do not impact the sample’s execution, though they might result in unexpected behavior for a player since the sample doesn’t provide any explanatory UI when these errors occur.
* 404 (“Not Found”) errors might occur when the Lobby service handles multiple incoming API calls in an arbitrary order, usually when leaving a lobby. They will also occur if trying to join an invalid lobby, such as one that has been deleted but still appears in the Lobby List before refreshing.
* 429 (“Too Many Requests”) errors occur if rate-limited operations happen too quickly. In particular, refreshing the lobby list too quickly results in 429 errors from the QueryLobbiesAsync call. Consult the Lobby documentation for details.
* 401 (“Unauthorized”) errors occur if the user enters the lobby menu before Auth sign-in completes, since all Lobby and Relay operations require Auth credentials.
* 409 (“Conflict”) errors occur if a player tries to join a lobby using the same credentials as another player. In particular, this will happen if you are trying to test with multiple standalone builds, since they share the same registry entry on your machine. To test with three or more players on one machine:
1. Create a duplicate project with Symbolic Links to the original Assets and Packages, so that it uses the same assets. Copy the ProjectSettings as well, but do not link them to the original. (The process for creating Symbolic Links will depend on your operating system.)
2. Open this project in a second Editor.
3. Under **Edit > Project Settings > Player**, modify the Product Name. This causes the duplicate project to have a new registry entry, so Auth will assign new credentials.
4. Verify that running the sample in either Play mode or a standalone build assigns a different default player name than the originals. This indicates different Auth credentials, preventing the 409 errors.

1001
Assets/Prefabs/UI/PopUpUI.prefab
文件差异内容过多而无法显示
查看文件

7
Assets/Prefabs/UI/PopUpUI.prefab.meta


fileFormatVersion: 2
guid: 79d6084439b78bb4eaf5232cb953fd87
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

44
Assets/Scripts/Infrastructure/AsyncRequest.cs


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LobbyRelaySample
{
/// <summary>
/// Both Lobby and Relay have need for asynchronous requests with some basic safety wrappers. This is a shared place for that.
/// </summary>
public static class AsyncRequest
{
public static async void DoRequest(Task task, Action onComplete)
{
string currentTrace = System.Environment.StackTrace; // For debugging. If we don't get the calling context here, it's lost once the async operation begins.
try
{ await task;
}
catch (Exception e)
{ Exception eFull = new Exception($"Call stack before async call:\n{currentTrace}\n", e);
throw eFull;
}
finally
{ onComplete?.Invoke();
}
}
public static async void DoRequest<T>(Task<T> task, Action<T> onComplete)
{
T result = default;
string currentTrace = System.Environment.StackTrace;
try
{ result = await task;
}
catch (Exception e)
{ Exception eFull = new Exception($"Call stack before async call:\n{currentTrace}\n", e);
throw eFull;
}
finally
{ onComplete?.Invoke(result);
}
}
}
}

11
Assets/Scripts/Infrastructure/AsyncRequest.cs.meta


fileFormatVersion: 2
guid: 255a690fe68a18c438e160a118cd8a6b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

32
Assets/Scripts/Infrastructure/LogHandlerSettings.cs


using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
namespace LobbyRelaySample
{
public class LogHandlerSettings : MonoBehaviour
{
[SerializeField]
[Tooltip("Only logs of this level or higher will appear in the console.")]
private LogMode m_editorLogVerbosity = LogMode.Critical;
[SerializeField]
private PopUpUI m_popUpPrefab;
[SerializeField]
private ErrorReaction m_errorReaction;
void Awake()
{
LogHandler.Get().mode = m_editorLogVerbosity;
LogHandler.Get().SetLogReactions(m_errorReaction);
}
public void SpawnErrorPopup(string errorMessage)
{
var popupInstance = Instantiate(m_popUpPrefab, transform);
popupInstance.ShowPopup(errorMessage, Color.red);
}
}
}

11
Assets/Scripts/Infrastructure/LogHandlerSettings.cs.meta


fileFormatVersion: 2
guid: 089231d71bcfb8d479b4f8b778b1026f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

25
Assets/Scripts/UI/PopUpUI.cs


using LobbyRelaySample.UI;
using TMPro;
using UnityEngine;
using UnityEngine.Serialization;
using UnityEngine.UI;
namespace LobbyRelaySample
{
public class PopUpUI : MonoBehaviour
{
[SerializeField]
TMP_InputField m_popupText;
public void ShowPopup(string newText, Color textColor = default)
{
m_popupText.SetTextWithoutNotify(newText);
m_popupText.textComponent.color = textColor;
}
public void Delete()
{
Destroy(gameObject);
}
}
}

11
Assets/Scripts/UI/PopUpUI.cs.meta


fileFormatVersion: 2
guid: e5f1bc94a7a7b5249a02001abc3096ef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

8
Assets/StreamingAssets.meta


fileFormatVersion: 2
guid: 856fc4d990badc74a96656143dedb8e9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

3
Assets/TextMesh Pro/Fonts.meta.private.0


fileFormatVersion: 2
guid: 077401255185464e9b7bb889ed8280e7
timeCreated: 1620062128

7
Assets/TextMesh Pro/Fonts.meta.private.0.meta


fileFormatVersion: 2
guid: 8562a96ab261c68478bc71961f545c2f
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

3
Assets/TextMesh Pro/Resources.meta.private.0


fileFormatVersion: 2
guid: 017af257aa3d4ade9644309997be39de
timeCreated: 1620062128

7
Assets/TextMesh Pro/Resources.meta.private.0.meta


fileFormatVersion: 2
guid: 4a3fdb949a9746940af94101f709f04d
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

3
Assets/TextMesh Pro/Shaders.meta.private.0


fileFormatVersion: 2
guid: 5915fb622e8b4e7a9815228d467e7167
timeCreated: 1620062115

7
Assets/TextMesh Pro/Shaders.meta.private.0.meta


fileFormatVersion: 2
guid: d6a8cd862f117884db82bd4017125774
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

3
Assets/TextMesh Pro/Sprites.meta.private.0


fileFormatVersion: 2
guid: 347d77b9f27146049993e70fd2c3ca78
timeCreated: 1620062128

7
Assets/TextMesh Pro/Sprites.meta.private.0.meta


fileFormatVersion: 2
guid: e58181612d0ba9d4da4aeafea973c347
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
正在加载...
取消
保存