浏览代码

Testing merge on this branch

/main/staging/host_icon
当前提交
4e514938
共有 33 个文件被更改,包括 615 次插入815 次删除
  1. 2
      Assets/Prefabs/GameManager.prefab
  2. 232
      Assets/Scenes/mainScene.unity
  3. 12
      Assets/Scripts/Auth/Identity.cs
  4. 6
      Assets/Scripts/Auth/NameGenerator.cs
  5. 2
      Assets/Scripts/Auth/SubIdentity_Authentication.cs
  6. 13
      Assets/Scripts/Game/LobbyServiceData.cs
  7. 9
      Assets/Scripts/Game/LobbyUser.cs
  8. 6
      Assets/Scripts/Game/LocalGameState.cs
  9. 1
      Assets/Scripts/Game/LocalLobby.cs
  10. 5
      Assets/Scripts/Game/ServerAddress.cs
  11. 48
      Assets/Scripts/Infrastructure/Locator.cs
  12. 6
      Assets/Scripts/Infrastructure/LogHandler.cs
  13. 82
      Assets/Scripts/Infrastructure/Messenger.cs
  14. 7
      Assets/Scripts/Infrastructure/Observed.cs
  15. 3
      Assets/Scripts/Infrastructure/ObserverBehaviour.cs
  16. 37
      Assets/Scripts/Infrastructure/UpdateSlow.cs
  17. 7
      Assets/Scripts/Lobby/LobbyAPIInterface.cs
  18. 14
      Assets/Scripts/Lobby/LobbyAsyncRequests.cs
  19. 19
      Assets/Scripts/Lobby/LobbyContentHeartbeat.cs
  20. 8
      Assets/Scripts/Lobby/ToLocalLobby.cs
  21. 16
      Assets/Scripts/Relay/RelayUtpClient.cs
  22. 4
      Assets/Scripts/Relay/RelayUtpHost.cs
  23. 17
      Assets/Scripts/Relay/RelayUtpSetup.cs
  24. 6
      Assets/Scripts/Tests/PlayMode/RelayRoundTripTests.cs
  25. 6
      Assets/Scripts/UI/SpinnerUI.cs
  26. 323
      Assets/Scripts/Game/GameManager.cs
  27. 106
      Assets/Scripts/Relay/RelayAPIInterface.cs
  28. 328
      Assets/Scripts/Game/GameStateManager.cs
  29. 105
      Assets/Scripts/Relay/RelayInterface.cs
  30. 0
      /Assets/Prefabs/GameManager.prefab.meta
  31. 0
      /Assets/Prefabs/GameManager.prefab
  32. 0
      /Assets/Scripts/Game/GameManager.cs.meta
  33. 0
      /Assets/Scripts/Relay/RelayAPIInterface.cs.meta

2
Assets/Prefabs/GameManager.prefab


- component: {fileID: 7716713811812636910}
- component: {fileID: 5193415626965589893}
m_Layer: 0
m_Name: GameStateManager
m_Name: GameManager
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0

232
Assets/Scenes/mainScene.unity


propertyPath: onValueChanged.m_PersistentCalls.m_Calls.Array.data[0].m_Arguments.m_IntArgument
value: 3
objectReference: {fileID: 0}
- target: {fileID: 302677088753936851, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 302677088753936851, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 302677088753936851, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 302677088753936851, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 302677088753936851, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 302677088753936851, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 326167899787007624, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0

- target: {fileID: 901738327287436208, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 941366801973286209, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_MinHeight
value: 40
objectReference: {fileID: 0}
- target: {fileID: 941366801973286209, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_PreferredWidth
value: 40
objectReference: {fileID: 0}
- target: {fileID: 941366801973286209, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_PreferredHeight
value: 40
objectReference: {fileID: 0}
- target: {fileID: 1193216679110899971, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_Alpha

propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2229739206168864254, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2229739206168864254, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2229739206168864254, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2229739206168864254, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2229739206168864254, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2229739206168864254, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2237378151666164065, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0

- target: {fileID: 2442889484872886228, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_fontColor32.rgba
value: 4291809231
objectReference: {fileID: 0}
- target: {fileID: 2491863691556184441, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2491863691556184441, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2491863691556184441, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2491863691556184441, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2491863691556184441, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2491863691556184441, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2545639037669962845, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y

propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4059480599373638996, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4059480599373638996, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4059480599373638996, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4059480599373638996, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4059480599373638996, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4059480599373638996, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4129102368663917704, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: -15.765015

objectReference: {fileID: 0}
- target: {fileID: 4822032080772604407, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: -0.000035897956
value: -0.2306938
objectReference: {fileID: 0}
- target: {fileID: 4824240073023402834, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y

propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5451244976481318432, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5451244976481318432, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5451244976481318432, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5451244976481318432, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5451244976481318432, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5451244976481318432, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5474746901992278031, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.x
value: 0

- target: {fileID: 5848337315848724223, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_fontSize
value: 22.05
objectReference: {fileID: 0}
- target: {fileID: 5864947474559023629, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5864947474559023629, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5864947474559023629, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5864947474559023629, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5864947474559023629, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5864947474559023629, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5880123119007547921, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.x

propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7138194943800857534, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7138194943800857534, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7138194943800857534, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7138194943800857534, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7138194943800857534, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7138194943800857534, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7198558056629795013, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_fontSize
value: 35.8

value: 0
objectReference: {fileID: 0}
- target: {fileID: 7327858540175541481, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7340838299578709300, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7340838299578709300, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7340838299578709300, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7340838299578709300, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7340838299578709300, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7340838299578709300, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}

propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8776735779140034451, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8776735779140034451, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8776735779140034451, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8776735779140034451, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8776735779140034451, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8776735779140034451, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8889734615304832804, guid: f1d618bdc6f1813449d428126e640aa5, type: 3}
propertyPath: m_AnchorMax.y
value: 1

m_Modifications:
- target: {fileID: 7716713811812636896, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_Name
value: GameStateManager
value: GameManager
objectReference: {fileID: 0}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_logMode

12
Assets/Scripts/Auth/Identity.cs


namespace LobbyRelaySample.Auth
{
/// <summary>
/// Each context will have its own identity needs, so we'll allow each to define whatever parameters it needs.
/// Represents some provider of credentials.
/// Each provider will have its own identity needs, so we'll allow each to define whatever parameters it needs.
/// Anything that accesses the contents should know what it's looking for.
/// </summary>
public class SubIdentity : Observed<SubIdentity>

}
/// <summary>
/// Our internal representation of a player, wrapping the data required for interfacing with the identities of that player in the services.
/// One will be created for the local player, as well as for each other member of the lobby.
/// Our internal representation of the local player's credentials, wrapping the data required for interfacing with the identities of that player in the services.
/// (In use here, it just wraps Auth, but it can be used to combine multiple sets of credentials into one concept of a player.)
public Identity()
{
m_subIdentities.Add(IIdentityType.Local, new SubIdentity());
m_subIdentities.Add(IIdentityType.Auth, new SubIdentity_Authentication());
}
public Identity(Action callbackOnAuthLogin)
{
m_subIdentities.Add(IIdentityType.Local, new SubIdentity());

6
Assets/Scripts/Auth/NameGenerator.cs


namespace LobbyRelaySample
{
/// <summary>
/// Just for fun, give a cute default player name if no name is provided.
/// Just for fun, give a cute default player name if no name is provided, based on a hash of their anonymous ID.
/// </summary>
public static class NameGenerator
{

else if (word == 1)
nameOutput.Append("Bear");
else if (word == 2)
nameOutput.Append("Cow");
nameOutput.Append("Crow");
else if (word == 3)
nameOutput.Append("Dog");
else if (word == 4)

else if (word == 15)
nameOutput.Append("Puffin");
else if (word == 16)
nameOutput.Append("Raven");
nameOutput.Append("Rabbit");
else if (word == 17)
nameOutput.Append("Snake");
else if (word == 18)

2
Assets/Scripts/Auth/SubIdentity_Authentication.cs


AuthenticationService.Instance.SignedOut += OnSignInChange;
if (!AuthenticationService.Instance.IsSignedIn)
await AuthenticationService.Instance.SignInAnonymouslyAsync(); // Note: We don't want to sign out later, since that changes the UAS anonymous token, which would prevent the player from exiting lobbies they're already in.
await AuthenticationService.Instance.SignInAnonymouslyAsync(); // Don't sign out later, since that changes the anonymous token, which would prevent the player from exiting lobbies they're already in.
onSigninComplete?.Invoke();
}

13
Assets/Scripts/Game/LobbyServiceData.cs


namespace LobbyRelaySample
{
public enum LobbyServiceState
/// <summary>
/// Used when displaying the lobby list, to indicate when we are awaiting an updated lobby query.
/// </summary>
public enum LobbyQueryState
{
Empty,
Fetching,

[System.Serializable]
public class LobbyServiceData : Observed<LobbyServiceData>
{
LobbyServiceState m_CurrentState = LobbyServiceState.Empty;
LobbyQueryState m_CurrentState = LobbyQueryState.Empty;
public LobbyServiceState State
public LobbyQueryState State
{
get { return m_CurrentState; }
set

Dictionary<string, LocalLobby> m_currentLobbies = new Dictionary<string, LocalLobby>();
/// <summary>
/// Will only trigger if the dictionary is set wholesale. Changes in the size, or contents will not trigger OnChanged
/// string is lobby ID, Key is the Lobby data representation of it
/// Maps from a lobby's ID to the local representation of it. This allows us to remember which remote lobbies are which LocalLobbies.
/// Will only trigger if the dictionary is set wholesale. Changes in the size or contents will not trigger OnChanged.
/// </summary>
public Dictionary<string, LocalLobby> CurrentLobbies
{

9
Assets/Scripts/Game/LobbyUser.cs


get => m_userStatus;
set
{
m_userStatus = value;
m_lastChanged = UserMembers.UserStatus;
OnChanged(this);
if (m_userStatus != value)
{
m_userStatus = value;
m_lastChanged = UserMembers.UserStatus;
OnChanged(this);
}
}
}

6
Assets/Scripts/Game/LocalGameState.cs


namespace LobbyRelaySample
{
/// <summary>
/// Current state of the local Game.
/// Set as a flag to allow for the unity inspector to select multiples for various UI features.
/// Current state of the local game.
/// Set as a flag to allow for the Inspector to select multiple valid states for various UI features.
/// </summary>
[Flags]
public enum GameState

}
/// <summary>
/// Awaits player input to change the local game Data
/// Awaits player input to change the local game data.
/// </summary>
[System.Serializable]
public class LocalGameState : Observed<LocalGameState>

1
Assets/Scripts/Game/LocalLobby.cs


/// <summary>
/// A local wrapper around a lobby's remote data, with additional functionality for providing that data to UI elements and tracking local player objects.
/// (The way that the Lobby service handles its data doesn't necessarily match our needs, so we need to map from that to this LocalLobby for use in the sample code.)
/// </summary>
[System.Serializable]
public class LocalLobby : Observed<LocalLobby>

5
Assets/Scripts/Game/ServerAddress.cs


using UnityEngine;
/// This is where your netcode would go, if you had it.
/// Just for displaying the anonymous Relay IP.
/// </summary>
public class ServerAddress
{

{
m_IP = ip;
m_Port = port;
Debug.Log($"Connected To Game Server: {ip}:{port}");
}
public override string ToString()

48
Assets/Scripts/Infrastructure/Locator.cs


namespace LobbyRelaySample
{
/// <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;
}
}
/// <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 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;
}
}
}

6
Assets/Scripts/Infrastructure/LogHandler.cs


{
public enum LogMode
{
Critical, // Errors only.
Critical, // Errors only
Warnings, // Errors and Warnings
Verbose // Everything
}

public LogMode mode = LogMode.Critical;
static LogHandler s_instance;
ILogHandler m_DefaultLogHandler = Debug.unityLogger.logHandler; //Store the unity default logger to print to console.
ILogHandler m_DefaultLogHandler = Debug.unityLogger.logHandler; // Store the default logger that prints to console.
public static LogHandler Get()
{

public void LogFormat(LogType logType, Object context, string format, params object[] args)
{
if (logType == LogType.Exception) // Exceptions are captured by LogException?
if (logType == LogType.Exception) // Exceptions are captured by LogException and should always be logged.
return;
if (logType == LogType.Error || logType == LogType.Assert)

82
Assets/Scripts/Infrastructure/Messenger.cs


namespace LobbyRelaySample
{
/// <summary>
/// Ensure that message contents are obvious but not dependent on spelling strings correctly.
/// </summary>
public enum MessageType
{
// These are assigned arbitrary explicit values so that if a MessageType is serialized and more enum values are later inserted/removed, the serialized values need not be reassigned.
// (If you want to remove a message, make sure it isn't serialized somewhere first.)
None = 0,
RenameRequest = 1,
JoinLobbyRequest = 2,
CreateLobbyRequest = 3,
QueryLobbies = 4,
ChangeGameState = 5,
LobbyUserStatus = 6,
UserSetEmote = 7,
EndGame = 8,
StartCountdown = 9,
CancelCountdown = 10,
ConfirmInGameState = 11,
}
/// <summary>
/// Something that wants to subscribe to messages from arbitrary, unknown senders.
/// </summary>
public interface IReceiveMessages
{
void OnReceiveMessage(MessageType type, object msg);
}
/// <summary>
/// Something to which IReceiveMessages can send/subscribe for arbitrary messages.
/// </summary>
public interface IMessenger : IReceiveMessages, IProvidable<IMessenger>
{
void Subscribe(IReceiveMessages receiver);
void Unsubscribe(IReceiveMessages receiver);
}
/// <summary>
/// This allows components with unrelated responsibilities to interact without becoming coupled, since message senders don't
/// need to know what (if anything) is receiving their messages.
/// </summary>
public class Messenger : IMessenger
{

m_receivers.Remove(receiver);
}
/// <summary>
/// Send a message to any subscribers, who will decide how to handle the message.
/// </summary>
/// <param name="msg">If there's some data relevant to the recipient, include it here.</param>
public virtual void OnReceiveMessage(MessageType type, object msg)
{
Stopwatch stopwatch = new Stopwatch();

if (previousProvider is Messenger)
m_receivers.AddRange((previousProvider as Messenger).m_receivers);
}
}
/// <summary>
/// Ensure that message contents are obvious but not dependent on spelling strings correctly.
/// </summary>
public enum MessageType
{
// These are assigned arbitrary explicit values so that if a MessageType is serialized and more enum values are later inserted/removed, the serialized values need not be reassigned.
// (If you want to remove a message, make sure it isn't serialized somewhere first.)
None = 0,
RenameRequest = 1,
JoinLobbyRequest = 2,
CreateLobbyRequest = 3,
QueryLobbies = 4,
ChangeGameState = 5,
LobbyUserStatus = 6,
UserSetEmote = 7,
EndGame = 8,
StartCountdown = 9,
CancelCountdown = 10,
ConfirmInGameState = 11,
}
/// <summary>
/// Something that wants to subscribe to messages from arbitrary, unknown senders.
/// </summary>
public interface IReceiveMessages
{
void OnReceiveMessage(MessageType type, object msg);
}
/// <summary>
/// Something to which IReceiveMessages can send/subscribe for arbitrary messages.
/// </summary>
public interface IMessenger : IReceiveMessages, IProvidable<IMessenger>
{
void Subscribe(IReceiveMessages receiver);
void Unsubscribe(IReceiveMessages receiver);
}
}

7
Assets/Scripts/Infrastructure/Observed.cs


namespace LobbyRelaySample
{
/// <summary>
/// In your Observed children, be sure to call OnChanged when setting the value of any property.
/// 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.
///
/// In your Observed child implementations, be sure to call OnChanged when setting the value of any property.
/// <typeparam name="T">The Data we want to view.</typeparam>
/// <typeparam name="T">The type of object to be observed.</typeparam>
public abstract class Observed<T>
{
/// <summary>

3
Assets/Scripts/Infrastructure/ObserverBehaviour.cs


namespace LobbyRelaySample
{
/// <summary>
/// Observes an Observed class, intitializes with Observed State when beginning observation
/// MonoBehaviour that will automatically handle setting up to observe something. It also exposes an event so some other component can effectively observe it as well.
/// <typeparam name="T"></typeparam>
public abstract class ObserverBehaviour<T> : MonoBehaviour where T : Observed<T>
{
public T observed { get; set; }

37
Assets/Scripts/Infrastructure/UpdateSlow.cs


{
public delegate void UpdateMethod(float dt);
public interface IUpdateSlow : IProvidable<IUpdateSlow>
{
void OnUpdate(float dt);
void Subscribe(UpdateMethod onUpdate);
void Unsubscribe(UpdateMethod onUpdate);
}
/// <summary>
/// A default implementation.
/// </summary>
public class UpdateSlowNoop : IUpdateSlow
{
public void OnUpdate(float dt) { }
public void Subscribe(UpdateMethod onUpdate) { }
public void Unsubscribe(UpdateMethod onUpdate) { }
public void OnReProvided(IUpdateSlow prev) { }
}
/// <summary>
/// Some objects might need to be on a slower update loop than the usual MonoBehaviour Update and without precise timing, e.g. to refresh data from services.
/// Some might also not want to be coupled to a Unity object at all but still need an update loop.

m_subscribers.AddRange((prevUpdateSlow as UpdateSlow).m_subscribers);
}
}
public interface IUpdateSlow : IProvidable<IUpdateSlow>
{
void OnUpdate(float dt);
void Subscribe(UpdateMethod onUpdate);
void Unsubscribe(UpdateMethod onUpdate);
}
/// <summary>
/// A default implementation.
/// </summary>
public class UpdateSlowNoop : IUpdateSlow
{
public void OnUpdate(float dt) { }
public void Subscribe(UpdateMethod onUpdate) { }
public void Unsubscribe(UpdateMethod onUpdate) { }
public void OnReProvided(IUpdateSlow prev) { }
}
}

7
Assets/Scripts/Lobby/LobbyAPIInterface.cs


namespace LobbyRelaySample.lobby
{
/// <summary>
/// Does all the interactions with the Lobby API.
/// Wrapper for all the interactions with the Lobby API.
/// <summary>
/// API calls are asynchronous, but for debugging and other reasons we want to reify them as objects so that they can be monitored.
/// </summary>
private class InProgressRequest<T>
{
public InProgressRequest(Task<T> task, Action<T> onComplete)

private async void DoRequest(Task<T> task, Action<T> onComplete)
{
T result = default;
string currentTrace = System.Environment.StackTrace;
string currentTrace = System.Environment.StackTrace; // If we don't get the calling context here, it's lost once the async operation begins.
try {
result = await task;
} catch (Exception e) {

14
Assets/Scripts/Lobby/LobbyAsyncRequests.cs


return response != null && response.Status >= 200 && response.Status < 300; // Uses HTTP status codes, so 2xx is a success.
}
#region We want to cache the lobby object so we don't query for it every time we need to do a different lobby operation or view current data.
#region Once connected to a lobby, cache the local lobby object so we don't query for it for every lobby operation.
// (This assumes that the player will be actively in just one lobby at a time, though they could passively be in more.)
private Queue<Action> m_pendingOperations = new Queue<Action>();
private string m_currentLobbyId = null;

}
}
/// <summary>Attempt to join an existing lobby. Either ID xor code can be null.</summary>
/// <summary>
/// Attempt to join an existing lobby. Either ID xor code can be null.
/// </summary>
public void JoinLobbyAsync(string lobbyId, string lobbyCode, LobbyUser localUser, Action<Lobby> onSuccess, Action onFailure)
{
string uasId = AuthenticationService.Instance.PlayerId;

}
}
/// <summary>Used for getting the list of all active lobbies, without needing full info for each.</summary>
/// <summary>
/// Used for getting the list of all active lobbies, without needing full info for each.
/// </summary>
/// <param name="onListRetrieved">If called with null, retrieval was unsuccessful. Else, this will be given a list of contents to display, as pairs of a lobby code and a display string for that lobby.</param>
public void RetrieveLobbyListAsync(Action<QueryResponse> onListRetrieved, Action<Response<QueryResponse>> onError = null, LobbyColor limitToColor = LobbyColor.None)
{

LobbyAPIInterface.UpdatePlayerAsync(m_lastKnownLobby.Id, playerId, dataCurr, (r) => { onComplete?.Invoke(); }, null, null);
}
/// <summary>Lobby can be provided info about Relay (or any other remote allocation) so it can add automatic disconnect handling.</summary>
/// <summary>
/// Lobby can be provided info about Relay (or any other remote allocation) so it can add automatic disconnect handling.
/// </summary>
public void UpdatePlayerRelayInfoAsync(string allocationId, string connectionInfo, Action onComplete)
{
if (!ShouldUpdateData(() => { UpdatePlayerRelayInfoAsync(allocationId, connectionInfo, onComplete); }, onComplete, true)) // Do retry here since the RelayUtpSetup that called this might be destroyed right after this.

19
Assets/Scripts/Lobby/LobbyContentHeartbeat.cs


using System;
using System.Collections.Generic;
using System.Collections.Generic;
// TODO: It might make sense to change UpdateSlow to, rather than have a fixed cycle on which everything is bound, be able to track when each thing should update?
// I.e. what I want here now is for when a lobby async request comes in, if it has already been long enough, it immediately fires and then sets a cooldown.
// This is still necessary for detecting new players, although I think we could hit a case where the relay join ends up coming in before the cooldown?
// So, we should be able to create a new LobbyUser that way as well.
// That is, creating a (local) player via Relay or via Lobby should go through the same mechanism...? Or do we hold onto the Relay data until the player exists?
/// Keep updated on changes to a joined lobby.
/// Keep updated on changes to a joined lobby, at a speed compliant with Lobby's rate limiting.
/// </summary>
public class LobbyContentHeartbeat
{

m_shouldPushData = true;
}
public void OnUpdate(float dt)
/// <summary>
/// If there have been any data changes since the last update, push them to Lobby. Regardless, pull for the most recent data.
/// (Unless we're already awaiting a query, in which case continue waiting.)
/// </summary>
private void OnUpdate(float dt)
{
if (m_isAwaitingQuery || m_localLobby == null)
return;

8
Assets/Scripts/Lobby/ToLocalLobby.cs


LobbyName = lobby.Name,
MaxPlayerCount = lobby.MaxPlayers,
RelayCode = lobby.Data?.ContainsKey("RelayCode") == true ? lobby.Data["RelayCode"].Value : null, // By providing RelayCode through the lobby data with Member visibility, we ensure a client is connected to the lobby before they could attempt a relay connection, preventing timing issues between them.
State = lobby.Data?.ContainsKey("State") == true ? (LobbyState) int.Parse(lobby.Data["State"].Value) : LobbyState.Lobby, // TODO: Consider TryParse, just in case (and below). Although, we don't have fail logic anyway...
State = lobby.Data?.ContainsKey("State") == true ? (LobbyState) int.Parse(lobby.Data["State"].Value) : LobbyState.Lobby,
Color = lobby.Data?.ContainsKey("Color") == true ? (LobbyColor) int.Parse(lobby.Data["Color"].Value) : LobbyColor.None
};

}
}
// If the player isn't connected to Relay, or if we just don't know about them yet, get the most recent data that the lobby knows.
// (If we have no local representation of the player, that gets added by the LocalLobby.)
// If the player isn't connected to Relay, get the most recent data that the lobby knows.
// (If we haven't seen this player yet, a new local representation of the player will have already been added by the LocalLobby.)
LobbyUser incomingData = new LobbyUser
{
IsHost = lobby.HostId.Equals(player.Id),

}
/// <summary>
/// Create a list of new LocalLobby from the content of a retrieved lobby.
/// Create a list of new LocalLobbies from the result of a lobby list query.
/// </summary>
public static List<LocalLobby> Convert(QueryResponse response)
{

16
Assets/Scripts/Relay/RelayUtpClient.cs


DoUserUpdate(m_networkDriver, conn, m_localUser);
}
public void Update()
private void Update()
{
OnUpdate();
}

}
}
// See the Write* methods for the expected event format.
private void ProcessNetworkEvent(NetworkConnection conn, DataStreamReader strm, NetworkEvent.Type cmd)
{
if (cmd == NetworkEvent.Type.Data)

}
protected virtual void ProcessNetworkEventDataAdditional(NetworkConnection conn, DataStreamReader strm, MsgType msgType, string id) { }
protected virtual void ProcessDisconnectEvent(NetworkConnection conn, DataStreamReader strm)
{
// The host disconnected, and Relay does not support host migration. So, all clients should disconnect.

}
/// <summary>
/// Relay uses raw pointers for efficiency. This converts them to byte arrays, assuming the stream contents are 1 byte for array length followed by contents.
/// </summary>
unsafe private string ReadLengthAndString(ref DataStreamReader strm)
{
byte length = strm.ReadByte();

return System.Text.Encoding.UTF8.GetString(bytes);
}
/// <summary>
/// Once a client is connected, send a message out alerting the host.
/// </summary>
private void SendInitialMessage(NetworkDriver driver, NetworkConnection connection)
{
WriteByte(driver, connection, m_localUser.ID, MsgType.NewPlayer, 0);

/// <summary>
/// When player data is updated, send out events for just the data that actually changed.
/// </summary>
private void DoUserUpdate(NetworkDriver driver, NetworkConnection connection, LobbyUser user)
{
// Only update with actual changes. (If multiple change at once, we send messages for each separately, but that shouldn't happen often.)

if (0 < (user.LastChanged & LobbyUser.UserMembers.UserStatus))
WriteByte(driver, connection, user.ID, MsgType.ReadyState, (byte)user.UserStatus);
}
/// <summary>
/// Sometimes (e.g. when a new player joins), we need to send out the full current state of this player.
/// </summary>
protected void ForceFullUserUpdate(NetworkDriver driver, NetworkConnection connection, LobbyUser user)
{
// Note that it would be better to send a single message with the full state, but for the sake of shorter code we'll leave that out here.

4
Assets/Scripts/Relay/RelayUtpHost.cs


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.
// TEMP logging
UnityEngine.Debug.LogError("Client disconnected!");
}

/// <summary>
/// In an actual game, after the countdown, there would be some step here where the host and all clients sync up on game state, load assets, etc.
/// Here, we will instead just signal an "in game" state that can be ended by the host.
/// Here, we will instead just signal an "in-game" state that can be ended by the host.
/// </summary>
public void SendInGameState()
{

17
Assets/Scripts/Relay/RelayUtpSetup.cs


if (m_networkDriver.Bind(NetworkEndPoint.AnyIpv4) != 0)
Debug.LogError("Failed to bind to Relay allocation.");
else
StartCoroutine(WaitForBindComplete()); // TODO: This is the only reason for being a MonoBehaviour?
StartCoroutine(WaitForBindComplete());
}
private IEnumerator WaitForBindComplete()

m_networkDriver.ScheduleUpdate().Complete();
yield return null; // TODO: Does this not proceed until a client connects as well?
yield return null;
}
OnBindingComplete();
}

#endregion
}
/// <summary>
/// Host logic: Request a new Allocation, and then both bind to it and request a join code. Once those are both complete, supply data back to the lobby.
/// </summary>
public class RelayUtpSetupHost : RelayUtpSetup
{
[Flags]

protected override void JoinRelay()
{
RelayInterface.AllocateAsync(m_localLobby.MaxPlayerCount, OnAllocation);
RelayAPIInterface.AllocateAsync(m_localLobby.MaxPlayerCount, OnAllocation);
RelayInterface.GetJoinCodeAsync(allocation.AllocationId, OnRelayCode);
RelayAPIInterface.GetJoinCodeAsync(allocation.AllocationId, OnRelayCode);
BindToAllocation(allocation.RelayServer.IpV4, allocation.RelayServer.Port, allocation.AllocationIdBytes, allocation.ConnectionData, allocation.ConnectionData, allocation.Key, 16);
}

}
}
/// <summary>
/// Client logic: Wait until the join code is retrieved from the lobby's shared data. Then, use that code to get the Allocation to bind to, and
/// then create a connection to the host.
/// </summary>
public class RelayUtpSetupClient : RelayUtpSetup
{
private JoinAllocation m_allocation;

{
if (m_localLobby.RelayCode != null)
{
RelayInterface.JoinAsync(m_localLobby.RelayCode, OnJoin);
RelayAPIInterface.JoinAsync(m_localLobby.RelayCode, OnJoin);
m_localLobby.onChanged -= OnLobbyChange;
}
}

6
Assets/Scripts/Tests/PlayMode/RelayRoundTripTests.cs


// Allocation
float timeout = 5;
Allocation allocation = null;
RelayInterface.AllocateAsync(4, (a) => { allocation = a; });
RelayAPIInterface.AllocateAsync(4, (a) => { allocation = a; });
while (allocation == null && timeout > 0)
{ yield return new WaitForSeconds(0.25f);
timeout -= 0.25f;

// Join code retrieval
timeout = 5;
string joinCode = null;
RelayInterface.GetJoinCodeAsync(allocationId, (j) => { joinCode = j; });
RelayAPIInterface.GetJoinCodeAsync(allocationId, (j) => { joinCode = j; });
while (joinCode == null && timeout > 0)
{ yield return new WaitForSeconds(0.25f);
timeout -= 0.25f;

// Joining with the join code
timeout = 5;
Response<JoinResponseBody> joinResponse = null;
RelayInterface.JoinAsync(joinCode, (j) => { joinResponse = j; });
RelayAPIInterface.JoinAsync(joinCode, (j) => { joinResponse = j; });
while (joinResponse == null && timeout > 0)
{ yield return new WaitForSeconds(0.25f);
timeout -= 0.25f;

6
Assets/Scripts/UI/SpinnerUI.cs


public override void ObservedUpdated(LobbyServiceData observed)
{
if (observed.State == LobbyServiceState.Fetching)
if (observed.State == LobbyQueryState.Fetching)
{
Show();
spinnerImage.Show();

else if (observed.State == LobbyServiceState.Error)
else if (observed.State == LobbyQueryState.Error)
{
spinnerImage.Hide();
errorTextVisibility.Show();

errorString.Append(codeString);
errorText.SetText(errorString.ToString());
}
else if (observed.State == LobbyServiceState.Fetched)
else if (observed.State == LobbyQueryState.Fetched)
{
if (observed.CurrentLobbies.Count < 1)
{

323
Assets/Scripts/Game/GameManager.cs


using LobbyRelaySample.Relay;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace LobbyRelaySample
{
/// <summary>
/// Sets up and runs the entire sample.
/// </summary>
public class GameManager : MonoBehaviour, IReceiveMessages
{
[SerializeField]
[Tooltip("Only logs of this level or higher will appear in the console.")]
private LogMode m_logMode = LogMode.Critical;
/// <summary>
/// All these should be assigned the observers in the scene at the start.
/// </summary>
#region UI elements that observe the local state. These are
[SerializeField]
private List<LocalGameStateObserver> m_GameStateObservers = new List<LocalGameStateObserver>();
[SerializeField]
private List<LocalLobbyObserver> m_LocalLobbyObservers = new List<LocalLobbyObserver>();
[SerializeField]
private List<LobbyUserObserver> m_LocalUserObservers = new List<LobbyUserObserver>();
[SerializeField]
private List<LobbyServiceDataObserver> m_LobbyServiceObservers = new List<LobbyServiceDataObserver>();
#endregion
private LocalGameState m_localGameState = new LocalGameState();
private LobbyUser m_localUser;
private LocalLobby m_localLobby;
private LobbyServiceData m_lobbyServiceData = new LobbyServiceData();
private LobbyContentHeartbeat m_lobbyContentHeartbeat = new LobbyContentHeartbeat();
private RelayUtpSetup m_relaySetup;
private RelayUtpClient m_relayClient;
/// <summary>Rather than a setter, this is usable in-editor. It won't accept an enum, however.</summary>
public void SetLobbyColorFilter(int color) { m_lobbyColorFilter = (LobbyColor)color; }
private LobbyColor m_lobbyColorFilter;
#region Setup
private void Awake()
{
// Do some arbitrary operations to instantiate singletons.
#pragma warning disable IDE0059 // Unnecessary assignment of a value
var unused = Locator.Get;
#pragma warning restore IDE0059
LogHandler.Get().mode = m_logMode;
Locator.Get.Provide(new Auth.Identity(OnAuthSignIn));
Application.wantsToQuit += OnWantToQuit;
}
private void Start()
{
m_localLobby = new LocalLobby { State = LobbyState.Lobby };
m_localUser = new LobbyUser();
m_localUser.DisplayName = "New Player";
Locator.Get.Messenger.Subscribe(this);
BeginObservers();
}
private void OnAuthSignIn()
{
Debug.Log("Signed in.");
m_localUser.ID = Locator.Get.Identity.GetSubIdentity(Auth.IIdentityType.Auth).GetContent("id");
m_localUser.DisplayName = NameGenerator.GetName(m_localUser.ID);
m_localLobby.AddPlayer(m_localUser); // The local LobbyUser object will be hooked into UI before the LocalLobby is populated during lobby join, so the LocalLobby must know about it already when that happens.
}
private void BeginObservers()
{
foreach (var gameStateObs in m_GameStateObservers)
gameStateObs.BeginObserving(m_localGameState);
foreach (var serviceObs in m_LobbyServiceObservers)
serviceObs.BeginObserving(m_lobbyServiceData);
foreach (var lobbyObs in m_LocalLobbyObservers)
lobbyObs.BeginObserving(m_localLobby);
foreach (var userObs in m_LocalUserObservers)
userObs.BeginObserving(m_localUser);
}
#endregion
/// <summary>
/// Primarily used for UI elements to communicate state changes, this will receive messages from arbitrary providers for user interactions.
/// </summary>
public void OnReceiveMessage(MessageType type, object msg)
{
if (type == MessageType.RenameRequest)
{ m_localUser.DisplayName = (string)msg;
}
else if (type == MessageType.CreateLobbyRequest)
{
var createLobbyData = (LocalLobby)msg;
LobbyAsyncRequests.Instance.CreateLobbyAsync(createLobbyData.LobbyName, createLobbyData.MaxPlayerCount, createLobbyData.Private, m_localUser, (r) =>
{ lobby.ToLocalLobby.Convert(r, m_localLobby);
OnCreatedLobby();
},
OnFailedJoin);
}
else if (type == MessageType.JoinLobbyRequest)
{
LocalLobby.LobbyData lobbyInfo = (LocalLobby.LobbyData)msg;
LobbyAsyncRequests.Instance.JoinLobbyAsync(lobbyInfo.LobbyID, lobbyInfo.LobbyCode, m_localUser, (r) =>
{ lobby.ToLocalLobby.Convert(r, m_localLobby);
OnJoinedLobby();
},
OnFailedJoin);
}
else if (type == MessageType.QueryLobbies)
{
m_lobbyServiceData.State = LobbyQueryState.Fetching;
LobbyAsyncRequests.Instance.RetrieveLobbyListAsync(
qr => {
if (qr != null)
OnLobbiesQueried(lobby.ToLocalLobby.Convert(qr));
},
er => {
long errorLong = 0;
if (er != null)
errorLong = er.Status;
OnLobbyQueryFailed(errorLong);
},
m_lobbyColorFilter);
}
else if (type == MessageType.ChangeGameState)
{ SetGameState((GameState)msg);
}
else if (type == MessageType.UserSetEmote)
{ EmoteType emote = (EmoteType)msg;
m_localUser.Emote = emote;
}
else if (type == MessageType.LobbyUserStatus)
{ m_localUser.UserStatus = (UserStatus)msg;
}
else if (type == MessageType.StartCountdown)
{ BeginCountDown();
}
else if (type == MessageType.CancelCountdown)
{ m_localLobby.State = LobbyState.Lobby;
m_localLobby.CountDownTime = 0;
}
else if (type == MessageType.ConfirmInGameState)
{ m_localUser.UserStatus = UserStatus.InGame;
m_localLobby.State = LobbyState.InGame;
}
else if (type == MessageType.EndGame)
{ m_localLobby.State = LobbyState.Lobby;
m_localLobby.CountDownTime = 0;
SetUserLobbyState();
}
}
private void SetGameState(GameState state)
{
bool isLeavingLobby = (state == GameState.Menu || state == GameState.JoinMenu) && m_localGameState.State == GameState.Lobby;
m_localGameState.State = state;
if (isLeavingLobby)
OnLeftLobby();
}
private void OnLobbiesQueried(IEnumerable<LocalLobby> lobbies)
{
var newLobbyDict = new Dictionary<string, LocalLobby>();
foreach (var lobby in lobbies)
newLobbyDict.Add(lobby.LobbyID, lobby);
m_lobbyServiceData.State = LobbyQueryState.Fetched;
m_lobbyServiceData.CurrentLobbies = newLobbyDict;
}
private void OnLobbyQueryFailed(long errorCode)
{
m_lobbyServiceData.lastErrorCode = errorCode;
m_lobbyServiceData.State = LobbyQueryState.Error;
}
private void OnCreatedLobby()
{
m_localUser.IsHost = true;
OnJoinedLobby();
}
private void OnJoinedLobby()
{
LobbyAsyncRequests.Instance.BeginTracking(m_localLobby.LobbyID);
m_lobbyContentHeartbeat.BeginTracking(m_localLobby, m_localUser);
SetUserLobbyState();
StartRelayConnection();
}
private void OnLeftLobby()
{
m_localUser.ResetState();
LobbyAsyncRequests.Instance.LeaveLobbyAsync(m_localLobby.LobbyID, ResetLocalLobby);
m_lobbyContentHeartbeat.EndTracking();
LobbyAsyncRequests.Instance.EndTracking();
if (m_relaySetup != null)
{ Component.Destroy(m_relaySetup);
m_relaySetup = null;
}
if (m_relayClient != null)
{ Component.Destroy(m_relayClient);
m_relayClient = null;
}
}
/// <summary>
/// Back to Join menu if we fail to join for whatever reason.
/// </summary>
private void OnFailedJoin()
{
SetGameState(GameState.JoinMenu);
}
private void StartRelayConnection()
{
if (m_localUser.IsHost)
m_relaySetup = gameObject.AddComponent<RelayUtpSetupHost>();
else
m_relaySetup = gameObject.AddComponent<RelayUtpSetupClient>();
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Connecting);
m_relaySetup.BeginRelayJoin(m_localLobby, m_localUser, OnRelayConnected);
}
private void OnRelayConnected(bool didSucceed, RelayUtpClient client)
{
Component.Destroy(m_relaySetup);
m_relaySetup = null;
if (!didSucceed)
{
Debug.LogError("Relay connection failed! Retrying in 5s...");
StartCoroutine(RetryRelayConnection());
return;
}
m_relayClient = client;
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Lobby);
}
private IEnumerator RetryRelayConnection()
{
yield return new WaitForSeconds(5);
StartRelayConnection();
}
private void BeginCountDown()
{
if (m_localLobby.State == LobbyState.CountDown)
return;
m_localLobby.CountDownTime = 4;
m_localLobby.State = LobbyState.CountDown;
StartCoroutine(CountDown());
}
/// <summary>
/// The CountdownUI will pick up on changes to the lobby's countdown timer. This can be interrupted if the lobby leaves the countdown state (via a CancelCountdown message).
/// </summary>
private IEnumerator CountDown()
{
while (m_localLobby.CountDownTime > 0)
{
yield return null;
if (m_localLobby.State != LobbyState.CountDown)
yield break;
m_localLobby.CountDownTime -= Time.deltaTime;
}
if (m_relayClient is RelayUtpHost)
(m_relayClient as RelayUtpHost).SendInGameState();
}
private void SetUserLobbyState()
{
SetGameState(GameState.Lobby);
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Lobby);
}
private void ResetLocalLobby()
{
m_localLobby.CopyObserved(new LocalLobby.LobbyData(), new Dictionary<string, LobbyUser>());
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.CountDownTime = 0;
m_localLobby.RelayServer = null;
}
#region Teardown
/// <summary>
/// In builds, if we are in a lobby and try to send a Leave request on application quit, it won't go through if we're quitting on the same frame.
/// So, we need to delay just briefly to let the request happen (though we don't need to wait for the result).
/// </summary>
private IEnumerator LeaveBeforeQuit()
{
ForceLeaveAttempt();
yield return null;
Application.Quit();
}
private bool OnWantToQuit()
{
bool canQuit = string.IsNullOrEmpty(m_localLobby?.LobbyID);
StartCoroutine(LeaveBeforeQuit());
return canQuit;
}
private void OnDestroy()
{
ForceLeaveAttempt();
}
private void ForceLeaveAttempt()
{
Locator.Get.Messenger.Unsubscribe(this);
if (!string.IsNullOrEmpty(m_localLobby?.LobbyID))
{
LobbyAsyncRequests.Instance.LeaveLobbyAsync(m_localLobby?.LobbyID, null);
m_localLobby = null;
}
}
#endregion
}
}

106
Assets/Scripts/Relay/RelayAPIInterface.cs


using System;
using System.Threading.Tasks;
using Unity.Services.Relay;
using Unity.Services.Relay.Allocations;
using Unity.Services.Relay.Models;
using UnityEngine;
namespace LobbyRelaySample.Relay
{
/// <summary>
/// Wrapper for all the interaction with the Relay API.
/// </summary>
public static class RelayAPIInterface
{
/// <summary>
/// API calls are asynchronous, but for debugging and other reasons we want to reify them as objects so that they can be monitored.
/// </summary>
private class InProgressRequest<T>
{
public InProgressRequest(Task<T> task, Action<T> onComplete)
{
DoRequest(task, onComplete);
}
private async void DoRequest(Task<T> task, Action<T> onComplete)
{
T result = default;
string currentTrace = System.Environment.StackTrace; // If we don't get the calling context here, it's lost once the async operation begins.
try {
result = await task;
} catch (Exception e) {
Exception eFull = new Exception($"Call stack before async call:\n{currentTrace}\n", e);
throw eFull;
} finally {
onComplete?.Invoke(result);
}
}
}
/// <summary>
/// A Relay Allocation represents a "server" for a new host.
/// </summary>
public static void AllocateAsync(int maxConnections, Action<Allocation> onComplete)
{
CreateAllocationRequest createAllocationRequest = new CreateAllocationRequest(new AllocationRequest(maxConnections));
var task = RelayService.AllocationsApiClient.CreateAllocationAsync(createAllocationRequest);
new InProgressRequest<Response<AllocateResponseBody>>(task, OnResponse);
void OnResponse(Response<AllocateResponseBody> response)
{
if (response == null)
Debug.LogError("Relay returned a null Allocation. It's possible the Relay service is currently down.");
else if (response.Status >= 200 && response.Status < 300)
onComplete?.Invoke(response.Result.Data.Allocation);
else
Debug.LogError($"Allocation returned a non Success code: {response.Status}");
};
}
/// <summary>
/// Only after an Allocation has been completed can a Relay join code be obtained. This code will be stored in the lobby's data as non-public
/// such that players can retrieve the Relay join code only after connecting to the lobby.
/// </summary>
public static void GetJoinCodeAsync(Guid hostAllocationId, Action<string> onComplete)
{
GetJoinCodeAsync(hostAllocationId, a =>
{
if (a.Status >= 200 && a.Status < 300)
onComplete.Invoke(a.Result.Data.JoinCode);
else
{
Debug.LogError($"Join Code Get returned a non Success code: {a.Status}");
}
});
}
private static void GetJoinCodeAsync(Guid hostAllocationId, Action<Response<JoinCodeResponseBody>> onComplete)
{
CreateJoincodeRequest joinCodeRequest = new CreateJoincodeRequest(new JoinCodeRequest(hostAllocationId));
var task = RelayService.AllocationsApiClient.CreateJoincodeAsync(joinCodeRequest);
new InProgressRequest<Response<JoinCodeResponseBody>>(task, onComplete);
}
/// <summary>
/// Clients call this to retrieve the host's Allocation via a Relay join code.
/// </summary>
public static void JoinAsync(string joinCode, Action<JoinAllocation> onComplete)
{
JoinAsync(joinCode, a =>
{
if (a.Status >= 200 && a.Status < 300)
onComplete.Invoke(a.Result.Data.Allocation);
else
{
Debug.LogError($"Join Call returned a non Success code: {a.Status}");
}
});
}
public static void JoinAsync(string joinCode, Action<Response<JoinResponseBody>> onComplete)
{
JoinRelayRequest joinRequest = new JoinRelayRequest(new JoinRequest(joinCode));
var task = RelayService.AllocationsApiClient.JoinRelayAsync(joinRequest);
new InProgressRequest<Response<JoinResponseBody>>(task, onComplete);
}
}
}

328
Assets/Scripts/Game/GameStateManager.cs


using LobbyRelaySample.Relay;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace LobbyRelaySample
{
// TODO: This is pretty bloated. Additionally, it needs a pass for removing redundant calls and organizing things in a more intuitive way and whatnot
public class GameStateManager : MonoBehaviour, IReceiveMessages
{
[SerializeField]
LogMode m_logMode = LogMode.Critical;
/// <summary>
/// All these should be assigned the observers in the scene at the start.
/// </summary>
[SerializeField]
List<LocalGameStateObserver> m_GameStateObservers = new List<LocalGameStateObserver>();
[SerializeField]
List<LocalLobbyObserver> m_LocalLobbyObservers = new List<LocalLobbyObserver>();
[SerializeField]
List<LobbyUserObserver> m_LocalUserObservers = new List<LobbyUserObserver>();
[SerializeField]
List<LobbyServiceDataObserver> m_LobbyServiceObservers = new List<LobbyServiceDataObserver>();
private LobbyContentHeartbeat m_lobbyContentHeartbeat = new LobbyContentHeartbeat();
LobbyUser m_localUser;
LocalLobby m_localLobby;
LobbyServiceData m_lobbyServiceData = new LobbyServiceData();
LocalGameState m_localGameState = new LocalGameState();
RelayUtpSetup m_relaySetup;
RelayUtpClient m_relayClient;
/// <summary>Rather than a setter, this is usable in-editor. It won't accept an enum, however.</summary>
public void SetLobbyColorFilter(int color) { m_lobbyColorFilter = (LobbyColor)color; }
private LobbyColor m_lobbyColorFilter;
public void Awake()
{
LogHandler.Get().mode = m_logMode;
// Do some arbitrary operations to instantiate singletons.
#pragma warning disable IDE0059 // Unnecessary assignment of a value
var unused = Locator.Get;
#pragma warning restore IDE0059 // Unnecessary assignment of a value
Locator.Get.Provide(new Auth.Identity(OnAuthSignIn));
Application.wantsToQuit += OnWantToQuit;
}
private void OnAuthSignIn()
{
Debug.Log("Signed in.");
m_localUser.ID = Locator.Get.Identity.GetSubIdentity(Auth.IIdentityType.Auth).GetContent("id");
m_localUser.DisplayName = NameGenerator.GetName(m_localUser.ID);
m_localLobby.AddPlayer(m_localUser); // The local LobbyUser object will be hooked into UI before the LocalLobby is populated during lobby join, so the LocalLobby must know about it already when that happens.
}
/// <summary>
/// Primarily used for UI elements to communicate state changes, this will receive messages from arbitrary providers for user interactions.
/// </summary>
public void OnReceiveMessage(MessageType type, object msg)
{
if (type == MessageType.RenameRequest)
{
m_localUser.DisplayName = (string)msg;
}
else if (type == MessageType.CreateLobbyRequest)
{
var createLobbyData = (LocalLobby)msg;
LobbyAsyncRequests.Instance.CreateLobbyAsync(createLobbyData.LobbyName, createLobbyData.MaxPlayerCount, createLobbyData.Private, m_localUser, (r) =>
{
lobby.ToLocalLobby.Convert(r, m_localLobby);
OnCreatedLobby();
}, OnFailedJoin);
}
else if (type == MessageType.JoinLobbyRequest)
{
LocalLobby.LobbyData lobbyInfo = (LocalLobby.LobbyData)msg;
LobbyAsyncRequests.Instance.JoinLobbyAsync(lobbyInfo.LobbyID, lobbyInfo.LobbyCode, m_localUser, (r) =>
{
lobby.ToLocalLobby.Convert(r, m_localLobby);
OnJoinedLobby();
}, OnFailedJoin);
}
else if (type == MessageType.QueryLobbies)
{
m_lobbyServiceData.State = LobbyServiceState.Fetching;
LobbyAsyncRequests.Instance.RetrieveLobbyListAsync(
qr =>
{
if (qr != null)
OnRefreshed(lobby.ToLocalLobby.Convert(qr));
}, er =>
{
long errorLong = 0;
if (er != null)
errorLong = er.Status;
OnRefreshFailed(errorLong);
},
m_lobbyColorFilter);
}
else if (type == MessageType.ChangeGameState)
{
SetGameState((GameState)msg);
}
else if (type == MessageType.UserSetEmote)
{
EmoteType emote = (EmoteType)msg;
m_localUser.Emote = emote;
}
else if (type == MessageType.LobbyUserStatus)
{
m_localUser.UserStatus = (UserStatus)msg;
}
else if (type == MessageType.StartCountdown)
{
BeginCountDown();
}
else if (type == MessageType.CancelCountdown)
{
m_localLobby.State = LobbyState.Lobby;
m_localLobby.CountDownTime = 0;
}
else if (type == MessageType.ConfirmInGameState)
{
m_localUser.UserStatus = UserStatus.InGame;
m_localLobby.State = LobbyState.InGame;
}
else if (type == MessageType.EndGame)
{
m_localLobby.State = LobbyState.Lobby;
m_localLobby.CountDownTime = 0;
SetUserLobbyState();
}
}
void Start()
{
m_localLobby = new LocalLobby { State = LobbyState.Lobby };
m_localUser = new LobbyUser();
m_localUser.DisplayName = "New Player";
Locator.Get.Messenger.Subscribe(this);
BeginObservers();
}
void BeginObservers()
{
foreach (var gameStateObs in m_GameStateObservers)
gameStateObs.BeginObserving(m_localGameState);
foreach (var serviceObs in m_LobbyServiceObservers)
serviceObs.BeginObserving(m_lobbyServiceData);
foreach (var lobbyObs in m_LocalLobbyObservers)
lobbyObs.BeginObserving(m_localLobby);
foreach (var userObs in m_LocalUserObservers)
userObs.BeginObserving(m_localUser);
}
void SetGameState(GameState state)
{
bool isLeavingLobby = (state == GameState.Menu || state == GameState.JoinMenu) && m_localGameState.State == GameState.Lobby;
m_localGameState.State = state;
if (isLeavingLobby)
OnLeftLobby();
}
void OnRefreshed(IEnumerable<LocalLobby> lobbies)
{
var newLobbyDict = new Dictionary<string, LocalLobby>();
foreach (var lobby in lobbies)
{
newLobbyDict.Add(lobby.LobbyID, lobby);
}
m_lobbyServiceData.State = LobbyServiceState.Fetched;
m_lobbyServiceData.CurrentLobbies = newLobbyDict;
}
void OnRefreshFailed(long errorCode)
{
m_lobbyServiceData.lastErrorCode = errorCode;
m_lobbyServiceData.State = LobbyServiceState.Error;
}
void OnCreatedLobby()
{
m_localUser.IsHost = true;
OnJoinedLobby();
}
void OnJoinedLobby()
{
LobbyAsyncRequests.Instance.BeginTracking(m_localLobby.LobbyID);
m_lobbyContentHeartbeat.BeginTracking(m_localLobby, m_localUser);
SetUserLobbyState();
StartRelayConnection();
}
void StartRelayConnection()
{
if (m_localUser.IsHost)
m_relaySetup = gameObject.AddComponent<RelayUtpSetupHost>();
else
m_relaySetup = gameObject.AddComponent<RelayUtpSetupClient>();
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Connecting);
m_relaySetup.BeginRelayJoin(m_localLobby, m_localUser, OnRelayConnected);
}
void OnRelayConnected(bool didSucceed, RelayUtpClient client)
{
Component.Destroy(m_relaySetup);
m_relaySetup = null;
if (!didSucceed)
{
Debug.LogError("Relay connection failed! Retrying in 5s...");
StartCoroutine(RetryRelayConnection());
return;
}
m_relayClient = client;
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Lobby);
}
IEnumerator RetryRelayConnection()
{
yield return new WaitForSeconds(5);
StartRelayConnection();
}
void OnLeftLobby()
{
m_localUser.ResetState();
LobbyAsyncRequests.Instance.LeaveLobbyAsync(m_localLobby.LobbyID, ResetLocalLobby);
m_lobbyContentHeartbeat.EndTracking();
LobbyAsyncRequests.Instance.EndTracking();
if (m_relaySetup != null)
{ Component.Destroy(m_relaySetup);
m_relaySetup = null;
}
if (m_relayClient != null)
{ Component.Destroy(m_relayClient);
m_relayClient = null;
}
}
/// <summary>
/// Back to Join menu if we fail to join for whatever reason.
/// </summary>
void OnFailedJoin()
{
SetGameState(GameState.JoinMenu);
}
void BeginCountDown()
{
if (m_localLobby.State == LobbyState.CountDown)
return;
m_localLobby.CountDownTime = 4;
m_localLobby.State = LobbyState.CountDown;
StartCoroutine(CountDown());
}
/// <summary>
/// The CountdownUI will pick up on changes to the lobby's countdown timer. This can be interrupted if the lobby leaves the countdown state (via a CancelCountdown message).
/// </summary>
IEnumerator CountDown()
{
while (m_localLobby.CountDownTime > 0)
{
yield return null;
if (m_localLobby.State != LobbyState.CountDown)
yield break;
m_localLobby.CountDownTime -= Time.deltaTime;
}
if (m_relayClient is RelayUtpHost)
(m_relayClient as RelayUtpHost).SendInGameState();
}
void SetUserLobbyState()
{
SetGameState(GameState.Lobby);
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Lobby);
}
void ResetLocalLobby()
{
m_localLobby.CopyObserved(new LocalLobby.LobbyData(), new Dictionary<string, LobbyUser>());
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.CountDownTime = 0;
m_localLobby.RelayServer = null;
}
void OnDestroy()
{
ForceLeaveAttempt();
}
bool OnWantToQuit()
{
bool canQuit = string.IsNullOrEmpty(m_localLobby?.LobbyID);
StartCoroutine(LeaveBeforeQuit());
return canQuit;
}
void ForceLeaveAttempt()
{
Locator.Get.Messenger.Unsubscribe(this);
if (!string.IsNullOrEmpty(m_localLobby?.LobbyID))
{
LobbyAsyncRequests.Instance.LeaveLobbyAsync(m_localLobby?.LobbyID, null);
m_localLobby = null;
}
}
/// <summary>
/// In builds, if we are in a lobby and try to send a Leave request on application quit, it won't go through if we're quitting on the same frame.
/// So, we need to delay just briefly to let the request happen (though we don't need to wait for the result).
/// </summary>
IEnumerator LeaveBeforeQuit()
{
ForceLeaveAttempt();
yield return null;
Application.Quit();
}
}
}

105
Assets/Scripts/Relay/RelayInterface.cs


using System;
using System.Threading.Tasks;
using Unity.Services.Relay;
using Unity.Services.Relay.Allocations;
using Unity.Services.Relay.Models;
using UnityEngine;
namespace LobbyRelaySample.Relay
{
/// <summary>
/// Does all the interaction with relay.
/// </summary>
public static class RelayInterface
{
private class InProgressRequest<T>
{
public InProgressRequest(Task<T> task, Action<T> onComplete)
{
DoRequest(task, onComplete);
}
private async void DoRequest(Task<T> task, Action<T> onComplete)
{
T result = default;
string currentTrace = System.Environment.StackTrace;
try {
result = await task;
} catch (Exception e) {
Exception eFull = new Exception($"Call stack before async call:\n{currentTrace}\n", e);
throw eFull;
} finally {
onComplete?.Invoke(result);
}
}
}
/// <summary>
/// Creates a Relay Server, and returns the Allocation (Response.Result.Data.Allocation)
/// </summary>
public static void AllocateAsync(int maxConnections, Action<Allocation> onComplete)
{
CreateAllocationRequest createAllocationRequest = new CreateAllocationRequest(new AllocationRequest(maxConnections));
var task = RelayService.AllocationsApiClient.CreateAllocationAsync(createAllocationRequest);
new InProgressRequest<Response<AllocateResponseBody>>(task, OnResponse);
void OnResponse(Response<AllocateResponseBody> response)
{
if (response == null)
Debug.LogError("Relay returned a null Allocation. It's possible the Relay service is currently down.");
else if (response.Status >= 200 && response.Status < 300)
onComplete?.Invoke(response.Result.Data.Allocation);
else
Debug.LogError($"Allocation returned a non Success code: {response.Status}");
};
}
/// <summary>
/// Get a JoinCode( Response.Result.Data.JoinCode) from an Allocated Server
/// </summary>
public static void GetJoinCodeAsync(Guid hostAllocationId, Action<Response<JoinCodeResponseBody>> onComplete)
{
CreateJoincodeRequest joinCodeRequest = new CreateJoincodeRequest(new JoinCodeRequest(hostAllocationId));
var task = RelayService.AllocationsApiClient.CreateJoincodeAsync(joinCodeRequest);
new InProgressRequest<Response<JoinCodeResponseBody>>(task, onComplete);
}
public static void GetJoinCodeAsync(Guid hostAllocationId, Action<string> onComplete)
{
GetJoinCodeAsync(hostAllocationId, a =>
{
if (a.Status >= 200 && a.Status < 300)
onComplete.Invoke(a.Result.Data.JoinCode);
else
{
Debug.LogError($"Join Code Get returned a non Success code: {a.Status}");
}
});
}
/// <summary>
/// Retrieve an Allocation(Response.Result.Data.Allocation) by join code
/// </summary>
public static void JoinAsync(string joinCode, Action<Response<JoinResponseBody>> onComplete)
{
JoinRelayRequest joinRequest = new JoinRelayRequest(new JoinRequest(joinCode));
var task = RelayService.AllocationsApiClient.JoinRelayAsync(joinRequest);
new InProgressRequest<Response<JoinResponseBody>>(task, onComplete);
}
public static void JoinAsync(string joinCode, Action<JoinAllocation> onComplete)
{
JoinAsync(joinCode, a =>
{
if (a.Status >= 200 && a.Status < 300)
onComplete.Invoke(a.Result.Data.Allocation);
else
{
Debug.LogError($"Join Call returned a non Success code: {a.Status}");
}
});
}
}
}

/Assets/Prefabs/GameStateManager.prefab.meta → /Assets/Prefabs/GameManager.prefab.meta

/Assets/Prefabs/GameStateManager.prefab → /Assets/Prefabs/GameManager.prefab

/Assets/Scripts/Game/GameStateManager.cs.meta → /Assets/Scripts/Game/GameManager.cs.meta

/Assets/Scripts/Relay/RelayInterface.cs.meta → /Assets/Scripts/Relay/RelayAPIInterface.cs.meta

正在加载...
取消
保存