浏览代码

Updating LobbyAPIInterface to match changes to the Lobby API from the package. Consolidating a little async behavior between Lobby and Relay.

/main/staging/Open_beta_Update
nathaniel.buck@unity3d.com 3 年前
当前提交
fb2375da
共有 9 个文件被更改,包括 148 次插入156 次删除
  1. 4
      Assets/Scripts/Game/GameManager.cs
  2. 125
      Assets/Scripts/Lobby/LobbyAPIInterface.cs
  3. 38
      Assets/Scripts/Lobby/LobbyAsyncRequests.cs
  4. 33
      Assets/Scripts/Relay/RelayAPIInterface.cs
  5. 45
      Assets/Scripts/Tests/PlayMode/LobbyRoundtripTests.cs
  6. 2
      Packages/manifest.json
  7. 2
      Packages/packages-lock.json
  8. 44
      Assets/Scripts/Infrastructure/AsyncRequest.cs
  9. 11
      Assets/Scripts/Infrastructure/AsyncRequest.cs.meta

4
Assets/Scripts/Game/GameManager.cs


er =>
{
long errorLong = 0;
if (er != null)
errorLong = er.Status;
OnLobbyQueryFailed(errorLong);
OnLobbyQueryFailed(errorLong); // TODO: What to supply here?
},
m_lobbyColorFilter);
}

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);
}
}
}

38
Assets/Scripts/Lobby/LobbyAsyncRequests.cs


Locator.Get.UpdateSlow.Subscribe(UpdateLobby); // Shouldn't need to unsubscribe since this instance won't be replaced.
}
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 Queue<Action> m_pendingOperations = new Queue<Action>();

string uasId = AuthenticationService.Instance.PlayerId;
LobbyAPIInterface.CreateLobbyAsync(uasId, lobbyName, maxPlayers, isPrivate, CreateInitialPlayerData(localUser), OnLobbyCreated);
void OnLobbyCreated(Response<Lobby> response)
// TODO: What about errors?
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)
{
List<QueryFilter> filters = new List<QueryFilter>();
if (limitToColor == LobbyColor.Orange)

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);
onError?.Invoke(response);
onError?.Invoke(response); // TODO: Hmm...how do we know if there was a failure?
}
}
/// <param name="onComplete">If no lobby is retrieved, this is given null.</param>

m_isMidRetrieve = true;
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);

2
Packages/manifest.json


"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-preview.1",
"com.unity.services.relay": "1.0.0-pre.3",
"com.unity.sysroot.linux-x86_64": "0.1.15-preview",
"com.unity.test-framework": "1.1.27",
"com.unity.textmeshpro": "3.0.6",

2
Packages/packages-lock.json


"url": "https://artifactory.prd.cds.internal.unity3d.com/artifactory/api/npm/upm-candidates"
},
"com.unity.services.relay": {
"version": "1.0.0-preview.1",
"version": "1.0.0-pre.3",
"depth": 0,
"source": "registry",
"dependencies": {

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:
正在加载...
取消
保存