浏览代码

cleanup : removed redundant infrastructure

/main/staging/2021_Upgrade/Async_Refactor
UnityJacob 2 年前
当前提交
a91d6f8a
共有 40 个文件被更改,包括 105 次插入1963 次删除
  1. 17
      Assets/Prefabs/GameManager.prefab
  2. 20
      Assets/Prefabs/UI/LobbyCanvas.prefab
  3. 3
      Assets/Scenes/mainScene.unity
  4. 36
      Assets/Scripts/GameLobby/Game/GameManager.cs
  5. 90
      Assets/Scripts/GameLobby/Infrastructure/CallbackValue.cs
  6. 2
      Assets/Scripts/GameLobby/Lobby/LobbySynchronizer.cs
  7. 2
      Assets/Scripts/GameLobby/NGO/SetupInGame.cs
  8. 4
      Assets/Scripts/GameLobby/UI/BackButtonUI.cs
  9. 2
      Assets/Scripts/GameLobby/UI/StartLobbyButtonUI.cs
  10. 37
      Assets/Scripts/GameLobby/Vivox/VivoxSetup.cs
  11. 55
      Assets/Scripts/GameLobby/Infrastructure/ObserverBehaviour.cs
  12. 11
      Assets/Scripts/GameLobby/Infrastructure/Observed.cs.meta
  13. 11
      Assets/Scripts/GameLobby/Infrastructure/ObserverBehaviour.cs.meta
  14. 11
      Assets/Scripts/GameLobby/Infrastructure/UpdateSlow.cs.meta
  15. 11
      Assets/Scripts/GameLobby/Infrastructure/Messenger.cs.meta
  16. 11
      Assets/Scripts/GameLobby/Infrastructure/Locator.cs.meta
  17. 101
      Assets/Scripts/GameLobby/Infrastructure/Locator.cs
  18. 145
      Assets/Scripts/GameLobby/Infrastructure/UpdateSlow.cs
  19. 38
      Assets/Scripts/GameLobby/Infrastructure/Observed.cs
  20. 129
      Assets/Scripts/GameLobby/Infrastructure/Messenger.cs
  21. 16
      Assets/Scripts/GameLobby/NGO/IInGameInputHandler.cs
  22. 11
      Assets/Scripts/GameLobby/NGO/IInGameInputHandler.cs.meta
  23. 11
      Assets/Scripts/GameLobby/Relay/RelayUtpClient.cs.meta
  24. 11
      Assets/Scripts/GameLobby/Relay/RelayUtpHost.cs.meta
  25. 11
      Assets/Scripts/GameLobby/Relay/RelayUtpSetup.cs.meta
  26. 57
      Assets/Scripts/GameLobby/Relay/RelayPendingApproval.cs
  27. 11
      Assets/Scripts/GameLobby/Relay/RelayPendingApproval.cs.meta
  28. 298
      Assets/Scripts/GameLobby/Relay/RelayUtpSetup.cs
  29. 296
      Assets/Scripts/GameLobby/Relay/RelayUtpClient.cs
  30. 220
      Assets/Scripts/GameLobby/Relay/RelayUtpHost.cs
  31. 11
      Assets/Scripts/GameLobby/Tests/Editor/ObserverTests.cs.meta
  32. 82
      Assets/Scripts/GameLobby/Tests/Editor/ObserverTests.cs
  33. 70
      Assets/Scripts/GameLobby/Tests/PlayMode/RelayRoundTripTests.cs
  34. 11
      Assets/Scripts/GameLobby/Tests/PlayMode/RelayRoundTripTests.cs.meta
  35. 11
      Assets/Scripts/GameLobby/Tests/PlayMode/UpdateSlowTests.cs.meta
  36. 184
      Assets/Scripts/GameLobby/Tests/PlayMode/UpdateSlowTests.cs
  37. 10
      Assets/Scripts/GameLobby/UI/ObserverPanel.cs
  38. 11
      Assets/Scripts/GameLobby/UI/ObserverPanel.cs.meta
  39. 0
      /Assets/Scripts/GameLobby/Infrastructure/CallbackValue.cs.meta
  40. 0
      /Assets/Scripts/GameLobby/Infrastructure/CallbackValue.cs

17
Assets/Prefabs/GameManager.prefab


m_Component:
- component: {fileID: 7716713811812636911}
- component: {fileID: 7716713811812636910}
- component: {fileID: 5193415626965589893}
m_Layer: 0
m_Name: GameManager
m_TagString: Untagged

m_Script: {fileID: 11500000, guid: b4f7225f73bfe6a4d9133ee45ac9cd73, type: 3}
m_Name:
m_EditorClassIdentifier:
m_LocalLobbyObservers: []
m_LocalUserObservers: []
--- !u!114 &5193415626965589893
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7716713811812636896}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9275e9be406280e4198464167a8b9186, type: 3}
m_Name:
m_EditorClassIdentifier:
m_durationToleranceMs: 50
m_doNotRemoveIfTooLong: 1

20
Assets/Prefabs/UI/LobbyCanvas.prefab


m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
m_IsActive: 0
--- !u!224 &8966797402027724594
RectTransform:
m_ObjectHideFlags: 0

objectReference: {fileID: 0}
- target: {fileID: 1919168897190896396, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchoredPosition.x
value: 208.5
value: 186.75
objectReference: {fileID: 0}
- target: {fileID: 1919168897190896396, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchoredPosition.y

objectReference: {fileID: 0}
- target: {fileID: 3210254045315593125, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchoredPosition.x
value: 49.5
value: 27.749992
objectReference: {fileID: 0}
- target: {fileID: 3210254045315593125, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchoredPosition.y

objectReference: {fileID: 0}
- target: {fileID: 3214500912641965185, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchoredPosition.x
value: 102.5
value: 80.74999
objectReference: {fileID: 0}
- target: {fileID: 3214500912641965185, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchoredPosition.y

objectReference: {fileID: 0}
- target: {fileID: 4467363028704636643, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_fontSize
value: 13
value: 12
objectReference: {fileID: 0}
- target: {fileID: 4558362294547660329, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchorMax.y

objectReference: {fileID: 0}
- target: {fileID: 6664205945102926799, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchoredPosition.x
value: 155.5
value: 133.75
objectReference: {fileID: 0}
- target: {fileID: 6664205945102926799, guid: 2ff073ec9c74c8942bd90a541dc41bfc, type: 3}
propertyPath: m_AnchoredPosition.y

objectReference: {fileID: 0}
- target: {fileID: 840905996306701940, guid: e269788e17cbca145bf78e8971aeb223, type: 3}
propertyPath: m_fontSize
value: 25
value: 21.3
objectReference: {fileID: 0}
- target: {fileID: 929943731109783885, guid: e269788e17cbca145bf78e8971aeb223, type: 3}
propertyPath: m_AnchorMax.y

objectReference: {fileID: 0}
- target: {fileID: 3246194187207366366, guid: e269788e17cbca145bf78e8971aeb223, type: 3}
propertyPath: m_fontSize
value: 25
value: 21.3
objectReference: {fileID: 0}
- target: {fileID: 3253464371495375142, guid: e269788e17cbca145bf78e8971aeb223, type: 3}
propertyPath: m_AnchorMax.y

objectReference: {fileID: 0}
- target: {fileID: 4917538085660885383, guid: e269788e17cbca145bf78e8971aeb223, type: 3}
propertyPath: m_fontSize
value: 25
value: 21.3
objectReference: {fileID: 0}
- target: {fileID: 5186258928042496532, guid: e269788e17cbca145bf78e8971aeb223, type: 3}
propertyPath: m_fontSize

objectReference: {fileID: 0}
- target: {fileID: 7824963406237393945, guid: e269788e17cbca145bf78e8971aeb223, type: 3}
propertyPath: m_fontSize
value: 25
value: 21.3
objectReference: {fileID: 0}
- target: {fileID: 8020923114782963594, guid: e269788e17cbca145bf78e8971aeb223, type: 3}
propertyPath: m_fontSize

3
Assets/Scenes/mainScene.unity


propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedComponents:
- {fileID: 5193415626965589893, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
m_SourcePrefab: {fileID: 100100000, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
--- !u!1001 &8628454959146822954
PrefabInstance:

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


using LobbyRelaySample.relay;
using System;
using System.Collections;
using System.Collections.Generic;

{
return;
}
SetCurrentLobbies(LobbyConverters.QueryToLocalList(qr));
}

await LobbyManager.UpdatePlayerDataAsync(LobbyConverters.LocalToRemoteUserData(m_LocalUser));
}
public void ChangeMenuState(GameState state)
public void UIChangeMenuState(GameState state)
var isQuittingGame = LocalGameState == GameState.Lobby &&
m_LocalLobby.LocalLobbyState.Value == LobbyState.InGame;
if (isQuittingGame)
{
//If we were in-game, make sure we stop by the lobby first
state = GameState.Lobby;
ClientQuitGame();
}
SetGameState(state);
}

}
}
public void ClientQuitGame()
{
EndGame();
m_setupInGame?.OnGameEnd();
}
public void EndGame()
{
if (m_LocalUser.IsHost.Value)

SendLocalLobbyData();
}
m_setupInGame?.OnGameEnd();
SetLobbyView();
}

{
// Do some arbitrary operations to instantiate singletons.
#pragma warning disable IDE0059 // Unnecessary assignment of a value
var _ = Locator.Get;
#pragma warning restore IDE0059
Application.wantsToQuit += OnWantToQuit;
m_LocalUser = new LocalPlayer("", 0, false, "LocalPlayer");
m_LocalLobby = new LocalLobby { LocalLobbyState = { Value = LobbyState.Lobby } };

void SetGameState(GameState state)
{
var isQuittingGame = LocalGameState == GameState.Lobby &&
m_LocalLobby.LocalLobbyState.Value == LobbyState.InGame;
if (isQuittingGame)
{
//If we were in-game, make sure we stop by the lobby first
state = GameState.Lobby;
EndGame();
}
var isLeavingLobby = (state == GameState.Menu || state == GameState.JoinMenu) &&
LocalGameState == GameState.Lobby;
LocalGameState = state;

LobbyList.CurrentLobbies = newLobbyDict;
LobbyList.QueryState.Value = LobbyQueryState.Fetched;
}
async Task CreateLobby()

90
Assets/Scripts/GameLobby/Infrastructure/CallbackValue.cs


using System;
using UnityEngine;
namespace LobbyRelaySample
{
public class CallbackValue<T>
{
public Action<T> onChanged;
public CallbackValue()
{
}
public CallbackValue(T cachedValue)
{
m_CachedValue = cachedValue;
}
public T Value
{
get => m_CachedValue;
set
{
if (m_CachedValue!=null&&m_CachedValue.Equals(value))
return;
m_CachedValue = value;
onChanged?.Invoke(m_CachedValue);
}
}
public void ForceSet(T value)
{
m_CachedValue = value;
onChanged?.Invoke(m_CachedValue);
}
public void SetNoCallback(T value)
{
m_CachedValue = value;
}
T m_CachedValue = default;
}
}
using System;
using UnityEngine;
namespace LobbyRelaySample
{
public class CallbackValue<T>
{
public Action<T> onChanged;
public CallbackValue()
{
}
public CallbackValue(T cachedValue)
{
m_CachedValue = cachedValue;
}
public T Value
{
get => m_CachedValue;
set
{
if (m_CachedValue!=null&&m_CachedValue.Equals(value))
return;
m_CachedValue = value;
onChanged?.Invoke(m_CachedValue);
}
}
public void ForceSet(T value)
{
m_CachedValue = value;
onChanged?.Invoke(m_CachedValue);
}
public void SetNoCallback(T value)
{
m_CachedValue = value;
}
T m_CachedValue = default;
}
}

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


// LogHandlerSettings.Instance.SpawnErrorPopup(
// "Host left the lobby! Disconnecting...");
// Locator.Get.Messenger.OnReceiveMessage(MessageType.EndGame, null);
// GameManager.Instance.ChangeMenuState(GameState.JoinMenu);
// GameManager.Instance.UIChangeMenuState(GameState.JoinMenu);
// }
//
// public void OnLobbyIdChanged(string lobbyID)

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


.gameObject); // Since this destroys the NetworkManager, that will kick off cleaning up networked objects.
SetMenuVisibility(true);
m_lobby.RelayCode.Value = "";
GameManager.Instance.EndGame();
m_doesNeedCleanup = false;
}
}

4
Assets/Scripts/GameLobby/UI/BackButtonUI.cs


public void ToJoinMenu()
{
Manager.ChangeMenuState(GameState.JoinMenu);
Manager.UIChangeMenuState(GameState.JoinMenu);
Manager.ChangeMenuState(GameState.Menu);
Manager.UIChangeMenuState(GameState.Menu);
}
}
}

2
Assets/Scripts/GameLobby/UI/StartLobbyButtonUI.cs


{
public void ToJoinMenu()
{
Manager.ChangeMenuState(GameState.JoinMenu);
Manager.UIChangeMenuState(GameState.JoinMenu);
}
}
}

37
Assets/Scripts/GameLobby/Vivox/VivoxSetup.cs


using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Services.Authentication;
using Unity.Services.Vivox;
using VivoxUnity;

onComplete?.Invoke(true);
}
catch (Exception ex)
{ UnityEngine.Debug.LogWarning("Vivox failed to login: " + ex.Message);
{
UnityEngine.Debug.LogWarning("Vivox failed to login: " + ex.Message);
onComplete?.Invoke(false);
}
finally

{
// Special case: It's possible for the player to leave the lobby between the time we called BeginConnect and the time we hit this callback.
// If that's the case, we should abort the rest of the connection process.
if (m_channelSession.ChannelState == ConnectionState.Disconnecting || m_channelSession.ChannelState == ConnectionState.Disconnected)
if (m_channelSession.ChannelState == ConnectionState.Disconnecting ||
m_channelSession.ChannelState == ConnectionState.Disconnected)
UnityEngine.Debug.LogWarning("Vivox channel is already disconnecting. Terminating the channel connect sequence.");
UnityEngine.Debug.LogWarning(
"Vivox channel is already disconnecting. Terminating the channel connect sequence.");
HandleEarlyDisconnect();
return;
}

userHandler.OnChannelJoined(m_channelSession);
}
catch (Exception ex)
{ UnityEngine.Debug.LogWarning("Vivox failed to connect: " + ex.Message);
{
UnityEngine.Debug.LogWarning("Vivox failed to connect: " + ex.Message);
onComplete?.Invoke(false);
m_channelSession?.Disconnect();
}

// in the lobby. So, wait until the connection is completed before disconnecting in that case.
if (m_channelSession.ChannelState == ConnectionState.Connecting)
{
UnityEngine.Debug.LogWarning("Vivox channel is trying to disconnect while trying to complete its connection. Will wait until connection completes.");
UnityEngine.Debug.LogWarning(
"Vivox channel is trying to disconnect while trying to complete its connection. Will wait until connection completes.");
HandleEarlyDisconnect();
return;
}

(result) => { m_loginSession.DeleteChannelSession(id); m_channelSession = null; });
(result) =>
{
m_loginSession.DeleteChannelSession(id);
m_channelSession = null;
});
foreach (VivoxUserHandler userHandler in m_userHandlers)
userHandler.OnChannelLeft();
}

Locator.Get.UpdateSlow.Subscribe(DisconnectOnceConnected, 0.2f);
DisconnectOnceConnected();
private void DisconnectOnceConnected(float unused)
async void DisconnectOnceConnected()
if (m_channelSession?.ChannelState == ConnectionState.Connecting)
while (m_channelSession?.ChannelState == ConnectionState.Connecting)
{
await Task.Delay(200);
Locator.Get.UpdateSlow.Unsubscribe(DisconnectOnceConnected);
}
LeaveLobbyChannel();
}

m_loginSession.Logout();
}
}
}
}

55
Assets/Scripts/GameLobby/Infrastructure/ObserverBehaviour.cs


using UnityEngine;
using UnityEngine.Events;
namespace LobbyRelaySample
{
/// <summary>
/// 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.
/// </summary>
public abstract class ObserverBehaviour<T> : MonoBehaviour where T : Observed<T>
{
public T observed { get; set; }
public UnityEvent<T> OnObservedUpdated;
protected virtual void UpdateObserver(T obs)
{
observed = obs;
OnObservedUpdated?.Invoke(observed);
}
public void BeginObserving(T target)
{
if (target == null)
{
Debug.LogError($"Needs a Target of type {typeof(T)} to begin observing.", gameObject);
return;
}
UpdateObserver(target);
observed.onChanged += UpdateObserver;
}
public void EndObserving()
{
if (observed == null)
return;
if (observed.onChanged != null)
observed.onChanged -= UpdateObserver;
observed = null;
}
void Awake()
{
if (observed == null)
return;
BeginObserving(observed);
}
void OnDestroy()
{
if (observed == null)
return;
EndObserving();
}
}
}

11
Assets/Scripts/GameLobby/Infrastructure/Observed.cs.meta


fileFormatVersion: 2
guid: 7e82dff8a66b9e44cb9eb762c81fdaa0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

11
Assets/Scripts/GameLobby/Infrastructure/ObserverBehaviour.cs.meta


fileFormatVersion: 2
guid: f9fd3c3f6bcd8a445a4bd47c7508f631
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

11
Assets/Scripts/GameLobby/Infrastructure/UpdateSlow.cs.meta


fileFormatVersion: 2
guid: 9275e9be406280e4198464167a8b9186
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

11
Assets/Scripts/GameLobby/Infrastructure/Messenger.cs.meta


fileFormatVersion: 2
guid: 47c48511ace7f90418fc8cf37c957816
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

11
Assets/Scripts/GameLobby/Infrastructure/Locator.cs.meta


fileFormatVersion: 2
guid: 33dfcb5ccf8344b46826775bca225e34
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

101
Assets/Scripts/GameLobby/Infrastructure/Locator.cs


using System;
using System.Collections.Generic;
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.
/// (See http://gameprogrammingpatterns.com/service-locator.html to learn more.)
/// </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 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 ngo.InGameInputHandlerNoop());
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 ngo.IInGameInputHandler InGameInputHandler => Locate<ngo.IInGameInputHandler>();
public void Provide(ngo.IInGameInputHandler inputHandler) { ProvideAny(inputHandler); }
// As you add more Provided types, be sure their default implementations are included in the constructor.
}
}

145
Assets/Scripts/GameLobby/Infrastructure/UpdateSlow.cs


using System.Collections.Generic;
using UnityEngine;
using Stopwatch = System.Diagnostics.Stopwatch;
namespace LobbyRelaySample
{
public delegate void UpdateMethod(float dt);
/// <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.
/// </summary>
public class UpdateSlow : MonoBehaviour, IUpdateSlow
{
class Subscriber
{
public UpdateMethod updateMethod;
public readonly float period;
public float periodCurrent;
public Subscriber(UpdateMethod updateMethod, float period)
{
this.updateMethod = updateMethod;
this.period = period;
this.periodCurrent = 0;
}
}
[SerializeField]
[Tooltip("If a subscriber to slow update takes longer than this to execute, it can be automatically unsubscribed.")]
float m_durationToleranceMs = 10;
[SerializeField]
[Tooltip("We ordinarily automatically remove a subscriber that takes too long. Otherwise, we'll simply log.")]
bool m_doNotRemoveIfTooLong = false;
List<Subscriber> m_subscribers = new List<Subscriber>();
public void Awake()
{
Locator.Get.Provide(this);
}
public void OnDestroy()
{
m_subscribers.Clear(); // We should clean up references in case they would prevent garbage collection.
}
/// <summary>
/// Subscribe in order to have onUpdate called approximately every period seconds (or every frame, if period <= 0).
/// Don't assume that onUpdate will be called in any particular order compared to other subscribers.
/// </summary>
public void Subscribe(UpdateMethod onUpdate, float period)
{
if (onUpdate == null)
return;
foreach (Subscriber currSub in m_subscribers)
if (currSub.updateMethod.Equals(onUpdate))
return;
m_subscribers.Add(new Subscriber(onUpdate, period));
}
/// <summary>Safe to call even if onUpdate was not previously Subscribed.</summary>
public void Unsubscribe(UpdateMethod onUpdate)
{
for (int sub = m_subscribers.Count - 1; sub >= 0; sub--)
if (m_subscribers[sub].updateMethod.Equals(onUpdate))
m_subscribers.RemoveAt(sub);
}
void Update()
{
OnUpdate(Time.deltaTime);
}
/// <summary>
/// Each frame, advance all subscribers. Any that have hit their period should then act, though if they take too long they could be removed.
/// </summary>
public void OnUpdate(float dt)
{
for (int s = m_subscribers.Count - 1; s >= 0; s--) // Iterate in reverse in case we need to remove something.
{
var sub = m_subscribers[s];
sub.periodCurrent += Time.deltaTime;
if (sub.periodCurrent > sub.period)
{
Stopwatch stopwatch = new Stopwatch();
UpdateMethod onUpdate = sub.updateMethod;
if (onUpdate == null) // In case something forgets to Unsubscribe when it dies.
{ Remove(s, $"Did not Unsubscribe from UpdateSlow: {onUpdate.Target} : {onUpdate.Method}");
continue;
}
if (onUpdate.Target == null) // Detect a local function that cannot be Unsubscribed since it could go out of scope.
{ Remove(s, $"Removed local function from UpdateSlow: {onUpdate.Target} : {onUpdate.Method}");
continue;
}
if (onUpdate.Method.ToString().Contains("<")) // Detect an anonymous function that cannot be Unsubscribed, by checking for a character that can't exist in a declared method name.
{ Remove(s, $"Removed anonymous from UpdateSlow: {onUpdate.Target} : {onUpdate.Method}");
continue;
}
stopwatch.Restart();
onUpdate?.Invoke(sub.periodCurrent);
stopwatch.Stop();
sub.periodCurrent = 0;
if (stopwatch.ElapsedMilliseconds > m_durationToleranceMs)
{
if (!m_doNotRemoveIfTooLong)
Remove(s, $"UpdateSlow subscriber took too long, removing: {onUpdate.Target} : {onUpdate.Method}");
else
Debug.LogWarning($"UpdateSlow subscriber took too long: {onUpdate.Target} : {onUpdate.Method}");
}
}
}
void Remove(int index, string msg)
{
m_subscribers.RemoveAt(index);
Debug.LogError(msg);
}
}
public void OnReProvided(IUpdateSlow prevUpdateSlow)
{
if (prevUpdateSlow is UpdateSlow)
m_subscribers.AddRange((prevUpdateSlow as UpdateSlow).m_subscribers);
}
}
public interface IUpdateSlow : IProvidable<IUpdateSlow>
{
void OnUpdate(float dt);
void Subscribe(UpdateMethod onUpdate, float period);
void Unsubscribe(UpdateMethod onUpdate);
}
/// <summary>
/// A default implementation.
/// </summary>
public class UpdateSlowNoop : IUpdateSlow
{
public void OnUpdate(float dt) { }
public void Subscribe(UpdateMethod onUpdate, float period) { }
public void Unsubscribe(UpdateMethod onUpdate) { }
public void OnReProvided(IUpdateSlow prev) { }
}
}

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


using System;
namespace LobbyRelaySample
{
/// <summary>
/// Something that exposes some data that, when changed, an observer would want to be notified about automatically.
/// Used for UI elements and for keeping our local Lobby state synchronized with the remote Lobby service data.
/// (See http://gameprogrammingpatterns.com/observer.html to learn more.)
///
/// In your Observed child implementations, be sure to call OnChanged when setting the value of any property.
/// </summary>
/// <typeparam name="T">The type of object to be observed.</typeparam>
public abstract class Observed<T>
{
/// <summary>
/// If you want to copy all of the values, and only trigger OnChanged once.
/// </summary>
/// <param name="oldObserved"></param>
public abstract void CopyObserved(T oldObserved);
public Action<T> onChanged { get; set; }
public Action<T> onDestroyed { get; set; }
/// <summary>
/// Should be implemented into every public property of the observed
/// </summary>
/// <param name="observed">Instance of the observed that changed.</param>
protected void OnChanged(T observed)
{
onChanged?.Invoke(observed);
}
protected void OnDestroyed(T observed)
{
onDestroyed?.Invoke(observed);
}
}
}

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


using System;
using System.Collections.Generic;
using UnityEngine;
using Stopwatch = System.Diagnostics.Stopwatch;
namespace LobbyRelaySample
{
/// <summary>
/// Core mechanism for routing messages to arbitrary listeners.
/// 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
{
List<IReceiveMessages> m_receivers = new List<IReceiveMessages>();
private const float k_durationToleranceMs = 15;
// We need to handle subscribers who modify the receiver list, e.g. a subscriber who unsubscribes in their OnReceiveMessage.
private Queue<Action> m_pendingReceivers = new Queue<Action>();
private int m_recurseCount = 0;
/// <summary>
/// Assume that you won't receive messages in a specific order.
/// </summary>
public virtual void Subscribe(IReceiveMessages receiver)
{
m_pendingReceivers.Enqueue(() => { DoSubscribe(receiver); });
void DoSubscribe(IReceiveMessages receiver)
{
if (receiver != null && !m_receivers.Contains(receiver))
m_receivers.Add(receiver);
}
}
public virtual void Unsubscribe(IReceiveMessages receiver)
{
m_pendingReceivers.Enqueue(() => { DoUnsubscribe(receiver); });
void DoUnsubscribe(IReceiveMessages receiver)
{
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)
{
if (m_recurseCount > 5)
{ Debug.LogError("OnReceiveMessage recursion detected! Is something calling OnReceiveMessage when it receives a message?");
return;
}
if (m_recurseCount == 0) // This will increment if a new or existing subscriber calls OnReceiveMessage while handling a message. This is expected occasionally but shouldn't go too deep.
while (m_pendingReceivers.Count > 0)
m_pendingReceivers.Dequeue()?.Invoke();
m_recurseCount++;
Stopwatch stopwatch = new Stopwatch();
foreach (IReceiveMessages receiver in m_receivers)
{
stopwatch.Restart();
receiver.OnReceiveMessage(type, msg);
stopwatch.Stop();
if (stopwatch.ElapsedMilliseconds > k_durationToleranceMs)
Debug.LogWarning($"Message recipient \"{receiver}\" took too long to process message \"{msg}\" of type {type}");
}
m_recurseCount--;
}
public void OnReProvided(IMessenger previousProvider)
{
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,
QuickJoin = 5,
ChangeMenuState = 100,
ConfirmInGameState = 101,
LobbyUserStatus = 102,
UserSetEmote = 103,
ClientUserApproved = 104,
ClientUserSeekingDisapproval = 105,
EndGame = 106,
StartCountdown = 200,
CancelCountdown = 201,
CompleteCountdown = 202,
MinigameBeginning = 203,
InstructionsShown = 204,
MinigameEnding = 205,
DisplayErrorPopup = 300,
}
/// <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);
}
}

16
Assets/Scripts/GameLobby/NGO/IInGameInputHandler.cs


namespace LobbyRelaySample.ngo
{
/// <summary>
/// Something that will handle player input while in the game.
/// </summary>
public interface IInGameInputHandler : IProvidable<IInGameInputHandler>
{
void OnPlayerInput(ulong playerId, SymbolObject selectedSymbol);
}
public class InGameInputHandlerNoop : IInGameInputHandler
{
public void OnPlayerInput(ulong playerId, SymbolObject selectedSymbol) { }
public void OnReProvided(IInGameInputHandler previousProvider) { }
}
}

11
Assets/Scripts/GameLobby/NGO/IInGameInputHandler.cs.meta


fileFormatVersion: 2
guid: c4f5cf8c17e4ba64ca47aa981116b358
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

11
Assets/Scripts/GameLobby/Relay/RelayUtpClient.cs.meta


fileFormatVersion: 2
guid: 57add7ba7b318f04a8781c247344cab8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

11
Assets/Scripts/GameLobby/Relay/RelayUtpHost.cs.meta


fileFormatVersion: 2
guid: 567f8fffb5a28a446b1e98cbd2510b0f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

11
Assets/Scripts/GameLobby/Relay/RelayUtpSetup.cs.meta


fileFormatVersion: 2
guid: 5857e5666b1ecf844b8280729adb6e6e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

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


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

11
Assets/Scripts/GameLobby/Relay/RelayPendingApproval.cs.meta


fileFormatVersion: 2
guid: dbd38eacd3a3f464d8a9a1b69efe37db
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

298
Assets/Scripts/GameLobby/Relay/RelayUtpSetup.cs


using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Networking.Transport;
using Unity.Networking.Transport.Relay;
using Unity.Services.Relay.Models;
using UnityEngine;
/*
namespace LobbyRelaySample.relay
{
/// <summary>
/// Responsible for setting up a connection with Relay using Unity Transport (UTP). A Relay Allocation is created by the host, and then all players
/// bind UTP to that Allocation in order to send data to each other.
/// Must be a MonoBehaviour since the binding process doesn't have asynchronous callback options.
/// </summary>
/*public abstract class RelayUtpSetup : MonoBehaviour
{
protected bool m_isRelayConnected = false;
protected NetworkDriver m_networkDriver;
protected List<NetworkConnection> m_connections;
protected NetworkEndPoint m_endpointForServer;
protected LocalLobby m_localLobby;
protected LocalPlayer m_localUser;
protected Action<bool, RelayUtpClient> m_onJoinComplete;
public static string AddressFromEndpoint(NetworkEndPoint endpoint)
{
return endpoint.Address.Split(':')[0];
}
public void BeginRelayJoin(
LocalLobby localLobby,
LocalPlayer localUser,
Action<bool, RelayUtpClient> onJoinComplete)
{
m_localLobby = localLobby;
m_localUser = localUser;
m_onJoinComplete = onJoinComplete;
JoinRelay();
}
protected abstract void JoinRelay();
/// <summary>
/// Determine the server endpoint for connecting to the Relay server, for either an Allocation or a JoinAllocation.
/// If DTLS encryption is available, and there's a secure server endpoint available, use that as a secure connection. Otherwise, just connect to the Relay IP unsecured.
/// </summary>
public static NetworkEndPoint GetEndpointForAllocation(
List<RelayServerEndpoint> endpoints,
string ip,
int port,
out bool isSecure)
{
#if ENABLE_MANAGED_UNITYTLS
foreach (RelayServerEndpoint endpoint in endpoints)
{
if (endpoint.Secure && endpoint.Network == RelayServerEndpoint.NetworkOptions.Udp)
{
isSecure = true;
return NetworkEndPoint.Parse(endpoint.Host, (ushort) endpoint.Port);
}
}
#endif
isSecure = false;
return NetworkEndPoint.Parse(ip, (ushort) port);
}
/// <summary>
/// Shared behavior for binding to the Relay allocation, which is required for use.
/// Note that a host will send bytes from the Allocation it creates, whereas a client will send bytes from the JoinAllocation it receives using a relay code.
/// </summary>
protected void BindToAllocation(
NetworkEndPoint serverEndpoint,
byte[] allocationIdBytes,
byte[] connectionDataBytes,
byte[] hostConnectionDataBytes,
byte[] hmacKeyBytes,
int connectionCapacity,
bool isSecure)
{
RelayAllocationId allocationId = ConvertAllocationIdBytes(allocationIdBytes);
RelayConnectionData connectionData = ConvertConnectionDataBytes(connectionDataBytes);
RelayConnectionData hostConnectionData = ConvertConnectionDataBytes(hostConnectionDataBytes);
RelayHMACKey key = ConvertHMACKeyBytes(hmacKeyBytes);
var relayServerData = new RelayServerData(ref serverEndpoint, 0, ref allocationId, ref connectionData,
ref hostConnectionData, ref key, isSecure);
relayServerData
.ComputeNewNonce(); // For security, the nonce value sent when authenticating the allocation must be increased.
var networkSettings = new NetworkSettings();
m_networkDriver = NetworkDriver.Create(networkSettings.WithRelayParameters(ref relayServerData));
m_connections = new List<NetworkConnection>(connectionCapacity);
if (m_networkDriver.Bind(NetworkEndPoint.AnyIpv4) != 0)
Debug.LogError("Failed to bind to Relay allocation.");
else
StartCoroutine(WaitForBindComplete());
}
private IEnumerator WaitForBindComplete()
{
while (!m_networkDriver.Bound)
{
m_networkDriver.ScheduleUpdate().Complete();
yield return null;
}
OnBindingComplete();
}
protected abstract void OnBindingComplete();
#region UTP uses pointers instead of managed arrays for performance reasons, so we use these helper functions to convert them.
unsafe private static RelayAllocationId ConvertAllocationIdBytes(byte[] allocationIdBytes)
{
fixed (byte* ptr = allocationIdBytes)
{
return RelayAllocationId.FromBytePointer(ptr, allocationIdBytes.Length);
}
}
unsafe private static RelayConnectionData ConvertConnectionDataBytes(byte[] connectionData)
{
fixed (byte* ptr = connectionData)
{
return RelayConnectionData.FromBytePointer(ptr, RelayConnectionData.k_Length);
}
}
unsafe private static RelayHMACKey ConvertHMACKeyBytes(byte[] hmac)
{
fixed (byte* ptr = hmac)
{
return RelayHMACKey.FromBytePointer(ptr, RelayHMACKey.k_Length);
}
}
#endregion
private void OnDestroy()
{
if (!m_isRelayConnected && m_networkDriver.IsCreated)
m_networkDriver.Dispose();
}
}
/// <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]
private enum JoinState
{
None = 0,
Bound = 1,
Joined = 2
}
private JoinState m_joinState = JoinState.None;
private Allocation m_allocation;
protected override void JoinRelay()
{
RelayAPIInterface.AllocateAsync(m_localLobby.MaxPlayerCount.Value, OnAllocation);
}
private void OnAllocation(Allocation allocation)
{
m_allocation = allocation;
RelayAPIInterface.GetJoinCodeAsync(allocation.AllocationId, OnRelayCode);
bool isSecure = false;
m_endpointForServer = GetEndpointForAllocation(allocation.ServerEndpoints, allocation.RelayServer.IpV4,
allocation.RelayServer.Port, out isSecure);
BindToAllocation(m_endpointForServer, allocation.AllocationIdBytes, allocation.ConnectionData,
allocation.ConnectionData, allocation.Key, 16, isSecure);
}
private void OnRelayCode(string relayCode)
{
m_localLobby.RelayCode.Value = relayCode;
m_localLobby.RelayServer.Value =
new ServerAddress(AddressFromEndpoint(m_endpointForServer), m_endpointForServer.Port);
m_joinState |= JoinState.Joined;
#pragma warning disable 4014
CheckForComplete();
#pragma warning restore 4014
}
protected override void OnBindingComplete()
{
if (m_networkDriver.Listen() != 0)
{
Debug.LogError("RelayUtpSetupHost failed to bind to the Relay Allocation.");
m_onJoinComplete(false, null);
}
else
{
Debug.Log("Relay host is bound.");
m_joinState |= JoinState.Bound;
#pragma warning disable 4014
CheckForComplete();
#pragma warning restore 4014
}
}
private void CheckForComplete()
{
if (m_joinState == (JoinState.Joined | JoinState.Bound) && this != null
) // this will equal null (i.e. this component has been destroyed) if the host left the lobby during the Relay connection sequence.
{
m_isRelayConnected = true;
RelayUtpHost host = gameObject.AddComponent<RelayUtpHost>();
host.Initialize(m_networkDriver, m_connections, m_localUser, m_localLobby);
m_onJoinComplete(true, host);
var connectionInfo = $"{m_allocation.RelayServer.IpV4}:{m_allocation.RelayServer.Port}";
// await LobbyManager.Instance.UpdatePlayerRelayInfoAsync(m_allocation.AllocationId.ToString(), m_localLobby.RelayCode, connectionInfo);
}
}
}
/// <summary>
/// Client logic: Wait until the Relay 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;
protected override void JoinRelay()
{
m_localLobby.RelayCode.onChanged += OnRelayChanged;
}
private void OnRelayChanged(string relayCode)
{
if (string.IsNullOrEmpty(relayCode))
return;
RelayAPIInterface.JoinAsync(m_localLobby.RelayCode.Value, OnJoin);
m_localLobby.RelayCode.onChanged -= OnRelayChanged;
}
private void OnJoin(JoinAllocation joinAllocation)
{
if (joinAllocation == null || this == null
) // The returned JoinAllocation is null if allocation failed. this would be destroyed already if you quit the lobby while Relay is connecting.
return;
m_allocation = joinAllocation;
bool isSecure = false;
m_endpointForServer = GetEndpointForAllocation(joinAllocation.ServerEndpoints,
joinAllocation.RelayServer.IpV4, joinAllocation.RelayServer.Port, out isSecure);
BindToAllocation(m_endpointForServer, joinAllocation.AllocationIdBytes, joinAllocation.ConnectionData,
joinAllocation.HostConnectionData, joinAllocation.Key, 1, isSecure);
m_localLobby.RelayServer.Value =
new ServerAddress(AddressFromEndpoint(m_endpointForServer), m_endpointForServer.Port);
}
protected override void OnBindingComplete()
{
#pragma warning disable 4014
ConnectToServer();
#pragma warning restore 4014
}
private async Task ConnectToServer()
{
// Once the client is bound to the Relay server, send a connection request.
m_connections.Add(m_networkDriver.Connect(m_endpointForServer));
while (m_networkDriver.GetConnectionState(m_connections[0]) == NetworkConnection.State.Connecting)
{
m_networkDriver.ScheduleUpdate().Complete();
await Task.Delay(100);
}
if (m_networkDriver.GetConnectionState(m_connections[0]) != NetworkConnection.State.Connected)
{
Debug.LogError("RelayUtpSetupClient could not connect to the host.");
m_onJoinComplete(false, null);
}
else if (this != null)
{
m_isRelayConnected = true;
RelayUtpClient client = gameObject.AddComponent<RelayUtpClient>();
client.Initialize(m_networkDriver, m_connections, m_localUser, m_localLobby);
m_onJoinComplete(true, client);
var connectionInfo = $"{m_allocation.RelayServer.IpV4}:{m_allocation.RelayServer.Port}";
await GameManager.Instance.LobbyManager.UpdatePlayerRelayInfoAsync(m_allocation.AllocationId.ToString(), m_localLobby.RelayCode.Value,connectionInfo);
}
}
}
}*/

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


using System;
using System.Collections.Generic;
using Unity.Networking.Transport;
using UnityEngine;
/*
namespace LobbyRelaySample.relay
{
public enum Approval { OK = 0, GameAlreadyStarted }
/// <summary>
/// This observes the local player and updates remote players over Relay when there are local changes, demonstrating basic data transfer over the Unity Transport (UTP).
/// Created after the connection to Relay has been confirmed.
/// If you are using the Unity Networking Package, you can use their Relay instead of building your own packets.
/// </summary>
public class RelayUtpClient : MonoBehaviour, IDisposable // This is a MonoBehaviour merely to have access to Update.
{
protected LocalPlayer m_localUser;
protected LocalLobby m_localLobby;
protected NetworkDriver m_networkDriver;
protected List<NetworkConnection> m_connections; // For clients, this has just one member, but for hosts it will have more.
protected bool m_IsRelayConnected { get { return m_localLobby.RelayServer != null; } }
protected bool m_hasSentInitialMessage = false;
const float k_heartbeatPeriod = 5;
bool m_hasDisposed = false;
protected enum MsgType { Ping = 0, NewPlayer, PlayerApprovalState, ReadyState, PlayerName, Emote, StartCountdown, CancelCountdown, ConfirmInGame, EndInGame, PlayerDisconnect }
public virtual void Initialize(NetworkDriver networkDriver, List<NetworkConnection> connections, LocalPlayer localUser, LocalLobby localLobby)
{
m_localUser = localUser;
m_localLobby = localLobby;
//m_localUser.onChanged += OnLocalChange;
m_networkDriver = networkDriver;
m_connections = connections;
Locator.Get.UpdateSlow.Subscribe(UpdateSlow, k_heartbeatPeriod);
}
protected virtual void Uninitialize()
{
//m_localUser.onChanged -= OnLocalChange;
Leave();
Locator.Get.UpdateSlow.Unsubscribe(UpdateSlow);
// Don't clean up the NetworkDriver here, or else our disconnect message won't get through to the host. The host will handle cleaning up the connection.
}
public void Dispose()
{
if (!m_hasDisposed)
{
Uninitialize();
m_hasDisposed = true;
}
}
public void OnDestroy()
{
Dispose();
}
private void OnLocalChange(LocalPlayer localUser)
{
if (m_connections.Count == 0) // This could be the case for the host alone in the lobby.
return;
foreach (NetworkConnection conn in m_connections)
DoUserUpdate(m_networkDriver, conn, m_localUser);
}
private void Update()
{
OnUpdate();
}
/// <summary>
/// Clients need to send any data over UTP periodically, or else Relay will remove them from the allocation.
/// </summary>
private void UpdateSlow(float dt)
{
if (!m_IsRelayConnected) // If disconnected from Relay for some reason, we *want* this client to timeout.
return;
foreach (NetworkConnection connection in m_connections)
WriteByte(m_networkDriver, connection, "0", MsgType.Ping, 0); // The ID doesn't matter here, so send a minimal number of bytes.
}
protected virtual void OnUpdate()
{
if (!m_hasSentInitialMessage)
ReceiveNetworkEvents(m_networkDriver); // Just on the first execution, make sure to handle any events that accumulated while completing the connection.
m_networkDriver.ScheduleUpdate().Complete(); // This pumps all messages, which pings the Relay allocation and keeps it alive. It should be called no more often than ReceiveNetworkEvents.
ReceiveNetworkEvents(m_networkDriver); // This reads the message queue which was just updated.
if (!m_hasSentInitialMessage)
SendInitialMessage(m_networkDriver, m_connections[0]); // On a client, the 0th (and only) connection is to the host.
}
private void ReceiveNetworkEvents(NetworkDriver driver)
{
NetworkConnection conn;
DataStreamReader strm;
NetworkEvent.Type cmd;
while ((cmd = driver.PopEvent(out conn, out strm)) != NetworkEvent.Type.Empty) // NetworkConnection also has PopEvent, but NetworkDriver.PopEvent automatically includes new connections.
{
ProcessNetworkEvent(conn, strm, cmd);
}
}
// See the Write* methods for the expected event format.
private void ProcessNetworkEvent(NetworkConnection conn, DataStreamReader strm, NetworkEvent.Type cmd)
{
if (cmd == NetworkEvent.Type.Data)
{
List<byte> msgContents = new List<byte>(ReadMessageContents(ref strm));
if (msgContents.Count < 3) // We require at a minimum - Message type, the length of the user ID, and the user ID.
return;
MsgType msgType = (MsgType)msgContents[0];
int idLength = msgContents[1];
if (msgContents.Count < idLength + 2)
{ UnityEngine.Debug.LogWarning($"Relay client processed message of length {idLength}, but contents were of length {msgContents.Count}.");
return;
}
string id = System.Text.Encoding.UTF8.GetString(msgContents.GetRange(2, idLength).ToArray());
if (!CanProcessDataEventFor(conn, msgType, id))
return;
msgContents.RemoveRange(0, 2 + idLength);
if (msgType == MsgType.PlayerApprovalState)
{
Approval approval = (Approval)msgContents[0];
// if (approval == Approval.OK && !m_localUser.IsApproved)
// OnApproved(m_networkDriver, conn);
// else if (approval == Approval.GameAlreadyStarted)
// LogHandlerSettings.Instance.SpawnErrorPopup( "Rejected: Game has already started.");
}
else if (msgType == MsgType.PlayerName)
{
int nameLength = msgContents[0];
string name = System.Text.Encoding.UTF8.GetString(msgContents.GetRange(1, nameLength).ToArray());
m_localLobby.LocalPlayers[id].DisplayName.Value = name;
}
else if (msgType == MsgType.Emote)
{
EmoteType emote = (EmoteType)msgContents[0];
m_localLobby.LocalPlayers[id].Emote.Value = emote;
}
else if (msgType == MsgType.ReadyState)
{
PlayerStatus status = (PlayerStatus)msgContents[0];
m_localLobby.LocalPlayers[id].PlayerStatus.Value = status;
}
else if (msgType == MsgType.StartCountdown)
GameManager.Instance.BeginCountDown();
else if (msgType == MsgType.CancelCountdown)
GameManager.Instance.CancelCountDown();
else if (msgType == MsgType.ConfirmInGame)
GameManager.Instance.ConfirmIngameState();
else if (msgType == MsgType.EndInGame)
GameManager.Instance.EndGame();
ProcessNetworkEventDataAdditional(conn, msgType, id);
}
else if (cmd == NetworkEvent.Type.Disconnect)
ProcessDisconnectEvent(conn, strm);
}
protected virtual bool CanProcessDataEventFor(NetworkConnection conn, MsgType type, string id)
{
// Don't react to our own messages. Also, don't need to hold onto messages if the ID is absent; clients should be initialized and in the lobby before they send events.
// (Note that this enforces lobby membership before processing any events besides an approval request, so a client is unable to fully use Relay unless they're in the lobby.)
return true;// != m_localUser.ID && (m_localUser.IsApproved && m_localLobby.LocalPlayers.ContainsKey(id) || type == MsgType.PlayerApprovalState);
}
protected virtual void ProcessNetworkEventDataAdditional(NetworkConnection conn, 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.
string msg;
if (m_IsRelayConnected)
msg = "The host disconnected! Leaving the lobby.";
else
msg = "Connection to host was lost. Leaving the lobby.";
Debug.LogError(msg);
LogHandlerSettings.Instance.SpawnErrorPopup( msg);
Leave();
GameManager.Instance.ChangeMenuState(GameState.JoinMenu);
}
/// <summary>
/// UTP uses raw pointers for efficiency (i.e. C-style byte* instead of byte[]).
/// ReadMessageContents converts them back to byte arrays, assuming the stream contains 1 byte for array length followed by contents.
/// Any actual pointer manipulation and so forth happens service-side, so we simply need to convert back to a byte array here.
/// </summary>
unsafe private byte[] ReadMessageContents(ref DataStreamReader strm) // unsafe is required to access the pointer.
{
int length = strm.Length;
byte[] bytes = new byte[length];
fixed (byte* ptr = bytes)
{
strm.ReadBytes(ptr, length);
}
return 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.Value, MsgType.NewPlayer, 0);
m_hasSentInitialMessage = true;
}
private void OnApproved(NetworkDriver driver, NetworkConnection connection)
{
// m_localUser.IsApproved = true;
ForceFullUserUpdate(driver, connection, m_localUser);
}
/// <summary>
/// When player data is updated, send out events for just the data that actually changed.
/// </summary>
private void DoUserUpdate(NetworkDriver driver, NetworkConnection connection, LocalPlayer user)
{
}
/// <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, LocalPlayer 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.
WriteString(driver, connection, user.ID.Value, MsgType.PlayerName, user.DisplayName.Value);
WriteByte(driver, connection, user.ID.Value, MsgType.Emote, (byte)user.Emote.Value);
WriteByte(driver, connection, user.ID.Value, MsgType.ReadyState, (byte)user.PlayerStatus.Value);
}
/// <summary>
/// Write string data as: [1 byte: msgType] [1 byte: id length N] [N bytes: id] [1 byte: string length M] [M bytes: string]
/// </summary>
protected void WriteString(NetworkDriver driver, NetworkConnection connection, string id, MsgType msgType, string str)
{
byte[] idBytes = System.Text.Encoding.UTF8.GetBytes(id);
byte[] strBytes = System.Text.Encoding.UTF8.GetBytes(str);
List<byte> message = new List<byte>(idBytes.Length + strBytes.Length + 3); // Extra 3 bytes for the msgType plus the ID and message lengths.
message.Add((byte)msgType);
message.Add((byte)idBytes.Length);
message.AddRange(idBytes);
message.Add((byte)strBytes.Length);
message.AddRange(strBytes);
SendMessageData(driver, connection, message);
}
/// <summary>
/// Write byte data as: [1 byte: msgType] [1 byte: id length N] [N bytes: id] [1 byte: data]
/// </summary>
protected void WriteByte(NetworkDriver driver, NetworkConnection connection, string id, MsgType msgType, byte value)
{
byte[] idBytes = System.Text.Encoding.UTF8.GetBytes(id);
List<byte> message = new List<byte>(idBytes.Length + 3); // Extra 3 bytes for the msgType, ID length, and the byte value.
message.Add((byte)msgType);
message.Add((byte)idBytes.Length);
message.AddRange(idBytes);
message.Add(value);
SendMessageData(driver, connection, message);
}
private void SendMessageData(NetworkDriver driver, NetworkConnection connection, List<byte> message)
{
if (driver.BeginSend(connection, out var dataStream) == 0)
{
byte[] bytes = message.ToArray();
unsafe // Similarly to ReadMessageContents, our data must be converted to a pointer before being sent.
{
fixed (byte* bytesPtr = bytes)
{
dataStream.WriteBytes(bytesPtr, message.Count);
driver.EndSend(dataStream);
}
}
}
}
/// <summary>
/// Disconnect from Relay, usually while leaving the lobby. (You can also call this elsewhere to see how Lobby will detect a Relay disconnect automatically.)
/// </summary>
public virtual void Leave()
{
foreach (NetworkConnection connection in m_connections)
// If the client calls Disconnect, the host might not become aware right away (depending on when the PubSub messages get pumped), so send a message over UTP instead.
WriteByte(m_networkDriver, connection, m_localUser.ID.Value, MsgType.PlayerDisconnect, 0);
m_localLobby.RelayServer = null;
}
}
}
*/

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


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

11
Assets/Scripts/GameLobby/Tests/Editor/ObserverTests.cs.meta


fileFormatVersion: 2
guid: 7d38eb6d210d9f54cb24ba4e5c8ea199
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

82
Assets/Scripts/GameLobby/Tests/Editor/ObserverTests.cs


using LobbyRelaySample;
using System.Collections;
using UnityEngine;
using UnityEngine.TestTools;
using Assert = UnityEngine.Assertions.Assert;
namespace Test
{
public class ObserverTests
{
/// <summary>
/// When an observed value changes, the Observer should automatically update.
/// </summary>
[UnityTest]
public IEnumerator ObserverChangeWhenObservedChanged()
{
var observed = new TestObserved();
var observer = new GameObject("PlayerObserver").AddComponent<TestObserverBehaviour>();
observer.BeginObserving(observed);
Assert.AreNotEqual("NewName", observed.StringField);
Assert.AreNotEqual("NewName", observer.displayStringField);
observed.StringField = "NewName";
yield return null;
Assert.AreEqual(observed.StringField, observer.displayStringField);
}
/// <summary>
/// When an Observer is registered, it should receive the observed field's initial value.
/// </summary>
/// <returns></returns>
[UnityTest]
public IEnumerator ObserverRegistersInitialChanges()
{
var observed = new TestObserved();
observed.StringField = "NewName";
var observer = new GameObject("PlayerObserver").AddComponent<TestObserverBehaviour>();
Assert.AreNotEqual(observed.StringField, observer.displayStringField);
observer.BeginObserving(observed);
yield return null;
Assert.AreEqual(observed.StringField, observer.displayStringField);
}
// We just have a couple Observers that update some arbitrary member, in this case a string.
class TestObserved : Observed<TestObserved>
{
string m_stringField;
public string StringField
{
get => m_stringField;
set
{
m_stringField = value;
OnChanged(this);
}
}
public override void CopyObserved(TestObserved oldObserved)
{
m_stringField = oldObserved.StringField;
OnChanged(this);
}
}
class TestObserverBehaviour : ObserverBehaviour<TestObserved>
{
public string displayStringField;
protected override void UpdateObserver(TestObserved observed)
{
base.UpdateObserver(observed);
displayStringField = observed.StringField;
}
}
}
}

70
Assets/Scripts/GameLobby/Tests/PlayMode/RelayRoundTripTests.cs


using System;
using System.Collections;
using LobbyRelaySample;
using NUnit.Framework;
using Test.Tools;
using Unity.Services.Relay;
using Unity.Services.Relay.Models;
using UnityEngine;
using UnityEngine.TestTools;
namespace Test
{
/// <summary>
/// Accesses the Authentication and Relay services in order to ensure we can connect to Relay and retrieve a join code.
/// RelayUtp* wraps the Relay API, so go through that in practice. This simply ensures the connection to the Lobby service is functional.
///
/// If the tests pass, you can assume you are connecting to the Relay service itself properly.
/// </summary>
public class RelayRoundTripTests
{
[OneTimeSetUp]
public void Setup()
{
Auth.Authenticate("testProfile");
}
/// <summary>
/// Create a Relay allocation, request a join code, and then join. Note that this is purely to ensure the service is functioning;
/// in practice, the RelayUtpSetup does more work to bind to the allocation and has slightly different logic for hosts vs. clients.
/// </summary>
[UnityTest]
public IEnumerator DoBaseRoundTrip()
{
yield return new WaitUntil(Auth.DoneAuthenticating);
// Allocation
Allocation allocation = null;
yield return AsyncTestHelper.Await(async () => allocation = await Relay.Instance.CreateAllocationAsync(4));
Guid allocationId = allocation.AllocationId;
var allocationIP = allocation.RelayServer.IpV4;
var allocationPort = allocation.RelayServer.Port;
Assert.NotNull(allocationId);
Assert.NotNull(allocationIP);
Assert.NotNull(allocationPort);
// Join code retrieval
string joinCode = null;
yield return AsyncTestHelper.Await(async () =>
joinCode = await Relay.Instance.GetJoinCodeAsync(allocationId));
Assert.False(string.IsNullOrEmpty(joinCode));
// Joining with the join code
JoinAllocation joinResponse = null;
yield return AsyncTestHelper.Await(async () =>
joinResponse = await Relay.Instance.JoinAllocationAsync(joinCode));
var codeIp = joinResponse.RelayServer.IpV4;
var codePort = joinResponse.RelayServer.Port;
Assert.AreEqual(codeIp, allocationIP);
Assert.AreEqual(codePort, allocationPort);
}
}
}

11
Assets/Scripts/GameLobby/Tests/PlayMode/RelayRoundTripTests.cs.meta


fileFormatVersion: 2
guid: 43b56a07c330a61438726da307bccf3c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

11
Assets/Scripts/GameLobby/Tests/PlayMode/UpdateSlowTests.cs.meta


fileFormatVersion: 2
guid: 94ea9496be79c774e97136dbab1a99a1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

184
Assets/Scripts/GameLobby/Tests/PlayMode/UpdateSlowTests.cs


using LobbyRelaySample;
using NUnit.Framework;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.TestTools;
namespace Test
{
/// <summary>
/// Testing some edge cases with the UpdateSlow.
/// </summary>
public class UpdateSlowTests
{
private GameObject m_updateSlowObj;
private List<Subscriber> m_activeSubscribers = new List<Subscriber>(); // For cleaning up, in case an Assert prevents a Subscriber from taking care of itself.
/// <summary>Trivial Subscriber to do some action every UpdateSlow.</summary>
private class Subscriber : IDisposable
{
private Action m_thingToDo;
public float prevDt;
public Subscriber(Action thingToDo, float period)
{
Locator.Get.UpdateSlow.Subscribe(OnUpdate, period);
m_thingToDo = thingToDo;
}
public void Dispose()
{
Locator.Get.UpdateSlow.Unsubscribe(OnUpdate);
}
private void OnUpdate(float dt)
{
m_thingToDo?.Invoke();
prevDt = dt;
}
}
[OneTimeSetUp]
public void Setup()
{
m_updateSlowObj = new GameObject("UpdateSlowTest");
m_updateSlowObj.AddComponent<UpdateSlow>();
}
[OneTimeTearDown]
public void Teardown()
{
GameObject.Destroy(m_updateSlowObj);
}
[UnityTearDown]
public IEnumerator PerTestTeardown()
{
foreach (Subscriber sub in m_activeSubscribers)
sub.Dispose();
m_activeSubscribers.Clear();
yield break;
}
[UnityTest]
public IEnumerator BasicBehavior_MultipleSubs()
{
int updateCount = 0;
float period = 1.5f;
Subscriber sub = new Subscriber(() => { updateCount++; }, period);
m_activeSubscribers.Add(sub);
yield return null;
Assert.AreEqual(0, updateCount, "Update loop should be slow and not update per-frame.");
yield return new WaitForSeconds(period - 0.1f);
Assert.AreEqual(0, updateCount, "Assuming a default period of 1.5s and a reasonable frame rate, the slow update should still not have hit.");
yield return new WaitForSeconds(0.1f);
Assert.AreEqual(1, updateCount, "Slow update period should have passed.");
Assert.AreNotEqual(period, sub.prevDt, "Slow update should have received the actual amount of time that passed, not necessarily its period.");
Assert.True(sub.prevDt - period < 0.05f && sub.prevDt - period > 0, "The time delta received by slow update should match the actual time since their previous update.");
yield return new WaitForSeconds(period);
Assert.AreEqual(2, updateCount, "Did the slow update again.");
Assert.AreNotEqual(period, sub.prevDt, "Slow update should have received the full time delta, not just its period, again.");
Assert.True(sub.prevDt - period < 0.05f && sub.prevDt - period > 0, "The time delta received by slow update should match the actual time since their previous update, again.");
float period2 = period - 0.2f;
Subscriber sub2 = new Subscriber(() => { updateCount += 7; }, period2);
m_activeSubscribers.Add(sub2);
yield return new WaitForSeconds(period);
Assert.AreEqual(10, updateCount, "There are two subscribers now.");
Assert.True(sub.prevDt - period < 0.05f && sub.prevDt - period > 0, "Slow update on the first subscriber should have received the full time delta with two subscribers.");
Assert.True(sub2.prevDt - period2 < 0.05f && sub2.prevDt - period2 > 0, "Slow update on the second subscriber should receive the actual time, even if its period is shorter.");
sub2.Dispose();
yield return new WaitForSeconds(period);
Assert.AreEqual(11, updateCount, "Should have unsubscribed just the one subscriber.");
sub.Dispose();
yield return new WaitForSeconds(period);
Assert.AreEqual(11, updateCount, "Should have unsubscribed the remaining subscriber.");
}
[UnityTest]
public IEnumerator BasicBehavior_UpdateEveryFrame()
{
int updateCount = 0;
Subscriber sub = new Subscriber(() => { updateCount++; }, 0);
m_activeSubscribers.Add(sub);
yield return null;
Assert.AreEqual(1, updateCount, "Update loop should update per-frame if a subscriber opts for that (#1).");
yield return null;
Assert.AreEqual(2, updateCount, "Update loop should update per-frame if a subscriber opts for that (#2).");
Assert.AreEqual(sub.prevDt, Time.deltaTime, "Subscriber should receive the correct update time since their previous update.");
sub.Dispose();
yield return new WaitForSeconds(0.5f);
Assert.AreEqual(2, updateCount, "Should have unsubscribed the subscriber.");
}
[UnityTest]
public IEnumerator HandleLambda()
{
int updateCount = 0;
float period = 0.5f;
Locator.Get.UpdateSlow.Subscribe((dt) => { updateCount++; }, period);
LogAssert.Expect(LogType.Error, new Regex(".*Removed anonymous.*"));
yield return new WaitForSeconds(period + 0.1f);
Assert.AreEqual(0, updateCount, "Lambdas should not be permitted, since they can't be Unsubscribed.");
Locator.Get.UpdateSlow.Subscribe(ThisIsALocalFunction, period);
LogAssert.Expect(LogType.Error, new Regex(".*Removed local function.*"));
yield return new WaitForSeconds(period + 0.1f);
Assert.AreEqual(0, updateCount, "Local functions should not be permitted, since they can't be Unsubscribed.");
void ThisIsALocalFunction(float dt) { }
}
[UnityTest]
public IEnumerator SubscribeNoDuplicates()
{
dummyOnUpdateCalls = 0;
Locator.Get.UpdateSlow.Subscribe(DummyOnUpdate, 1);
Locator.Get.UpdateSlow.Subscribe(DummyOnUpdate, 0.1f);
yield return new WaitForSeconds(0.9f);
Assert.AreEqual(0, dummyOnUpdateCalls, "The second Subscribe call should not have gone through.");
yield return new WaitForSeconds(0.2f);
Assert.AreEqual(1, dummyOnUpdateCalls, "The first Subscribe call should have gone through.");
Locator.Get.UpdateSlow.Unsubscribe(DummyOnUpdate);
yield return new WaitForSeconds(1);
Assert.AreEqual(1, dummyOnUpdateCalls, "Unsubscribe should work as expected.");
}
private int dummyOnUpdateCalls = 0;
private void DummyOnUpdate(float dt) { dummyOnUpdateCalls++; }
[UnityTest]
public IEnumerator WhatIfASubscriberIsVerySlow()
{
int updateCount = 0;
string inefficientString = "";
float period = 1.5f;
Subscriber sub = new Subscriber(() =>
{ for (int n = 0; n < 12345; n++)
inefficientString += n.ToString();
updateCount++;
}, period);
m_activeSubscribers.Add(sub);
LogAssert.Expect(LogType.Error, new Regex(".*took too long.*"));
yield return new WaitForSeconds(period + 0.1f);
Assert.AreEqual(1, updateCount, "Executed the slow update.");
yield return new WaitForSeconds(period);
Assert.AreEqual(1, updateCount, "Should have removed the offending subscriber.");
}
}
}

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


namespace LobbyRelaySample.UI
{
/// <summary>
/// Observer UI panel base class. This allows UI elements to be shown or hidden based on an Observed element.
/// </summary>
public abstract class ObserverPanel<T> : UIPanelBase where T : Observed<T>
{
public abstract void ObservedUpdated(T observed);
}
}

11
Assets/Scripts/GameLobby/UI/ObserverPanel.cs.meta


fileFormatVersion: 2
guid: c66ac5417981e7644a211425067687d6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

/Assets/Scripts/GameLobby/Infrastructure/Actionvalue.cs.meta → /Assets/Scripts/GameLobby/Infrastructure/CallbackValue.cs.meta

/Assets/Scripts/GameLobby/Infrastructure/Actionvalue.cs → /Assets/Scripts/GameLobby/Infrastructure/CallbackValue.cs

正在加载...
取消
保存