Browse Source

Updated latest Lobby Rename and Moved Relay allocation to Countdown.

/dev
UnityJacob 1 year ago
commit
85de15c7
58 changed files with 854 additions and 794 deletions
  1. 3
      .gitignore
  2. 16
      Assets/Prefabs/UI/LobbyCodeCanvas.prefab
  3. 53
      Assets/Scenes/mainScene.unity
  4. 135
      Assets/Scripts/Entities/GameStateManager.cs
  5. 4
      Assets/Scripts/Entities/LobbyServiceData.cs
  6. 542
      Assets/Scripts/Entities/LocalLobby.cs
  7. 24
      Assets/Scripts/Tests/Editor/LobbyTests.cs
  8. 4
      Assets/Scripts/UI/CountdownUI.cs
  9. 4
      Assets/Scripts/UI/CreateMenuUI.cs
  10. 2
      Assets/Scripts/UI/JoinCreateRoomUI.cs
  11. 37
      Assets/Scripts/UI/JoinMenuUI.cs
  12. 12
      Assets/Scripts/UI/LobbyButtonUI.cs
  13. 4
      Assets/Scripts/UI/LobbyStateVisibilityUI.cs
  14. 6
      Assets/Scripts/UI/LobbyUsersUI.cs
  15. 4
      Assets/Scripts/UI/RelayCodeUI.cs
  16. 8
      Assets/Scripts/UI/RoomCodeUI.cs
  17. 4
      Assets/Scripts/UI/ServerAddressUI.cs
  18. 4
      Assets/Scripts/UI/ServerNameUI.cs
  19. 4
      Assets/TempDeleteAllRooms.cs
  20. 200
      Assets/Scripts/Infrastructure/Locator.cs
  21. 126
      Assets/Scripts/Infrastructure/LogHandler.cs
  22. 176
      Assets/Scripts/Infrastructure/Messenger.cs
  23. 276
      Assets/Scripts/Infrastructure/UpdateSlow.cs
  24. 0
      /Assets/Scripts/Entities/LocalLobby.cs.meta
  25. 0
      /Assets/Scripts/Entities/LocalLobbyObserver.cs.meta
  26. 0
      /Assets/Scripts/Entities/ReadyCheck.cs.meta
  27. 0
      /Assets/Scripts/Entities/LocalLobby.cs
  28. 0
      /Assets/Scripts/Tests/PlayMode/LobbyReadyCheckTests.cs.meta
  29. 0
      /Assets/Scripts/Tests/PlayMode/LobbyRoundtripTests.cs.meta
  30. 0
      /Assets/Scripts/Lobby.meta
  31. 0
      /Assets/Scripts/Infrastructure/Locator.cs.meta
  32. 0
      /Assets/Scripts/Infrastructure/LogHandler.cs.meta
  33. 0
      /Assets/Scripts/Infrastructure/Messenger.cs.meta
  34. 0
      /Assets/Scripts/Infrastructure/UpdateSlow.cs.meta
  35. 0
      /Assets/Scripts/Infrastructure/Locator.cs
  36. 0
      /Assets/Scripts/Infrastructure/LogHandler.cs
  37. 0
      /Assets/Scripts/Infrastructure/Messenger.cs
  38. 0
      /Assets/Scripts/Infrastructure/UpdateSlow.cs
  39. 0
      /Assets/Scripts/Lobby/LobbyContentHeartbeat.cs.meta
  40. 0
      /Assets/Scripts/Lobby/LobbyAPIInterface.cs.meta
  41. 0
      /Assets/Scripts/Lobby/LobbyListHeartbeat.cs.meta
  42. 0
      /Assets/Scripts/Lobby/LobbyAsyncRequests.cs.meta
  43. 0
      /Assets/Scripts/Lobby/ToLocalLobby.cs.meta

3
.gitignore


.idea
.idea/
.UserSettings
.UserSettings/

16
Assets/Prefabs/UI/LobbyCodeCanvas.prefab


m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 22
m_fontSizeBase: 22
m_fontSize: 18
m_fontSizeBase: 18
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18

m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_SizeDelta: {x: 200, y: 25}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &1118541987231860831
CanvasRenderer:

m_Script: {fileID: 11500000, guid: a6e005db2e7b3d94d9409975660cf97c, type: 3}
m_Name:
m_EditorClassIdentifier:
m_onVisibilityChange:
m_PersistentCalls:
m_Calls: []
showing: 0
roomCodeText: {fileID: 5578852939709204548}
--- !u!114 &699060394989383769

m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
observeOnStart: 1
--- !u!1 &2798863108443093305
GameObject:
m_ObjectHideFlags: 0

m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 20.55
m_fontSize: 14
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontSizeMin: 14
m_fontSizeMax: 22
m_fontStyle: 2
m_HorizontalAlignment: 2
m_VerticalAlignment: 512

53
Assets/Scenes/mainScene.unity


m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: Delete all my rooms
m_Text: Delete all my lobbies
--- !u!222 &1790515946
CanvasRenderer:
m_ObjectHideFlags: 0

m_Script: {fileID: 11500000, guid: 51373dc3c6ac79b4f8e36ac7c4419205, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &2637199315837045697 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 3903006825828350709, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
m_PrefabInstance: {fileID: 2637199315837045693}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 70dfc2fde0a9ef04eaff29a138f0bf45, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &2637199315837045698 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 618971913928185130, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}

- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalUserObservers.Array.size
value: 3
objectReference: {fileID: 0}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalLobbyObservers.Array.size
value: 9
objectReference: {fileID: 0}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_GameStateObservers.Array.data[0]

propertyPath: m_LocalUserObservers.Array.data[3]
value:
objectReference: {fileID: 0}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalLobbyObservers.Array.data[0]
value:
objectReference: {fileID: 1886099429}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalLobbyObservers.Array.data[1]
value:
objectReference: {fileID: 648562208}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalLobbyObservers.Array.data[2]
value:
objectReference: {fileID: 1412109061}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalLobbyObservers.Array.data[3]
value:
objectReference: {fileID: 297599733}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalLobbyObservers.Array.data[4]
value:
objectReference: {fileID: 2637199315837045697}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalLobbyObservers.Array.data[5]
value:
objectReference: {fileID: 2130620598}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalLobbyObservers.Array.data[6]
value:
objectReference: {fileID: 2074106027}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalLobbyObservers.Array.data[7]
value:
objectReference: {fileID: 309485569}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalLobbyObservers.Array.data[8]
value:
objectReference: {fileID: 2126854580}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LobbyServerObservers.Array.data[0]
value:

135
Assets/Scripts/Entities/GameStateManager.cs


[SerializeField]
List<LocalGameStateObserver> m_GameStateObservers = new List<LocalGameStateObserver>();
[SerializeField]
List<LobbyDataObserver> m_LobbyDataObservers = new List<LobbyDataObserver>();
List<LocalLobbyObserver> m_LocalLobbyObservers = new List<LocalLobbyObserver>();
private RoomsContentHeartbeat m_roomsContentHeartbeat = new RoomsContentHeartbeat();
private LobbyContentHeartbeat m_roomsContentHeartbeat = new LobbyContentHeartbeat();
LobbyData m_lobbyData;
LocalLobby m_localLobby;
LobbyReadyCheck m_LobbyReadyCheck;
ReadyCheck m_ReadyCheck;
public void Awake()
{

var unused = Locator.Get;
#pragma warning restore IDE0059 // Unnecessary assignment of a value
Locator.Get.Provide(new Auth.Identity(OnAuthSignIn));
m_LobbyReadyCheck = new LobbyReadyCheck(null, 7);
m_ReadyCheck = new ReadyCheck(7);
Application.wantsToQuit += OnWantToQuit;
}

{
m_localUser.DisplayName = (string)msg;
}
else if (type == MessageType.CreateRoomRequest)
else if (type == MessageType.CreateLobbyRequest)
var createRoomData = (LobbyData)msg;
RoomsQuery.Instance.CreateRoomAsync(createRoomData.LobbyName, createRoomData.MaxPlayerCount, createRoomData.Private, (r) =>
var createRoomData = (LocalLobby)msg;
LobbyAsyncRequests.Instance.CreateLobbyAsync(createRoomData.LobbyName, createRoomData.MaxPlayerCount, createRoomData.Private, (r) =>
Lobby.ToLobbyData.Convert(r, m_lobbyData, m_localUser);
Lobby.ToLocalLobby.Convert(r, m_localLobby, m_localUser);
else if (type == MessageType.JoinRoomRequest)
else if (type == MessageType.JoinLobbyRequest)
RoomsQuery.Instance.JoinRoomAsync(roomData.RoomID, roomData.RoomCode, (r) =>
LobbyAsyncRequests.Instance.JoinLobbyAsync(roomData.LobbyID, roomData.LobbyCode, (r) =>
Lobby.ToLobbyData.Convert(r, m_lobbyData, m_localUser);
Lobby.ToLocalLobby.Convert(r, m_localLobby, m_localUser);
else if (type == MessageType.QueryRooms)
else if (type == MessageType.QueryLobbies)
RoomsQuery.Instance.RetrieveRoomListAsync(
LobbyAsyncRequests.Instance.RetrieveLobbyListAsync(
OnRefreshed(Lobby.ToLobbyData.Convert(qr));
OnRefreshed(Lobby.ToLocalLobby.Convert(qr));
}, er =>
{
long errorLong = 0;

}
else if (type == MessageType.Client_EndReadyCountdownAt)
{
m_lobbyData.TargetEndTime = (DateTime)msg;
m_localLobby.TargetEndTime = (DateTime)msg;
BeginCountDown();
}
else if (type == MessageType.ToLobby)

void Start()
{
m_lobbyData = new LobbyData
m_localLobby = new LocalLobby
};
m_localUser = new LobbyUser();
m_localUser.DisplayName = "New Player";

m_GameStateObservers.Add(gameStateObs);
}
foreach (var lobbyData in FindObjectsOfType<LobbyDataObserver>())
foreach (var localLobby in FindObjectsOfType<LocalLobbyObserver>())
if (!lobbyData.observeOnStart)
if (!localLobby.observeOnStart)
if (!m_LobbyDataObservers.Contains(lobbyData))
m_LobbyDataObservers.Add(lobbyData);
if (!m_LocalLobbyObservers.Contains(localLobby))
m_LocalLobbyObservers.Add(localLobby);
}
foreach (var lobbyUserObs in FindObjectsOfType<LobbyUserObserver>())

if (!m_LobbyServiceObservers.Contains(lobbyServiceObs))
m_LobbyServiceObservers.Add(lobbyServiceObs);
}
if (m_LobbyDataObservers.Count < 8)
Debug.LogWarning($"Scene has less than the default expected Lobby Data Observers, ensure all the observers in the scene that need to watch the Local Lobby Data are registered in the LobbyDataObservers List.");
if (m_LocalLobbyObservers.Count < 8)
Debug.LogWarning($"Scene has less than the default expected Local Lobby Observers, ensure all the observers in the scene that need to watch the Local Lobby are registered in the LocalLobbyObservers List.");
if (m_LocalUserObservers.Count < 3)
Debug.LogWarning($"Scene has less than the default expected Local User Observers, ensure all the observers in the scene that need to watch the gameState are registered in the LocalUserObservers List.");

gameStateObs.BeginObserving(m_localGameState);
}
foreach (var lobbyObs in m_LobbyDataObservers)
foreach (var lobbyObs in m_LocalLobbyObservers)
{
if (lobbyObs == null)
{

lobbyObs.BeginObserving(m_lobbyData);
lobbyObs.BeginObserving(m_localLobby);
}
foreach (var userObs in m_LocalUserObservers)

if (isLeavingRoom)
OnLeftRoom();
}
void OnRefreshed(IEnumerable<LobbyData> lobbies)
void OnRefreshed(IEnumerable<LocalLobby> lobbies)
var newLobbyDict = new Dictionary<string, LobbyData>();
var newLobbyDict = new Dictionary<string, LocalLobby>();
newLobbyDict.Add(lobby.RoomID, lobby);
newLobbyDict.Add(lobby.LobbyID, lobby);
}
m_lobbyServiceData.State = LobbyServiceState.Fetched;

void OnCreatedRoom()
{
OnJoinedRoom();
RelayInterface.AllocateAsync(m_lobbyData.MaxPlayerCount, OnGotRelayAllocation);
}
void OnGotRelayAllocation(Allocation allocationID)

void OnGotRelayCode(string relayCode)
{
m_lobbyData.RelayCode = relayCode;
m_localLobby.RelayCode = relayCode;
RoomsQuery.Instance.BeginTracking(m_lobbyData.RoomID);
m_roomsContentHeartbeat.BeginTracking(m_lobbyData, m_localUser);
LobbyAsyncRequests.Instance.BeginTracking(m_localLobby.LobbyID);
m_roomsContentHeartbeat.BeginTracking(m_localLobby, m_localUser);
RoomsQuery.Instance.UpdatePlayerDataAsync(displayNameData, null);
LobbyAsyncRequests.Instance.UpdatePlayerDataAsync(displayNameData, null);
RoomsQuery.Instance.LeaveRoomAsync(m_lobbyData.RoomID, ResetLobbyData);
LobbyAsyncRequests.Instance.LeaveLobbyAsync(m_localLobby.LobbyID, ResetLocalLobby);
RoomsQuery.Instance.EndTracking();
LobbyAsyncRequests.Instance.EndTracking();
}
/// <summary>

{
SetGameState(GameState.JoinMenu);
}
/// <summary>
/// We do the Relay server Allocations right before we do the relay join allocations, as waiting too long will
/// cause the relay server to get cleand up by the service
/// </summary>
if (m_lobbyData.State == LobbyState.CountDown)
if (m_localLobby.State == LobbyState.CountDown)
m_lobbyData.CountDownTime = m_lobbyData.TargetEndTime.Subtract(DateTime.Now).Seconds;
m_lobbyData.State = LobbyState.CountDown;
RelayInterface.AllocateAsync(m_localLobby.MaxPlayerCount, OnGotRelayAllocation);
m_localLobby.CountDownTime = m_localLobby.TargetEndTime.Subtract(DateTime.Now).Seconds;
m_localLobby.State = LobbyState.CountDown;
m_LobbyReadyCheck.EndCheckingForReady();
while (m_lobbyData.CountDownTime > 0)
m_ReadyCheck.EndCheckingForReady();
while (m_localLobby.CountDownTime > 0)
if (m_lobbyData.State != LobbyState.CountDown)
if (m_localLobby.State != LobbyState.CountDown)
m_lobbyData.CountDownTime = m_lobbyData.TargetEndTime.Subtract(DateTime.Now).Seconds;
m_localLobby.CountDownTime = m_localLobby.TargetEndTime.Subtract(DateTime.Now).Seconds;
m_lobbyData.State = LobbyState.InGame;
RelayInterface.JoinAsync(m_lobbyData.RelayCode, OnJoinedGame);
m_localLobby.State = LobbyState.InGame;
RelayInterface.JoinAsync(m_localLobby.RelayCode, OnJoinedGame);
m_lobbyData.RelayServer = new ServerAddress(ip, port);
m_localLobby.RelayServer = new ServerAddress(ip, port);
m_lobbyData.State = LobbyState.Lobby;
m_lobbyData.CountDownTime = 0;
m_lobbyData.RelayServer = null;
m_localLobby.State = LobbyState.Lobby;
m_localLobby.CountDownTime = 0;
m_localLobby.RelayServer = null;
m_localLobby.RelayCode = null;
SetUserLobbyState();
}

m_localUser.UserStatus = UserStatus.Lobby;
if (m_localUser.IsHost)
m_LobbyReadyCheck.BeginCheckingForReady();
m_ReadyCheck.BeginCheckingForReady();
void ResetLobbyData()
void ResetLocalLobby()
m_lobbyData.CopyObserved(new LobbyInfo(), new Dictionary<string, LobbyUser>());
m_lobbyData.CountDownTime = 0;
m_lobbyData.RelayServer = null;
m_LobbyReadyCheck.EndCheckingForReady();
m_localLobby.CopyObserved(new LobbyInfo(), new Dictionary<string, LobbyUser>());
m_localLobby.CountDownTime = 0;
m_localLobby.RelayServer = null;
m_ReadyCheck.EndCheckingForReady();
}
void OnDestroy()

bool OnWantToQuit()
{
bool canQuit = string.IsNullOrEmpty(m_lobbyData?.RoomID);
bool canQuit = string.IsNullOrEmpty(m_localLobby?.LobbyID);
StartCoroutine(LeaveBeforeQuit());
return canQuit;
}

Locator.Get.Messenger.Unsubscribe(this);
if (!string.IsNullOrEmpty(m_lobbyData?.RoomID))
if (!string.IsNullOrEmpty(m_localLobby?.LobbyID))
RoomsQuery.Instance.LeaveRoomAsync(m_lobbyData?.RoomID, null);
m_lobbyData = null;
LobbyAsyncRequests.Instance.LeaveLobbyAsync(m_localLobby?.LobbyID, null);
m_localLobby = null;
}
}

IEnumerator LeaveBeforeQuit()
{
ForceLeaveAttempt();
// TEMP: Since we're temporarily (as of 6/31/21) deleting empty rooms when we leave them manually, we'll delay a bit to ensure that happens.
//yield return null;
yield return new WaitForSeconds(0.5f);

4
Assets/Scripts/Entities/LobbyServiceData.cs


}
}
Dictionary<string, LobbyData> m_currentLobbies = new Dictionary<string, LobbyData>();
Dictionary<string, LocalLobby> m_currentLobbies = new Dictionary<string, LocalLobby>();
public Dictionary<string, LobbyData> CurrentLobbies
public Dictionary<string, LocalLobby> CurrentLobbies
{
get { return m_currentLobbies; }
set

542
Assets/Scripts/Entities/LocalLobby.cs


using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace LobbyRelaySample
{
[Flags]
public enum LobbyState
{
Lobby = 1,
CountDown = 2,
InGame = 4
}
public struct LobbyInfo
{
public string RoomID { get; set; }
public string RoomCode { get; set; }
public string RelayCode { get; set; }
public string LobbyName { get; set; }
public bool Private { get; set; }
public int MaxPlayerCount { get; set; }
public LobbyState State { get; set; }
public long? AllPlayersReadyTime { get; set; }
public LobbyInfo(LobbyInfo existing)
{
RoomID = existing.RoomID;
RoomCode = existing.RoomCode;
RelayCode = existing.RelayCode;
LobbyName = existing.LobbyName;
Private = existing.Private;
MaxPlayerCount = existing.MaxPlayerCount;
State = existing.State;
AllPlayersReadyTime = existing.AllPlayersReadyTime;
}
public LobbyInfo(string roomCode)
{
RoomID = null;
RoomCode = roomCode;
RelayCode = null;
LobbyName = null;
Private = false;
MaxPlayerCount = -1;
State = LobbyState.Lobby;
AllPlayersReadyTime = null;
}
}
/// <summary>
/// The local lobby data that the game can observe
/// </summary>
[System.Serializable]
public class LobbyData : Observed<LobbyData>
{
Dictionary<string, LobbyUser> m_LobbyUsers = new Dictionary<string, LobbyUser>();
public Dictionary<string, LobbyUser> LobbyUsers => m_LobbyUsers;
#region LocalLobbyData
private LobbyInfo m_data;
public LobbyInfo Data
{
get { return new LobbyInfo(m_data); }
}
float m_CountDownTime;
public float CountDownTime
{
get { return m_CountDownTime; }
set
{
m_CountDownTime = value;
OnChanged(this);
}
}
DateTime m_TargetEndTime;
public DateTime TargetEndTime
{
get => m_TargetEndTime;
set
{
m_TargetEndTime = value;
OnChanged(this);
}
}
ServerAddress m_relayServer;
public ServerAddress RelayServer
{
get => m_relayServer;
set
{
m_relayServer = value;
OnChanged(this);
}
}
#endregion
public void AddPlayer(LobbyUser user)
{
if (m_LobbyUsers.ContainsKey(user.ID))
{
Debug.LogError($"Cant add player {user.DisplayName}({user.ID}) to room: {RoomID} twice");
return;
}
DoAddPlayer(user);
OnChanged(this);
}
private void DoAddPlayer(LobbyUser user)
{
m_LobbyUsers.Add(user.ID, user);
user.onChanged += OnChangedUser;
}
public void RemovePlayer(LobbyUser user)
{
DoRemoveUser(user);
OnChanged(this);
}
private void DoRemoveUser(LobbyUser user)
{
if (!m_LobbyUsers.ContainsKey(user.ID))
{
Debug.LogWarning($"Player {user.DisplayName}({user.ID}) does not exist in room: {RoomID}");
return;
}
m_LobbyUsers.Remove(user.ID);
user.onChanged -= OnChangedUser;
}
private void OnChangedUser(LobbyUser user)
{
OnChanged(this);
}
public string RoomID
{
get => m_data.RoomID;
set
{
m_data.RoomID = value;
OnChanged(this);
}
}
public string RoomCode
{
get => m_data.RoomCode;
set
{
m_data.RoomCode = value;
OnChanged(this);
}
}
public string RelayCode
{
get => m_data.RelayCode;
set
{
m_data.RelayCode = value;
OnChanged(this);
}
}
public string LobbyName
{
get => m_data.LobbyName;
set
{
m_data.LobbyName = value;
OnChanged(this);
}
}
public LobbyState State
{
get => m_data.State;
set
{
m_data.State = value;
OnChanged(this);
}
}
public bool Private
{
get => m_data.Private;
set
{
m_data.Private = value;
OnChanged(this);
}
}
public int PlayerCount => m_LobbyUsers.Count;
public int MaxPlayerCount
{
get => m_data.MaxPlayerCount;
set
{
m_data.MaxPlayerCount = value;
OnChanged(this);
}
}
public long? AllPlayersReadyTime => m_data.AllPlayersReadyTime;
/// <summary>
/// Checks if we have n players that have the Status.
/// -1 Count means you need all Lobbyusers
/// </summary>
/// <returns>True if enough players are of the input status.</returns>
public bool PlayersOfState(UserStatus status, int playersCount = -1)
{
var statePlayers = m_LobbyUsers.Values.Count(user => user.UserStatus == status);
if (playersCount < 0)
return statePlayers == m_LobbyUsers.Count;
return statePlayers == playersCount;
}
public void CopyObserved(LobbyInfo info, Dictionary<string, LobbyUser> oldUsers)
{
m_data = info;
if (oldUsers == null)
m_LobbyUsers = new Dictionary<string, LobbyUser>();
else
{
List<LobbyUser> toRemove = new List<LobbyUser>();
foreach (var user in m_LobbyUsers)
{
if (oldUsers.ContainsKey(user.Key))
user.Value.CopyObserved(oldUsers[user.Key]);
else
toRemove.Add(user.Value);
}
foreach (var remove in toRemove)
{
DoRemoveUser(remove);
}
foreach (var oldUser in oldUsers)
{
if (!m_LobbyUsers.ContainsKey(oldUser.Key))
DoAddPlayer(oldUser.Value);
}
}
OnChanged(this);
}
public override void CopyObserved(LobbyData oldObserved)
{
CopyObserved(oldObserved.Data, oldObserved.m_LobbyUsers);
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace LobbyRelaySample
{
[Flags]
public enum LobbyState
{
Lobby = 1,
CountDown = 2,
InGame = 4
}
public struct LobbyInfo
{
public string LobbyID { get; set; }
public string LobbyCode { get; set; }
public string RelayCode { get; set; }
public string LobbyName { get; set; }
public bool Private { get; set; }
public int MaxPlayerCount { get; set; }
public LobbyState State { get; set; }
public long? AllPlayersReadyTime { get; set; }
public LobbyInfo(LobbyInfo existing)
{
LobbyID = existing.LobbyID;
LobbyCode = existing.LobbyCode;
RelayCode = existing.RelayCode;
LobbyName = existing.LobbyName;
Private = existing.Private;
MaxPlayerCount = existing.MaxPlayerCount;
State = existing.State;
AllPlayersReadyTime = existing.AllPlayersReadyTime;
}
public LobbyInfo(string lobbyCode)
{
LobbyID = null;
LobbyCode = lobbyCode;
RelayCode = null;
LobbyName = null;
Private = false;
MaxPlayerCount = -1;
State = LobbyState.Lobby;
AllPlayersReadyTime = null;
}
}
/// <summary>
/// The local lobby data that the game can observe
/// </summary>
[System.Serializable]
public class LocalLobby : Observed<LocalLobby>
{
Dictionary<string, LobbyUser> m_LobbyUsers = new Dictionary<string, LobbyUser>();
public Dictionary<string, LobbyUser> LobbyUsers => m_LobbyUsers;
#region LocalLobbyData
private LobbyInfo m_data;
public LobbyInfo Data
{
get { return new LobbyInfo(m_data); }
}
float m_CountDownTime;
public float CountDownTime
{
get { return m_CountDownTime; }
set
{
m_CountDownTime = value;
OnChanged(this);
}
}
DateTime m_TargetEndTime;
public DateTime TargetEndTime
{
get => m_TargetEndTime;
set
{
m_TargetEndTime = value;
OnChanged(this);
}
}
ServerAddress m_relayServer;
public ServerAddress RelayServer
{
get => m_relayServer;
set
{
m_relayServer = value;
OnChanged(this);
}
}
#endregion
public void AddPlayer(LobbyUser user)
{
if (m_LobbyUsers.ContainsKey(user.ID))
{
Debug.LogError($"Cant add player {user.DisplayName}({user.ID}) to lobby: {LobbyID} twice");
return;
}
DoAddPlayer(user);
OnChanged(this);
}
private void DoAddPlayer(LobbyUser user)
{
m_LobbyUsers.Add(user.ID, user);
user.onChanged += OnChangedUser;
}
public void RemovePlayer(LobbyUser user)
{
DoRemoveUser(user);
OnChanged(this);
}
private void DoRemoveUser(LobbyUser user)
{
if (!m_LobbyUsers.ContainsKey(user.ID))
{
Debug.LogWarning($"Player {user.DisplayName}({user.ID}) does not exist in lobby: {LobbyID}");
return;
}
m_LobbyUsers.Remove(user.ID);
user.onChanged -= OnChangedUser;
}
private void OnChangedUser(LobbyUser user)
{
OnChanged(this);
}
public string LobbyID
{
get => m_data.LobbyID;
set
{
m_data.LobbyID = value;
OnChanged(this);
}
}
public string LobbyCode
{
get => m_data.LobbyCode;
set
{
m_data.LobbyCode = value;
OnChanged(this);
}
}
public string RelayCode
{
get => m_data.RelayCode;
set
{
m_data.RelayCode = value;
OnChanged(this);
}
}
public string LobbyName
{
get => m_data.LobbyName;
set
{
m_data.LobbyName = value;
OnChanged(this);
}
}
public LobbyState State
{
get => m_data.State;
set
{
m_data.State = value;
OnChanged(this);
}
}
public bool Private
{
get => m_data.Private;
set
{
m_data.Private = value;
OnChanged(this);
}
}
public int PlayerCount => m_LobbyUsers.Count;
public int MaxPlayerCount
{
get => m_data.MaxPlayerCount;
set
{
m_data.MaxPlayerCount = value;
OnChanged(this);
}
}
public long? AllPlayersReadyTime => m_data.AllPlayersReadyTime;
/// <summary>
/// Checks if we have n players that have the Status.
/// -1 Count means you need all Lobbyusers
/// </summary>
/// <returns>True if enough players are of the input status.</returns>
public bool PlayersOfState(UserStatus status, int playersCount = -1)
{
var statePlayers = m_LobbyUsers.Values.Count(user => user.UserStatus == status);
if (playersCount < 0)
return statePlayers == m_LobbyUsers.Count;
return statePlayers == playersCount;
}
public void CopyObserved(LobbyInfo info, Dictionary<string, LobbyUser> oldUsers)
{
m_data = info;
if (oldUsers == null)
m_LobbyUsers = new Dictionary<string, LobbyUser>();
else
{
List<LobbyUser> toRemove = new List<LobbyUser>();
foreach (var user in m_LobbyUsers)
{
if (oldUsers.ContainsKey(user.Key))
user.Value.CopyObserved(oldUsers[user.Key]);
else
toRemove.Add(user.Value);
}
foreach (var remove in toRemove)
{
DoRemoveUser(remove);
}
foreach (var oldUser in oldUsers)
{
if (!m_LobbyUsers.ContainsKey(oldUser.Key))
DoAddPlayer(oldUser.Value);
}
}
OnChanged(this);
}
public override void CopyObserved(LocalLobby oldObserved)
{
CopyObserved(oldObserved.Data, oldObserved.m_LobbyUsers);
}
}
}

24
Assets/Scripts/Tests/Editor/LobbyTests.cs


{
public class LobbyTests
{
LobbyData m_LobbyData;
LocalLobby m_LocalLobby;
const int k_TestUserCount = 3;

m_LobbyData = new LobbyData();
m_LocalLobby = new LocalLobby();
m_LobbyData.AddPlayer(new LobbyUser
m_LocalLobby.AddPlayer(new LobbyUser
{
ID = i.ToString()
});

[Test]
public void LobbyPlayerStateTest()
{
Assert.False(m_LobbyData.PlayersOfState(UserStatus.Ready));
Assert.False(m_LocalLobby.PlayersOfState(UserStatus.Ready));
m_LobbyData.LobbyUsers["0"].UserStatus = UserStatus.Ready;
Assert.False(m_LobbyData.PlayersOfState(UserStatus.Ready));
Assert.True(m_LobbyData.PlayersOfState(UserStatus.Ready, 1));
m_LocalLobby.LobbyUsers["0"].UserStatus = UserStatus.Ready;
Assert.False(m_LocalLobby.PlayersOfState(UserStatus.Ready));
Assert.True(m_LocalLobby.PlayersOfState(UserStatus.Ready, 1));
m_LobbyData.LobbyUsers["1"].UserStatus = UserStatus.Ready;
Assert.False(m_LobbyData.PlayersOfState(UserStatus.Ready));
Assert.True(m_LobbyData.PlayersOfState(UserStatus.Ready, 2));
m_LocalLobby.LobbyUsers["1"].UserStatus = UserStatus.Ready;
Assert.False(m_LocalLobby.PlayersOfState(UserStatus.Ready));
Assert.True(m_LocalLobby.PlayersOfState(UserStatus.Ready, 2));
m_LobbyData.LobbyUsers["2"].UserStatus = UserStatus.Ready;
m_LocalLobby.LobbyUsers["2"].UserStatus = UserStatus.Ready;
Assert.True(m_LobbyData.PlayersOfState(UserStatus.Ready));
Assert.True(m_LocalLobby.PlayersOfState(UserStatus.Ready));
}
}
}

4
Assets/Scripts/UI/CountdownUI.cs


namespace LobbyRelaySample.UI
{
public class CountdownUI : ObserverPanel<LobbyData>
public class CountdownUI : ObserverPanel<LocalLobby>
public override void ObservedUpdated(LobbyData observed)
public override void ObservedUpdated(LocalLobby observed)
{
if (observed.CountDownTime <= 0)
return;

4
Assets/Scripts/UI/CreateMenuUI.cs


public class CreateMenuUI : UIPanelBase
{
[SerializeField]
LobbyData m_ServerRequestData = new LobbyData { LobbyName = "New Lobby", MaxPlayerCount = 4 };
LocalLobby m_ServerRequestData = new LocalLobby { LobbyName = "New Lobby", MaxPlayerCount = 4 };
public void SetServerName(string serverName)
{

public void OnCreatePressed()
{
Locator.Get.Messenger.OnReceiveMessage(MessageType.CreateRoomRequest, m_ServerRequestData);
Locator.Get.Messenger.OnReceiveMessage(MessageType.CreateLobbyRequest, m_ServerRequestData);
}
}
}

2
Assets/Scripts/UI/JoinCreateRoomUI.cs


if (observed.State == GameState.JoinMenu)
{
Show();
Locator.Get.Messenger.OnReceiveMessage(MessageType.QueryRooms, null);
Locator.Get.Messenger.OnReceiveMessage(MessageType.QueryLobbies, null);
}
else
{

37
Assets/Scripts/UI/JoinMenuUI.cs


/// Key: Lobby ID, Value Lobby UI
/// </summary>
Dictionary<string, LobbyButtonUI> m_LobbyButtons = new Dictionary<string, LobbyButtonUI>();
Dictionary<string, LobbyData> m_LobbyData = new Dictionary<string, LobbyData>();
string m_targetRoomID;
string m_targetRoomJoinCode;
Dictionary<string, LocalLobby> m_LocalLobby = new Dictionary<string, LocalLobby>();
LobbyInfo m_lobbyDataSelected;
LobbyInfo m_localLobbySelected;
public void LobbyButtonSelected(LobbyData lobby)
public void LobbyButtonSelected(LocalLobby lobby)
m_lobbyDataSelected = lobby.Data;
m_localLobbySelected = lobby.Data;
m_lobbyDataSelected = new LobbyInfo(newCode.ToUpper());
m_localLobbySelected = new LobbyInfo(newCode.ToUpper());
Locator.Get.Messenger.OnReceiveMessage(MessageType.JoinRoomRequest, m_lobbyDataSelected);
Locator.Get.Messenger.OnReceiveMessage(MessageType.JoinLobbyRequest, m_localLobbySelected);
Locator.Get.Messenger.OnReceiveMessage(MessageType.QueryRooms, null);
Locator.Get.Messenger.OnReceiveMessage(MessageType.QueryLobbies, null);
}
public override void ObservedUpdated(LobbyServiceData observed)

}
foreach (string key in previousKeys) // Need to remove any lobbies from the list that no longer exist.
RemoveLobbyButton(m_LobbyData[key]);
RemoveLobbyButton(m_LocalLobby[key]);
bool CanDisplay(LobbyData lobby)
bool CanDisplay(LocalLobby lobby)
{
return lobby.Data.State == LobbyState.Lobby && !lobby.Private;
}

/// </summary>
void AddNewLobbyButton(string roomCode, LobbyData lobby)
void AddNewLobbyButton(string roomCode, LocalLobby lobby)
lobbyButtonInstance.GetComponent<LobbyDataObserver>().BeginObserving(lobby);
lobbyButtonInstance.GetComponent<LocalLobbyObserver>().BeginObserving(lobby);
m_LobbyData.Add(roomCode, lobby);
m_LocalLobby.Add(roomCode, lobby);
void UpdateLobbyButton(string roomCode, LobbyData lobby)
void UpdateLobbyButton(string roomCode, LocalLobby lobby)
void RemoveLobbyButton(LobbyData lobby)
void RemoveLobbyButton(LocalLobby lobby)
var lobbyID = lobby.RoomID;
var lobbyID = lobby.LobbyID;
lobbyButton.GetComponent<LobbyDataObserver>().EndObserving();
lobbyButton.GetComponent<LocalLobbyObserver>().EndObserving();
m_LobbyData.Remove(lobbyID);
m_LocalLobby.Remove(lobbyID);
Destroy(lobbyButton.gameObject);
}
}

12
Assets/Scripts/UI/LobbyButtonUI.cs


namespace LobbyRelaySample.UI
{
[RequireComponent(typeof(LobbyDataObserver))]
[RequireComponent(typeof(LocalLobbyObserver))]
public class LobbyButtonUI : MonoBehaviour
{
[SerializeField]

/// <summary>
/// Subscribed to on instantiation to pass our lobby data back
/// </summary>
public UnityEvent<LobbyData> onLobbyPressed;
LobbyDataObserver m_DataObserver;
public UnityEvent<LocalLobby> onLobbyPressed;
LocalLobbyObserver m_DataObserver;
m_DataObserver = GetComponent<LobbyDataObserver>();
m_DataObserver = GetComponent<LocalLobbyObserver>();
}
/// <summary>

onLobbyPressed?.Invoke(m_DataObserver.observed);
}
public void UpdateLobby(LobbyData lobby)
public void UpdateLobby(LocalLobby lobby)
public void OnRoomUpdated(LobbyData data)
public void OnRoomUpdated(LocalLobby data)
{
lobbyNameText.SetText(data.LobbyName);
lobbyCountText.SetText($"{data.PlayerCount}/{data.MaxPlayerCount}");

4
Assets/Scripts/UI/LobbyStateVisibilityUI.cs


namespace LobbyRelaySample.UI
{
public class LobbyStateVisibilityUI : ObserverPanel<LobbyData>
public class LobbyStateVisibilityUI : ObserverPanel<LocalLobby>
public override void ObservedUpdated(LobbyData observed)
public override void ObservedUpdated(LocalLobby observed)
{
if (m_ShowThisWhen.HasFlag(observed.State))
Show();

6
Assets/Scripts/UI/LobbyUsersUI.cs


/// <summary>
/// Watches for changes in the Lobby's player List
/// </summary>
[RequireComponent(typeof(LobbyDataObserver))]
public class LobbyUsersUI : ObserverPanel<LobbyData>
[RequireComponent(typeof(LocalLobbyObserver))]
public class LobbyUsersUI : ObserverPanel<LocalLobby>
{
[SerializeField]
List<LobbyUserCardUI> m_PlayerCardSlots = new List<LobbyUserCardUI>();

/// When the observed data updates, we need to detect changes to the list of players.
/// </summary>
public override void ObservedUpdated(LobbyData observed)
public override void ObservedUpdated(LocalLobby observed)
{
for (int id = m_CurrentUsers.Count - 1; id >= 0; id--) // We might remove users if they aren't in the new data, so iterate backwards.
{

4
Assets/Scripts/UI/RelayCodeUI.cs


/// <summary>
/// Read Only input field (for copy/paste reasons) Watches for the changes in the lobby's Relay Code
/// </summary>
public class RelayCodeUI : ObserverPanel<LobbyData>
public class RelayCodeUI : ObserverPanel<LocalLobby>
public override void ObservedUpdated(LobbyData observed)
public override void ObservedUpdated(LocalLobby observed)
{
if (!string.IsNullOrEmpty(observed.RelayCode))
{

8
Assets/Scripts/UI/RoomCodeUI.cs


/// <summary>
/// Read Only input field (for copy/paste reasons) Watches for the changes in the lobby's Room Code
/// </summary>
public class RoomCodeUI : ObserverPanel<LobbyData>
public class RoomCodeUI : ObserverPanel<LocalLobby>
public override void ObservedUpdated(LobbyData observed)
public override void ObservedUpdated(LocalLobby observed)
if (!string.IsNullOrEmpty(observed.RoomCode))
if (!string.IsNullOrEmpty(observed.LobbyCode))
roomCodeText.text = observed.RoomCode;
roomCodeText.text = observed.LobbyCode;
Show();
}
else

4
Assets/Scripts/UI/ServerAddressUI.cs


namespace LobbyRelaySample.UI
{
public class ServerAddressUI : ObserverPanel<LobbyData>
public class ServerAddressUI : ObserverPanel<LocalLobby>
public override void ObservedUpdated(LobbyData observed)
public override void ObservedUpdated(LocalLobby observed)
{
m_IPAddressText.SetText(observed.RelayServer?.ToString());
}

4
Assets/Scripts/UI/ServerNameUI.cs


namespace LobbyRelaySample.UI
{
public class ServerNameUI : ObserverPanel<LobbyData>
public class ServerNameUI : ObserverPanel<LocalLobby>
public override void ObservedUpdated(LobbyData observed)
public override void ObservedUpdated(LocalLobby observed)
{
m_ServerNameText.SetText(observed.LobbyName);
}

4
Assets/TempDeleteAllRooms.cs


public void OnButton()
{
LobbyRelaySample.Lobby.RoomsInterface.QueryAllRoomsAsync((qr) => { DoDeletes(qr); });
LobbyRelaySample.Lobby.LobbyAPIInterface.QueryAllLobbiesAsync((qr) => { DoDeletes(qr); });
}
private void DoDeletes(Unity.Services.Rooms.Response<Unity.Services.Rooms.Models.QueryResponse> response)

{
foreach (var room in rooms)
{
LobbyRelaySample.Lobby.RoomsInterface.DeleteRoomAsync(room.Id, null); // The onComplete callback isn't called in some error cases, e.g. a 403 when we don't have permissions, so don't block on it.
LobbyRelaySample.Lobby.LobbyAPIInterface.DeleteLobbyAsync(room.Id, null); // The onComplete callback isn't called in some error cases, e.g. a 403 when we don't have permissions, so don't block on it.
yield return new WaitForSeconds(1); // We need to wait a little to avoid 429's, but we might not run an onComplete depending on how the delete call fails.
}
}

200
Assets/Scripts/Infrastructure/Locator.cs


using LobbyRelaySample.Auth;
using System;
using System.Collections.Generic;
namespace LobbyRelaySample
{
/// <summary>
/// Allows Located services to transfer data to their replacements if needed.
/// </summary>
/// <typeparam name="T">The base interface type you want to Provide.</typeparam>
public interface IProvidable<T>
{
void OnReProvided(T previousProvider);
}
/// <summary>
/// Base Locator behavior, without static access.
/// </summary>
public class LocatorBase
{
private Dictionary<Type, object> m_provided = new Dictionary<Type, object>();
/// <summary>
/// On construction, we can prepare default implementations of any services we expect to be required. This way, if for some reason the actual implementations
/// are never Provided (e.g. for tests), nothing will break.
/// </summary>
public LocatorBase()
{
Provide(new Messenger());
Provide(new UpdateSlowNoop());
Provide(new IdentityNoop());
FinishConstruction();
}
protected virtual void FinishConstruction() { }
/// <summary>
/// Call this to indicate that something is available for global access.
/// </summary>
private void ProvideAny<T>(T instance) where T : IProvidable<T>
{
Type type = typeof(T);
if (m_provided.ContainsKey(type))
{
var previousProvision = (T)m_provided[type];
instance.OnReProvided(previousProvision);
m_provided.Remove(type);
}
m_provided.Add(type, instance);
}
/// <summary>
/// If a T has previously been Provided, this will retrieve it. Else, null is returned.
/// </summary>
private T Locate<T>() where T : class
{
Type type = typeof(T);
if (!m_provided.ContainsKey(type))
return null;
return m_provided[type] as T;
}
// To limit global access to only components that should have it, and to reduce programmer error, we'll declare explicit flavors of Provide and getters for them.
public IMessenger Messenger => Locate<IMessenger>();
public void Provide(IMessenger messenger) { ProvideAny(messenger); }
public IUpdateSlow UpdateSlow => Locate<IUpdateSlow>();
public void Provide(IUpdateSlow updateSlow) { ProvideAny(updateSlow); }
public IIdentity Identity => Locate<IIdentity>();
public void Provide(IIdentity identity) { ProvideAny(identity); }
// As you add more Provided types, be sure their default implementations are included in the constructor.
}
/// <summary>
/// Anything which provides itself to a Locator can then be globally accessed. This should be a single access point for things that *want* to be singleton (that is,
/// when they want to be available for use by arbitrary, unknown clients) but might not always be available or might need alternate flavors for tests, logging, etc.
/// </summary>
public class Locator : LocatorBase
{
private static Locator s_instance;
public static Locator Get
{
get
{
if (s_instance == null)
s_instance = new Locator();
return s_instance;
}
}
protected override void FinishConstruction()
{
s_instance = this;
}
}
using LobbyRelaySample.Auth;
using System;
using System.Collections.Generic;
namespace LobbyRelaySample
{
/// <summary>
/// Allows Located services to transfer data to their replacements if needed.
/// </summary>
/// <typeparam name="T">The base interface type you want to Provide.</typeparam>
public interface IProvidable<T>
{
void OnReProvided(T previousProvider);
}
/// <summary>
/// Base Locator behavior, without static access.
/// </summary>
public class LocatorBase
{
private Dictionary<Type, object> m_provided = new Dictionary<Type, object>();
/// <summary>
/// On construction, we can prepare default implementations of any services we expect to be required. This way, if for some reason the actual implementations
/// are never Provided (e.g. for tests), nothing will break.
/// </summary>
public LocatorBase()
{
Provide(new Messenger());
Provide(new UpdateSlowNoop());
Provide(new IdentityNoop());
FinishConstruction();
}
protected virtual void FinishConstruction() { }
/// <summary>
/// Call this to indicate that something is available for global access.
/// </summary>
private void ProvideAny<T>(T instance) where T : IProvidable<T>
{
Type type = typeof(T);
if (m_provided.ContainsKey(type))
{
var previousProvision = (T)m_provided[type];
instance.OnReProvided(previousProvision);
m_provided.Remove(type);
}
m_provided.Add(type, instance);
}
/// <summary>
/// If a T has previously been Provided, this will retrieve it. Else, null is returned.
/// </summary>
private T Locate<T>() where T : class
{
Type type = typeof(T);
if (!m_provided.ContainsKey(type))
return null;
return m_provided[type] as T;
}
// To limit global access to only components that should have it, and to reduce programmer error, we'll declare explicit flavors of Provide and getters for them.
public IMessenger Messenger => Locate<IMessenger>();
public void Provide(IMessenger messenger) { ProvideAny(messenger); }
public IUpdateSlow UpdateSlow => Locate<IUpdateSlow>();
public void Provide(IUpdateSlow updateSlow) { ProvideAny(updateSlow); }
public IIdentity Identity => Locate<IIdentity>();
public void Provide(IIdentity identity) { ProvideAny(identity); }
// As you add more Provided types, be sure their default implementations are included in the constructor.
}
/// <summary>
/// Anything which provides itself to a Locator can then be globally accessed. This should be a single access point for things that *want* to be singleton (that is,
/// when they want to be available for use by arbitrary, unknown clients) but might not always be available or might need alternate flavors for tests, logging, etc.
/// </summary>
public class Locator : LocatorBase
{
private static Locator s_instance;
public static Locator Get
{
get
{
if (s_instance == null)
s_instance = new Locator();
return s_instance;
}
}
protected override void FinishConstruction()
{
s_instance = this;
}
}
}

126
Assets/Scripts/Infrastructure/LogHandler.cs


using System;
using UnityEngine;
using Object = UnityEngine.Object;
namespace LobbyRelaySample
{
public enum LogMode
{
Critical, // Errors only.
Warnings, // Errors and Warnings
Verbose // Everything
}
/// <summary>
/// Overrides the Default Unity Logging with our own
/// </summary>
public class LogHandler : ILogHandler
{
public LogMode mode = LogMode.Critical;
static LogHandler s_instance;
ILogHandler m_DefaultLogHandler = Debug.unityLogger.logHandler; //Store the unity default logger to print to console.
public static LogHandler Get()
{
if (s_instance != null) return s_instance;
s_instance = new LogHandler();
Debug.unityLogger.logHandler = s_instance;
return s_instance;
}
public void LogFormat(LogType logType, Object context, string format, params object[] args)
{
if (logType == LogType.Exception) // Exceptions are captured by LogException?
return;