浏览代码

Removed remaining relay changes from synchronizer.

BUG Can't see the Lobby?
BUG Players dont update their state.
/main/staging/2021_Upgrade/Async_Refactor
当前提交
567ddd9b
共有 8 个文件被更改,包括 500 次插入574 次删除
  1. 38
      Assets/Scripts/GameLobby/Game/GameManager.cs
  2. 17
      Assets/Scripts/GameLobby/Game/LobbyUser.cs
  3. 44
      Assets/Scripts/GameLobby/Game/LocalLobby.cs
  4. 4
      Assets/Scripts/GameLobby/Infrastructure/Observed.cs
  5. 13
      Assets/Scripts/GameLobby/Lobby/LobbyConverters.cs
  6. 667
      Assets/Scripts/GameLobby/Lobby/LobbyManager.cs
  7. 279
      Assets/Scripts/GameLobby/Lobby/LobbySynchronizer.cs
  8. 12
      Assets/Scripts/GameLobby/Relay/RelayUtpClient.cs

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


m_LocalUser.DisplayName = (string)msg;
}
else if (type == MessageType.ClientUserApproved)
{
ConfirmApproval();
}
else if (type == MessageType.UserSetEmote)
{
EmoteType emote = (EmoteType)msg;

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>

m_LobbySynchronizer.StartSynch(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.
// In particular, we should prevent players from joining voice chat until they are approved.
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Lobby);
if (m_LocalUser.IsHost)
{
// StartRelayConnection();
StartVivoxJoin();
}
else
{
// StartRelayConnection();
}
StartVivoxJoin();
}
void OnLeftLobby()

}
m_RelayClient = client;
if (m_LocalUser.IsHost)
CompleteRelayConnection();
else
Debug.Log("Client is now waiting for approval...");
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Lobby);
}
}

if (m_LocalLobby != null && m_LocalLobby.LobbyID == lobbyId && !string.IsNullOrEmpty(lobbyId)) // Ensure we didn't leave the lobby during this waiting period.
doConnection?.Invoke();
}
void ConfirmApproval()
{
if (!m_LocalUser.IsHost && m_LocalUser.IsApproved)
{
CompleteRelayConnection();
StartVivoxJoin();
}
}
void CompleteRelayConnection()
{
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Lobby);
}
void SetUserLobbyState()

17
Assets/Scripts/GameLobby/Game/LobbyUser.cs


m_data.IsHost = value;
m_lastChanged = UserMembers.IsHost;
OnChanged(this);
if (value)
IsApproved = true;
}
}
}

}
}
public bool IsApproved // Clients joining the lobby should be approved by the host before they can interact.
{
get => m_data.IsApproved;
set
{
if (!m_data.IsApproved && value) // Don't be un-approved except by a call to ResetState.
{
m_data.IsApproved = value;
m_lastChanged = UserMembers.IsApproved;
OnChanged(this);
Locator.Get.Messenger.OnReceiveMessage(MessageType.ClientUserApproved, null);
}
}
}
public override void CopyObserved(LobbyUser observed)
{

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


{
Dictionary<string, LobbyUser> m_LobbyUsers = new Dictionary<string, LobbyUser>();
public Dictionary<string, LobbyUser> LobbyUsers => m_LobbyUsers;
//Should be set to true when pushing lobby to the cloud, and set to false when done pulling.
//This is because we could get more than just our changes when we receive the latest lobby from our calls.
public bool changedByLobbySynch;
#region LocalLobbyData

public int MaxPlayerCount { get; set; }
public LobbyState State { get; set; }
public LobbyColor Color { get; set; }
public long State_LastEdit { get; set; }
public long Color_LastEdit { get; set; }
public long RelayNGOCode_LastEdit { get; set; }
public long LastEdit { get; set; }
public LobbyData(LobbyData existing)
{

MaxPlayerCount = existing.MaxPlayerCount;
State = existing.State;
Color = existing.Color;
State_LastEdit = existing.State_LastEdit;
Color_LastEdit = existing.Color_LastEdit;
RelayNGOCode_LastEdit = existing.RelayNGOCode_LastEdit;
LastEdit = existing.LastEdit;
AvailableSlots = existing.AvailableSlots;
Locked = existing.Locked;
}

MaxPlayerCount = -1;
State = LobbyState.Lobby;
Color = LobbyColor.None;
State_LastEdit = 0;
Color_LastEdit = 0;
RelayNGOCode_LastEdit = 0;
LastEdit = 0;
}
public override string ToString()

sb.Append("LobbyState: ");
sb.AppendLine(State.ToString());
sb.Append("Lobby State Last Edit: ");
sb.AppendLine(new DateTime(State_LastEdit).ToString());
sb.AppendLine(new DateTime(LastEdit).ToString());
sb.Append("Color Last Edit: ");
sb.AppendLine(new DateTime(Color_LastEdit).ToString());
sb.Append("Relay NGO last Edit: ");
sb.AppendLine(new DateTime(RelayNGOCode_LastEdit).ToString());
return sb.ToString();
}
}

public LocalLobby()
{
onChanged += (lobby) => { m_Data.LastEdit = DateTime.Now.ToFileTimeUtc(); };
}
/// <summary>Used only for visual output of the Relay connection info. The obfuscated Relay server IP is obtained during allocation in the RelayUtpSetup.</summary>
public ServerAddress RelayServer

void AddUser(LobbyUser user)
{
Debug.Log($"Adding User: {user.DisplayName} - {user.ID}");
m_LobbyUsers.Add(user.ID, user);
user.onChanged += OnChangedUser;
}

set
{
m_Data.RelayNGOCode = value;
m_Data.RelayNGOCode_LastEdit = DateTime.Now.Ticks;
OnChanged(this);
}
}

set
{
m_Data.State = value;
m_Data.State_LastEdit = DateTime.Now.Ticks;
OnChanged(this);
}
}

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

var pendingState = lobbyData.State;
var pendingColor = lobbyData.Color;
var pendingNgoCode = lobbyData.RelayNGOCode;
if (m_Data.State_LastEdit > lobbyData.State_LastEdit)
pendingState = m_Data.State;
if (m_Data.Color_LastEdit > lobbyData.Color_LastEdit)
pendingColor = m_Data.Color;
if (m_Data.RelayNGOCode_LastEdit > lobbyData.RelayNGOCode_LastEdit)
pendingNgoCode = m_Data.RelayNGOCode;
pendingState = m_Data.State;
pendingColor = m_Data.Color;
pendingNgoCode = m_Data.RelayNGOCode;
m_Data = lobbyData;
m_Data.State = pendingState;
m_Data.Color = pendingColor;

4
Assets/Scripts/GameLobby/Infrastructure/Observed.cs


/// Something that exposes some data that, when changed, an observer would want to be notified about automatically.
/// Used for UI elements and for keeping our local Lobby state synchronized with the remote Lobby service data.
/// (See http://gameprogrammingpatterns.com/observer.html to learn more.)
///
///
/// In your Observed child implementations, be sure to call OnChanged when setting the value of any property.
/// </summary>
/// <typeparam name="T">The type of object to be observed.</typeparam>

public Action<T> onDestroyed { get; set; }
/// <summary>
/// Should be implemented into every public property of the observed
/// Should be implemented into every public property of the observed
/// </summary>
/// <param name="observed">Instance of the observed that changed.</param>
protected void OnChanged(T observed)

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


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());
data.Add("LastEdit", lobby.Data.LastEdit.ToString());
return data;
}

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("DisplayName", user.DisplayName);
data.Add("UserStatus", ((int)user.UserStatus).ToString());
data.Add("Emote", ((int)user.Emote).ToString());
return data;

Private = remoteLobby.IsPrivate,
LobbyName = remoteLobby.Name,
MaxPlayerCount = remoteLobby.MaxPlayers,
State_LastEdit = remoteLobby.LastUpdated.Ticks,
LastEdit = remoteLobby.LastUpdated.ToFileTimeUtc(),
Color_LastEdit = remoteLobby.Data?.ContainsKey("Color_LastEdit") == true ? long.Parse(remoteLobby.Data["Color_LastEdit"].Value) : localLobbyToUpdate.Data.Color_LastEdit,
RelayNGOCode_LastEdit = remoteLobby.Data?.ContainsKey("RelayNGOCode_LastEdit") == true ? long.Parse(remoteLobby.Data["RelayNGOCode_LastEdit"].Value) : localLobbyToUpdate.Data.RelayNGOCode_LastEdit
};
Dictionary<string, LobbyUser> lobbyUsers = new Dictionary<string, LobbyUser>();

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
ID = player.Id,
};
lobbyUsers.Add(incomingData.ID, incomingData);
}

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


namespace LobbyRelaySample
{
/// <summary>
/// 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>
///
/// Manages one Lobby at a time, Only entry points to a lobby with ID is via JoinAsync, CreateAsync, and QuickJoinAsync
public class LobbyManager : IDisposable
{
//Once connected to a lobby, cache the local lobby object so we don't query for it for every lobby operation.
// (This assumes that the game will be actively in just one lobby at a time, though they could be in more on the service side.)
/// <summary>
/// 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>
///
/// Manages one Lobby at a time, Only entry points to a lobby with ID is via JoinAsync, CreateAsync, and QuickJoinAsync
public class LobbyManager : IDisposable
{
//Once connected to a lobby, cache the local lobby object so we don't query for it for every lobby operation.
// (This assumes that the game will be actively in just one lobby at a time, though they could be in more on the service side.)
public Lobby CurrentLobby => m_CurrentLobby;
Lobby m_CurrentLobby;
public Lobby CurrentLobby => m_CurrentLobby;
Lobby m_CurrentLobby;
const int
k_maxLobbiesToShow = 16; // If more are necessary, consider retrieving paginated results or using filters.
const int
k_maxLobbiesToShow = 16; // If more are necessary, consider retrieving paginated results or using filters.
#region Rate Limiting
#region Rate Limiting
public enum RequestType
{
Query = 0,
Join,
QuickJoin,
Host
}
public enum RequestType
{
Query = 0,
Join,
QuickJoin,
Host
}
public bool InLobby()
{
if (m_CurrentLobby == null)
{
Debug.LogError("LobbyManager not currently in a lobby. Did you CreateLobbyAsync or JoinLobbyAsync?");
return false;
}
public bool InLobby()
{
if (m_CurrentLobby == null)
{
Debug.LogError("LobbyManager not currently in a lobby. Did you CreateLobbyAsync or JoinLobbyAsync?");
return false;
}
return true;
}
return true;
}
public RateLimiter GetRateLimit(RequestType type)
{
if (type == RequestType.Join)
return m_JoinCooldown;
else if (type == RequestType.QuickJoin)
return m_QuickJoinCooldown;
else if (type == RequestType.Host)
return m_CreateCooldown;
return m_QueryCooldown;
}
public RateLimiter GetRateLimit(RequestType type)
{
if (type == RequestType.Join)
return m_JoinCooldown;
else if (type == RequestType.QuickJoin)
return m_QuickJoinCooldown;
else if (type == RequestType.Host)
return m_CreateCooldown;
return m_QueryCooldown;
}
// Rate Limits are posted here: https://docs.unity.com/lobby/rate-limits.html
// Rate Limits are posted here: https://docs.unity.com/lobby/rate-limits.html
RateLimiter m_QueryCooldown = new RateLimiter(1f);
RateLimiter m_CreateCooldown = new RateLimiter(3f);
RateLimiter m_JoinCooldown = new RateLimiter(3f);
RateLimiter m_QuickJoinCooldown = new RateLimiter(10f);
RateLimiter m_GetLobbyCooldown = new RateLimiter(1f);
RateLimiter m_DeleteLobbyCooldown = new RateLimiter(.2f);
RateLimiter m_UpdateLobbyCooldown = new RateLimiter(.3f);
RateLimiter m_UpdatePlayerCooldown = new RateLimiter(.3f);
RateLimiter m_LeaveLobbyOrRemovePlayer = new RateLimiter(.3f);
RateLimiter m_HeartBeatCooldown = new RateLimiter(6f);
RateLimiter m_QueryCooldown = new RateLimiter(1f);
RateLimiter m_CreateCooldown = new RateLimiter(3f);
RateLimiter m_JoinCooldown = new RateLimiter(3f);
RateLimiter m_QuickJoinCooldown = new RateLimiter(10f);
RateLimiter m_GetLobbyCooldown = new RateLimiter(1f);
RateLimiter m_DeleteLobbyCooldown = new RateLimiter(.2f);
RateLimiter m_UpdateLobbyCooldown = new RateLimiter(.3f);
RateLimiter m_UpdatePlayerCooldown = new RateLimiter(.3f);
RateLimiter m_LeaveLobbyOrRemovePlayer = new RateLimiter(.3f);
RateLimiter m_HeartBeatCooldown = new RateLimiter(6f);
#endregion
#endregion
Dictionary<string, PlayerDataObject> CreateInitialPlayerData(LobbyUser user)
{
Dictionary<string, PlayerDataObject> data = new Dictionary<string, PlayerDataObject>();
Dictionary<string, PlayerDataObject> CreateInitialPlayerData(LobbyUser user)
{
Dictionary<string, PlayerDataObject> data = new Dictionary<string, PlayerDataObject>();
var displayNameObject = new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, user.DisplayName);
data.Add("DisplayName", displayNameObject);
return data;
}
var displayNameObject = new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, user.DisplayName);
data.Add("DisplayName", displayNameObject);
return data;
}
public async Task<Lobby> CreateLobbyAsync(string lobbyName, int maxPlayers, bool isPrivate, LobbyUser localUser)
{
if (m_CreateCooldown.IsInCooldown)
{
UnityEngine.Debug.LogWarning("Create Lobby hit the rate limit.");
return null;
}
public async Task<Lobby> CreateLobbyAsync(string lobbyName, int maxPlayers, bool isPrivate, LobbyUser localUser)
{
if (m_CreateCooldown.IsInCooldown)
{
UnityEngine.Debug.LogWarning("Create Lobby hit the rate limit.");
return null;
}
await m_CreateCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Creating");
await m_CreateCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Creating");
try
{
string uasId = AuthenticationService.Instance.PlayerId;
try
{
string uasId = AuthenticationService.Instance.PlayerId;
CreateLobbyOptions createOptions = new CreateLobbyOptions
{
IsPrivate = isPrivate,
Player = new Player(id: uasId, data: CreateInitialPlayerData(localUser))
};
m_CurrentLobby = await LobbyService.Instance.CreateLobbyAsync(lobbyName, maxPlayers, createOptions);
CreateLobbyOptions createOptions = new CreateLobbyOptions
{
IsPrivate = isPrivate,
Player = new Player(id: uasId, data: CreateInitialPlayerData(localUser))
};
m_CurrentLobby = await LobbyService.Instance.CreateLobbyAsync(lobbyName, maxPlayers, createOptions);
HeartBeatLoop();
HeartBeatLoop();
return m_CurrentLobby;
}
catch (Exception ex)
{
Debug.LogError($"Lobby Create failed:\n{ex}");
return null;
}
}
return m_CurrentLobby;
}
catch (Exception ex)
{
Debug.LogError($"Lobby Create failed:\n{ex}");
return null;
}
}
public async Task<Lobby> JoinLobbyAsync(string lobbyId, string lobbyCode, LobbyUser localUser)
{
if (m_JoinCooldown.IsInCooldown ||
(lobbyId == null && lobbyCode == null))
{
return null;
}
public async Task<Lobby> JoinLobbyAsync(string lobbyId, string lobbyCode, LobbyUser localUser)
{
if (m_JoinCooldown.IsInCooldown ||
(lobbyId == null && lobbyCode == null))
{
return null;
}
await m_JoinCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Joining");
await m_JoinCooldown.WaitUntilCooldown();
Debug.Log($"{localUser.DisplayName}({localUser.ID}) Joining Lobby- {lobbyId} with {lobbyCode}");
string uasId = AuthenticationService.Instance.PlayerId;
var playerData = CreateInitialPlayerData(localUser);
if (!string.IsNullOrEmpty(lobbyId))
{
JoinLobbyByIdOptions joinOptions = new JoinLobbyByIdOptions
{ Player = new Player(id: uasId, data: playerData) };
m_CurrentLobby = await LobbyService.Instance.JoinLobbyByIdAsync(lobbyId, joinOptions);
}
else
{
JoinLobbyByCodeOptions joinOptions = new JoinLobbyByCodeOptions
{ Player = new Player(id: uasId, data: playerData) };
m_CurrentLobby = await LobbyService.Instance.JoinLobbyByCodeAsync(lobbyCode, joinOptions);
}
string uasId = AuthenticationService.Instance.PlayerId;
var playerData = CreateInitialPlayerData(localUser);
if (!string.IsNullOrEmpty(lobbyId))
{
JoinLobbyByIdOptions joinOptions = new JoinLobbyByIdOptions
{Player = new Player(id: uasId, data: playerData)};
m_CurrentLobby = await LobbyService.Instance.JoinLobbyByIdAsync(lobbyId, joinOptions);
}
else
{
JoinLobbyByCodeOptions joinOptions = new JoinLobbyByCodeOptions
{Player = new Player(id: uasId, data: playerData)};
m_CurrentLobby = await LobbyService.Instance.JoinLobbyByCodeAsync(lobbyCode, joinOptions);
}
return m_CurrentLobby;
}
return m_CurrentLobby;
}
public async Task<Lobby> QuickJoinLobbyAsync(LobbyUser localUser, LobbyColor limitToColor = LobbyColor.None)
{
//We dont want to queue a quickjoin
if (m_QuickJoinCooldown.IsInCooldown)
{
UnityEngine.Debug.LogWarning("Quick Join Lobby hit the rate limit.");
return null;
}
public async Task<Lobby> QuickJoinLobbyAsync(LobbyUser localUser, LobbyColor limitToColor = LobbyColor.None)
{
//We dont want to queue a quickjoin
if (m_QuickJoinCooldown.IsInCooldown)
{
UnityEngine.Debug.LogWarning("Quick Join Lobby hit the rate limit.");
return null;
}
await m_QuickJoinCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Quick Joining.");
var filters = LobbyColorToFilters(limitToColor);
string uasId = AuthenticationService.Instance.PlayerId;
await m_QuickJoinCooldown.WaitUntilCooldown();
Debug.Log("Lobby - QuickJoining.");
var filters = LobbyColorToFilters(limitToColor);
string uasId = AuthenticationService.Instance.PlayerId;
var joinRequest = new QuickJoinLobbyOptions
{
Filter = filters,
Player = new Player(id: uasId, data: CreateInitialPlayerData(localUser))
};
return m_CurrentLobby = await LobbyService.Instance.QuickJoinLobbyAsync(joinRequest);
}
var joinRequest = new QuickJoinLobbyOptions
{
Filter = filters,
Player = new Player(id: uasId, data: CreateInitialPlayerData(localUser))
};
public async Task<QueryResponse> RetrieveLobbyListAsync(LobbyColor limitToColor = LobbyColor.None)
{
await m_QueryCooldown.WaitUntilCooldown();
return m_CurrentLobby = await LobbyService.Instance.QuickJoinLobbyAsync(joinRequest);
}
Debug.Log("Lobby - Retrieving List.");
public async Task<QueryResponse> RetrieveLobbyListAsync(LobbyColor limitToColor = LobbyColor.None)
{
await m_QueryCooldown.WaitUntilCooldown();
var filters = LobbyColorToFilters(limitToColor);
Debug.Log("Lobby - Retrieving List.");
QueryLobbiesOptions queryOptions = new QueryLobbiesOptions
{
Count = k_maxLobbiesToShow,
Filters = filters
};
return await LobbyService.Instance.QueryLobbiesAsync(queryOptions);
}
var filters = LobbyColorToFilters(limitToColor);
List<QueryFilter> LobbyColorToFilters(LobbyColor limitToColor)
{
List<QueryFilter> filters = new List<QueryFilter>();
if (limitToColor == LobbyColor.Orange)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int)LobbyColor.Orange).ToString(),
QueryFilter.OpOptions.EQ));
else if (limitToColor == LobbyColor.Green)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int)LobbyColor.Green).ToString(),
QueryFilter.OpOptions.EQ));
else if (limitToColor == LobbyColor.Blue)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int)LobbyColor.Blue).ToString(),
QueryFilter.OpOptions.EQ));
return filters;
}
QueryLobbiesOptions queryOptions = new QueryLobbiesOptions
{
Count = k_maxLobbiesToShow,
Filters = filters
};
return await LobbyService.Instance.QueryLobbiesAsync(queryOptions);
}
public async Task<Lobby> GetLobbyAsync(string lobbyId = null)
{
if (!InLobby())
return null;
await m_GetLobbyCooldown.WaitUntilCooldown();
lobbyId ??= m_CurrentLobby.Id;
return m_CurrentLobby = await LobbyService.Instance.GetLobbyAsync(lobbyId);
}
List<QueryFilter> LobbyColorToFilters(LobbyColor limitToColor)
{
List<QueryFilter> filters = new List<QueryFilter>();
if (limitToColor == LobbyColor.Orange)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int) LobbyColor.Orange).ToString(),
QueryFilter.OpOptions.EQ));
else if (limitToColor == LobbyColor.Green)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int) LobbyColor.Green).ToString(),
QueryFilter.OpOptions.EQ));
else if (limitToColor == LobbyColor.Blue)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int) LobbyColor.Blue).ToString(),
QueryFilter.OpOptions.EQ));
return filters;
}
public async Task LeaveLobbyAsync()
{
await m_LeaveLobbyOrRemovePlayer.WaitUntilCooldown();
if (!InLobby())
return;
string playerId = AuthenticationService.Instance.PlayerId;
Debug.Log($"{playerId} leaving Lobby {m_CurrentLobby.Id}");
await LobbyService.Instance.RemovePlayerAsync(m_CurrentLobby.Id, playerId);
m_CurrentLobby = null;
}
public async Task<Lobby> GetLobbyAsync(string lobbyId = null)
{
if (!InLobby())
return null;
await m_GetLobbyCooldown.WaitUntilCooldown();
lobbyId ??= m_CurrentLobby.Id;
return m_CurrentLobby = await LobbyService.Instance.GetLobbyAsync(lobbyId);
}
public async Task<Lobby> UpdatePlayerDataAsync(Dictionary<string, string> data)
{
if (!InLobby())
return null;
await m_UpdatePlayerCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Updating Player Data");
public async Task LeaveLobbyAsync()
{
await m_LeaveLobbyOrRemovePlayer.WaitUntilCooldown();
if (!InLobby())
return;
string playerId = AuthenticationService.Instance.PlayerId;
Debug.Log($"{playerId} leaving Lobby {m_CurrentLobby.Id}");
string playerId = AuthenticationService.Instance.PlayerId;
Dictionary<string, PlayerDataObject> dataCurr = new Dictionary<string, PlayerDataObject>();
foreach (var dataNew in data)
{
PlayerDataObject dataObj = new PlayerDataObject(visibility: PlayerDataObject.VisibilityOptions.Member,
value: dataNew.Value);
if (dataCurr.ContainsKey(dataNew.Key))
dataCurr[dataNew.Key] = dataObj;
else
dataCurr.Add(dataNew.Key, dataObj);
}
await LobbyService.Instance.RemovePlayerAsync(m_CurrentLobby.Id, playerId);
m_CurrentLobby = null;
}
UpdatePlayerOptions updateOptions = new UpdatePlayerOptions
{
Data = dataCurr,
AllocationId = null,
ConnectionInfo = null
};
return m_CurrentLobby =
await LobbyService.Instance.UpdatePlayerAsync(m_CurrentLobby.Id, playerId, updateOptions);
}
public async Task<Lobby> UpdatePlayerDataAsync(Dictionary<string, string> data)
{
if (!InLobby())
return null;
await m_UpdatePlayerCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Updating Player Data");
public async Task<Lobby> UpdatePlayerRelayInfoAsync(string lobbyID, string allocationId, string connectionInfo)
{
if (!InLobby())
return null;
await m_UpdatePlayerCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Relay Info (Player)");
string playerId = AuthenticationService.Instance.PlayerId;
Dictionary<string, PlayerDataObject> dataCurr = new Dictionary<string, PlayerDataObject>();
foreach (var dataNew in data)
{
PlayerDataObject dataObj = new PlayerDataObject(visibility: PlayerDataObject.VisibilityOptions.Member,
value: dataNew.Value);
if (dataCurr.ContainsKey(dataNew.Key))
dataCurr[dataNew.Key] = dataObj;
else
dataCurr.Add(dataNew.Key, dataObj);
}
string playerId = AuthenticationService.Instance.PlayerId;
UpdatePlayerOptions updateOptions = new UpdatePlayerOptions
{
Data = dataCurr,
AllocationId = null,
ConnectionInfo = null
};
return m_CurrentLobby =
await LobbyService.Instance.UpdatePlayerAsync(m_CurrentLobby.Id, playerId, updateOptions);
}
UpdatePlayerOptions updateOptions = new UpdatePlayerOptions
{
Data = new Dictionary<string, PlayerDataObject>(),
AllocationId = allocationId,
ConnectionInfo = connectionInfo
};
return m_CurrentLobby = await LobbyService.Instance.UpdatePlayerAsync(lobbyID, playerId, updateOptions);
}
public async Task<Lobby> UpdateLobbyDataAsync(Dictionary<string, string> data)
{
if (!InLobby())
return null;
await m_UpdateLobbyCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Updating Lobby Data");
public async Task<Lobby> UpdatePlayerRelayInfoAsync(string lobbyID, string allocationId, string connectionInfo)
{
if (!InLobby())
return null;
await m_UpdatePlayerCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Relay Info (Player)");
Dictionary<string, DataObject> dataCurr = m_CurrentLobby.Data ?? new Dictionary<string, DataObject>();
string playerId = AuthenticationService.Instance.PlayerId;
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.
DataObject.IndexOptions index = dataNew.Key == "Color" ? DataObject.IndexOptions.N1 : 0;
DataObject
dataObj = new DataObject(DataObject.VisibilityOptions.Public, dataNew.Value,
index); // Public so that when we request the list of lobbies, we can get info about them for filtering.
if (dataCurr.ContainsKey(dataNew.Key))
dataCurr[dataNew.Key] = dataObj;
else
dataCurr.Add(dataNew.Key, dataObj);
UpdatePlayerOptions updateOptions = new UpdatePlayerOptions
{
Data = new Dictionary<string, PlayerDataObject>(),
AllocationId = allocationId,
ConnectionInfo = connectionInfo
};
return m_CurrentLobby = await LobbyService.Instance.UpdatePlayerAsync(lobbyID, playerId, updateOptions);
}
//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")
{
Enum.TryParse(dataNew.Value, out LobbyState lobbyState);
shouldLock = lobbyState != LobbyState.Lobby;
}
}
public async Task<Lobby> UpdateLobbyDataAsync(Dictionary<string, string> data)
{
if (!InLobby())
return null;
await m_UpdateLobbyCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Updating Lobby Data");
UpdateLobbyOptions updateOptions = new UpdateLobbyOptions { Data = dataCurr, IsLocked = shouldLock };
return m_CurrentLobby = await LobbyService.Instance.UpdateLobbyAsync(m_CurrentLobby.Id, updateOptions);
}
Dictionary<string, DataObject> dataCurr = m_CurrentLobby.Data ?? new Dictionary<string, DataObject>();
public async Task DeleteLobbyAsync()
{
if (!InLobby())
return;
await m_DeleteLobbyCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Deleting Lobby");
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.
DataObject.IndexOptions index = dataNew.Key == "Color" ? DataObject.IndexOptions.N1 : 0;
DataObject
dataObj = new DataObject(DataObject.VisibilityOptions.Public, dataNew.Value,
index); // Public so that when we request the list of lobbies, we can get info about them for filtering.
if (dataCurr.ContainsKey(dataNew.Key))
dataCurr[dataNew.Key] = dataObj;
else
dataCurr.Add(dataNew.Key, dataObj);
await LobbyService.Instance.DeleteLobbyAsync(m_CurrentLobby.Id);
}
//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")
{
Enum.TryParse(dataNew.Value, out LobbyState lobbyState);
shouldLock = lobbyState != LobbyState.Lobby;
}
}
public void Dispose()
{
m_CurrentLobby = null;
}
UpdateLobbyOptions updateOptions = new UpdateLobbyOptions {Data = dataCurr, IsLocked = shouldLock};
return m_CurrentLobby = await LobbyService.Instance.UpdateLobbyAsync(m_CurrentLobby.Id, updateOptions);
}
public async Task DeleteLobbyAsync()
{
if (!InLobby())
return;
await m_DeleteLobbyCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Deleting Lobby");
await LobbyService.Instance.DeleteLobbyAsync(m_CurrentLobby.Id);
}
public void Dispose()
{
m_CurrentLobby = null;
}
#region HeartBeat
#region HeartBeat
async Task SendHeartbeatPingAsync()
{
if (!InLobby())
return;
if (m_HeartBeatCooldown.IsInCooldown)
return;
await m_HeartBeatCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Heartbeat");
async Task SendHeartbeatPingAsync()
{
if (!InLobby())
return;
if (m_HeartBeatCooldown.IsInCooldown)
return;
await m_HeartBeatCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Heartbeat");
await LobbyService.Instance.SendHeartbeatPingAsync(m_CurrentLobby.Id);
}
await LobbyService.Instance.SendHeartbeatPingAsync(m_CurrentLobby.Id);
}
async Task HeartBeatLoop()
{
while (m_CurrentLobby != null)
{
await SendHeartbeatPingAsync();
await Task.Delay(8000);
}
}
async Task HeartBeatLoop()
{
while (m_CurrentLobby != null)
{
await SendHeartbeatPingAsync();
await Task.Delay(8000);
}
}
#endregion
}
#endregion
}
//Manages the Cooldown for each service call.
//Adds a buffer to account for ping times.
public class RateLimiter
{
public Action<bool> onCooldownChange;
public readonly float cooldownSeconds;
public readonly int coolDownMS;
public readonly int pingBufferMS;
//(If you're still getting rate limit errors, try increasing the pingBuffer)
public RateLimiter(float cooldownSeconds, int pingBuffer = 100)
{
this.cooldownSeconds = cooldownSeconds;
pingBufferMS = pingBuffer;
coolDownMS =
Mathf.CeilToInt(this.cooldownSeconds * 1000) +
pingBufferMS;
}
//Manages the Cooldown for each service call.
//Adds a buffer to account for ping times.
public class RateLimiter
{
public Action<bool> onCooldownChange;
public readonly float cooldownSeconds;
public readonly int coolDownMS;
public readonly int pingBufferMS;
//(If you're still getting rate limit errors, try increasing the pingBuffer)
public RateLimiter(float cooldownSeconds, int pingBuffer = 100)
{
this.cooldownSeconds = cooldownSeconds;
pingBufferMS = pingBuffer;
coolDownMS =
Mathf.CeilToInt(this.cooldownSeconds * 1000) +
pingBufferMS;
}
public async Task WaitUntilCooldown()
{
//No Queue!
if (!m_IsInCooldown)
{
public async Task WaitUntilCooldown()
{
//No Queue!
if (!m_IsInCooldown)
{
CooldownAsync();
CooldownAsync();
return;
}
return;
}
while (m_IsInCooldown)
{
await Task.Delay(10);
}
}
while (m_IsInCooldown)
{
await Task.Delay(10);
}
}
async Task CooldownAsync()
{
IsInCooldown = true;
await Task.Delay(coolDownMS);
IsInCooldown = false;
}
async Task CooldownAsync()
{
IsInCooldown = true;
await Task.Delay(coolDownMS);
IsInCooldown = false;
}
bool m_IsInCooldown = false;
bool m_IsInCooldown = false;
public bool IsInCooldown
{
get => m_IsInCooldown;
private set
{
if (m_IsInCooldown != value)
{
m_IsInCooldown = value;
onCooldownChange?.Invoke(m_IsInCooldown);
}
}
}
}
}
public bool IsInCooldown
{
get => m_IsInCooldown;
private set
{
if (m_IsInCooldown != value)
{
m_IsInCooldown = value;
onCooldownChange?.Invoke(m_IsInCooldown);
}
}
}
}
}

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


using System.Threading.Tasks;
using LobbyRelaySample.lobby;
using Unity.Services.Lobbies.Models;
using UnityEngine;
/// <summary>
/// Keep updated on changes to a joined lobby, at a speed compliant with Lobby's rate limiting.
/// </summary>
public class LobbySynchronizer : IReceiveMessages, IDisposable
{
LocalLobby m_LocalLobby;
LobbyUser m_LocalUser;
LobbyManager m_LobbyManager;
bool m_LocalChanges = false;
/// <summary>
/// Keep updated on changes to a joined lobby, at a speed compliant with Lobby's rate limiting.
/// </summary>
public class LobbySynchronizer : IReceiveMessages, IDisposable
{
LocalLobby m_LocalLobby;
LobbyUser 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.
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;
int m_lifetime = 0;
const int k_UpdateIntervalMS = 1000;
public LobbySynchronizer(LobbyManager lobbyManager)
{
m_LobbyManager = lobbyManager;
}
public LobbySynchronizer(LobbyManager lobbyManager)
{
m_LobbyManager = lobbyManager;
}
public void StartSynch(LocalLobby localLobby, LobbyUser localUser)
{
m_LocalUser = localUser;
m_LocalLobby = localLobby;
m_LocalLobby.onChanged += OnLocalLobbyChanged;
m_LocalChanges = true;
Locator.Get.Messenger.Subscribe(this);
public void StartSynch(LocalLobby localLobby, LobbyUser localUser)
{
m_LocalUser = localUser;
m_LocalLobby = localLobby;
m_LocalLobby.onChanged += OnLocalLobbyChanged;
m_LocalChanges = true;
Locator.Get.Messenger.Subscribe(this);
UpdateLoopAsync();
UpdateLoopAsync();
m_lifetime = 0;
}
m_lifetime = 0;
}
public void EndSynch()
{
m_LocalChanges = false;
public void EndSynch()
{
m_LocalChanges = false;
Locator.Get.Messenger.Unsubscribe(this);
if (m_LocalLobby != null)
m_LocalLobby.onChanged -= OnLocalLobbyChanged;
Locator.Get.Messenger.Unsubscribe(this);
if (m_LocalLobby != null)
m_LocalLobby.onChanged -= OnLocalLobbyChanged;
m_LocalLobby = null;
}
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);
}
}
//TODO Stop players from joining lobby while game is underway.
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>
async Task UpdateLoopAsync()
{
Lobby latestLobby = 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)
{
if (m_LocalChanges)
{
m_LocalLobby.changedByLobbySynch = true;
latestLobby = await PushDataToLobby();
}
else
latestLobby = await m_LobbyManager.GetLobbyAsync();
while (m_LocalLobby != null)
{
if (UserTimedOut())
{
LeaveLobbyBecauseTimeout();
break;
}
if (IfRemoteLobbyChanged(latestLobby))
LobbyConverters.RemoteToLocal(latestLobby, m_LocalLobby);
m_LocalLobby.changedByLobbySynch = false;
if (m_LocalChanges)
latestLobby = await PushDataToLobby();
else
latestLobby = await m_LobbyManager.GetLobbyAsync();
if (!LobbyHasHost())
{
LeaveLobbyBecauseNoHost();
break;
}
if (IfRemoteLobbyChanged(latestLobby))
LobbyConverters.RemoteToLocal(latestLobby, m_LocalLobby);
if (!LobbyHasHost())
{
LeaveLobbyBecauseNoHost();
break;
}
m_lifetime += k_UpdateIntervalMS;
await Task.Delay(k_UpdateIntervalMS);
}
m_lifetime += k_UpdateIntervalMS;
await Task.Delay(k_UpdateIntervalMS);
}
bool IfRemoteLobbyChanged(Lobby remoteLobby)
{
var remoteLobbyTime = remoteLobby.LastUpdated.ToFileTimeUtc();
var localLobbyTime = m_LocalLobby.Data.LastEdit;
var isLocalOutOfDate = remoteLobbyTime > localLobbyTime;
return isLocalOutOfDate;
}
bool IfRemoteLobbyChanged(Lobby remoteLobby)
{
return remoteLobby.LastUpdated.ToFileTime() > m_LocalLobby.Data.State_LastEdit;
}
async Task<Lobby> PushDataToLobby()
{
m_LocalChanges = false;
bool UserTimedOut()
{
return m_LocalUser.IsApproved && m_lifetime > k_approvalMaxMS;
}
if (m_LocalUser.IsHost)
await m_LobbyManager.UpdateLobbyDataAsync(
LobbyConverters.LocalToRemoteData(m_LocalLobby));
async Task<Lobby> PushDataToLobby()
{
m_LocalChanges = false;
m_LocalLobby.changedByLobbySynch = true;
return await m_LobbyManager.UpdatePlayerDataAsync(
LobbyConverters.LocalToRemoteUserData(m_LocalUser));
}
if (m_LocalUser.IsHost)
await m_LobbyManager.UpdateLobbyDataAsync(
LobbyConverters.LocalToRemoteData(m_LocalLobby));
bool LobbyHasHost()
{
if (!m_LocalUser.IsHost)
{
foreach (var lobbyUser in m_LocalLobby.LobbyUsers)
{
if (lobbyUser.Value.IsHost)
return true;
}
return await m_LobbyManager.UpdatePlayerDataAsync(
LobbyConverters.LocalToRemoteUserData(m_LocalUser));
}
return false;
}
return true;
}
bool LobbyHasHost()
{
if (!m_LocalUser.IsHost)
{
foreach (var lobbyUser in m_LocalLobby.LobbyUsers)
{
if (lobbyUser.Value.IsHost)
return true;
}
void LeaveLobbyBecauseNoHost()
{
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);
}
}
return false;
}
void OnLocalLobbyChanged(LocalLobby localLobby)
{
if (string.IsNullOrEmpty(localLobby.LobbyID)
) // When the player leaves, their LocalLobby is cleared out.
{
EndSynch();
return;
}
return true;
}
//Catch for infinite update looping from the synchronizer.
if (localLobby.changedByLobbySynch)
{
return;
}
void LeaveLobbyBecauseTimeout()
{
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup,
"Connection attempt timed out!");
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeMenuState, GameState.JoinMenu);
}
m_LocalChanges = true;
}
void LeaveLobbyBecauseNoHost()
{
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);
}
}
void OnLocalLobbyChanged(LocalLobby localLobby)
{
if (string.IsNullOrEmpty(localLobby.LobbyID)
) // When the player leaves, their LocalLobby is cleared out.
{
EndSynch();
return;
}
//Catch for infinite update looping from the synchronizer.
if (localLobby.changedByLobbySynch)
{
localLobby.changedByLobbySynch = false;
return;
}
m_LocalChanges = true;
}
public void Dispose()
{
EndSynch();
}
}
}
public void Dispose()
{
EndSynch();
}
}
}

12
Assets/Scripts/GameLobby/Relay/RelayUtpClient.cs


if (msgType == MsgType.PlayerApprovalState)
{
Approval approval = (Approval)msgContents[0];
if (approval == Approval.OK && !m_localUser.IsApproved)
OnApproved(m_networkDriver, conn);
else if (approval == Approval.GameAlreadyStarted)
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup, "Rejected: Game has already started.");
// if (approval == Approval.OK && !m_localUser.IsApproved)
// OnApproved(m_networkDriver, conn);
// else if (approval == Approval.GameAlreadyStarted)
// Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup, "Rejected: Game has already started.");
}
else if (msgType == MsgType.PlayerName)
{

{
// Don't react to our own messages. Also, don't need to hold onto messages if the ID is absent; clients should be initialized and in the lobby before they send events.
// (Note that this enforces lobby membership before processing any events besides an approval request, so a client is unable to fully use Relay unless they're in the lobby.)
return id != m_localUser.ID && (m_localUser.IsApproved && m_localLobby.LobbyUsers.ContainsKey(id) || type == MsgType.PlayerApprovalState);
return true;// != m_localUser.ID && (m_localUser.IsApproved && m_localLobby.LobbyUsers.ContainsKey(id) || type == MsgType.PlayerApprovalState);
}
protected virtual void ProcessNetworkEventDataAdditional(NetworkConnection conn, MsgType msgType, string id) { }
protected virtual void ProcessDisconnectEvent(NetworkConnection conn, DataStreamReader strm)

}
private void OnApproved(NetworkDriver driver, NetworkConnection connection)
{
m_localUser.IsApproved = true;
// m_localUser.IsApproved = true;
ForceFullUserUpdate(driver, connection, m_localUser);
}

正在加载...
取消
保存