浏览代码

feat : Working Callbacks!!!!

-still need to remove commented out code
/main/staging/2021_Upgrade/Async_Refactor
Jacob 2 年前
当前提交
e462c1d1
共有 16 个文件被更改,包括 662 次插入629 次删除
  1. 2
      Assets/Prefabs/UI/LobbyUserList.prefab
  2. 2
      Assets/Prefabs/UI/UserCardPanel.prefab
  3. 20
      Assets/Scripts/GameLobby/Game/GameManager.cs
  4. 14
      Assets/Scripts/GameLobby/Game/LocalLobby.cs
  5. 6
      Assets/Scripts/GameLobby/Game/LocalPlayer.cs
  6. 27
      Assets/Scripts/GameLobby/Lobby/LobbyManager.cs
  7. 346
      Assets/Scripts/GameLobby/Lobby/LobbySynchronizer.cs
  8. 47
      Assets/Scripts/GameLobby/NGO/SetupInGame.cs
  9. 108
      Assets/Scripts/GameLobby/Relay/RelayPendingApproval.cs
  10. 434
      Assets/Scripts/GameLobby/Relay/RelayUtpHost.cs
  11. 4
      Assets/Scripts/GameLobby/Tests/PlayMode/LobbyRoundtripTests.cs
  12. 188
      Assets/Scripts/GameLobby/Tests/PlayMode/UtpTests.cs
  13. 30
      Assets/Scripts/GameLobby/UI/InLobbyUserUI.cs
  14. 10
      Assets/Scripts/GameLobby/UI/LobbyEntryUI.cs
  15. 45
      Assets/Scripts/GameLobby/UI/LobbyUserListUI.cs
  16. 8
      Assets/StreamingAssets.meta

2
Assets/Prefabs/UI/LobbyUserList.prefab


m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 0
m_PresetInfoIsWorld: 1
--- !u!114 &4463750083940306577
MonoBehaviour:
m_ObjectHideFlags: 0

2
Assets/Prefabs/UI/UserCardPanel.prefab


- {fileID: 21300000, guid: bc8c0e2bc04ce93488efe4ea898047a9, type: 3}
- {fileID: 21300000, guid: f18a89c12a346a8488ed507a0bd79d2e, type: 3}
- {fileID: 21300000, guid: 0fca28892f34375439c32b1ee1c8d655, type: 3}
m_vivoxUserHandler: {fileID: 4826507163535553981}
m_VivoxUserHandler: {fileID: 4826507163535553981}
--- !u!114 &7885056472121154813
MonoBehaviour:
m_ObjectHideFlags: 0

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


}
LobbyConverters.RemoteToLocal(lobby, m_LocalLobby);
await LobbyManager.SubscribeToLobbyChanges(lobby.Id, m_LocalLobby);
await LobbyManager.SubscribeToLocalLobbyChanges(lobby.Id, m_LocalLobby);
CreateLobby();
}
catch (Exception exception)

}
LobbyConverters.RemoteToLocal(lobby, m_LocalLobby);
await LobbyManager.SubscribeToLobbyChanges(lobby.Id, m_LocalLobby);
await LobbyManager.SubscribeToLocalLobbyChanges(lobby.Id, m_LocalLobby);
JoinLobby();
}
catch (Exception exception)

m_LocalUser.ID.Value = localId;
m_LocalUser.DisplayName.Value = randomName;
m_LocalUser.Index.Value = 0;
m_LocalLobby.AddPlayer(0, m_LocalUser); // The local LocalPlayer object will be hooked into UI
m_LocalLobby.AddPlayer(m_LocalUser); // The local LocalPlayer object will be hooked into UI
}
#endregion

bool isLeavingLobby = (state == GameState.Menu || state == GameState.JoinMenu) &&
LocalGameState == GameState.Lobby;
LocalGameState == GameState.Lobby;
LocalGameState = state;
Debug.Log($"Switching Game State to : {LocalGameState}");
if (isLeavingLobby)

LobbyManager.LeaveLobbyAsync();
#pragma warning restore 4014
ResetLocalLobby();
m_LobbySynchronizer.EndSynch();
m_VivoxSetup.LeaveLobbyChannel();
}

{
yield return new WaitForSeconds(5);
if (m_LocalLobby != null && m_LocalLobby.LobbyID.Value == lobbyId && !string.IsNullOrEmpty(lobbyId)
) // Ensure we didn't leave the lobby during this waiting period.
) // Ensure we didn't leave the lobby during this waiting period.
doConnection?.Invoke();
}

SetGameState(GameState.Lobby);
SetUserStatus(m_LocalUser.ID.Value, UserStatus.Lobby);
SetUserStatus(m_LocalUser.Index.Value, UserStatus.Lobby);
m_LocalLobby
.AddPlayer(m_LocalUser); // As before, the local player will need to be plugged into UI before the lobby join actually happens.
m_LocalLobby.AddPlayer(m_LocalUser); // As before, the local player will need to be plugged into UI before the lobby join actually happens.
m_LocalLobby.RelayServer = null;
}

void OnDestroy()
{
ForceLeaveAttempt();
m_LobbySynchronizer.Dispose();
LobbyManager.Dispose();
}

#endregion
}
}
}

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


{
public bool CanSetChanged = true;
public Action<Dictionary<int, LocalPlayer>> onUserListChanged;
public Action<LocalPlayer> onUserJoined;
public Action<int> onUserLeft;
Dictionary<int, LocalPlayer> m_LocalPlayers = new Dictionary<int, LocalPlayer>();

return m_LocalPlayers[index];
}
public void AddPlayer(int index, LocalPlayer user)
public void AddPlayer(LocalPlayer user)
if (m_LocalPlayers.ContainsKey(index))
if (m_LocalPlayers.ContainsKey(user.Index.Value))
{
Debug.LogError(
$"Cant add player {user.DisplayName.Value}({user.ID.Value}) to lobby: {LobbyID.Value} twice");

user.IsHost.onChanged += BoolChangedCallback;
user.UserStatus.onChanged += EmoteChangedCallback;
onUserListChanged?.Invoke(m_LocalPlayers);
onUserJoined?.Invoke(user);
}
public void RemovePlayer(int removePlayer)

player.DisplayName.onChanged -= StringChangedCallback;
player.IsHost.onChanged -= BoolChangedCallback;
player.UserStatus.onChanged -= EmoteChangedCallback;
onUserListChanged?.Invoke(m_LocalPlayers);
onUserLeft?.Invoke(removePlayer);
}
void EmoteChangedCallback(EmoteType emote)

return sb.ToString();
}
}
}
}

6
Assets/Scripts/GameLobby/Game/LocalPlayer.cs


public CallbackValue<string> ID = new CallbackValue<string>("");
public CallbackValue<int> Index = new CallbackValue<int>(0);
public DateTime LastUpdated
public DateTime LastUpdated;
ID.Value = id;
ID.Value = id;
}
public void ResetState()

UserStatus.Value = LobbyRelaySample.UserStatus.Menu;
}
}
}
}

27
Assets/Scripts/GameLobby/Lobby/LobbyManager.cs


return await LobbyService.Instance.QueryLobbiesAsync(queryOptions);
}
//TODO Finish this
public async Task SubscribeToLobbyChanges(string lobbyID, LocalLobby localLobby)
public async Task SubscribeToLocalLobbyChanges(string lobbyID, LocalLobby localLobby)
{
m_LobbyEventCallbacks.LobbyChanged += async changes =>
{

PlayersLeft();
if (changes.PlayerData.Changed)
PlayersChanged();
PlayerDataChanged();
void LobbyChanged()
{

: UserStatus.Lobby;
var newPlayer = new LocalPlayer(id, index, isHost, displayName, emote, userStatus);
localLobby.AddPlayer(playerChanges.PlayerIndex, newPlayer);
localLobby.AddPlayer(newPlayer);
foreach (var leftPlayerIndex in changes.PlayerLeft.Value)
{
localLobby.RemovePlayer(leftPlayerIndex);
}
void PlayersChanged()
void PlayerDataChanged()
{
foreach (var lobbyPlayerChanges in changes.PlayerData.Value)
{

//void ApplyToLobby(Models.Lobby lobby);
};
m_LobbyEventCallbacks.LobbyEventConnectionStateChanged += lobbyEventConnectionState =>
{
Debug.Log($"Lobby Event Changed {lobbyEventConnectionState}");
};
m_LobbyEventCallbacks.KickedFromLobby += () =>
{
Debug.Log("Left Lobby");
};
List<QueryFilter> LobbyColorToFilters(LobbyColor limitToColor)
{

}
}
}
}
}

346
Assets/Scripts/GameLobby/Lobby/LobbySynchronizer.cs


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 LobbySynchronizer : IDisposable
{
LocalLobby m_LocalLobby;
LocalPlayer m_LocalUser;
LobbyManager m_LobbyManager;
bool m_LocalChanges = false;
const int k_approvalMaxMS = 10000; // Used for determining if a user should timeout if they are unable to connect.
int m_lifetime = 0;
const int k_UpdateIntervalMS = 1000;
public LobbySynchronizer(LobbyManager lobbyManager)
{
m_LobbyManager = lobbyManager;
}
public void StartSynch(LocalLobby localLobby, LocalPlayer localUser)
{
m_LocalUser = localUser;
m_LocalLobby = localLobby;
m_LocalLobby.LobbyID.onChanged += OnLobbyIdChanged;
m_LocalChanges = true;
#pragma warning disable 4014
UpdateLoopAsync();
#pragma warning restore 4014
m_lifetime = 0;
}
public void EndSynch()
{
m_LocalChanges = false;
if (m_LocalLobby != null)
m_LocalLobby.LobbyID.onChanged -= OnLobbyIdChanged;
m_LocalLobby = null;
}
/// <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>
async Task UpdateLoopAsync()
{
Lobby latestLobby = null;
while (m_LocalLobby != null)
{
latestLobby = await GetLatestRemoteLobby();
if (IfRemoteLobbyChanged(latestLobby))
{
//Pulling remote changes, and applying them to the local lobby usually flags it as changed,
//Causing another pull, the RemoteToLocal converter ensures this does not happen by flagging the lobby.
LobbyConverters.RemoteToLocal(latestLobby, m_LocalLobby, false);
}
Debug.Log(m_LocalLobby.ToString());
if (!LobbyHasHost())
{
LeaveLobbyBecauseNoHost();
break;
}
var areAllusersReady = AreAllUsersReady();
if (areAllusersReady && m_LocalLobby.LocalLobbyState.Value == LobbyState.Lobby)
{
GameManager.Instance.BeginCountdown();
}
else if (!areAllusersReady && m_LocalLobby.LocalLobbyState.Value == LobbyState.CountDown)
{
GameManager.Instance.CancelCountDown();
}
m_lifetime += k_UpdateIntervalMS;
await Task.Delay(k_UpdateIntervalMS);
}
}
async Task<Lobby> GetLatestRemoteLobby()
{
Lobby latestLobby = null;
if (m_LocalLobby.IsLobbyChanged())
{
latestLobby = await PushDataToLobby();
}
else
{
latestLobby = await m_LobbyManager.GetLobbyAsync();
}
return latestLobby;
}
bool IfRemoteLobbyChanged(Lobby remoteLobby)
{
var remoteLobbyTime = remoteLobby.LastUpdated.ToFileTimeUtc();
var localLobbyTime = m_LocalLobby.LastUpdated.Value;
var isLocalOutOfDate = remoteLobbyTime > localLobbyTime;
return isLocalOutOfDate;
}
async Task<Lobby> PushDataToLobby()
{
m_LocalChanges = false;
if (m_LocalUser.IsHost.Value)
await m_LobbyManager.UpdateLobbyDataAsync(
LobbyConverters.LocalToRemoteData(m_LocalLobby));
return await m_LobbyManager.UpdatePlayerDataAsync(
LobbyConverters.LocalToRemoteUserData(m_LocalUser));
}
bool AreAllUsersReady()
{
foreach (var lobbyUser in m_LocalLobby.LocalPlayers.Values)
{
if (lobbyUser.UserStatus.Value != UserStatus.Ready)
{
return false;
}
}
return true;
}
bool LobbyHasHost()
{
if (!m_LocalUser.IsHost.Value)
{
foreach (var lobbyUser in m_LocalLobby.LocalPlayers)
{
if (lobbyUser.Value.IsHost.Value)
return true;
}
return false;
}
return true;
}
void LeaveLobbyBecauseNoHost()
{
LogHandlerSettings.Instance.SpawnErrorPopup(
"Host left the lobby! Disconnecting...");
Locator.Get.Messenger.OnReceiveMessage(MessageType.EndGame, null);
GameManager.Instance.ChangeMenuState(GameState.JoinMenu);
}
public void OnLobbyIdChanged(string lobbyID)
{
if (string.IsNullOrEmpty(lobbyID)
) // When the player leaves, their LocalLobby is cleared out.
{
EndSynch();
}
}
public void Dispose()
{
EndSynch();
}
}
}
// namespace LobbyRelaySample
// {
// /// <summary>
// /// Keep updated on changes to a joined lobby, at a speed compliant with Lobby's rate limiting.
// /// </summary>
// public class LobbySynchronizer : IDisposable
// {
// LocalLobby m_LocalLobby;
// LocalPlayer m_LocalUser;
// LobbyManager m_LobbyManager;
// bool m_LocalChanges = false;
//
// const int k_approvalMaxMS = 10000; // Used for determining if a user should timeout if they are unable to connect.
//
// int m_lifetime = 0;
// const int k_UpdateIntervalMS = 1000;
//
// public LobbySynchronizer(LobbyManager lobbyManager)
// {
// m_LobbyManager = lobbyManager;
// }
//
// public void StartSynch(LocalLobby localLobby, LocalPlayer localUser)
// {
// m_LocalUser = localUser;
// m_LocalLobby = localLobby;
// m_LocalLobby.LobbyID.onChanged += OnLobbyIdChanged;
// m_LocalChanges = true;
// #pragma warning disable 4014
// UpdateLoopAsync();
// #pragma warning restore 4014
// m_lifetime = 0;
// }
//
//
// public void EndSynch()
// {
// m_LocalChanges = false;
//
// if (m_LocalLobby != null)
// m_LocalLobby.LobbyID.onChanged -= OnLobbyIdChanged;
//
// m_LocalLobby = null;
// }
//
// /// <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>
// async Task UpdateLoopAsync()
// {
// Lobby latestLobby = null;
//
// while (m_LocalLobby != null)
// {
// latestLobby = await GetLatestRemoteLobby();
//
// if (IfRemoteLobbyChanged(latestLobby))
// {
// //Pulling remote changes, and applying them to the local lobby usually flags it as changed,
// //Causing another pull, the RemoteToLocal converter ensures this does not happen by flagging the lobby.
// LobbyConverters.RemoteToLocal(latestLobby, m_LocalLobby, false);
// }
// Debug.Log(m_LocalLobby.ToString());
//
// if (!LobbyHasHost())
// {
// LeaveLobbyBecauseNoHost();
// break;
// }
//
// var areAllusersReady = AreAllUsersReady();
// if (areAllusersReady && m_LocalLobby.LocalLobbyState.Value == LobbyState.Lobby)
// {
// GameManager.Instance.BeginCountdown();
// }
// else if (!areAllusersReady && m_LocalLobby.LocalLobbyState.Value == LobbyState.CountDown)
// {
// GameManager.Instance.CancelCountDown();
// }
//
// m_lifetime += k_UpdateIntervalMS;
// await Task.Delay(k_UpdateIntervalMS);
// }
// }
//
// async Task<Lobby> GetLatestRemoteLobby()
// {
// Lobby latestLobby = null;
// if (m_LocalLobby.IsLobbyChanged())
// {
// latestLobby = await PushDataToLobby();
// }
// else
// {
// latestLobby = await m_LobbyManager.GetLobbyAsync();
// }
//
// return latestLobby;
// }
//
// bool IfRemoteLobbyChanged(Lobby remoteLobby)
// {
// var remoteLobbyTime = remoteLobby.LastUpdated.ToFileTimeUtc();
// var localLobbyTime = m_LocalLobby.LastUpdated.Value;
// var isLocalOutOfDate = remoteLobbyTime > localLobbyTime;
// return isLocalOutOfDate;
// }
//
// async Task<Lobby> PushDataToLobby()
// {
// m_LocalChanges = false;
//
// if (m_LocalUser.IsHost.Value)
// await m_LobbyManager.UpdateLobbyDataAsync(
// LobbyConverters.LocalToRemoteData(m_LocalLobby));
//
// return await m_LobbyManager.UpdatePlayerDataAsync(
// LobbyConverters.LocalToRemoteUserData(m_LocalUser));
// }
//
// bool AreAllUsersReady()
// {
// foreach (var lobbyUser in m_LocalLobby.LocalPlayers.Values)
// {
// if (lobbyUser.UserStatus.Value != UserStatus.Ready)
// {
// return false;
// }
// }
//
// return true;
// }
//
// bool LobbyHasHost()
// {
// if (!m_LocalUser.IsHost.Value)
// {
// foreach (var lobbyUser in m_LocalLobby.LocalPlayers)
// {
// if (lobbyUser.Value.IsHost.Value)
// return true;
// }
//
// return false;
// }
//
// return true;
// }
//
// void LeaveLobbyBecauseNoHost()
// {
// LogHandlerSettings.Instance.SpawnErrorPopup(
// "Host left the lobby! Disconnecting...");
// Locator.Get.Messenger.OnReceiveMessage(MessageType.EndGame, null);
// GameManager.Instance.ChangeMenuState(GameState.JoinMenu);
// }
//
// public void OnLobbyIdChanged(string lobbyID)
// {
// if (string.IsNullOrEmpty(lobbyID)
// ) // When the player leaves, their LocalLobby is cleared out.
// {
// EndSynch();
// }
// }
//
// public void Dispose()
// {
// EndSynch();
// }
// }
// }

47
Assets/Scripts/GameLobby/NGO/SetupInGame.cs


using System;
using System.Collections.Generic;
using Unity.Networking.Transport;
using Unity.Services.Relay.Models;
using UnityEngine;
using UnityEngine.SocialPlatforms;

m_lobby.RelayNGOCode.Value = joincode;
bool isSecure = false;
var endpoint = RelayUtpSetup.GetEndpointForAllocation(allocation.ServerEndpoints,
var endpoint = GetEndpointForAllocation(allocation.ServerEndpoints,
transport.SetHostRelayData(RelayUtpSetup.AddressFromEndpoint(endpoint), endpoint.Port,
transport.SetHostRelayData(AddressFromEndpoint(endpoint), endpoint.Port,
allocation.AllocationIdBytes, allocation.Key, allocation.ConnectionData, isSecure);
}

var joinAllocation = await Relay.Instance.JoinAllocationAsync(m_lobby.RelayCode.Value);
bool isSecure = false;
var endpoint = RelayUtpSetup.GetEndpointForAllocation(joinAllocation.ServerEndpoints,
var endpoint = GetEndpointForAllocation(joinAllocation.ServerEndpoints,
transport.SetClientRelayData(RelayUtpSetup.AddressFromEndpoint(endpoint), endpoint.Port,
transport.SetClientRelayData(AddressFromEndpoint(endpoint), endpoint.Port,
private void OnConnectionVerified()
/// <summary>
/// Determine the server endpoint for connecting to the Relay server, for either an Allocation or a JoinAllocation.
/// If DTLS encryption is available, and there's a secure server endpoint available, use that as a secure connection. Otherwise, just connect to the Relay IP unsecured.
/// </summary>
NetworkEndPoint GetEndpointForAllocation(
List<RelayServerEndpoint> endpoints,
string ip,
int port,
out bool isSecure)
{
#if ENABLE_MANAGED_UNITYTLS
foreach (RelayServerEndpoint endpoint in endpoints)
{
if (endpoint.Secure && endpoint.Network == RelayServerEndpoint.NetworkOptions.Udp)
{
isSecure = true;
return NetworkEndPoint.Parse(endpoint.Host, (ushort)endpoint.Port);
}
}
#endif
isSecure = false;
return NetworkEndPoint.Parse(ip, (ushort)port);
}
string AddressFromEndpoint(NetworkEndPoint endpoint)
{
return endpoint.Address.Split(':')[0];
}
void OnConnectionVerified()
{
m_hasConnectedViaNGO = true;
}

public void ConfirmInGameState()
{
}
public void MiniGameBeginning()

// If this localPlayer hasn't successfully connected via NGO, forcibly exit the minigame.
LogHandlerSettings.Instance.SpawnErrorPopup( "Failed to join the game.");
LogHandlerSettings.Instance.SpawnErrorPopup("Failed to join the game.");
OnGameEnd();
}
}

108
Assets/Scripts/GameLobby/Relay/RelayPendingApproval.cs


using System;
using Unity.Networking.Transport;
namespace LobbyRelaySample.relay
{
/// <summary>
/// The Relay host doesn't need to know what might approve or disapprove of a pending connection, so this will
/// broadcast a message that approval is being sought, and if nothing disapproves, the connection will be permitted.
/// </summary>
public class RelayPendingApproval : IDisposable
{
NetworkConnection m_pendingConnection;
private bool m_hasDisposed = false;
private const float k_waitTime = 0.1f;
private Action<NetworkConnection, Approval> m_onResult;
public string ID { get; private set; }
public RelayPendingApproval(NetworkConnection conn, Action<NetworkConnection, Approval> onResult, string id)
{
m_pendingConnection = conn;
m_onResult = onResult;
ID = id;
Locator.Get.UpdateSlow.Subscribe(Approve, k_waitTime);
Locator.Get.Messenger.OnReceiveMessage(MessageType.ClientUserSeekingDisapproval, (Action<Approval>)Disapprove);
}
~RelayPendingApproval() { Dispose(); }
private void Approve(float unused)
{
try
{ m_onResult?.Invoke(m_pendingConnection, Approval.OK);
}
finally
{ Dispose();
}
}
public void Disapprove(Approval reason)
{
try
{ m_onResult?.Invoke(m_pendingConnection, reason);
}
finally
{ Dispose();
}
}
public void Dispose()
{
if (!m_hasDisposed)
{
Locator.Get.UpdateSlow.Unsubscribe(Approve);
m_hasDisposed = true;
}
}
}
}
// namespace LobbyRelaySample.relay
// {
// /// <summary>
// /// The Relay host doesn't need to know what might approve or disapprove of a pending connection, so this will
// /// broadcast a message that approval is being sought, and if nothing disapproves, the connection will be permitted.
// /// </summary>
// public class RelayPendingApproval : IDisposable
// {
// NetworkConnection m_pendingConnection;
// private bool m_hasDisposed = false;
// private const float k_waitTime = 0.1f;
// private Action<NetworkConnection, Approval> m_onResult;
// public string ID { get; private set; }
//
// public RelayPendingApproval(NetworkConnection conn, Action<NetworkConnection, Approval> onResult, string id)
// {
// m_pendingConnection = conn;
// m_onResult = onResult;
// ID = id;
// Locator.Get.UpdateSlow.Subscribe(Approve, k_waitTime);
// Locator.Get.Messenger.OnReceiveMessage(MessageType.ClientUserSeekingDisapproval, (Action<Approval>)Disapprove);
// }
// ~RelayPendingApproval() { Dispose(); }
//
// private void Approve(float unused)
// {
// try
// { m_onResult?.Invoke(m_pendingConnection, Approval.OK);
// }
// finally
// { Dispose();
// }
// }
//
// public void Disapprove(Approval reason)
// {
// try
// { m_onResult?.Invoke(m_pendingConnection, reason);
// }
// finally
// { Dispose();
// }
// }
//
// public void Dispose()
// {
// if (!m_hasDisposed)
// {
// Locator.Get.UpdateSlow.Unsubscribe(Approve);
// m_hasDisposed = true;
// }
// }
// }
// }

434
Assets/Scripts/GameLobby/Relay/RelayUtpHost.cs


using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Networking.Transport;
namespace LobbyRelaySample.relay
{
/// <summary>
/// In addition to maintaining a heartbeat with the Relay server to keep it from timing out, the host player must pass network events
/// from clients to all other clients, since they don't connect to each other.
/// If you are using the Unity Networking Package, you can use their Relay instead of building your own packets.
/// </summary>
public class RelayUtpHost : RelayUtpClient, IReceiveMessages
{
public override void Initialize(NetworkDriver networkDriver, List<NetworkConnection> connections,
LocalPlayer localUser, LocalLobby localLobby)
{
base.Initialize(networkDriver, connections, localUser, localLobby);
m_hasSentInitialMessage =
true; // The host will be alone in the lobby at first, so they need not send any messages right away.
Locator.Get.Messenger.Subscribe(this);
}
protected override void Uninitialize()
{
base.Uninitialize();
Locator.Get.Messenger.Unsubscribe(this);
m_networkDriver.Dispose();
}
protected override void OnUpdate()
{
if (!m_IsRelayConnected
) // If Relay was disconnected somehow, stop taking actions that will keep the allocation alive.
return;
base.OnUpdate();
UpdateConnections();
}
/// <summary>
/// When a new client connects, first determine if they are allowed to do so.
/// If so, they need to be updated with the current state of everyone else.
/// If not, they should be informed and rejected.
/// </summary>
void OnNewConnection(NetworkConnection conn, string id)
{
new RelayPendingApproval(conn, NewConnectionApprovalResult, id);
}
void NewConnectionApprovalResult(NetworkConnection conn, Approval result)
{
WriteByte(m_networkDriver, conn, m_localUser.ID.Value, MsgType.PlayerApprovalState, (byte)result);
if (result == Approval.OK && conn.IsCreated)
{
foreach (var user in m_localLobby.LocalPlayers)
ForceFullUserUpdate(m_networkDriver, conn, user.Value);
m_connections.Add(conn);
}
else
{
conn.Disconnect(m_networkDriver);
}
}
protected override bool CanProcessDataEventFor(NetworkConnection conn, MsgType type, string id)
{
// Don't send through data from one client to everyone else if they haven't been approved yet. (They should also not be sending data if not approved, so this is a backup.)
return id != m_localUser.ID.Value &&
(m_localLobby.LocalPlayers.ContainsKey(id) && m_connections.Contains(conn) ||
type == MsgType.NewPlayer);
}
protected override void ProcessNetworkEventDataAdditional(NetworkConnection conn, MsgType msgType, string id)
{
// Forward messages from clients to other clients.
if (msgType == MsgType.PlayerName)
{
string name = m_localLobby.LocalPlayers[id].DisplayName.Value;
foreach (NetworkConnection otherConn in m_connections)
{
if (otherConn == conn)
continue;
WriteString(m_networkDriver, otherConn, id, msgType, name);
}
}
else if (msgType == MsgType.Emote || msgType == MsgType.ReadyState)
{
byte value = msgType == MsgType.Emote
? (byte)m_localLobby.LocalPlayers[id].Emote.Value
: (byte)m_localLobby.LocalPlayers[id].UserStatus.Value;
foreach (NetworkConnection otherConn in m_connections)
{
if (otherConn == conn)
continue;
WriteByte(m_networkDriver, otherConn, id, msgType, value);
}
}
else if (msgType == MsgType.NewPlayer)
OnNewConnection(conn, id);
else if (msgType == MsgType.PlayerDisconnect
) // Clients message the host when they intend to disconnect, or else the host ends up keeping the connection open.
{
UnityEngine.Debug.LogWarning("Disconnecting a client due to a disconnect message.");
conn.Disconnect(m_networkDriver);
m_connections.Remove(conn);
#pragma warning disable 4014
/*var queryCooldownMilliseconds = LobbyManager.Instance.GetRateLimit(LobbyManager.RequestType.Query)
.m_CoolDownMS;
// The user ready status lives in the lobby data, which won't update immediately, but we need to use it to identify if all remaining players have readied.
// So, we'll wait two lobby update loops before we check remaining players to ensure the lobby has received the disconnect message.
WaitAndCheckUsers(queryCooldownMilliseconds*2);*/
#pragma warning restore 4014
return;
}
// If a client has changed state, check if this changes whether all players have readied.
if (msgType == MsgType.ReadyState)
CheckIfAllUsersReady();
}
async Task WaitAndCheckUsers(int milliSeconds)
{
await Task.Delay(milliSeconds);
CheckIfAllUsersReady();
}
protected override void ProcessDisconnectEvent(NetworkConnection conn, DataStreamReader strm)
{
// When a disconnect from the host occurs, no additional action is required. This override just prevents the base behavior from occurring.
// We rely on the PlayerDisconnect message instead of this disconnect message since this message might not arrive for a long time after the disconnect actually occurs.
}
public void OnUserStatusChanged()
{
CheckIfAllUsersReady();
}
public void OnReceiveMessage(MessageType type, object msg)
{
if (type == MessageType.EndGame
) // This assumes that only the host will have the End Game button available; otherwise, clients need to be able to send this message, too.
{
foreach (NetworkConnection connection in m_connections)
WriteByte(m_networkDriver, connection, m_localUser.ID.Value, MsgType.EndInGame, 0);
}
}
//TODO Move this to Host Check. Host pushes Start Signal to all users?
void CheckIfAllUsersReady()
{
bool haveAllReadied = true;
foreach (var user in m_localLobby.LocalPlayers)
{
if (user.Value.UserStatus.Value != UserStatus.Ready)
{
haveAllReadied = false;
break;
}
}
if (haveAllReadied && m_localLobby.LocalLobbyState.Value == LobbyState.Lobby
) // Need to notify both this client and all others that all players have readied.
{
Locator.Get.Messenger.OnReceiveMessage(MessageType.StartCountdown, null);
foreach (NetworkConnection connection in m_connections)
WriteByte(m_networkDriver, connection, m_localUser.ID.Value, MsgType.StartCountdown, 0);
}
else if (!haveAllReadied && m_localLobby.LocalLobbyState.Value == LobbyState.CountDown
) // Someone cancelled during the countdown, so abort the countdown.
{
Locator.Get.Messenger.OnReceiveMessage(MessageType.CancelCountdown, null);
foreach (NetworkConnection connection in m_connections)
WriteByte(m_networkDriver, connection, m_localUser.ID.Value, MsgType.CancelCountdown, 0);
}
}
/// <summary>
/// After the countdown, the host and all clients need to be alerted to sync up on game state, load assets, etc.
/// </summary>
public void SendInGameState()
{
GameManager.Instance.ConfirmIngameState();
foreach (NetworkConnection connection in m_connections)
WriteByte(m_networkDriver, connection, m_localUser.ID.Value, MsgType.ConfirmInGame, 0);
}
/// <summary>
/// Clean out destroyed connections, and accept all new ones.
/// </summary>
void UpdateConnections()
{
for (int c = m_connections.Count - 1; c >= 0; c--)
{
if (!m_connections[c].IsCreated)
m_connections.RemoveAt(c);
}
while (true)
{
var conn = m_networkDriver
.Accept(); // Note that since we pumped the event queue earlier in Update, m_networkDriver has been updated already this frame.
if (!conn.IsCreated
) // "Nothing more to accept" is signalled by returning an invalid connection from Accept.
break;
// Although the connection is created (i.e. Accepted), we still need to approve it, which will trigger when receiving the NewPlayer message from that client.
}
}
public override void Leave()
{
foreach (NetworkConnection connection in m_connections)
connection.Disconnect(
m_networkDriver); // Note that Lobby won't receive the disconnect immediately, so its auto-disconnect takes 30-40s, if needed.
m_connections.Clear();
m_localLobby.RelayServer = null;
}
}
}
//
// namespace LobbyRelaySample.relay
// {
// /// <summary>
// /// In addition to maintaining a heartbeat with the Relay server to keep it from timing out, the host player must pass network events
// /// from clients to all other clients, since they don't connect to each other.
// /// If you are using the Unity Networking Package, you can use their Relay instead of building your own packets.
// /// </summary>
// public class RelayUtpHost : RelayUtpClient, IReceiveMessages
// {
// public override void Initialize(NetworkDriver networkDriver, List<NetworkConnection> connections,
// LocalPlayer localUser, LocalLobby localLobby)
// {
// base.Initialize(networkDriver, connections, localUser, localLobby);
// m_hasSentInitialMessage =
// true; // The host will be alone in the lobby at first, so they need not send any messages right away.
// Locator.Get.Messenger.Subscribe(this);
// }
//
// protected override void Uninitialize()
// {
// base.Uninitialize();
// Locator.Get.Messenger.Unsubscribe(this);
// m_networkDriver.Dispose();
// }
//
// protected override void OnUpdate()
// {
// if (!m_IsRelayConnected
// ) // If Relay was disconnected somehow, stop taking actions that will keep the allocation alive.
// return;
// base.OnUpdate();
// UpdateConnections();
// }
//
// /// <summary>
// /// When a new client connects, first determine if they are allowed to do so.
// /// If so, they need to be updated with the current state of everyone else.
// /// If not, they should be informed and rejected.
// /// </summary>
// void OnNewConnection(NetworkConnection conn, string id)
// {
// new RelayPendingApproval(conn, NewConnectionApprovalResult, id);
// }
//
// void NewConnectionApprovalResult(NetworkConnection conn, Approval result)
// {
// WriteByte(m_networkDriver, conn, m_localUser.ID.Value, MsgType.PlayerApprovalState, (byte)result);
// if (result == Approval.OK && conn.IsCreated)
// {
// foreach (var user in m_localLobby.LocalPlayers)
// ForceFullUserUpdate(m_networkDriver, conn, user.Value);
// m_connections.Add(conn);
// }
// else
// {
// conn.Disconnect(m_networkDriver);
// }
// }
//
// protected override bool CanProcessDataEventFor(NetworkConnection conn, MsgType type, string id)
// {
// // Don't send through data from one client to everyone else if they haven't been approved yet. (They should also not be sending data if not approved, so this is a backup.)
// return id != m_localUser.ID.Value &&
// (m_localLobby.LocalPlayers.ContainsKey(id) && m_connections.Contains(conn) ||
// type == MsgType.NewPlayer);
// }
//
// protected override void ProcessNetworkEventDataAdditional(NetworkConnection conn, MsgType msgType, string id)
// {
// // Forward messages from clients to other clients.
// if (msgType == MsgType.PlayerName)
// {
// string name = m_localLobby.LocalPlayers[id].DisplayName.Value;
// foreach (NetworkConnection otherConn in m_connections)
// {
// if (otherConn == conn)
// continue;
// WriteString(m_networkDriver, otherConn, id, msgType, name);
// }
// }
// else if (msgType == MsgType.Emote || msgType == MsgType.ReadyState)
// {
// byte value = msgType == MsgType.Emote
// ? (byte)m_localLobby.LocalPlayers[id].Emote.Value
// : (byte)m_localLobby.LocalPlayers[id].UserStatus.Value;
// foreach (NetworkConnection otherConn in m_connections)
// {
// if (otherConn == conn)
// continue;
// WriteByte(m_networkDriver, otherConn, id, msgType, value);
// }
// }
// else if (msgType == MsgType.NewPlayer)
// OnNewConnection(conn, id);
// else if (msgType == MsgType.PlayerDisconnect
// ) // Clients message the host when they intend to disconnect, or else the host ends up keeping the connection open.
// {
// UnityEngine.Debug.LogWarning("Disconnecting a client due to a disconnect message.");
// conn.Disconnect(m_networkDriver);
// m_connections.Remove(conn);
//
// #pragma warning disable 4014
// /*var queryCooldownMilliseconds = LobbyManager.Instance.GetRateLimit(LobbyManager.RequestType.Query)
// .m_CoolDownMS;
// // The user ready status lives in the lobby data, which won't update immediately, but we need to use it to identify if all remaining players have readied.
// // So, we'll wait two lobby update loops before we check remaining players to ensure the lobby has received the disconnect message.
// WaitAndCheckUsers(queryCooldownMilliseconds*2);*/
// #pragma warning restore 4014
// return;
// }
//
// // If a client has changed state, check if this changes whether all players have readied.
// if (msgType == MsgType.ReadyState)
// CheckIfAllUsersReady();
// }
//
// async Task WaitAndCheckUsers(int milliSeconds)
// {
// await Task.Delay(milliSeconds);
// CheckIfAllUsersReady();
// }
//
// protected override void ProcessDisconnectEvent(NetworkConnection conn, DataStreamReader strm)
// {
// // When a disconnect from the host occurs, no additional action is required. This override just prevents the base behavior from occurring.
// // We rely on the PlayerDisconnect message instead of this disconnect message since this message might not arrive for a long time after the disconnect actually occurs.
// }
//
// public void OnUserStatusChanged()
// {
// CheckIfAllUsersReady();
// }
//
// public void OnReceiveMessage(MessageType type, object msg)
// {
//
// if (type == MessageType.EndGame
// ) // This assumes that only the host will have the End Game button available; otherwise, clients need to be able to send this message, too.
// {
// foreach (NetworkConnection connection in m_connections)
// WriteByte(m_networkDriver, connection, m_localUser.ID.Value, MsgType.EndInGame, 0);
// }
// }
//
// //TODO Move this to Host Check. Host pushes Start Signal to all users?
// void CheckIfAllUsersReady()
// {
// bool haveAllReadied = true;
// foreach (var user in m_localLobby.LocalPlayers)
// {
// if (user.Value.UserStatus.Value != UserStatus.Ready)
// {
// haveAllReadied = false;
// break;
// }
// }
//
// if (haveAllReadied && m_localLobby.LocalLobbyState.Value == LobbyState.Lobby
// ) // Need to notify both this client and all others that all players have readied.
// {
// Locator.Get.Messenger.OnReceiveMessage(MessageType.StartCountdown, null);
// foreach (NetworkConnection connection in m_connections)
// WriteByte(m_networkDriver, connection, m_localUser.ID.Value, MsgType.StartCountdown, 0);
// }
// else if (!haveAllReadied && m_localLobby.LocalLobbyState.Value == LobbyState.CountDown
// ) // Someone cancelled during the countdown, so abort the countdown.
// {
// Locator.Get.Messenger.OnReceiveMessage(MessageType.CancelCountdown, null);
// foreach (NetworkConnection connection in m_connections)
// WriteByte(m_networkDriver, connection, m_localUser.ID.Value, MsgType.CancelCountdown, 0);
// }
// }
//
// /// <summary>
// /// After the countdown, the host and all clients need to be alerted to sync up on game state, load assets, etc.
// /// </summary>
// public void SendInGameState()
// {
// GameManager.Instance.ConfirmIngameState();
// foreach (NetworkConnection connection in m_connections)
// WriteByte(m_networkDriver, connection, m_localUser.ID.Value, MsgType.ConfirmInGame, 0);
// }
//
// /// <summary>
// /// Clean out destroyed connections, and accept all new ones.
// /// </summary>
// void UpdateConnections()
// {
// for (int c = m_connections.Count - 1; c >= 0; c--)
// {
// if (!m_connections[c].IsCreated)
// m_connections.RemoveAt(c);
// }
//
// while (true)
// {
// var conn = m_networkDriver
// .Accept(); // Note that since we pumped the event queue earlier in Update, m_networkDriver has been updated already this frame.
// if (!conn.IsCreated
// ) // "Nothing more to accept" is signalled by returning an invalid connection from Accept.
// break;
//
// // Although the connection is created (i.e. Accepted), we still need to approve it, which will trigger when receiving the NewPlayer message from that client.
// }
// }
//
// public override void Leave()
// {
// foreach (NetworkConnection connection in m_connections)
// connection.Disconnect(
// m_networkDriver); // Note that Lobby won't receive the disconnect immediately, so its auto-disconnect takes 30-40s, if needed.
// m_connections.Clear();
// m_localLobby.RelayServer = null;
// }
// }
// }

4
Assets/Scripts/GameLobby/Tests/PlayMode/LobbyRoundtripTests.cs


#pragma warning disable 4014
TestAuthSetup();
#pragma warning restore 4014
m_LocalUser = new LocalPlayer(Auth.ID(), false, "TESTPLAYER");
m_LocalUser = new LocalPlayer(Auth.ID(), 0, false, "TESTPLAYER");
m_LobbyManager = new LobbyManager();
}

Assert.IsTrue(difference < 50 && difference >= 0);
}
}
}
}

188
Assets/Scripts/GameLobby/Tests/PlayMode/UtpTests.cs


using UnityEngine;
using UnityEngine.TestTools;
namespace Test
{
public class UtpTests
{
class RelayUtpTest : RelayUtpSetupHost
{
public Action<NetworkEndPoint, bool> OnGetEndpoint { private get; set; }
public void JoinRelayPublic()
{
JoinRelay();
}
protected override void JoinRelay()
{
RelayAPIInterface.AllocateAsync(1, OnAllocation);
void OnAllocation(Allocation allocation)
{
bool isSecure = false;
NetworkEndPoint endpoint = GetEndpointForAllocation(allocation.ServerEndpoints, allocation.RelayServer.IpV4, allocation.RelayServer.Port, out isSecure);
OnGetEndpoint?.Invoke(endpoint, isSecure);
// The allocation will be cleaned up automatically, since we won't be pinging it regularly.
}
}
}
GameObject m_dummy;
//Only used when testing DTLS
#pragma warning disable CS0414 // This is the "assigned but its value is never used" warning, which will otherwise appear when DTLS is unavailable.
private bool m_didSigninComplete = false;
#pragma warning restore CS0414
[OneTimeSetUp]
public void Setup()
{
m_dummy = new GameObject();
#pragma warning disable 4014
TestAuthSetup();
#pragma warning restore 4014
}
async Task TestAuthSetup()
{
await Auth.Authenticate("test");
}
[OneTimeTearDown]
public void Teardown()
{
GameObject.Destroy(m_dummy);
}
[UnityTest]
public IEnumerator DTLSCheck()
{
#if ENABLE_MANAGED_UNITYTLS
yield return AsyncTestHelper.Await(async () => await Auth.Authenticating());
RelayUtpTest relaySetup = m_dummy.AddComponent<RelayUtpTest>();
relaySetup.OnGetEndpoint = OnGetEndpoint;
bool? isSecure = null;
NetworkEndPoint endpoint = default;
relaySetup.JoinRelayPublic();
float timeout = 5;
while (!isSecure.HasValue && timeout > 0)
{
timeout -= 0.25f;
yield return new WaitForSeconds(0.25f);
}
Component.Destroy(relaySetup);
Assert.IsTrue(timeout > 0, "Timeout check.");
Assert.IsTrue(isSecure, "Should have a secure server endpoint.");
Assert.IsTrue(endpoint.IsValid, "Endpoint should be valid.");
void OnGetEndpoint(NetworkEndPoint resultEndpoint, bool resultIsSecure)
{
endpoint = resultEndpoint;
isSecure = resultIsSecure;
}
#else
Assert.Ignore("DTLS encryption for Relay is not currently available for this version of Unity.");
yield break;
#endif
}
}
}
// namespace Test
// {
// public class UtpTests
// {
// class RelayUtpTest : RelayUtpSetupHost
// {
// public Action<NetworkEndPoint, bool> OnGetEndpoint { private get; set; }
//
// public void JoinRelayPublic()
// {
// JoinRelay();
// }
//
// protected override void JoinRelay()
// {
// RelayAPIInterface.AllocateAsync(1, OnAllocation);
//
// void OnAllocation(Allocation allocation)
// {
// bool isSecure = false;
// NetworkEndPoint endpoint = GetEndpointForAllocation(allocation.ServerEndpoints, allocation.RelayServer.IpV4, allocation.RelayServer.Port, out isSecure);
// OnGetEndpoint?.Invoke(endpoint, isSecure);
// // The allocation will be cleaned up automatically, since we won't be pinging it regularly.
// }
// }
// }
//
// GameObject m_dummy;
// //Only used when testing DTLS
// #pragma warning disable CS0414 // This is the "assigned but its value is never used" warning, which will otherwise appear when DTLS is unavailable.
// private bool m_didSigninComplete = false;
// #pragma warning restore CS0414
//
// [OneTimeSetUp]
// public void Setup()
// {
// m_dummy = new GameObject();
// #pragma warning disable 4014
// TestAuthSetup();
// #pragma warning restore 4014
// }
//
// async Task TestAuthSetup()
// {
// await Auth.Authenticate("test");
// }
//
// [OneTimeTearDown]
// public void Teardown()
// {
// GameObject.Destroy(m_dummy);
// }
//
// [UnityTest]
// public IEnumerator DTLSCheck()
// {
// #if ENABLE_MANAGED_UNITYTLS
//
// yield return AsyncTestHelper.Await(async () => await Auth.Authenticating());
//
//
// RelayUtpTest relaySetup = m_dummy.AddComponent<RelayUtpTest>();
// relaySetup.OnGetEndpoint = OnGetEndpoint;
// bool? isSecure = null;
// NetworkEndPoint endpoint = default;
//
// relaySetup.JoinRelayPublic();
// float timeout = 5;
// while (!isSecure.HasValue && timeout > 0)
// {
// timeout -= 0.25f;
// yield return new WaitForSeconds(0.25f);
// }
// Component.Destroy(relaySetup);
// Assert.IsTrue(timeout > 0, "Timeout check.");
//
// Assert.IsTrue(isSecure, "Should have a secure server endpoint.");
// Assert.IsTrue(endpoint.IsValid, "Endpoint should be valid.");
//
// void OnGetEndpoint(NetworkEndPoint resultEndpoint, bool resultIsSecure)
// {
// endpoint = resultEndpoint;
// isSecure = resultIsSecure;
// }
//
// #else
//
// Assert.Ignore("DTLS encryption for Relay is not currently available for this version of Unity.");
// yield break;
//
// #endif
// }
// }
// }

30
Assets/Scripts/GameLobby/UI/InLobbyUserUI.cs


Sprite[] m_EmoteIcons;
[SerializeField]
vivox.VivoxUserHandler m_vivoxUserHandler;
vivox.VivoxUserHandler m_VivoxUserHandler;
LocalPlayer m_localPlayer;
LocalPlayer m_LocalPlayer;
m_localPlayer = myLocalPlayer;
m_LocalPlayer = myLocalPlayer;
SetDisplayName(m_localPlayer.DisplayName.Value);
SetDisplayName(m_LocalPlayer.DisplayName.Value);
m_vivoxUserHandler.SetId(UserId);
m_VivoxUserHandler.SetId(UserId);
m_localPlayer.DisplayName.onChanged += SetDisplayName;
m_localPlayer.UserStatus.onChanged += SetUserStatus;
m_localPlayer.Emote.onChanged += SetEmote;
m_localPlayer.IsHost.onChanged += SetIsHost;
m_LocalPlayer.DisplayName.onChanged += SetDisplayName;
m_LocalPlayer.UserStatus.onChanged += SetUserStatus;
m_LocalPlayer.Emote.onChanged += SetEmote;
m_LocalPlayer.IsHost.onChanged += SetIsHost;
m_localPlayer.DisplayName.onChanged -= SetDisplayName;
m_localPlayer.UserStatus.onChanged -= SetUserStatus;
m_localPlayer.Emote.onChanged -= SetEmote;
m_localPlayer.IsHost.onChanged -= SetIsHost;
m_LocalPlayer.DisplayName.onChanged -= SetDisplayName;
m_LocalPlayer.UserStatus.onChanged -= SetUserStatus;
m_LocalPlayer.Emote.onChanged -= SetEmote;
m_LocalPlayer.IsHost.onChanged -= SetIsHost;
public void OnUserLeft()
public void ResetUI()
m_localPlayer = null;
m_LocalPlayer = null;
}
void SetDisplayName(string displayName)

10
Assets/Scripts/GameLobby/UI/LobbyEntryUI.cs


SetLobbyname(m_Lobby.LobbyName.Value);
SetLobbyCount(m_Lobby.PlayerCount);
m_Lobby.LobbyName.onChanged += SetLobbyname;
m_Lobby.onUserListChanged += (dict) =>
m_Lobby.onUserJoined += (_) =>
SetLobbyCount(dict.Count);
SetLobbyCount(m_Lobby.PlayerCount);
};
m_Lobby.onUserLeft += (_) =>
{
SetLobbyCount(m_Lobby.PlayerCount);
};
}

lobbyCountText.SetText($"{count}/{m_Lobby.MaxPlayerCount.Value}");
}
}
}
}

45
Assets/Scripts/GameLobby/UI/LobbyUserListUI.cs


{
base.Start();
GameManager.Instance.LocalLobby.onUserListChanged += OnUsersChanged;
GameManager.Instance.LocalLobby.onUserJoined += OnUserJoined;
GameManager.Instance.LocalLobby.onUserLeft += OnUserLeft;
void OnUsersChanged(Dictionary<int, LocalPlayer> newUserDict)
void OnUserJoined(LocalPlayer localPlayer)
for (int id = m_UserUIObjects.Count - 1;
id >= 0;
id--) // We might remove users if they aren't in the new data, so iterate backwards.
{
string userId = m_UserUIObjects[id];
if (!newUserDict.ContainsKey(userId))
{
foreach (var ui in m_UserUIObjects)
{
if (ui.UserId == userId)
{
ui.OnUserLeft();
OnUserLeft(userId);
}
}
}
}
var lobbySlot = m_UserUIObjects[localPlayer.Index.Value];
// If there are new players, we need to hook them into the UI.
foreach (var lobbyUserKvp in newUserDict)
{
if (m_CurrentUsers.Contains(lobbyUserKvp.Key))
continue;
m_CurrentUsers.Add(lobbyUserKvp.Key);
foreach (var pcu in m_UserUIObjects)
{
if (pcu.IsAssigned)
continue;
pcu.SetUser(lobbyUserKvp.Value);
break;
}
}
lobbySlot.SetUser(localPlayer);
void OnUserLeft(int userID)
void OnUserLeft(int i)
m_UserUIObjects.RemoveAt(userID);
m_UserUIObjects[i].ResetUI();
}
}

8
Assets/StreamingAssets.meta


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