// 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.
ExceptioneFull=newException($"Call stack before async call:\n{currentTrace}\n",e);// TODO: Are we still missing Relay exceptions after the update?
throweFull;
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(Exceptione)
{
ParseServiceException(e);
ExceptioneFull=newException($"Call stack before async call:\n{currentTrace}\n",e);
throweFull;
UnityEngine.Debug.LogError($"AsyncRequest threw an exception. Call stack before async call:\n{currentTrace}\n");
privateboolm_isHandlingPending=false;// Just in case a pending operation tries to enqueue itself again.
if(!m_isHandlingPending)
m_pendingOperations.Enqueue(action);
m_pendingOperations.Enqueue(action);
}
privateboolm_isInCooldown=false;
privatevoidOnUpdate(floatdt)
{
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)
intnumPending=m_pendingOperations.Count;// It's possible a pending operation will re-enqueue itself or new operations, which should wait until the next loop.
NetworkObjectplayerCursor=NetworkObject.Instantiate(m_playerCursorPrefab);// Note that the client will not receive the cursor object reference, so the cursor must handle initializing itself.
/// The game will begin either when all players have connected successfully or after a timeout.
/// </summary>
privatevoidBeginGame()
{
m_canSpawnInGameObjects=true;
{
EndGame_ClientRpc();
yieldreturnnull;
SendEndGameSignal();
SendLocalEndGameSignal();
}
[ClientRpc]
return;
SendEndGameSignal();
SendLocalEndGameSignal();
privatevoidSendEndGameSignal()
privatevoidSendLocalEndGameSignal()
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.
/// 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.
privateNetworkVariable<Vector3>m_position=newNetworkVariable<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.
publicclassRelayUtpNGOSetupHost:MonoBehaviour// If this is a MonoBehaviour, it can be added to the InGameRunner object for easier cleanup on game end.
publicclassRelayUtpNGOSetupHost:MonoBehaviour// This is a MonoBehaviour so that it can be added to the InGameRunner object for easier cleanup on game end.
{
privateSetupInGamem_setupInGame;
privateLocalLobbym_localLobby;
privatevoidOnJoin(JoinAllocationjoinAllocation)
{
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.
/// 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.
privateList<int>m_fullSequence=newList<int>();// This is owned by the host, and each index is assigned as a NetworkVariable to each SymbolObject.
privateNetworkList<int>m_targetSequence;// This is owned by the host but needs to be available to all clients, so it's a NetworkedList here.
privateDictionary<ulong,int>m_targetSequenceIndexPerPlayer=newDictionary<ulong,int>();// Also owned by the host, indexed by client ID.
privateDictionary<ulong,int>m_targetSequenceIndexPerPlayer=newDictionary<ulong,int>();// Each player's current target. Also owned by the host, indexed by client ID.
publicvoidAwake()
{
publicoverridevoidOnNetworkSpawn()
{
if(IsHost)
ChooseSymbols();
m_localId=NetworkManager.Singleton.LocalClientId;
AddClient_ServerRpc(m_localId);
}
privatevoidChooseSymbols()
{
// Choose some subset of the list of symbols to be present in this game, along with a target sequence.
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(intn=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.
intnumTargetSequences=(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.
/// 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>
privatestructRectCut
{
points.Clear();
rects.Enqueue(newRectCut(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.
// 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]privatefloatm_speed=1;
privateboolm_isConnected=false;
privateboolm_hasGameStarted=false;
/// <summary>
/// Verify both that the game has started and that the network connection is working before moving the symbols.
// 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 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>
publicclassRelayUtpClient:MonoBehaviour// This is a MonoBehaviour merely to have access to Update.
publicclassRelayUtpClient:MonoBehaviour,IDisposable// This is a MonoBehaviour merely to have access to Update.
// 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.
}
publicvoidDispose()
{
if(!m_hasDisposed)
{
Uninitialize();
m_hasDisposed=true;
}
Uninitialize();
Dispose();
}
privatevoidOnLocalChange(LobbyUserlocalUser)
// The host disconnected, and Relay does not support host migration. So, all clients should disconnect.
stringmsg;
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.";
elseif(msgType==MsgType.PlayerDisconnect)// Clients message the host when they intend to disconnect, or else the host ends up keeping the connection open.
// 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.
// 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.