UnityJacob
2 年前
当前提交
a91d6f8a
共有 40 个文件被更改,包括 105 次插入 和 1963 次删除
-
17Assets/Prefabs/GameManager.prefab
-
20Assets/Prefabs/UI/LobbyCanvas.prefab
-
3Assets/Scenes/mainScene.unity
-
36Assets/Scripts/GameLobby/Game/GameManager.cs
-
90Assets/Scripts/GameLobby/Infrastructure/CallbackValue.cs
-
2Assets/Scripts/GameLobby/Lobby/LobbySynchronizer.cs
-
2Assets/Scripts/GameLobby/NGO/SetupInGame.cs
-
4Assets/Scripts/GameLobby/UI/BackButtonUI.cs
-
2Assets/Scripts/GameLobby/UI/StartLobbyButtonUI.cs
-
37Assets/Scripts/GameLobby/Vivox/VivoxSetup.cs
-
55Assets/Scripts/GameLobby/Infrastructure/ObserverBehaviour.cs
-
11Assets/Scripts/GameLobby/Infrastructure/Observed.cs.meta
-
11Assets/Scripts/GameLobby/Infrastructure/ObserverBehaviour.cs.meta
-
11Assets/Scripts/GameLobby/Infrastructure/UpdateSlow.cs.meta
-
11Assets/Scripts/GameLobby/Infrastructure/Messenger.cs.meta
-
11Assets/Scripts/GameLobby/Infrastructure/Locator.cs.meta
-
101Assets/Scripts/GameLobby/Infrastructure/Locator.cs
-
145Assets/Scripts/GameLobby/Infrastructure/UpdateSlow.cs
-
38Assets/Scripts/GameLobby/Infrastructure/Observed.cs
-
129Assets/Scripts/GameLobby/Infrastructure/Messenger.cs
-
16Assets/Scripts/GameLobby/NGO/IInGameInputHandler.cs
-
11Assets/Scripts/GameLobby/NGO/IInGameInputHandler.cs.meta
-
11Assets/Scripts/GameLobby/Relay/RelayUtpClient.cs.meta
-
11Assets/Scripts/GameLobby/Relay/RelayUtpHost.cs.meta
-
11Assets/Scripts/GameLobby/Relay/RelayUtpSetup.cs.meta
-
57Assets/Scripts/GameLobby/Relay/RelayPendingApproval.cs
-
11Assets/Scripts/GameLobby/Relay/RelayPendingApproval.cs.meta
-
298Assets/Scripts/GameLobby/Relay/RelayUtpSetup.cs
-
296Assets/Scripts/GameLobby/Relay/RelayUtpClient.cs
-
220Assets/Scripts/GameLobby/Relay/RelayUtpHost.cs
-
11Assets/Scripts/GameLobby/Tests/Editor/ObserverTests.cs.meta
-
82Assets/Scripts/GameLobby/Tests/Editor/ObserverTests.cs
-
70Assets/Scripts/GameLobby/Tests/PlayMode/RelayRoundTripTests.cs
-
11Assets/Scripts/GameLobby/Tests/PlayMode/RelayRoundTripTests.cs.meta
-
11Assets/Scripts/GameLobby/Tests/PlayMode/UpdateSlowTests.cs.meta
-
184Assets/Scripts/GameLobby/Tests/PlayMode/UpdateSlowTests.cs
-
10Assets/Scripts/GameLobby/UI/ObserverPanel.cs
-
11Assets/Scripts/GameLobby/UI/ObserverPanel.cs.meta
-
0/Assets/Scripts/GameLobby/Infrastructure/CallbackValue.cs.meta
-
0/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; |
|||
} |
|||
} |
|
|||
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(); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 7e82dff8a66b9e44cb9eb762c81fdaa0 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: f9fd3c3f6bcd8a445a4bd47c7508f631 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: 9275e9be406280e4198464167a8b9186 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: 47c48511ace7f90418fc8cf37c957816 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: 33dfcb5ccf8344b46826775bca225e34 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
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.
|
|||
} |
|||
} |
|
|||
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) { } |
|||
} |
|||
|
|||
} |
|
|||
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); |
|||
} |
|||
} |
|||
} |
|
|||
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); |
|||
} |
|||
} |
|
|||
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) { } |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: c4f5cf8c17e4ba64ca47aa981116b358 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: 57add7ba7b318f04a8781c247344cab8 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: 567f8fffb5a28a446b1e98cbd2510b0f |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: 5857e5666b1ecf844b8280729adb6e6e |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
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;
|
|||
// }
|
|||
// }
|
|||
// }
|
|||
// }
|
|
|||
fileFormatVersion: 2 |
|||
guid: dbd38eacd3a3f464d8a9a1b69efe37db |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
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); |
|||
} |
|||
} |
|||
} |
|||
}*/ |
|
|||
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; |
|||
} |
|||
} |
|||
} |
|||
*/ |
|
|||
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;
|
|||
// }
|
|||
// }
|
|||
// }
|
|
|||
fileFormatVersion: 2 |
|||
guid: 7d38eb6d210d9f54cb24ba4e5c8ea199 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
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; |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
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); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 43b56a07c330a61438726da307bccf3c |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: 94ea9496be79c774e97136dbab1a99a1 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
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."); |
|||
} |
|||
} |
|||
} |
|
|||
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); |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: c66ac5417981e7644a211425067687d6 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
撰写
预览
正在加载...
取消
保存
Reference in new issue