浏览代码

Working wire implementation (Only passes the lobby color, and relay code around)

/main/staging
当前提交
02574016
共有 22 个文件被更改,包括 1603 次插入1472 次删除
  1. 992
      Assets/Art/Font/CheckboxFLF SDF.asset
  2. 13
      Assets/Prefabs/NGO/InGameLogic.prefab
  3. 75
      Assets/Scripts/GameLobby/Game/GameManager.cs
  4. 132
      Assets/Scripts/GameLobby/Game/LocalLobby.cs
  5. 2
      Assets/Scripts/GameLobby/Infrastructure/Messenger.cs
  6. 31
      Assets/Scripts/GameLobby/Lobby/LobbyAPIInterface.cs
  7. 144
      Assets/Scripts/GameLobby/Lobby/LobbyAsyncRequests.cs
  8. 32
      Assets/Scripts/GameLobby/Vivox/VivoxUserHandler.cs
  9. 35
      Packages/manifest.json
  10. 105
      Packages/packages-lock.json
  11. 2
      ProjectSettings/GraphicsSettings.asset
  12. 28
      ProjectSettings/PackageManagerSettings.asset
  13. 4
      ProjectSettings/Packages/com.unity.services.vivox/Settings.json
  14. 5
      ProjectSettings/ProjectSettings.asset
  15. 4
      ProjectSettings/ProjectVersion.txt
  16. 999
      ~Documentation/Images/2_lobby.PNG
  17. 138
      Assets/Scripts/GameLobby/Lobby/LobbyContentUpdater.cs
  18. 105
      Assets/Scripts/GameLobby/Lobby/LobbyConverters.cs
  19. 76
      Assets/Scripts/GameLobby/Lobby/ToLocalLobby.cs
  20. 153
      Assets/Scripts/GameLobby/Lobby/LobbyContentHeartbeat.cs
  21. 0
      /Assets/Scripts/GameLobby/Lobby/LobbyConverters.cs.meta
  22. 0
      /Assets/Scripts/GameLobby/Lobby/LobbyContentUpdater.cs.meta

992
Assets/Art/Font/CheckboxFLF SDF.asset
文件差异内容过多而无法显示
查看文件

13
Assets/Prefabs/NGO/InGameLogic.prefab


m_Script: {fileID: 11500000, guid: 593a2fe42fa9d37498c96f9a383b6521, type: 3}
m_Name:
m_EditorClassIdentifier:
DontDestroy: 1
RunInBackground: 0
LogLevel: 1
NetworkConfig:

m_Name:
m_EditorClassIdentifier:
m_ProtocolType: 1
m_MaxPacketQueueSize: 256
m_MaxPayloadSize: 6144
m_MaxSendQueueSize: 98304
m_MaximumPacketSize: 1400
m_MaxPacketQueueSize: 512
m_SendQueueBatchSize: 6144
m_HeartbeatTimeoutMS: 500
m_ConnectTimeoutMS: 1000
m_MaxConnectAttempts: 60

Port: 7777
ServerListenAddress:
DebugSimulator:
PacketDelayMS: 0
PacketJitterMS: 0
PacketDropRate: 0
--- !u!1 &413870477192997562
GameObject:
m_ObjectHideFlags: 0

serializedVersion: 2
m_Bits: 1
m_VolumeTrigger: {fileID: 0}
m_VolumeFrameworkUpdateModeOption: 2
m_RenderPostProcessing: 0
m_Antialiasing: 0
m_AntialiasingQuality: 2

75
Assets/Scripts/GameLobby/Game/GameManager.cs


public class GameManager : MonoBehaviour, IReceiveMessages
{
#region UI elements that observe the local state. These should be assigned the observers in the scene during Start.
/// <summary>
/// The Observer/Observed Pattern is great for keeping the UI in Sync with the actual Values.
/// Each list below represents a single Observed class that gets updated by other parts of the code, and will

/// </summary>
[SerializeField]
private List<LocalMenuStateObserver> m_LocalMenuStateObservers = new List<LocalMenuStateObserver>();
[SerializeField]

private LobbyUser m_localUser;
private LocalLobby m_localLobby;
private LobbyServiceData m_lobbyServiceData = new LobbyServiceData();
private LobbyContentHeartbeat m_lobbyContentHeartbeat = new LobbyContentHeartbeat();
private LobbyContentUpdater m_LobbyContentUpdater = new LobbyContentUpdater();
private RelayUtpSetup m_relaySetup;
private RelayUtpClient m_relayClient;

private void OnAuthSignIn()
{
Debug.Log("Signed in.");
m_localUser.ID = Locator.Get.Identity.GetSubIdentity(Auth.IIdentityType.Auth).GetContent("id");
m_localUser.DisplayName = NameGenerator.GetName(m_localUser.ID);
m_localLobby.AddPlayer(m_localUser); // The local LobbyUser object will be hooked into UI before the LocalLobby is populated during lobby join, so the LocalLobby must know about it already when that happens.

/// <summary>
/// TODO Wire is a good update to remove the monolithic observers and move to observed values instead, on a Singleton gameManager
/// </summary>
private void BeginObservers()
{
foreach (var gameStateObs in m_LocalMenuStateObservers)

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

{
LocalLobby.LobbyData createLobbyData = (LocalLobby.LobbyData)msg;
LobbyAsyncRequests.Instance.CreateLobbyAsync(createLobbyData.LobbyName, createLobbyData.MaxPlayerCount, createLobbyData.Private, m_localUser, (r) =>
{ lobby.ToLocalLobby.Convert(r, m_localLobby);
{
lobby.LobbyConverters.RemoteToLocal(r, m_localLobby);
OnCreatedLobby();
},
OnFailedJoin);

LocalLobby.LobbyData lobbyInfo = (LocalLobby.LobbyData)msg;
LobbyAsyncRequests.Instance.JoinLobbyAsync(lobbyInfo.LobbyID, lobbyInfo.LobbyCode, m_localUser, (r) =>
{ lobby.ToLocalLobby.Convert(r, m_localLobby);
{
lobby.LobbyConverters.RemoteToLocal(r, m_localLobby);
OnJoinedLobby();
},
OnFailedJoin);

m_lobbyServiceData.State = LobbyQueryState.Fetching;
LobbyAsyncRequests.Instance.RetrieveLobbyListAsync(
qr => {
qr =>
{
OnLobbiesQueried(lobby.ToLocalLobby.Convert(qr));
OnLobbiesQueried(lobby.LobbyConverters.QueryToLocalList(qr));
er => {
er =>
{
OnLobbyQueryFailed();
},
m_lobbyColorFilter);

LobbyAsyncRequests.Instance.QuickJoinLobbyAsync(m_localUser, m_lobbyColorFilter, (r) =>
{ lobby.ToLocalLobby.Convert(r, m_localLobby);
{
lobby.LobbyConverters.RemoteToLocal(r, m_localLobby);
OnJoinedLobby();
},
OnFailedJoin);

{
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup, "Empty Name not allowed."); // Lobby error type, then HTTP error type.
return;
}
}
{ ConfirmApproval();
{
ConfirmApproval();
{ EmoteType emote = (EmoteType)msg;
{
EmoteType emote = (EmoteType)msg;
{ m_localUser.UserStatus = (UserStatus)msg;
{
m_localUser.UserStatus = (UserStatus)msg;
{ m_localLobby.State = LobbyState.CountDown;
{
m_localLobby.State = LobbyState.CountDown;
{ m_localLobby.State = LobbyState.Lobby;
{
m_localLobby.State = LobbyState.Lobby;
{ if (m_relayClient is RelayUtpHost)
{
if (m_relayClient is RelayUtpHost)
{ SetGameState((GameState)msg);
{
SetGameState((GameState)msg);
{ m_localUser.UserStatus = UserStatus.InGame;
{
m_localUser.UserStatus = UserStatus.InGame;
{ m_localLobby.State = LobbyState.Lobby;
{
m_localLobby.State = LobbyState.Lobby;
SetUserLobbyState();
}
}

private void OnJoinedLobby()
{
LobbyAsyncRequests.Instance.BeginTracking(m_localLobby.LobbyID);
m_lobbyContentHeartbeat.BeginTracking(m_localLobby, m_localUser);
m_LobbyContentUpdater.BeginTracking(m_localLobby, m_localUser);
SetUserLobbyState();
// The host has the opportunity to reject incoming players, but to do so the player needs to connect to Relay without having game logic available.

{
m_localUser.ResetState();
LobbyAsyncRequests.Instance.LeaveLobbyAsync(m_localLobby.LobbyID, ResetLocalLobby);
m_lobbyContentHeartbeat.EndTracking();
LobbyAsyncRequests.Instance.EndTracking();
m_LobbyContentUpdater.EndTracking();
{ Component.Destroy(m_relaySetup);
{
Component.Destroy(m_relaySetup);
if (m_relayClient != null)
{
m_relayClient.Dispose();

void OnVivoxLoginComplete(bool didSucceed)
{
if (!didSucceed)
{ Debug.LogError("Vivox login failed! Retrying in 5s...");
{
Debug.LogError("Vivox login failed! Retrying in 5s...");
StartCoroutine(RetryConnection(StartVivoxLogin, m_localLobby.LobbyID));
}
}

void OnVivoxJoinComplete(bool didSucceed)
{
if (!didSucceed)
{ Debug.LogError("Vivox connection failed! Retrying in 5s...");
{
Debug.LogError("Vivox connection failed! Retrying in 5s...");
StartCoroutine(RetryConnection(StartVivoxJoin, m_localLobby.LobbyID));
}
}

m_relaySetup = null;
if (!didSucceed)
{ Debug.LogError("Relay connection failed! Retrying in 5s...");
{
Debug.LogError("Relay connection failed! Retrying in 5s...");
StartCoroutine(RetryConnection(StartRelayConnection, m_localLobby.LobbyID));
return;
}

132
Assets/Scripts/GameLobby/Game/LocalLobby.cs


using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine.Serialization;
namespace LobbyRelaySample
{

InGame = 4
}
public enum LobbyColor { None = 0, Orange = 1, Green = 2, Blue = 3 }
public enum LobbyColor
{
None = 0,
Orange = 1,
Green = 2,
Blue = 3
}
/// <summary>
/// A local wrapper around a lobby's remote data, with additional functionality for providing that data to UI elements and tracking local player objects.

{
Dictionary<string, LobbyUser> m_LobbyUsers = new Dictionary<string, LobbyUser>();
public Dictionary<string, LobbyUser> LobbyUsers => m_LobbyUsers;
public bool pullUpdate;
#region LocalLobbyData
#region LocalLobbyData
public struct LobbyData
{
public string LobbyID { get; set; }

public string LobbyName { get; set; }
public bool Private { get; set; }
public bool Locked { get; set; }
public int AvailableSlots { get; set; }
public int MaxPlayerCount { get; set; }
public LobbyState State { get; set; }
public LobbyColor Color { get; set; }

State_LastEdit = existing.State_LastEdit;
Color_LastEdit = existing.Color_LastEdit;
RelayNGOCode_LastEdit = existing.RelayNGOCode_LastEdit;
AvailableSlots = existing.AvailableSlots;
Locked = existing.Locked;
}
public LobbyData(string lobbyCode)

State_LastEdit = 0;
Color_LastEdit = 0;
RelayNGOCode_LastEdit = 0;
AvailableSlots = 4;
Locked = false;
}
private LobbyData m_data;
public LobbyData Data
{
get { return new LobbyData(m_data); }
public override string ToString()
{
StringBuilder sb = new StringBuilder("Lobby : ");
sb.AppendLine(LobbyName);
sb.Append("ID: ");
sb.AppendLine(LobbyID);
sb.Append("Code: ");
sb.AppendLine(LobbyCode);
sb.Append("Private: ");
sb.AppendLine(Private.ToString());
sb.Append("Locked: ");
sb.AppendLine(Locked.ToString());
sb.Append("Max Players: ");
sb.AppendLine(MaxPlayerCount.ToString());
sb.Append("AvailableSlots: ");
sb.AppendLine(AvailableSlots.ToString());
sb.Append("LobbyState: ");
sb.AppendLine(State.ToString());
sb.Append("Lobby State Last Edit: ");
sb.AppendLine(new DateTime(State_LastEdit).ToString());
sb.Append("LobbyColor: ");
sb.AppendLine(Color.ToString());
sb.Append("Color Last Edit: ");
sb.AppendLine(new DateTime(Color_LastEdit).ToString());
sb.Append("RelayCode: ");
sb.AppendLine(RelayCode);
sb.Append("RelayNGO: ");
sb.AppendLine(RelayNGOCode);
sb.Append("Relay NGO last Edit: ");
sb.AppendLine(new DateTime(RelayNGOCode_LastEdit).ToString());
return sb.ToString();
}
ServerAddress m_relayServer;
public LobbyData Data => m_Data;
LobbyData m_Data;
ServerAddress m_RelayServer;
get => m_relayServer;
get => m_RelayServer;
m_relayServer = value;
m_RelayServer = value;
OnChanged(this);
}
}

public string LobbyID
{
get => m_data.LobbyID;
get => m_Data.LobbyID;
m_data.LobbyID = value;
m_Data.LobbyID = value;
OnChanged(this);
}
}

get => m_data.LobbyCode;
get => m_Data.LobbyCode;
m_data.LobbyCode = value;
m_Data.LobbyCode = value;
OnChanged(this);
}
}

get => m_data.RelayCode;
get => m_Data.RelayCode;
m_data.RelayCode = value;
m_Data.RelayCode = value;
OnChanged(this);
}
}

get => m_data.RelayNGOCode;
get => m_Data.RelayNGOCode;
m_data.RelayNGOCode = value;
m_data.RelayNGOCode_LastEdit = DateTime.Now.Ticks;
m_Data.RelayNGOCode = value;
m_Data.RelayNGOCode_LastEdit = DateTime.Now.Ticks;
OnChanged(this);
}
}

get => m_data.LobbyName;
get => m_Data.LobbyName;
m_data.LobbyName = value;
m_Data.LobbyName = value;
OnChanged(this);
}
}

get => m_data.State;
get => m_Data.State;
m_data.State = value;
m_data.State_LastEdit = DateTime.Now.Ticks;
m_Data.State = value;
m_Data.State_LastEdit = DateTime.Now.Ticks;
get => m_data.Private;
get => m_Data.Private;
m_data.Private = value;
m_Data.Private = value;
OnChanged(this);
}
}

public int MaxPlayerCount
{
get => m_data.MaxPlayerCount;
get => m_Data.MaxPlayerCount;
m_data.MaxPlayerCount = value;
m_Data.MaxPlayerCount = value;
OnChanged(this);
}
}

get => m_data.Color;
get => m_Data.Color;
if (m_data.Color != value)
{ m_data.Color = value;
m_data.Color_LastEdit = DateTime.Now.Ticks;
if (m_Data.Color != value)
{
m_Data.Color = value;
m_Data.Color_LastEdit = DateTime.Now.Ticks;
OnChanged(this);
}
}

var pendingState = data.State;
var pendingColor = data.Color;
var pendingNgoCode = data.RelayNGOCode;
if (m_data.State_LastEdit > data.State_LastEdit)
pendingState = m_data.State;
if (m_data.Color_LastEdit > data.Color_LastEdit)
pendingColor = m_data.Color;
if (m_data.RelayNGOCode_LastEdit > data.RelayNGOCode_LastEdit)
pendingNgoCode = m_data.RelayNGOCode;
m_data = data;
m_data.State = pendingState;
m_data.Color = pendingColor;
m_data.RelayNGOCode = pendingNgoCode;
if (m_Data.State_LastEdit > data.State_LastEdit)
pendingState = m_Data.State;
if (m_Data.Color_LastEdit > data.Color_LastEdit)
pendingColor = m_Data.Color;
if (m_Data.RelayNGOCode_LastEdit > data.RelayNGOCode_LastEdit)
pendingNgoCode = m_Data.RelayNGOCode;
m_Data = data;
m_Data.State = pendingState;
m_Data.Color = pendingColor;
m_Data.RelayNGOCode = pendingNgoCode;
if (currUsers == null)
m_LobbyUsers = new Dictionary<string, LobbyUser>();

2
Assets/Scripts/GameLobby/Infrastructure/Messenger.cs


public class Messenger : IMessenger
{
private List<IReceiveMessages> m_receivers = new List<IReceiveMessages>();
private const float k_durationToleranceMs = 10;
private const float k_durationToleranceMs = 15;
// We need to handle subscribers who modify the receiver list, e.g. a subscriber who unsubscribes in their OnReceiveMessage.
private Queue<Action> m_pendingReceivers = new Queue<Action>();

31
Assets/Scripts/GameLobby/Lobby/LobbyAPIInterface.cs


IsPrivate = isPrivate,
Player = new Player(id: requesterUASId, data: localUserData)
};
var task = Lobbies.Instance.CreateLobbyAsync(lobbyName, maxPlayers, createOptions);
var task = LobbyService.Instance.CreateLobbyAsync(lobbyName, maxPlayers, createOptions);
var task = Lobbies.Instance.DeleteLobbyAsync(lobbyId);
var task = LobbyService.Instance.DeleteLobbyAsync(lobbyId);
AsyncRequestLobby.Instance.DoRequest(task, onComplete);
}

var task = Lobbies.Instance.JoinLobbyByCodeAsync(lobbyCode, joinOptions);
var task = LobbyService.Instance.JoinLobbyByCodeAsync(lobbyCode, joinOptions);
AsyncRequestLobby.Instance.DoRequest(task, onComplete);
}

var task = Lobbies.Instance.JoinLobbyByIdAsync(lobbyId, joinOptions);
var task = LobbyService.Instance.JoinLobbyByIdAsync(lobbyId, joinOptions);
AsyncRequestLobby.Instance.DoRequest(task, onComplete);
}

Player = new Player(id: requesterUASId, data: localUserData)
};
var task = Lobbies.Instance.QuickJoinLobbyAsync(joinRequest);
var task = LobbyService.Instance.QuickJoinLobbyAsync(joinRequest);
var task = Lobbies.Instance.RemovePlayerAsync(lobbyId, requesterUASId);
var task = LobbyService.Instance.RemovePlayerAsync(lobbyId, requesterUASId);
AsyncRequestLobby.Instance.DoRequest(task, onComplete);
}

Count = k_maxLobbiesToShow,
Filters = filters
};
var task = Lobbies.Instance.QueryLobbiesAsync(queryOptions);
var task = LobbyService.Instance.QueryLobbiesAsync(queryOptions);
var task = Lobbies.Instance.GetLobbyAsync(lobbyId);
var task = LobbyService.Instance.GetLobbyAsync(lobbyId);
AsyncRequestLobby.Instance.DoRequest(task, onComplete);
}

UpdateLobbyOptions updateOptions = new UpdateLobbyOptions { Data = data , IsLocked = shouldLock};
var task = Lobbies.Instance.UpdateLobbyAsync(lobbyId, updateOptions);
UpdateLobbyOptions updateOptions = new UpdateLobbyOptions { Data = data, IsLocked = shouldLock };
var task = LobbyService.Instance.UpdateLobbyAsync(lobbyId, updateOptions);
AsyncRequestLobby.Instance.DoRequest(task, onComplete);
}

AllocationId = allocationId,
ConnectionInfo = connectionInfo
};
var task = Lobbies.Instance.UpdatePlayerAsync(lobbyId, playerId, updateOptions);
var task = LobbyService.Instance.UpdatePlayerAsync(lobbyId, playerId, updateOptions);
public static void SubscribeToLobbyUpdates(string lobbyId, LobbyEventCallbacks lobbyEvent, Action<ILobbyEvents> onLobbySubscribed)
{
var task = LobbyService.Instance.SubscribeToLobbyEventsAsync(lobbyId, lobbyEvent);
AsyncRequestLobby.Instance.DoRequest(task, onLobbySubscribed);
}
var task = Lobbies.Instance.SendHeartbeatPingAsync(lobbyId);
var task = LobbyService.Instance.SendHeartbeatPingAsync(lobbyId);
AsyncRequestLobby.Instance.DoRequest(task, null);
}
}

144
Assets/Scripts/GameLobby/Lobby/LobbyAsyncRequests.cs


using LobbyRelaySample.lobby;
using System;
using System.Collections.Generic;
using System.Linq;
using Unity.Services.Lobbies;
using UnityEngine;
/// An abstraction layer between the direct calls into the Lobby API and the outcomes you actually want. E.g. you can request to get a readable list of
/// An abstraction layer between the direct calls into the Lobby API and the outcomes you actually want. E.g. you can request to get a readable list of
/// current lobbies and not need to make the query call directly.
/// </summary>
public class LobbyAsyncRequests

}
}
public LobbyAsyncRequests()
{
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.
}
#region Lobby
#region Once connected to a lobby, cache the local lobby object so we don't query for it for every lobby operation.
public Action<Lobby> onLobbyUpdated;
//Once connected to a lobby, cache the local lobby object so we don't query for it for every lobby operation.
private string m_currentLobbyId = null;
private Lobby m_lastKnownLobby;
public Lobby CurrentLobby => m_lastKnownLobby;
public void BeginTracking(string lobbyId)
{
m_currentLobbyId = lobbyId;
}
public void EndTracking()
{
m_currentLobbyId = null;
m_lastKnownLobby = null;
m_heartbeatTime = 0;
}
private void UpdateLobby(float unused)
{
if (!string.IsNullOrEmpty(m_currentLobbyId))
RetrieveLobbyAsync(m_currentLobbyId, OnComplete);
void OnComplete(Lobby lobby)
{
if (lobby != null)
{
m_lastKnownLobby = lobby;
}
}
}
Lobby m_RemoteLobby;
/// <summary>
/// Store the LobbySubscription so we can unsubscribe later.
/// </summary>
ILobbyEvents m_lobbySubscription;
LobbyEventCallbacks m_lobbyEvents = new LobbyEventCallbacks();
#endregion

return data;
}
void BeginListening(string lobbyID)
{
m_lobbyEvents = new LobbyEventCallbacks();
m_lobbyEvents.LobbyChanged += OnRemoteLobbyChanged;
LobbyAPIInterface.SubscribeToLobbyUpdates(lobbyID, m_lobbyEvents, sub =>
{
m_lobbySubscription = sub;
m_lobbySubscription.SubscribeAsync();
});
}
void EndListening()
{
m_lobbySubscription.UnsubscribeAsync();
m_RemoteLobby = null;
m_lobbySubscription = null;
m_lobbyEvents = null;
}
void OnRemoteLobbyChanged(ILobbyChanges changes)
{
if (changes.LobbyDeleted)
{
EndListening();
return;
}
//Synching the cloud lobby
changes.ApplyToLobby(m_RemoteLobby);
onLobbyUpdated?.Invoke(m_RemoteLobby);
}
/// <summary>
/// Attempt to create a new lobby and then join it.
/// </summary>

if (response == null)
onFailure?.Invoke();
else
{
JoinLobby(response);
}
}
}

if (response == null)
onFailure?.Invoke();
else
{
JoinLobby(response);
}
}
}

if (response == null)
onFailure?.Invoke();
else
{
JoinLobby(response);
}
void JoinLobby(Lobby response)
{
m_RemoteLobby = response;
BeginListening(m_RemoteLobby.Id);
}
/// <summary>
/// Used for getting the list of all active lobbies, without needing full info for each.
/// </summary>

Debug.Log("Retrieving Lobby List");
if (!m_rateLimitQuery.CanCall())
{
onListRetrieved?.Invoke(null);

return filters;
}
/// <param name="onComplete">If no lobby is retrieved, or if this call hits the rate limit, this is given null.</param>
private void RetrieveLobbyAsync(string lobbyId, Action<Lobby> onComplete)
{
if (!m_rateLimitQuery.CanCall())
{
onComplete?.Invoke(null);
UnityEngine.Debug.LogWarning("Retrieve Lobby hit the rate limit.");
return;
}
LobbyAPIInterface.GetLobbyAsync(lobbyId, OnGet);
void OnGet(Lobby response)
{
onComplete?.Invoke(response); // FUTURE: Consider passing in the exception code here (and elsewhere) to, e.g., specifically handle a 404 indicating a Relay auto-disconnect.
}
}
/// <summary>
/// Attempt to leave a lobby, and then delete it if no players remain.
/// </summary>

void OnLeftLobby()
{
onComplete?.Invoke();
m_RemoteLobby = null;
EndListening();
// Lobbies will automatically delete the lobby if unoccupied, so we don't need to take further action.
}

dataCurr.Add(dataNew.Key, dataObj);
}
LobbyAPIInterface.UpdatePlayerAsync(m_lastKnownLobby.Id, playerId, dataCurr, (result) => {
if (result != null)
m_lastKnownLobby = result; // Store the most up-to-date lobby now since we have it, instead of waiting for the next heartbeat.
LobbyAPIInterface.UpdatePlayerAsync(m_RemoteLobby.Id, playerId, dataCurr, (result) =>
{
onComplete?.Invoke();
}, null, null);
}

if (!ShouldUpdateData(() => { UpdatePlayerRelayInfoAsync(allocationId, connectionInfo, onComplete); }, onComplete, true)) // Do retry here since the RelayUtpSetup that called this might be destroyed right after this.
return;
string playerId = Locator.Get.Identity.GetSubIdentity(Auth.IIdentityType.Auth).GetContent("id");
LobbyAPIInterface.UpdatePlayerAsync(m_lastKnownLobby.Id, playerId, new Dictionary<string, PlayerDataObject>(), (r) => { onComplete?.Invoke(); }, allocationId, connectionInfo);
LobbyAPIInterface.UpdatePlayerAsync(m_RemoteLobby.Id, playerId, new Dictionary<string, PlayerDataObject>(), (r) => { onComplete?.Invoke(); }, allocationId, connectionInfo);
}
/// <param name="data">Key-value pairs, which will overwrite any existing data for these keys. Presumed to be available to all lobby members but not publicly.</param>

return;
Lobby lobby = m_lastKnownLobby;
Dictionary<string, DataObject> dataCurr = lobby.Data ?? new Dictionary<string, DataObject>();
Dictionary<string, DataObject> dataCurr = m_RemoteLobby.Data ?? new Dictionary<string, DataObject>();
var shouldLock = false;
var shouldLock = false;
foreach (var dataNew in data)
{
// Special case: We want to be able to filter on our color data, so we need to supply an arbitrary index to retrieve later. Uses N# for numerics, instead of S# for strings.

dataCurr[dataNew.Key] = dataObj;
else
dataCurr.Add(dataNew.Key, dataObj);
//Special Use: Get the state of the Local lobby so we can lock it from appearing in queries if it's not in the "Lobby" State
if (dataNew.Key == "State")
{

}
}
LobbyAPIInterface.UpdateLobbyAsync(lobby.Id, dataCurr, shouldLock, (result) =>
LobbyAPIInterface.UpdateLobbyAsync(m_RemoteLobby.Id, dataCurr, shouldLock, (result) =>
m_lastKnownLobby = result;
m_RemoteLobby = result;
onComplete?.Invoke();
});
}

return false;
}
Lobby lobby = m_lastKnownLobby;
if (lobby == null)
if (m_RemoteLobby == null)
{
if (shouldRetryIfLobbyNull)
m_rateLimitQuery.EnqueuePendingOperation(caller);

if (m_heartbeatTime > k_heartbeatPeriod)
{
m_heartbeatTime -= k_heartbeatPeriod;
LobbyAPIInterface.HeartbeatPlayerAsync(m_lastKnownLobby.Id);
LobbyAPIInterface.HeartbeatPlayerAsync(m_RemoteLobby.Id);
}
}

public void EnqueuePendingOperation(Action action)
{
//We probably dont want many of the same actions added to fire off multiple times.
if (!m_pendingOperations.Contains(action))
return;
m_pendingOperations.Enqueue(action);
}

32
Assets/Scripts/GameLobby/Vivox/VivoxUserHandler.cs


private string m_vivoxId;
private const int k_volumeMin = -50, k_volumeMax = 20; // From the Vivox docs, the valid range is [-50, 50] but anything above 25 risks being painfully loud.
public static float NormalizedVolumeDefault { get { return (0f - k_volumeMin) / (k_volumeMax - k_volumeMin); } }
public static float NormalizedVolumeDefault
{
get { return (0f - k_volumeMin) / (k_volumeMax - k_volumeMin); }
}
public void Start()
{

public void SetId(string id)
{
m_id = id;
// Vivox appends additional info to the ID we provide, in order to associate it with a specific channel. We'll construct m_vivoxId to match the ID used by Vivox.
// FUTURE: This isn't yet available. When using Auth, the Vivox ID will match this format:
// Account account = new Account(id);

bool isThisUser = username == m_id;
if (isThisUser)
{ m_vivoxId = keyEventArg.Key; // Since we couldn't construct the Vivox ID earlier, retrieve it here.
{
m_vivoxId = keyEventArg.Key; // Since we couldn't construct the Vivox ID earlier, retrieve it here.
if(!participant.IsMutedForAll)
m_lobbyUserVolumeUI.EnableVoice(false);//Should check if user is muted or not.
if (!participant.IsMutedForAll)
m_lobbyUserVolumeUI.EnableVoice(false); //Should check if user is muted or not.
if(!participant.LocalMute)
m_lobbyUserVolumeUI.EnableVoice(false);//Should check if user is muted or not.
if (!participant.LocalMute)
m_lobbyUserVolumeUI.EnableVoice(false); //Should check if user is muted or not.
else
m_lobbyUserVolumeUI.DisableVoice(false);
}

bool isThisUser = username == m_id;
if (isThisUser)
{ m_lobbyUserVolumeUI.DisableVoice(true);
{
m_lobbyUserVolumeUI.DisableVoice(true);
private void OnParticipantValueUpdated(object sender, ValueEventArg<string, IParticipant> valueEventArg)
{
var source = (VivoxUnity.IReadOnlyDictionary<string, IParticipant>)sender;

if (property == "UnavailableCaptureDevice")
{
if (participant.UnavailableCaptureDevice)
{ m_lobbyUserVolumeUI.DisableVoice(false);
participant.SetIsMuteForAll(m_vivoxId, true, null); // Note: If you add more places where a player might be globally muted, a state machine might be required for accurate logic.
{
m_lobbyUserVolumeUI.DisableVoice(false);
participant.SetIsMuteForAll(true, null); // Note: If you add more places where a player might be globally muted, a state machine might be required for accurate logic.
{ m_lobbyUserVolumeUI.EnableVoice(false);
participant.SetIsMuteForAll(m_vivoxId, false, null); // Also note: This call is asynchronous, so it's possible to exit the lobby before this completes, resulting in a Vivox error.
{
m_lobbyUserVolumeUI.EnableVoice(false);
participant.SetIsMuteForAll(false, null); // Also note: This call is asynchronous, so it's possible to exit the lobby before this completes, resulting in a Vivox error.
}
}
else if (property == "IsMutedForAll")

35
Packages/manifest.json


{
"dependencies": {
"com.unity.2d.sprite": "1.0.0",
"com.unity.collab-proxy": "1.11.2",
"com.unity.collab-proxy": "1.15.13",
"com.unity.ide.visualstudio": "2.0.11",
"com.unity.ide.vscode": "1.2.4",
"com.unity.netcode.adapter.utp": "1.0.0-pre.6",
"com.unity.netcode.gameobjects": "1.0.0-pre.6",
"com.unity.ide.visualstudio": "2.0.14",
"com.unity.ide.vscode": "1.2.5",
"com.unity.netcode.adapter.utp": "1.0.0-pre.3",
"com.unity.netcode.gameobjects": "1.0.0-pre.2",
"com.unity.render-pipelines.universal": "10.6.0",
"com.unity.services.authentication": "1.0.0-pre.6",
"com.unity.services.core": "1.1.0-pre.11",
"com.unity.services.lobby": "1.0.0-pre.6",
"com.unity.render-pipelines.universal": "10.8.1",
"com.unity.services.authentication": "1.0.0-pre.37",
"com.unity.services.core": "1.2.0",
"com.unity.services.lobby": "1.0.0-pre.7",
"com.unity.services.vivox": "15.1.150002-pre.1",
"com.unity.services.vivox": "15.1.170000-pre.1",
"com.unity.services.wire": "1.0.0-preview.20",
"com.unity.test-framework": "1.1.29",
"com.unity.test-framework": "1.1.31",
"com.unity.textmeshpro": "3.0.6",
"com.unity.toolchain.win-x86_64-linux-x86_64": "0.1.20-preview",
"com.unity.transport": "1.0.0-pre.9",

"com.unity.modules.wind": "1.0.0",
"com.unity.modules.xr": "1.0.0"
},
"scopedRegistries": []
"scopedRegistries": [
{
"name": "Internal Candidates Registry",
"url": "https://artifactory.prd.it.unity3d.com/artifactory/api/npm/upm-candidates",
"scopes": [
"com.unity.services.wire",
"com.unity.services.lobby",
"com.unity.services.authentication"
]
}
]
}

105
Packages/packages-lock.json


"dependencies": {}
},
"com.unity.burst": {
"version": "1.6.4",
"depth": 2,
"version": "1.5.5",
"depth": 1,
"source": "registry",
"dependencies": {
"com.unity.mathematics": "1.2.1"

"com.unity.collab-proxy": {
"version": "1.11.2",
"version": "1.15.13",
"dependencies": {},
"dependencies": {
"com.unity.services.core": "1.0.1"
},
"url": "https://packages.unity.com"
},
"com.unity.collections": {

"url": "https://packages.unity.com"
},
"com.unity.ide.visualstudio": {
"version": "2.0.11",
"version": "2.0.14",
"depth": 0,
"source": "registry",
"dependencies": {

},
"com.unity.ide.vscode": {
"version": "1.2.4",
"version": "1.2.5",
"depth": 0,
"source": "registry",
"dependencies": {},

"version": "1.2.5",
"depth": 2,
"version": "1.2.1",
"depth": 1,
"version": "1.0.0-pre.6",
"version": "1.0.0-pre.3",
"com.unity.netcode.gameobjects": "1.0.0-pre.6",
"com.unity.transport": "1.0.0-pre.14"
"com.unity.netcode.gameobjects": "1.0.0-pre.3",
"com.unity.transport": "1.0.0-pre.7"
"version": "1.0.0-pre.6",
"depth": 0,
"version": "1.0.0-pre.3",
"depth": 1,
"com.unity.modules.ai": "1.0.0",
"com.unity.modules.physics": "1.0.0",
"com.unity.modules.physics2d": "1.0.0",
"com.unity.collections": "1.1.0"
"com.unity.collections": "1.0.0-pre.5"
},
"url": "https://packages.unity.com"
},

"url": "https://packages.unity.com"
},
"com.unity.nuget.newtonsoft-json": {
"version": "2.0.0",
"depth": 0,
"version": "3.0.1",
"depth": 1,
"version": "10.6.0",
"version": "10.8.1",
"com.unity.ugui": "1.0.0"
"com.unity.ugui": "1.0.0",
"com.unity.modules.physics": "1.0.0",
"com.unity.modules.jsonserialize": "1.0.0"
"version": "10.6.0",
"version": "10.8.1",
"com.unity.render-pipelines.core": "10.6.0",
"com.unity.shadergraph": "10.6.0"
"com.unity.render-pipelines.core": "10.8.1",
"com.unity.shadergraph": "10.8.1"
},
"url": "https://packages.unity.com"
},

"url": "https://packages.unity.com"
},
"com.unity.services.authentication": {
"version": "1.0.0-pre.6",
"depth": 0,
"version": "1.0.0-pre.73",
"depth": 1,
"com.unity.services.core": "1.1.0-pre.9",
"com.unity.services.core": "1.1.0-pre.77",
"url": "https://packages.unity.com"
"url": "https://artifactory.prd.it.unity3d.com/artifactory/api/npm/upm-candidates"
"version": "1.1.0-pre.11",
"version": "1.2.0",
"com.unity.nuget.newtonsoft-json": "2.0.0"
"com.unity.nuget.newtonsoft-json": "3.0.1",
"com.unity.modules.androidjni": "1.0.0"
"version": "1.0.0-pre.6",
"version": "1.0.0-pre.7",
"com.unity.services.core": "1.1.0-pre.10",
"com.unity.services.core": "1.1.0-pre.77",
"com.unity.nuget.newtonsoft-json": "2.0.0",
"com.unity.services.authentication": "1.0.0-pre.6"
"com.unity.nuget.newtonsoft-json": "3.0.1",
"com.unity.services.authentication": "1.0.0-pre.73"
"url": "https://packages.unity.com"
"url": "https://artifactory.prd.it.unity3d.com/artifactory/api/npm/upm-candidates"
},
"com.unity.services.relay": {
"version": "1.0.1-pre.3",

"url": "https://packages.unity.com"
},
"com.unity.services.vivox": {
"version": "15.1.150002-pre.1",
"version": "15.1.170000-pre.1",
"com.unity.settings-manager": "1.0.3",
"com.unity.services.core": "1.1.0-pre.8",
"com.unity.settings-manager": "2.0.0",
"com.unity.services.core": "1.1.0-pre.10",
"com.unity.services.wire": {
"version": "1.0.0-preview.20",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.services.core": "1.2.0",
"com.unity.nuget.newtonsoft-json": "3.0.1",
"com.unity.services.authentication": "1.0.0-pre.37"
},
"url": "https://artifactory.prd.it.unity3d.com/artifactory/api/npm/upm-candidates"
},
"version": "1.0.3",
"version": "2.0.0",
"depth": 1,
"source": "registry",
"dependencies": {},

"version": "10.6.0",
"version": "10.8.1",
"com.unity.render-pipelines.core": "10.6.0",
"com.unity.render-pipelines.core": "10.8.1",
"com.unity.searcher": "4.3.2"
},
"url": "https://packages.unity.com"

"url": "https://packages.unity.com"
},
"com.unity.test-framework": {
"version": "1.1.29",
"version": "1.1.31",
"depth": 0,
"source": "registry",
"dependencies": {

"url": "https://packages.unity.com"
},
"com.unity.transport": {
"version": "1.0.0-pre.14",
"depth": 1,
"version": "1.0.0-pre.9",
"depth": 0,
"com.unity.burst": "1.6.4",
"com.unity.mathematics": "1.2.5"
"com.unity.burst": "1.5.5",
"com.unity.mathematics": "1.2.1"
},
"url": "https://packages.unity.com"
},

2
ProjectSettings/GraphicsSettings.asset


- {fileID: 10783, guid: 0000000000000000f000000000000000, type: 0}
m_PreloadedShaders: []
m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0}
m_CustomRenderPipeline: {fileID: 11400000, guid: f54ab92f70892f44eba32f1c414f5b4a, type: 2}
m_CustomRenderPipeline: {fileID: 0}
m_TransparencySortMode: 0
m_TransparencySortAxis: {x: 0, y: 0, z: 1}
m_DefaultRenderingPath: 1

28
ProjectSettings/PackageManagerSettings.asset


m_Scopes: []
m_IsDefault: 1
m_Capabilities: 7
- m_Id: scoped:Internal Candidates Registry
m_Name: Internal Candidates Registry
m_Url: https://artifactory.prd.it.unity3d.com/artifactory/api/npm/upm-candidates
m_Scopes:
- com.unity.services.wire
- com.unity.services.lobby
- com.unity.services.authentication
m_IsDefault: 0
m_Capabilities: 0
m_Id:
m_Name:
m_Url:
m_Scopes: []
m_Id: scoped:Internal Candidates Registry
m_Name: Internal Candidates Registry
m_Url: https://artifactory.prd.it.unity3d.com/artifactory/api/npm/upm-candidates
m_Scopes:
- com.unity.services.wire
- com.unity.services.lobby
- com.unity.services.authentication
m_Name:
m_Url:
m_Name: Internal Candidates Registry
m_Url: https://artifactory.prd.it.unity3d.com/artifactory/api/npm/upm-candidates
-
- com.unity.services.wire
- com.unity.services.lobby
- com.unity.services.authentication
m_SelectedScopeIndex: 0

4
ProjectSettings/Packages/com.unity.services.vivox/Settings.json


{
"m_Name": "Settings",
"m_Path": "ProjectSettings/Packages/com.unity.services.vivox/Settings.json",
"value": "{\"m_Value\":false}"
"value": "{\"m_Value\":true}"
},
{
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",

5
ProjectSettings/ProjectSettings.asset


androidSupportedAspectRatio: 1
androidMaxAspectRatio: 2.1
applicationIdentifier:
Standalone: com.unity.services.samples.lobby-rooms
Standalone: com.unity.services.samples.game-lobby
buildNumber:
Standalone: 0
iPhone: 0

webGLLinkerTarget: 1
webGLThreadsSupport: 0
webGLDecompressionFallback: 0
scriptingDefineSymbols: {}
scriptingDefineSymbols:
1:
additionalCompilerArguments: {}
platformArchitecture: {}
scriptingBackend: {}

4
ProjectSettings/ProjectVersion.txt


m_EditorVersion: 2020.3.20f1
m_EditorVersionWithRevision: 2020.3.20f1 (41c4e627c95f)
m_EditorVersion: 2020.3.31f1
m_EditorVersionWithRevision: 2020.3.31f1 (6b54b7616050)

999
~Documentation/Images/2_lobby.PNG
文件差异内容过多而无法显示
查看文件

138
Assets/Scripts/GameLobby/Lobby/LobbyContentUpdater.cs


using System;
using LobbyRelaySample.lobby;
using Unity.Services.Lobbies.Models;
using UnityEngine;
namespace LobbyRelaySample
{
/// <summary>
/// Keep updated on changes to a joined lobby, at a speed compliant with Lobby's rate limiting.
/// </summary>
public class LobbyContentUpdater : IReceiveMessages
{
private LocalLobby m_LocalLobby;
private LobbyUser m_LocalUser;
private bool m_ShouldPushData = false;
private const float k_approvalMaxTime = 10; // Used for determining if a user should timeout if they are unable to connect.
private float m_lifetime = 0;
public void BeginTracking(LocalLobby localLobby, LobbyUser localUser)
{
m_LocalUser = localUser;
m_LocalLobby = localLobby;
m_LocalLobby.onChanged += OnLocalLobbyChanged;
m_ShouldPushData = true;
Locator.Get.Messenger.Subscribe(this);
Locator.Get.UpdateSlow.Subscribe(OnUpdate, 1.5f);
m_lifetime = 0;
LobbyAsyncRequests.Instance.onLobbyUpdated += OnRemoteLobbyUpdated;
}
public void EndTracking()
{
m_ShouldPushData = false;
Locator.Get.Messenger.Unsubscribe(this);
Locator.Get.UpdateSlow.Unsubscribe(OnUpdate);
if (m_LocalLobby != null)
m_LocalLobby.onChanged -= OnLocalLobbyChanged;
LobbyAsyncRequests.Instance.onLobbyUpdated -= OnRemoteLobbyUpdated;
m_LocalLobby = null;
}
public void OnReceiveMessage(MessageType type, object msg)
{
if (type == MessageType.ClientUserSeekingDisapproval)
{
bool shouldDisapprove = m_LocalLobby.State != LobbyState.Lobby; // By not refreshing, it's possible to have a lobby in the lobby list UI after its countdown starts and then try joining.
if (shouldDisapprove)
(msg as Action<relay.Approval>)?.Invoke(relay.Approval.GameAlreadyStarted);
}
}
void OnLocalLobbyChanged(LocalLobby changed)
{
if (string.IsNullOrEmpty(changed.LobbyID)) // When the player leaves, their LocalLobby is cleared out but maintained.
{
EndTracking();
return;
}
if (changed.pullUpdate)
{
changed.pullUpdate = false;
return;
}
m_ShouldPushData = true;
}
/// <summary>
/// If there have been any data changes since the last update, push them to Lobby. Regardless, pull for the most recent data.
/// (Unless we're already awaiting a query, in which case continue waiting.)
/// </summary>
private void OnUpdate(float dt)
{
m_lifetime += dt;
if (m_LocalLobby == null)
return;
if (m_LocalUser.IsHost)
LobbyAsyncRequests.Instance.DoLobbyHeartbeat(dt);
if (!m_LocalUser.IsApproved && m_lifetime > k_approvalMaxTime)
{
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup, "Connection attempt timed out!");
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeMenuState, GameState.JoinMenu);
}
if (m_ShouldPushData)
PushDataToLobby();
void PushDataToLobby()
{
m_ShouldPushData = false;
if (m_LocalUser.IsHost)
{
DoLobbyDataPush();
}
DoPlayerDataPush();
}
void DoLobbyDataPush()
{
LobbyAsyncRequests.Instance.UpdateLobbyDataAsync(LobbyConverters.LocalToRemoteData(m_LocalLobby), null);
}
void DoPlayerDataPush()
{
LobbyAsyncRequests.Instance.UpdatePlayerDataAsync(LobbyConverters.LocalToRemoteUserData(m_LocalUser), null);
}
}
void OnRemoteLobbyUpdated(Lobby lobby)
{
m_LocalLobby.pullUpdate = true;
//synching our local lobby
LobbyConverters.RemoteToLocal(lobby, m_LocalLobby);
//Dont push data this tick, since we "pulled"s
if (!m_LocalUser.IsHost)
{
foreach (var lobbyUser in m_LocalLobby.LobbyUsers)
{
if (lobbyUser.Value.IsHost)
return;
}
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup, "Host left the lobby! Disconnecting...");
Locator.Get.Messenger.OnReceiveMessage(MessageType.EndGame, null);
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeMenuState, GameState.JoinMenu);
}
}
}
}

105
Assets/Scripts/GameLobby/Lobby/LobbyConverters.cs


using System.Collections.Generic;
using Unity.Services.Lobbies.Models;
using UnityEngine;
namespace LobbyRelaySample.lobby
{
/// <summary>
/// QueryToLocalList the lobby resulting from a request into a LocalLobby for use in the game logic.
/// </summary>
public static class LobbyConverters
{
public static Dictionary<string, string> LocalToRemoteData(LocalLobby lobby)
{
Dictionary<string, string> data = new Dictionary<string, string>();
data.Add("RelayCode", lobby.RelayCode);
data.Add("RelayNGOCode", lobby.RelayNGOCode);
data.Add("State", ((int)lobby.State).ToString()); // Using an int is smaller than using the enum state's name.
data.Add("Color", ((int)lobby.Color).ToString());
data.Add("State_LastEdit", lobby.Data.State_LastEdit.ToString());
data.Add("Color_LastEdit", lobby.Data.Color_LastEdit.ToString());
data.Add("RelayNGOCode_LastEdit", lobby.Data.RelayNGOCode_LastEdit.ToString());
return data;
}
public static Dictionary<string, string> LocalToRemoteUserData(LobbyUser user)
{
Dictionary<string, string> data = new Dictionary<string, string>();
if (user == null || string.IsNullOrEmpty(user.ID))
return data;
data.Add("DisplayName", user.DisplayName); // The lobby doesn't need to know any data beyond the name and state; Relay will handle the rest.
data.Add("UserStatus", ((int)user.UserStatus).ToString());
return data;
}
/// <summary>
/// Create a new LocalLobby from the content of a retrieved lobby. Its data can be copied into an existing LocalLobby for use.
/// </summary>
public static void RemoteToLocal(Lobby lobby, LocalLobby lobbyToUpdate)
{
//Copy Data from Lobby into Local lobby fields
LocalLobby.LobbyData info = new LocalLobby.LobbyData(lobbyToUpdate.Data)
{
LobbyID = lobby.Id,
LobbyCode = lobby.LobbyCode,
Private = lobby.IsPrivate,
LobbyName = lobby.Name,
MaxPlayerCount = lobby.MaxPlayers,
RelayCode = lobby.Data?.ContainsKey("RelayCode") == true ? lobby.Data["RelayCode"].Value : lobbyToUpdate.RelayCode, // By providing RelayCode through the lobby data with Member visibility, we ensure a client is connected to the lobby before they could attempt a relay connection, preventing timing issues between them.
RelayNGOCode = lobby.Data?.ContainsKey("RelayNGOCode") == true ? lobby.Data["RelayNGOCode"].Value : lobbyToUpdate.RelayNGOCode,
State = lobby.Data?.ContainsKey("State") == true ? (LobbyState)int.Parse(lobby.Data["State"].Value) : LobbyState.Lobby,
Color = lobby.Data?.ContainsKey("Color") == true ? (LobbyColor)int.Parse(lobby.Data["Color"].Value) : LobbyColor.None,
State_LastEdit = lobby.Data?.ContainsKey("State_LastEdit") == true ? long.Parse(lobby.Data["State_LastEdit"].Value) : lobbyToUpdate.Data.State_LastEdit,
Color_LastEdit = lobby.Data?.ContainsKey("Color_LastEdit") == true ? long.Parse(lobby.Data["Color_LastEdit"].Value) : lobbyToUpdate.Data.Color_LastEdit,
RelayNGOCode_LastEdit = lobby.Data?.ContainsKey("RelayNGOCode_LastEdit") == true ? long.Parse(lobby.Data["RelayNGOCode_LastEdit"].Value) : lobbyToUpdate.Data.RelayNGOCode_LastEdit
};
Dictionary<string, LobbyUser> lobbyUsers = new Dictionary<string, LobbyUser>();
foreach (var player in lobby.Players)
{
// If we already know about this player and this player is already connected to Relay, don't overwrite things that Relay might be changing.
if (player.Data?.ContainsKey("UserStatus") == true && int.TryParse(player.Data["UserStatus"].Value, out int status))
{
if (status > (int)UserStatus.Connecting && lobbyToUpdate.LobbyUsers.ContainsKey(player.Id))
{
lobbyUsers.Add(player.Id, lobbyToUpdate.LobbyUsers[player.Id]);
continue;
}
}
// If the player isn't connected to Relay, get the most recent data that the lobby knows.
// (If we haven't seen this player yet, a new local representation of the player will have already been added by the LocalLobby.)
LobbyUser incomingData = new LobbyUser
{
IsHost = lobby.HostId.Equals(player.Id),
DisplayName = player.Data?.ContainsKey("DisplayName") == true ? player.Data["DisplayName"].Value : default,
Emote = player.Data?.ContainsKey("Emote") == true ? (EmoteType)int.Parse(player.Data["Emote"].Value) : default,
UserStatus = player.Data?.ContainsKey("UserStatus") == true ? (UserStatus)int.Parse(player.Data["UserStatus"].Value) : UserStatus.Connecting,
ID = player.Id
};
lobbyUsers.Add(incomingData.ID, incomingData);
}
//Push all the data at once so we don't call OnChanged for each variable
lobbyToUpdate.CopyObserved(info, lobbyUsers);
}
/// <summary>
/// Create a list of new LocalLobbies from the result of a lobby list query.
/// </summary>
public static List<LocalLobby> QueryToLocalList(QueryResponse response)
{
List<LocalLobby> retLst = new List<LocalLobby>();
foreach (var lobby in response.Results)
retLst.Add(RemoteToNewLocal(lobby));
return retLst;
}
private static LocalLobby RemoteToNewLocal(Lobby lobby)
{
LocalLobby data = new LocalLobby();
RemoteToLocal(lobby, data);
return data;
}
}
}

76
Assets/Scripts/GameLobby/Lobby/ToLocalLobby.cs


using System.Collections.Generic;
using Unity.Services.Lobbies.Models;
namespace LobbyRelaySample.lobby
{
/// <summary>
/// Convert the lobby resulting from a request into a LocalLobby for use in the game logic.
/// </summary>
public static class ToLocalLobby
{
/// <summary>
/// Create a new LocalLobby from the content of a retrieved lobby. Its data can be copied into an existing LocalLobby for use.
/// </summary>
public static void Convert(Lobby lobby, LocalLobby outputToHere)
{
LocalLobby.LobbyData info = new LocalLobby.LobbyData // Technically, this is largely redundant after the first assignment, but it won't do any harm to assign it again.
{ LobbyID = lobby.Id,
LobbyCode = lobby.LobbyCode,
Private = lobby.IsPrivate,
LobbyName = lobby.Name,
MaxPlayerCount = lobby.MaxPlayers,
RelayCode = lobby.Data?.ContainsKey("RelayCode") == true ? lobby.Data["RelayCode"].Value : null, // By providing RelayCode through the lobby data with Member visibility, we ensure a client is connected to the lobby before they could attempt a relay connection, preventing timing issues between them.
RelayNGOCode = lobby.Data?.ContainsKey("RelayNGOCode") == true ? lobby.Data["RelayNGOCode"].Value : null,
State = lobby.Data?.ContainsKey("State") == true ? (LobbyState) int.Parse(lobby.Data["State"].Value) : LobbyState.Lobby,
Color = lobby.Data?.ContainsKey("Color") == true ? (LobbyColor) int.Parse(lobby.Data["Color"].Value) : LobbyColor.None,
State_LastEdit = lobby.Data?.ContainsKey("State_LastEdit") == true ? long.Parse(lobby.Data["State_LastEdit"].Value) : 0,
Color_LastEdit = lobby.Data?.ContainsKey("Color_LastEdit") == true ? long.Parse(lobby.Data["Color_LastEdit"].Value) : 0,
RelayNGOCode_LastEdit = lobby.Data?.ContainsKey("RelayNGOCode_LastEdit") == true ? long.Parse(lobby.Data["RelayNGOCode_LastEdit"].Value) : 0,
};
Dictionary<string, LobbyUser> lobbyUsers = new Dictionary<string, LobbyUser>();
foreach (var player in lobby.Players)
{
// If we already know about this player and this player is already connected to Relay, don't overwrite things that Relay might be changing.
if (player.Data?.ContainsKey("UserStatus") == true && int.TryParse(player.Data["UserStatus"].Value, out int status))
{
if (status > (int)UserStatus.Connecting && outputToHere.LobbyUsers.ContainsKey(player.Id))
{
lobbyUsers.Add(player.Id, outputToHere.LobbyUsers[player.Id]);
continue;
}
}
// If the player isn't connected to Relay, get the most recent data that the lobby knows.
// (If we haven't seen this player yet, a new local representation of the player will have already been added by the LocalLobby.)
LobbyUser incomingData = new LobbyUser
{
IsHost = lobby.HostId.Equals(player.Id),
DisplayName = player.Data?.ContainsKey("DisplayName") == true ? player.Data["DisplayName"].Value : default,
Emote = player.Data?.ContainsKey("Emote") == true ? (EmoteType)int.Parse(player.Data["Emote"].Value) : default,
UserStatus = player.Data?.ContainsKey("UserStatus") == true ? (UserStatus)int.Parse(player.Data["UserStatus"].Value) : UserStatus.Connecting,
ID = player.Id
};
lobbyUsers.Add(incomingData.ID, incomingData);
}
outputToHere.CopyObserved(info, lobbyUsers);
}
/// <summary>
/// Create a list of new LocalLobbies from the result of a lobby list query.
/// </summary>
public static List<LocalLobby> Convert(QueryResponse response)
{
List<LocalLobby> retLst = new List<LocalLobby>();
foreach (var lobby in response.Results)
retLst.Add(Convert(lobby));
return retLst;
}
private static LocalLobby Convert(Lobby lobby)
{
LocalLobby data = new LocalLobby();
Convert(lobby, data);
return data;
}
}
}

153
Assets/Scripts/GameLobby/Lobby/LobbyContentHeartbeat.cs


using System;
using System.Collections.Generic;
using LobbyRemote = Unity.Services.Lobbies.Models.Lobby;
namespace LobbyRelaySample
{
/// <summary>
/// Keep updated on changes to a joined lobby, at a speed compliant with Lobby's rate limiting.
/// </summary>
public class LobbyContentHeartbeat : IReceiveMessages
{
private LocalLobby m_localLobby;
private LobbyUser m_localUser;
private int m_awaitingQueryCount = 0;
private bool m_shouldPushData = false;
private const float k_approvalMaxTime = 10; // Used for determining if a user should timeout if they are unable to connect.
private float m_lifetime = 0;
public void BeginTracking(LocalLobby lobby, LobbyUser localUser)
{
m_localLobby = lobby;
m_localUser = localUser;
Locator.Get.UpdateSlow.Subscribe(OnUpdate, 1.5f);
Locator.Get.Messenger.Subscribe(this);
m_localLobby.onChanged += OnLocalLobbyChanged;
m_shouldPushData = true; // Ensure the initial presence of a new player is pushed to the lobby; otherwise, when a non-host joins, the LocalLobby never receives their data until they push something new.
m_lifetime = 0;
}
public void EndTracking()
{
m_shouldPushData = false;
Locator.Get.UpdateSlow.Unsubscribe(OnUpdate);
Locator.Get.Messenger.Unsubscribe(this);
if (m_localLobby != null)
m_localLobby.onChanged -= OnLocalLobbyChanged;
m_localLobby = null;
m_localUser = null;
}
private void OnLocalLobbyChanged(LocalLobby changed)
{
if (string.IsNullOrEmpty(changed.LobbyID)) // When the player leaves, their LocalLobby is cleared out but maintained.
EndTracking();
m_shouldPushData = true;
}
public void OnReceiveMessage(MessageType type, object msg)
{
if (type == MessageType.ClientUserSeekingDisapproval)
{
bool shouldDisapprove = m_localLobby.State != LobbyState.Lobby; // By not refreshing, it's possible to have a lobby in the lobby list UI after its countdown starts and then try joining.
if (shouldDisapprove)
(msg as Action<relay.Approval>)?.Invoke(relay.Approval.GameAlreadyStarted);
}
}
/// <summary>
/// If there have been any data changes since the last update, push them to Lobby. Regardless, pull for the most recent data.
/// (Unless we're already awaiting a query, in which case continue waiting.)
/// </summary>
private void OnUpdate(float dt)
{
m_lifetime += dt;
if (m_awaitingQueryCount > 0 || m_localLobby == null)
return;
if (m_localUser.IsHost)
LobbyAsyncRequests.Instance.DoLobbyHeartbeat(dt);
if (!m_localUser.IsApproved && m_lifetime > k_approvalMaxTime)
{
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup, "Connection attempt timed out!");
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeMenuState, GameState.JoinMenu);
}
if (m_shouldPushData)
PushDataToLobby();
else
OnRetrieve();
void PushDataToLobby()
{
m_shouldPushData = false;
if (m_localUser.IsHost)
{
m_awaitingQueryCount++;
DoLobbyDataPush();
}
m_awaitingQueryCount++;
DoPlayerDataPush();
}
void DoLobbyDataPush()
{
LobbyAsyncRequests.Instance.UpdateLobbyDataAsync(RetrieveLobbyData(m_localLobby), () => { if (--m_awaitingQueryCount <= 0) OnRetrieve(); });
}
void DoPlayerDataPush()
{
LobbyAsyncRequests.Instance.UpdatePlayerDataAsync(RetrieveUserData(m_localUser), () => { if (--m_awaitingQueryCount <= 0) OnRetrieve(); });
}
void OnRetrieve()
{
LobbyRemote lobbyRemote = LobbyAsyncRequests.Instance.CurrentLobby;
if (lobbyRemote == null) return;
bool prevShouldPush = m_shouldPushData;
var prevState = m_localLobby.State;
lobby.ToLocalLobby.Convert(lobbyRemote, m_localLobby);
m_shouldPushData = prevShouldPush;
// If the host suddenly leaves, the Lobby service will automatically handle disconnects after about 30s, but we can try to do a disconnect sooner if we detect it.
if (!m_localUser.IsHost)
{
foreach (var lobbyUser in m_localLobby.LobbyUsers)
{
if (lobbyUser.Value.IsHost)
return;
}
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup, "Host left the lobby! Disconnecting...");
Locator.Get.Messenger.OnReceiveMessage(MessageType.EndGame, null);
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeMenuState, GameState.JoinMenu);
}
}
}
private static Dictionary<string, string> RetrieveLobbyData(LocalLobby lobby)
{
Dictionary<string, string> data = new Dictionary<string, string>();
data.Add("RelayCode", lobby.RelayCode);
data.Add("RelayNGOCode", lobby.RelayNGOCode);
data.Add("State", ((int)lobby.State).ToString()); // Using an int is smaller than using the enum state's name.
data.Add("Color", ((int)lobby.Color).ToString());
data.Add("State_LastEdit", lobby.Data.State_LastEdit.ToString());
data.Add("Color_LastEdit", lobby.Data.Color_LastEdit.ToString());
data.Add("RelayNGOCode_LastEdit", lobby.Data.RelayNGOCode_LastEdit.ToString());
return data;
}
private static Dictionary<string, string> RetrieveUserData(LobbyUser user)
{
Dictionary<string, string> data = new Dictionary<string, string>();
if (user == null || string.IsNullOrEmpty(user.ID))
return data;
data.Add("DisplayName", user.DisplayName); // The lobby doesn't need to know any data beyond the name and state; Relay will handle the rest.
data.Add("UserStatus", ((int)user.UserStatus).ToString());
return data;
}
}
}

/Assets/Scripts/GameLobby/Lobby/ToLocalLobby.cs.meta → /Assets/Scripts/GameLobby/Lobby/LobbyConverters.cs.meta

/Assets/Scripts/GameLobby/Lobby/LobbyContentHeartbeat.cs.meta → /Assets/Scripts/GameLobby/Lobby/LobbyContentUpdater.cs.meta

正在加载...
取消
保存