浏览代码

Brought in latest staging changes.

/main/staging/ngo_gameplay_tweaks
当前提交
f63b493c
共有 18 个文件被更改,包括 141 次插入124 次删除
  1. 28
      Assets/Prefabs/NGO/PlayerCursor.prefab
  2. 13
      Assets/Scripts/Game/GameManager.cs
  3. 8
      Assets/Scripts/Infrastructure/AsyncRequest.cs
  4. 10
      Assets/Scripts/Lobby/LobbyAsyncRequests.cs
  5. 3
      Assets/Scripts/NGO/IInGameInputHandler.cs
  6. 20
      Assets/Scripts/NGO/InGameRunner.cs
  7. 8
      Assets/Scripts/NGO/NetworkedDataStore.cs
  8. 20
      Assets/Scripts/NGO/PlayerCursor.cs
  9. 6
      Assets/Scripts/NGO/RelayNGOUtpSetup.cs
  10. 8
      Assets/Scripts/NGO/Scorer.cs
  11. 77
      Assets/Scripts/NGO/SequenceSelector.cs
  12. 10
      Assets/Scripts/NGO/SetupInGame.cs
  13. 8
      Assets/Scripts/NGO/SymbolContainer.cs
  14. 3
      Assets/Scripts/NGO/SymbolData.cs
  15. 2
      Assets/Scripts/NGO/SymbolObject.cs
  16. 23
      Assets/Scripts/Relay/RelayUtpClient.cs
  17. 14
      Assets/Scripts/Relay/RelayUtpHost.cs
  18. 4
      Assets/Scripts/Tests/PlayMode/LobbyRoundtripTests.cs

28
Assets/Prefabs/NGO/PlayerCursor.prefab


m_Component:
- component: {fileID: 3227847727972158004}
- component: {fileID: -1321688216342888635}
- component: {fileID: 1724769857655924793}
- component: {fileID: 4403165428829500588}
- component: {fileID: -1476701697690489341}
m_Layer: 0

AlwaysReplicateAsRoot: 0
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
--- !u!114 &1724769857655924793
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3227847727972158006}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e96cb6065543e43c4a752faaa1468eb1, type: 3}
m_Name:
m_EditorClassIdentifier:
SyncPositionX: 1
SyncPositionY: 1
SyncPositionZ: 1
SyncRotAngleX: 0
SyncRotAngleY: 0
SyncRotAngleZ: 0
SyncScaleX: 0
SyncScaleY: 0
SyncScaleZ: 0
PositionThreshold: 0.001
RotAngleThreshold: 0.01
ScaleThreshold: 0.01
InLocalSpace: 0
Interpolate: 0
CanCommitToTransform: 0
--- !u!114 &4403165428829500588
MonoBehaviour:
m_ObjectHideFlags: 0

13
Assets/Scripts/Game/GameManager.cs


m_relaySetup = null;
}
if (m_relayClient != null)
{ Component.Destroy(m_relayClient);
m_relayClient = null;
{
m_relayClient.Dispose();
StartCoroutine(FinishCleanup());
// We need to delay slightly to give the disconnect message sent during Dispose time to reach the host, so that we don't destroy the connection without it being flushed first.
IEnumerator FinishCleanup()
{
yield return null;
Component.Destroy(m_relayClient);
m_relayClient = null;
}
}
}

8
Assets/Scripts/Infrastructure/AsyncRequest.cs


catch (Exception e)
{
ParseServiceException(e);
Exception eFull = new Exception($"Call stack before async call:\n{currentTrace}\n", e); // TODO: Are we still missing Relay exceptions after the update?
throw eFull;
UnityEngine.Debug.LogError($"AsyncRequest threw an exception. Call stack before async call:\n{currentTrace}\n"); // Note that we log here instead of creating a new Exception in case of a change in calling context during the async call. E.g. Relay has its own exception handling that would intercept this call stack.
throw;
}
finally
{ onComplete?.Invoke();

catch (Exception e)
{
ParseServiceException(e);
Exception eFull = new Exception($"Call stack before async call:\n{currentTrace}\n", e);
throw eFull;
UnityEngine.Debug.LogError($"AsyncRequest threw an exception. Call stack before async call:\n{currentTrace}\n");
throw;
}
finally
{ onComplete?.Invoke(result);

10
Assets/Scripts/Lobby/LobbyAsyncRequests.cs


private float m_timeSinceLastCall = float.MaxValue;
private readonly float m_cooldownTime;
private Queue<Action> m_pendingOperations = new Queue<Action>();
private bool m_isHandlingPending = false; // Just in case a pending operation tries to enqueue itself again.
if (!m_isHandlingPending)
m_pendingOperations.Enqueue(action);
m_pendingOperations.Enqueue(action);
}
private bool m_isInCooldown = false;

private void OnUpdate(float dt)
{
m_timeSinceLastCall += dt;
m_isHandlingPending = false; // (Backup in case a pending operation hit an exception.)
if (m_timeSinceLastCall >= m_cooldownTime)
{
IsInCooldown = false;

m_isHandlingPending = true;
while (m_pendingOperations.Count > 0)
int numPending = m_pendingOperations.Count; // It's possible a pending operation will re-enqueue itself or new operations, which should wait until the next loop.
for (; numPending > 0; numPending--)
m_isHandlingPending = false;
}
}
}

3
Assets/Scripts/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 id, SymbolObject selectedSymbol);

20
Assets/Scripts/NGO/InGameRunner.cs


}
/// <summary>
/// To verify the connection, invoke a server RPC call that then invokes a client RPC call.
/// To verify the connection, invoke a server RPC call that then invokes a client RPC call. After this, the actual setup occurs.
// (Besides, we will need to display instructions, which has downtime during which symbol objects can be spawned.)
}
[ClientRpc]

VerifyConnectionConfirm_ServerRpc(m_localUserData);
}
/// Once the connection is confirmed, check if all players have connected.
/// Once the connection is confirmed, spawn a player cursor and check if all players have connected.
NetworkObject playerCursor = NetworkObject.Instantiate(m_playerCursorPrefab);
NetworkObject playerCursor = NetworkObject.Instantiate(m_playerCursorPrefab); // Note that the client will not receive the cursor object reference, so the cursor must handle initializing itself.
playerCursor.SpawnWithOwnership(clientData.id);
playerCursor.name += clientData.name;
m_dataStore.AddPlayer(clientData.id, clientData.name);

}
}
/// <summary>
/// The game will begin either when all players have connected successfully or after a timeout.
/// </summary>
private void BeginGame()
{
m_canSpawnInGameObjects = true;

{
EndGame_ClientRpc();
yield return null;
SendEndGameSignal();
SendLocalEndGameSignal();
}
[ClientRpc]

return;
SendEndGameSignal();
SendLocalEndGameSignal();
private void SendEndGameSignal()
private void SendLocalEndGameSignal()
Locator.Get.Messenger.OnReceiveMessage(MessageType.EndGame, null); // We only send this message if the game completes, since the player remains in the lobby. If the player leaves with the back button, that instead sends them to the menu.
Locator.Get.Messenger.OnReceiveMessage(MessageType.EndGame, null); // We only send this message if the game completes, since the player remains in the lobby in that case. If the player leaves with the back button, that instead sends them to the menu.
m_onGameEnd();
}

8
Assets/Scripts/NGO/NetworkedDataStore.cs


namespace LobbyRelaySample.ngo
{
/// <summary>
/// A place to store data needed by networked behaviors. Each client has an instance, but the server's instance stores the actual data.
/// A place to store data needed by networked behaviors. Each client has an instance so they can retrieve data, but the server's instance stores the actual data.
/// </summary>
public class NetworkedDataStore : NetworkBehaviour
{

private Dictionary<ulong, PlayerData> m_playerData = new Dictionary<ulong, PlayerData>();
private ulong m_localId;
// Clients will need to retrieve the host's player data since it isn't synchronized. During that process, they will supply these callbacks
// Clients will need to retrieve the host's player data since it isn't synchronized. During that process, they will supply these callbacks.
// Since we use RPC calls to retrieve data, these callbacks need to be retained (since the scope of the method that the client calls to request
// data will be left in order to make the server RPC call).
private Action<PlayerData> m_onGetCurrentCallback;
private UnityEvent<PlayerData> m_onEachPlayerCallback;

}
/// <summary>
/// Retrieve the data for all players from 1st to last place, calling onEachPlayer for each.
/// Retrieve the data for all players in order from 1st to last place, calling onEachPlayer for each.
/// </summary>
public void GetAllPlayerData(UnityEvent<PlayerData> onEachPlayer)
{

20
Assets/Scripts/NGO/PlayerCursor.cs


namespace LobbyRelaySample.ngo
{
/// <summary>
/// Each player's cursor needs to be controlled by them and visible to the other players.
/// This cursor object will follow the owning player's mouse cursor and be visible to the other players.
/// The host will use this object's movement for detecting collision with symbol objects.
/// </summary>
[RequireComponent(typeof(Collider))]
public class PlayerCursor : NetworkBehaviour, IReceiveMessages

[SerializeField] private TMPro.TMP_Text m_nameOutput = default;
private Camera m_mainCamera;
private NetworkVariable<Vector3> m_position = new NetworkVariable<Vector3>(NetworkVariableReadPermission.Everyone, Vector3.zero);
private NetworkVariable<Vector3> m_position = new NetworkVariable<Vector3>(NetworkVariableReadPermission.Everyone, Vector3.zero); // (Using a NetworkTransform to sync position would also work.)
// If the local player cursor spawns before this cursor's owner, the owner's data won't be available yet. This is used to retrieve the data later.
private Action<ulong, Action<PlayerData>> m_retrieveName;
// The host is responsible for determining if a player has successfully selected a symbol object, since collisions should be handled serverside.

Locator.Get.Messenger.Subscribe(this);
}
// We can't pass object references as RPC calls by default, and we don't have a different convenient way to, I think,
// get the object spawned on the server to assign some member on the client, so instead let's retrieve dynamically what we need.
// I guess I'd just have a "singleton" to hold the references?
/// <summary>
/// This cursor is spawned in dynamically but needs references to some scene objects. Pushing full object references over RPC calls
/// is an option if we create custom data for each and ensure they're all spawned on the host correctly, but it's simpler to do
/// some one-time retrieval here instead.
/// This also sets up the visuals to make remote player cursors appear distinct from the local player's cursor.
/// </summary>
public override void OnNetworkSpawn()
{
m_retrieveName = NetworkedDataStore.Instance.GetPlayerData;

m_nameOutput.text = data.name;
}
// Don't love having the input here, but it doesn't need to be anywhere else.
// It'd be better to have a separate input handler, but we don't need the mouse input anywhere else, so keep it simple.
private bool IsSelectInputHit()
{
return Input.GetMouseButtonDown(0);

SendInput_ServerRpc(m_localId);
}
[ServerRpc] // Leave RequireOwnership = true for these so that only the player whose cursor this is can make updates.
[ServerRpc] // Leave (RequireOwnership = true) for these so that only the player whose cursor this is can make updates.
private void SetPosition_ServerRpc(Vector3 position)
{
m_position.Value = position;

6
Assets/Scripts/NGO/RelayNGOUtpSetup.cs


namespace LobbyRelaySample.ngo
{
/*
* When using the Relay adapter for UTP to connect the NetworkManager for Netcode for GameObjects (NGO), we need to provide the Allocation info without manually binding to it.
* To use Netcode for GameObjects (NGO), we use the Relay adapter for UTP, attached to a NetworkManager. This needs to be provided the Allocation info before we bind to it.
* In actual use, if you are using NGO for your game's networking, you would not also use the RelayUtpSetupHost/RelayUtpSetupClient at all, since their direct data transmission would be unnecessary.
* We keep both versions for this sample to demonstrate how each is set up, whether you want to just use Lobby + Relay or use NGO as well.
*/

/// </summary>
public class RelayUtpNGOSetupHost : MonoBehaviour // If this is a MonoBehaviour, it can be added to the InGameRunner object for easier cleanup on game end.
public class RelayUtpNGOSetupHost : MonoBehaviour // This is a MonoBehaviour so that it can be added to the InGameRunner object for easier cleanup on game end.
{
private SetupInGame m_setupInGame;
private LocalLobby m_localLobby;

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.
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;
bool isSecure = false;
var endpoint = RelayUtpSetup.GetEndpointForAllocation(joinAllocation.ServerEndpoints, joinAllocation.RelayServer.IpV4, joinAllocation.RelayServer.Port, out isSecure);

8
Assets/Scripts/NGO/Scorer.cs


using System.Collections;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using TMPro;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.Events;

private ulong m_localId;
[SerializeField] private TMP_Text m_scoreOutputText = default;
[Tooltip("When the game ends, this will be provided the results in order of rank (1st-place first, and so on).")]
[Tooltip("When the game ends, this will be called once for each player in order of rank (1st-place first, and so on).")]
[SerializeField] private UnityEvent<PlayerData> m_onGameEnd = default;
public override void OnNetworkSpawn()

int newScore = m_dataStore.UpdateScore(id, 1);
UpdateScoreOutput_ClientRpc(id, newScore);
}
public void ScoreFailure(ulong id)
{
int newScore = m_dataStore.UpdateScore(id, -1);

77
Assets/Scripts/NGO/SequenceSelector.cs


using System;
using System.Collections.Generic;
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.UI;

{
/// <summary>
/// Handles selecting the randomized sequence of symbols to spawn. This also selects a subset of the selected symbols to be the target
/// sequence that each player needs to select in order.
/// Handles selecting the randomized sequence of symbols to spawn, choosing a subset to be the ordered target sequence that each player needs to select.
/// This also handles selecting randomized positions for the symbols, and it sets up the target sequence animation for the instruction sequence.
/// </summary>
public class SequenceSelector : NetworkBehaviour, IReceiveMessages
{

private List<int> m_fullSequence = new List<int>(); // This is owned by the host, and each index is assigned as a NetworkVariable to each SymbolObject.
private NetworkList<int> m_targetSequence; // This is owned by the host but needs to be available to all clients, so it's a NetworkedList here.
private Dictionary<ulong, int> m_targetSequenceIndexPerPlayer = new Dictionary<ulong, int>(); // Also owned by the host, indexed by client ID.
private Dictionary<ulong, int> m_targetSequenceIndexPerPlayer = new Dictionary<ulong, int>(); // Each player's current target. Also owned by the host, indexed by client ID.
public void Awake()
{

public override void OnNetworkSpawn()
{
if (IsHost)
ChooseSymbols();
m_localId = NetworkManager.Singleton.LocalClientId;
AddClient_ServerRpc(m_localId);
}
private void ChooseSymbols()
{
// Choose some subset of the list of symbols to be present in this game, along with a target sequence.
int numSymbolTypes = 8;
List<int> symbolsForThisGame = SelectSymbols(m_symbolData.m_availableSymbols.Count, numSymbolTypes);
m_targetSequence.Add(symbolsForThisGame[0]);
m_targetSequence.Add(symbolsForThisGame[1]);
m_targetSequence.Add(symbolsForThisGame[2]);
// Then, ensure that the target sequence is present in order throughout most of the full set of symbols to spawn.
int numTargetSequences = (int)(k_symbolCount * 2 / 3f) / 3; // About 2/3 of the symbols will be definitely part of the target sequence.
for (; numTargetSequences >= 0; numTargetSequences--)
// Choose some subset of the list of symbols to be present in this game, along with a target sequence.
List<int> symbolsForThisGame = SelectSymbols(m_symbolData.m_availableSymbols.Count, 8);
m_targetSequence.Add(symbolsForThisGame[0]);
m_targetSequence.Add(symbolsForThisGame[1]);
m_targetSequence.Add(symbolsForThisGame[2]);
m_fullSequence.Add(m_targetSequence[2]); // We want a List instead of a Queue or Stack for faster insertion, but we will remove indices backwards so as to not resize other entries.
m_fullSequence.Add(m_targetSequence[1]);
m_fullSequence.Add(m_targetSequence[0]);
}
// Then, fill in with a good mix of the remaining symbols.
for (int n = 3; n < numSymbolTypes - 1; n++)
AddHalfRemaining(n, 2);
AddHalfRemaining(numSymbolTypes - 1, 1); // 1 as the divider ensures all remaining symbols get an index.
// Then, ensure that the target sequence is present in order throughout most of the full set of symbols to spawn.
int numTargetSequences = (int)(k_symbolCount * 2/3f) / 3; // About 2/3 of the symbols will be definitely part of the target sequence.
for (; numTargetSequences >= 0; numTargetSequences--)
{ m_fullSequence.Add(m_targetSequence[2]); // We want a List instead of a Queue or Stack for faster insertion, but we will remove indices backwards so as to not reshift other entries.
m_fullSequence.Add(m_targetSequence[1]);
m_fullSequence.Add(m_targetSequence[0]);
}
// Then, fill in with a good mix of the remaining symbols.
AddHalfRemaining(3, 2);
AddHalfRemaining(4, 2);
AddHalfRemaining(5, 2);
AddHalfRemaining(6, 2);
AddHalfRemaining(7, 1);
void AddHalfRemaining(int symbolIndex, int divider)
void AddHalfRemaining(int symbolIndex, int divider)
{
int remaining = k_symbolCount - m_fullSequence.Count;
for (int n = 0; n < remaining / divider; n++)
int remaining = k_symbolCount - m_fullSequence.Count;
for (int n = 0; n < remaining / divider; n++)
{
int randomIndex = UnityEngine.Random.Range(0, m_fullSequence.Count);
m_fullSequence.Insert(randomIndex, symbolsForThisGame[symbolIndex]);
}
int randomIndex = UnityEngine.Random.Range(0, m_fullSequence.Count);
m_fullSequence.Insert(randomIndex, symbolsForThisGame[symbolIndex]);
m_localId = NetworkManager.Singleton.LocalClientId;
AddClient_ServerRpc(m_localId);
}
[ServerRpc(RequireOwnership = false)]

public void Update()
{
// We can't guarantee timing with the host's selection of the target sequence, so retrieve it once it's available.
// A client can't guarantee timing with the host's selection of the target sequence, so retrieve it once it's available.
if (!m_hasReceivedTargetSequence && m_targetSequence.Count > 0)
{
for (int n = 0; n < m_targetSequence.Count; n++)

/// <summary>
/// If the index is correct, this will advance the current sequence index.
/// </summary>
/// <returns>True if the correct symbol index was chosen, false otherwise.</returns>
public bool ConfirmSymbolCorrect(ulong id, int symbolIndex)
{
int index = m_targetSequenceIndexPerPlayer[id];

}
/// <summary>
/// Used for the binary space partition (BSP) algorithm, which makes alternating "cuts" to subdivide rectangles.
/// Used for the binary space partition (BSP) algorithm, which makes alternating "cuts" to subdivide rectangles while maintaining a buffer of space between them.
/// This ensures all symbols will be randomly (though not uniformly) distributed without overlapping each other.
/// </summary>
private struct RectCut
{

points.Clear();
rects.Enqueue(new RectCut(bounds, -1)); // Start with an extra horizontal cut since the space is so tall.
// For each rect, subdivide it with an alternating cut, and then enqueue for recursion until enough points are chosen or the rects are all too small.
// This ensures a reasonable distribution of points which won't cause overlaps, though it will not necessarily be uniform.
// For each rect, subdivide it with an alternating cut, and then enqueue the two smaller rects for recursion until enough points are chosen or the rects are all too small.
while (rects.Count + points.Count < count && rects.Count > 0)
{
RectCut currRect = rects.Dequeue();

10
Assets/Scripts/NGO/SetupInGame.cs


go.SetActive(areVisible);
}
/// <summary>
/// The prefab with the NetworkManager contains all of the assets and logic needed to set up the NGO minigame.
/// The UnityTransport needs to also be set up with a new Allocation from Relay.
/// </summary>
private void CreateNetworkManager()
{
m_inGameManagerObj = GameObject.Instantiate(m_prefabNetworkManager);

else
m_inGameManagerObj.AddComponent<RelayUtpNGOSetupClient>().Initialize(this, m_lobby, () => { m_initializeTransport(transport); m_networkManager.StartClient(); });
}
private void OnConnectionVerified()
{ m_hasConnectedViaNGO = true;

{ m_localUser = user; // Same, regarding redundancy.
}
/// <summary>
/// Once the Relay Allocation is created, this passes its data to the UnityTransport.
/// </summary>
public void SetRelayServerData(string address, int port, byte[] allocationBytes, byte[] key, byte[] connectionData, byte[] hostConnectionData, bool isSecure)
{
m_initializeTransport = (transport) => { transport.SetRelayServerData(address, (ushort)port, allocationBytes, key, connectionData, hostConnectionData, isSecure); };

{
if (!m_hasConnectedViaNGO)
{
// If this player hasn't successfully connected via NGO, get booted.
// If this player hasn't successfully connected via NGO, forcibly exit the minigame.
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup, "Failed to join the game.");
OnGameEnd();
}

8
Assets/Scripts/NGO/SymbolContainer.cs


namespace LobbyRelaySample.ngo
{
// Note: The SymbolObjects, which will be children of this object, need their NetworkTransforms to have IsLocalSpace set to true. Otherwise, they might get desynced.
// (This would manifest as packet loss errors.)
// Also note: The initial position of the SymbolObject prefab is set to be outside the camera view in the Z-direction, so that it doesn't interpolate past the actual
// position when it spawns on a client (as opposed to in the Y-direction, since this SymbolContainer is also moving downward).
/// <summary>
/// Rather than track movement data for every symbol object, the symbols will all be parented under one container that will move.
/// It will not begin that movement until it both has been Spawned on the network and it has been informed that the game has started.

[SerializeField] private float m_speed = 1;
private bool m_isConnected = false;
private bool m_hasGameStarted = false;
/// <summary>
/// Verify both that the game has started and that the network connection is working before moving the symbols.
/// </summary>
private void OnGameStarted()
{
m_hasGameStarted = true;

3
Assets/Scripts/NGO/SymbolData.cs


namespace LobbyRelaySample.ngo
{
/// <summary>
/// Associates a symbol index with the sprite to display for symbol objects matching the index.
/// </summary>
public class SymbolData : ScriptableObject
{
[SerializeField] public List<Sprite> m_availableSymbols;

2
Assets/Scripts/NGO/SymbolObject.cs


{
// Actually destroying the symbol objects can cause garbage collection and other delays that might lead to desyncs.
// Disabling the networked object can also cause issues, so instead, just move the object, and it will be cleaned up once the NetworkManager is destroyed.
// (If object pooling, this is where to instead return it to the pool.)
// (If we used object pooling, this is where we would instead return it to the pool.)
this.transform.localPosition += Vector3.forward * 500;
}
}

23
Assets/Scripts/Relay/RelayUtpClient.cs


using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Unity.Networking.Transport;
using UnityEngine;

/// 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.
/// </summary>
public class RelayUtpClient : MonoBehaviour // This is a MonoBehaviour merely to have access to Update.
public class RelayUtpClient : MonoBehaviour, IDisposable // This is a MonoBehaviour merely to have access to Update.
{
protected LobbyUser m_localUser;
protected LocalLobby m_localLobby;

protected bool m_hasSentInitialMessage = false;
private const float k_heartbeatPeriod = 5;
private bool m_hasDisposed = false;
protected enum MsgType { Ping = 0, NewPlayer, PlayerApprovalState, ReadyState, PlayerName, Emote, StartCountdown, CancelCountdown, ConfirmInGame, EndInGame, PlayerDisconnect }

m_localUser.onChanged -= OnLocalChange;
Leave();
Locator.Get.UpdateSlow.Unsubscribe(UpdateSlow);
m_connections.Clear();
m_networkDriver.Dispose();
// 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;
}
Uninitialize();
Dispose();
}
private void OnLocalChange(LobbyUser localUser)

// The host disconnected, and Relay does not support host migration. So, all clients should disconnect.
string msg;
if (m_IsRelayConnected)
msg = "Host disconnected! Leaving the lobby.";
msg = "The host disconnected! Leaving the lobby.";
else
msg = "Connection to host was lost. Leaving the lobby.";

14
Assets/Scripts/Relay/RelayUtpHost.cs


{
base.Uninitialize();
Locator.Get.Messenger.Unsubscribe(this);
m_networkDriver.Dispose();
}
protected override void OnUpdate()

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.
{
conn.Disconnect(m_networkDriver);
conn.Disconnect(m_networkDriver);
m_connections.Remove(conn);
LobbyAsyncRequests.Instance.GetRateLimit(LobbyAsyncRequests.RequestType.Query).EnqueuePendingOperation(WaitToCheckForUsers);
// 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.
void WaitToCheckForUsers()
{ LobbyAsyncRequests.Instance.GetRateLimit(LobbyAsyncRequests.RequestType.Query).EnqueuePendingOperation(CheckIfAllUsersReady);
}
}
// If a client has changed state, check if this changes whether all players have readied.

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.
// TODO: If a client disconnects, see if remaining players are all already ready.
UnityEngine.Debug.LogError("Client disconnected!");
// 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 OnReceiveMessage(MessageType type, object msg)

4
Assets/Scripts/Tests/PlayMode/LobbyRoundtripTests.cs


[UnityTest]
public IEnumerator OnCompletesOnFailure()
{
LogAssert.Expect(LogType.Exception, new Regex(".*400 Bad Request.*"));
if (!m_didSigninComplete)
yield return new WaitForSeconds(3);
if (!m_didSigninComplete)

LogAssert.ignoreFailingMessages = true; // Multiple errors will appears for the exception.
LobbyAPIInterface.CreateLobbyAsync("ThisStringIsInvalidHere", "lobby name", 123, false, m_mockUserData, (r) => { didComplete = (r == null); });
float timeout = 5;
while (didComplete == null && timeout > 0)

LogAssert.ignoreFailingMessages = false;
Assert.Greater(timeout, 0, "Timeout check");
Assert.NotNull(didComplete, "Should have called onComplete, even if the async request failed.");
Assert.True(didComplete, "The returned object will be null, so expect to need to handle it.");

正在加载...
取消
保存