浏览代码

Merging staging and UI Bugs

/main/staging
当前提交
94ea6209
共有 19 个文件被更改,包括 403 次插入602 次删除
  1. 13
      Assets/Prefabs/UI/PlayerInteractionPanel.prefab
  2. 27
      Assets/Scenes/mainScene.unity
  3. 109
      Assets/Scripts/Game/GameManager.cs
  4. 32
      Assets/Scripts/Game/LobbyUser.cs
  5. 31
      Assets/Scripts/Game/LocalLobby.cs
  6. 25
      Assets/Scripts/Infrastructure/Messenger.cs
  7. 2
      Assets/Scripts/Infrastructure/Observed.cs
  8. 12
      Assets/Scripts/Lobby/LobbyAsyncRequests.cs
  9. 53
      Assets/Scripts/Lobby/LobbyContentHeartbeat.cs
  10. 4
      Assets/Scripts/Lobby/ToLocalLobby.cs
  11. 29
      Assets/Scripts/Relay/RelayUtpClient.cs
  12. 39
      Assets/Scripts/Relay/RelayUtpHost.cs
  13. 2
      Assets/Scripts/Relay/RelayUtpSetup.cs
  14. 10
      Assets/Scripts/UI/CountdownUI.cs
  15. 472
      Assets/TextMesh Pro/Resources/Fonts & Materials/Roboto-Bold SDF.asset
  16. 66
      Assets/Scripts/Game/Countdown.cs
  17. 11
      Assets/Scripts/Game/Countdown.cs.meta
  18. 57
      Assets/Scripts/Relay/RelayPendingApproval.cs
  19. 11
      Assets/Scripts/Relay/RelayPendingApproval.cs.meta

13
Assets/Prefabs/UI/PlayerInteractionPanel.prefab


- component: {fileID: 4155147274821193738}
- component: {fileID: 4969504009400302254}
- component: {fileID: 8388542991005336197}
- component: {fileID: 1918870058264573867}
m_Layer: 5
m_Name: CountDownUI
m_TagString: Untagged

m_Interactable: 0
m_BlocksRaycasts: 0
m_IgnoreParentGroups: 0
--- !u!114 &1918870058264573867
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 580481917308754637}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d125c6cac111c6442ac5b07a1f313fa4, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1 &609596107351326384
GameObject:
m_ObjectHideFlags: 0

27
Assets/Scenes/mainScene.unity


m_Script: {fileID: 11500000, guid: 78d292f3bd9f1614cb744dcb4fe3ac12, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &1014339014 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 4601477874412550420, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}
m_PrefabInstance: {fileID: 8628454959146822954}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5b3b588e7ae40ec4ca35fdb9404513ab, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &1217229506 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 6326316181187829680, guid: 9aae991127b410c45a001ecd7f75311d, type: 3}

objectReference: {fileID: 0}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalLobbyObservers.Array.size
value: 8
value: 7
objectReference: {fileID: 0}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_vivoxUserHandlers.Array.data[0]

- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalLobbyObservers.Array.data[0]
value:
objectReference: {fileID: 1014339014}
objectReference: {fileID: 1412109061}
objectReference: {fileID: 1412109061}
objectReference: {fileID: 297599733}
objectReference: {fileID: 297599733}
objectReference: {fileID: 2130620598}
objectReference: {fileID: 2130620598}
objectReference: {fileID: 2074106027}
objectReference: {fileID: 2074106027}
objectReference: {fileID: 309485569}
objectReference: {fileID: 309485569}
objectReference: {fileID: 2126854580}
objectReference: {fileID: 2126854580}
objectReference: {fileID: 1511612118}
- target: {fileID: 7716713811812636910, guid: f80fc24bab3dcda459a2669321e2e5a4, type: 3}
propertyPath: m_LocalLobbyObservers.Array.data[7]
value:

109
Assets/Scripts/Game/GameManager.cs


/// </summary>
public void OnReceiveMessage(MessageType type, object msg)
{
if (type == MessageType.RenameRequest)
{
string name = (string)msg;
if (string.IsNullOrWhiteSpace(name))
{
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup, "Empty Name not allowed."); // Lobby error type, then HTTP error type.
return;
}
m_localUser.DisplayName = (string)msg;
}
else if (type == MessageType.CreateLobbyRequest)
if (type == MessageType.CreateLobbyRequest)
{
var createLobbyData = (LocalLobby)msg;
LobbyAsyncRequests.Instance.CreateLobbyAsync(createLobbyData.LobbyName, createLobbyData.MaxPlayerCount, createLobbyData.Private, m_localUser, (r) =>

},
m_lobbyColorFilter);
}
else if (type == MessageType.ChangeGameState)
{ SetGameState((GameState)msg);
else if (type == MessageType.QuickJoin)
{
LobbyAsyncRequests.Instance.QuickJoinLobbyAsync(m_localUser, m_lobbyColorFilter, (r) =>
{ lobby.ToLocalLobby.Convert(r, m_localLobby);
OnJoinedLobby();
},
OnFailedJoin);
}
else if (type == MessageType.RenameRequest)
{
string name = (string)msg;
if (string.IsNullOrWhiteSpace(name))
{
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup, "Empty Name not allowed."); // Lobby error type, then HTTP error type.
return;
}
m_localUser.DisplayName = (string)msg;
}
else if (type == MessageType.ClientUserApproved)
{ ConfirmApproval();
}
else if (type == MessageType.UserSetEmote)
{ EmoteType emote = (EmoteType)msg;

{ m_localUser.UserStatus = (UserStatus)msg;
}
else if (type == MessageType.StartCountdown)
{ BeginCountDown();
{ m_localLobby.State = LobbyState.CountDown;
m_localLobby.CountDownTime = 0;
}
else if (type == MessageType.CompleteCountdown)
{ if (m_relayClient is RelayUtpHost)
(m_relayClient as RelayUtpHost).SendInGameState();
}
else if (type == MessageType.ChangeGameState)
{ SetGameState((GameState)msg);
}
else if (type == MessageType.ConfirmInGameState)
{ m_localUser.UserStatus = UserStatus.InGame;

{ m_localLobby.State = LobbyState.Lobby;
m_localLobby.CountDownTime = 0;
else if (type == MessageType.QuickJoin)
{
LobbyAsyncRequests.Instance.QuickJoinLobbyAsync(m_localUser, m_lobbyColorFilter, (r) =>
{ lobby.ToLocalLobby.Convert(r, m_localLobby);
OnJoinedLobby();
},
OnFailedJoin);
}
else if (type == MessageType.SetPlayerSound)
{
var playerSound = (LobbyUserAudio)msg;
}
}
}
private void SetGameState(GameState state)
{

LobbyAsyncRequests.Instance.BeginTracking(m_localLobby.LobbyID);
m_lobbyContentHeartbeat.BeginTracking(m_localLobby, m_localUser);
SetUserLobbyState();
StartRelayConnection();
StartVivoxJoin();
// The host has the opportunity to reject incoming players, but to do so the player needs to connect to Relay without having game logic available.
// In particular, we should prevent players from joining voice chat until they are approved.
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Connecting);
if (m_localUser.IsHost)
{
StartRelayConnection();
StartVivoxJoin();
}
else
{
StartRelayConnection();
}
}
private void OnLeftLobby()

m_relaySetup = gameObject.AddComponent<RelayUtpSetupHost>();
else
m_relaySetup = gameObject.AddComponent<RelayUtpSetupClient>();
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Connecting);
m_relaySetup.BeginRelayJoin(m_localLobby, m_localUser, OnRelayConnected);
void OnRelayConnected(bool didSucceed, RelayUtpClient client)

}
m_relayClient = client;
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Lobby);
if (m_localUser.IsHost)
CompleteRelayConnection();
else
Debug.Log("Client is now waiting for approval...");
}
}

doConnection?.Invoke();
}
private void BeginCountDown()
{
if (m_localLobby.State == LobbyState.CountDown)
return;
m_localLobby.CountDownTime = 4;
m_localLobby.State = LobbyState.CountDown;
StartCoroutine(CountDown());
}
/// <summary>
/// The CountdownUI will pick up on changes to the lobby's countdown timer. This can be interrupted if the lobby leaves the countdown state (via a CancelCountdown message).
/// </summary>
private IEnumerator CountDown()
private void ConfirmApproval()
while (m_localLobby.CountDownTime > 0)
if (!m_localUser.IsHost && m_localUser.IsApproved)
yield return null;
if (m_localLobby.State != LobbyState.CountDown)
yield break;
m_localLobby.CountDownTime -= Time.deltaTime;
CompleteRelayConnection();
StartVivoxJoin();
}
if (m_relayClient is RelayUtpHost)
(m_relayClient as RelayUtpHost).SendInGameState();
private void CompleteRelayConnection()
{
OnReceiveMessage(MessageType.LobbyUserStatus, UserStatus.Lobby);
}
private void SetUserLobbyState()

{
m_localLobby.CopyObserved(new LocalLobby.LobbyData(), new Dictionary<string, LobbyUser>());
m_localLobby.AddPlayer(m_localUser); // As before, the local player will need to be plugged into UI before the lobby join actually happens.
m_localLobby.CountDownTime = 0;
m_localLobby.RelayServer = null;
}

32
Assets/Scripts/Game/LobbyUser.cs


[Serializable]
public class LobbyUser : Observed<LobbyUser>
{
public LobbyUser(bool isHost = false, string displayName = null, string id = null, EmoteType emote = EmoteType.None, UserStatus userStatus = UserStatus.Menu)
public LobbyUser(bool isHost = false, string displayName = null, string id = null, EmoteType emote = EmoteType.None, UserStatus userStatus = UserStatus.Menu, bool isApproved = false)
m_data = new UserData(isHost, displayName, id, emote, userStatus);
m_data = new UserData(isHost, displayName, id, emote, userStatus, isApproved);
}
#region Local UserData

public string ID { get; set; }
public EmoteType Emote { get; set; }
public UserStatus UserStatus { get; set; }
public bool IsApproved { get; set; }
public UserData(bool isHost, string displayName, string id, EmoteType emote, UserStatus userStatus)
public UserData(bool isHost, string displayName, string id, EmoteType emote, UserStatus userStatus, bool isApproved)
{
IsHost = isHost;
DisplayName = displayName;

IsApproved = isApproved;
}
}

{
m_data = new UserData(false, m_data.DisplayName, m_data.ID, EmoteType.None, UserStatus.Menu); // ID and DisplayName should persist since this might be the local user.
m_data = new UserData(false, m_data.DisplayName, m_data.ID, EmoteType.None, UserStatus.Menu, false); // ID and DisplayName should persist since this might be the local user.
}
#endregion

DisplayName = 2,
Emote = 4,
ID = 8,
UserStatus = 16
}
UserStatus = 16,
IsApproved = 32
}
private UserMembers m_lastChanged;
public UserMembers LastChanged => m_lastChanged;

m_data.IsHost = value;
m_lastChanged = UserMembers.IsHost;
OnChanged(this);
if (value)
IsApproved = true;
}
}
}

m_userStatus = value;
m_lastChanged = UserMembers.UserStatus;
OnChanged(this);
}
}
}
public bool IsApproved // Clients joining the lobby should be approved by the host before they can interact.
{
get => m_data.IsApproved;
set
{
if (!m_data.IsApproved && value) // Don't be un-approved except by a call to ResetState.
{
m_data.IsApproved = value;
m_lastChanged = UserMembers.IsApproved;
OnChanged(this);
Locator.Get.Messenger.OnReceiveMessage(MessageType.ClientUserApproved, null);
}
}
}

31
Assets/Scripts/Game/LocalLobby.cs


public int MaxPlayerCount { get; set; }
public LobbyState State { get; set; }
public LobbyColor Color { get; set; }
public long State_LastEdit { get; set; }
public long Color_LastEdit { get; set; }
public LobbyData(LobbyData existing)
{

MaxPlayerCount = existing.MaxPlayerCount;
State = existing.State;
Color = existing.Color;
State_LastEdit = existing.State_LastEdit;
Color_LastEdit = existing.Color_LastEdit;
}
public LobbyData(string lobbyCode)

MaxPlayerCount = -1;
State = LobbyState.Lobby;
Color = LobbyColor.None;
State_LastEdit = 0;
Color_LastEdit = 0;
}
}

get { return new LobbyData(m_data); }
}
float m_CountDownTime;
public float CountDownTime
{
get { return m_CountDownTime; }
set
{
m_CountDownTime = value;
OnChanged(this);
}
}
ServerAddress m_relayServer;

set
{
m_data.State = value;
m_data.State_LastEdit = DateTime.Now.Ticks;
OnChanged(this);
}
}

{
if (m_data.Color != value)
{ m_data.Color = value;
m_data.Color_LastEdit = DateTime.Now.Ticks;
OnChanged(this);
}
}

{
// It's possible for the host to edit the lobby in between the time they last pushed lobby data and the time their pull for new lobby data completes.
// If that happens, the edit will be lost, so instead we maintain the time of last edit to detect that case.
var pendingState = data.State;
var pendingColor = data.Color;
if (m_data.State_LastEdit > data.State_LastEdit)
pendingState = m_data.State;
if (m_data.Color_LastEdit > data.Color_LastEdit)
pendingColor = m_data.Color;
m_data.State = pendingState;
m_data.Color = pendingColor;
if (currUsers == null)
m_LobbyUsers = new Dictionary<string, LobbyUser>();
else

25
Assets/Scripts/Infrastructure/Messenger.cs


JoinLobbyRequest = 2,
CreateLobbyRequest = 3,
QueryLobbies = 4,
ChangeGameState = 5,
LobbyUserStatus = 6,
UserSetEmote = 7,
EndGame = 8,
StartCountdown = 9,
CancelCountdown = 10,
ConfirmInGameState = 11,
DisplayErrorPopup = 12,
SetPlayerSound = 13,
QuickJoin = 14
QuickJoin = 5,
ChangeGameState = 100,
ConfirmInGameState = 101,
LobbyUserStatus = 102,
UserSetEmote = 103,
ClientUserApproved = 104,
ClientUserSeekingDisapproval = 105,
EndGame = 106,
StartCountdown = 200,
CancelCountdown = 201,
CompleteCountdown = 202,
DisplayErrorPopup = 300,
}
/// <summary>

2
Assets/Scripts/Infrastructure/Observed.cs


onChanged?.Invoke(observed);
}
protected void OnDestroy(T observed)
protected void OnDestroyed(T observed)
{
onDestroyed?.Invoke(observed);
}

12
Assets/Scripts/Lobby/LobbyAsyncRequests.cs


dataCurr.Add(dataNew.Key, dataObj);
}
LobbyAPIInterface.UpdatePlayerAsync(m_lastKnownLobby.Id, playerId, dataCurr, (r) => { onComplete?.Invoke(); }, null, null);
LobbyAPIInterface.UpdatePlayerAsync(m_lastKnownLobby.Id, playerId, dataCurr, (result) => {
if (result != null)
m_lastKnownLobby = result; // Store the most up-to-date lobby now since we have it, instead of waiting for the next heartbeat.
onComplete?.Invoke();
}, null, null);
}
/// <summary>

dataCurr.Add(dataNew.Key, dataObj);
}
LobbyAPIInterface.UpdateLobbyAsync(lobby.Id, dataCurr, (r) => { onComplete?.Invoke(); });
LobbyAPIInterface.UpdateLobbyAsync(lobby.Id, dataCurr, (result) => {
if (result != null)
m_lastKnownLobby = result;
onComplete?.Invoke();
});
}
/// <summary>

53
Assets/Scripts/Lobby/LobbyContentHeartbeat.cs


using System.Collections.Generic;
using System;
using System.Collections.Generic;
using LobbyRemote = Unity.Services.Lobbies.Models.Lobby;
namespace LobbyRelaySample

/// </summary>
public class LobbyContentHeartbeat
public class LobbyContentHeartbeat : IReceiveMessages
private bool m_isAwaitingQuery = false;
private int m_awaitingQueryCount = 0;
private const float k_approvalMaxTime = 10; // Used for determining if a user should timeout if they are unable to connect.
private float m_lifetime = 0;
Locator.Get.Messenger.Subscribe(this);
m_lifetime = 0;
}
public void EndTracking()

Locator.Get.Messenger.Unsubscribe(this);
if (m_localLobby != null)
m_localLobby.onChanged -= OnLocalLobbyChanged;
m_localLobby = null;

m_shouldPushData = true;
}
public void OnReceiveMessage(MessageType type, object msg)
{
if (type == MessageType.ClientUserSeekingDisapproval)
{
bool shouldDisapprove = m_localLobby.State != LobbyState.Lobby; // By not refreshing, it's possible to have a lobby in the lobby list UI after its countdown starts and then try joining.
if (shouldDisapprove)
(msg as Action<relay.Approval>)?.Invoke(relay.Approval.GameAlreadyStarted);
}
}
/// <summary>
/// If there have been any data changes since the last update, push them to Lobby. Regardless, pull for the most recent data.
/// (Unless we're already awaiting a query, in which case continue waiting.)

if (m_isAwaitingQuery || m_localLobby == null)
m_lifetime += dt;
if (m_awaitingQueryCount > 0 || m_localLobby == null)
m_isAwaitingQuery = true; // Note that because we make async calls, if one of them fails and doesn't call our callback, this will never be reset to false.
if (!m_localUser.IsApproved && m_lifetime > k_approvalMaxTime)
{
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup, "Connection attempt timed out!");
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeGameState, GameState.JoinMenu);
}
if (m_shouldPushData)
PushDataToLobby();
else

void PushDataToLobby()
{
if (m_localUser == null)
{
m_isAwaitingQuery = false;
return; // Don't revert m_shouldPushData yet, so that we can retry.
}
{
m_awaitingQueryCount++;
else
DoPlayerDataPush();
}
m_awaitingQueryCount++;
DoPlayerDataPush();
LobbyAsyncRequests.Instance.UpdateLobbyDataAsync(RetrieveLobbyData(m_localLobby), () => { DoPlayerDataPush(); });
LobbyAsyncRequests.Instance.UpdateLobbyDataAsync(RetrieveLobbyData(m_localLobby), () => { if (--m_awaitingQueryCount <= 0) OnRetrieve(); });
LobbyAsyncRequests.Instance.UpdatePlayerDataAsync(RetrieveUserData(m_localUser), () => { m_isAwaitingQuery = false; });
LobbyAsyncRequests.Instance.UpdatePlayerDataAsync(RetrieveUserData(m_localUser), () => { if (--m_awaitingQueryCount <= 0) OnRetrieve(); });
m_isAwaitingQuery = false;
LobbyRemote lobbyRemote = LobbyAsyncRequests.Instance.CurrentLobby;
if (lobbyRemote == null) return;
bool prevShouldPush = m_shouldPushData;

data.Add("RelayCode", lobby.RelayCode);
data.Add("State", ((int)lobby.State).ToString()); // Using an int is smaller than using the enum state's name.
data.Add("Color", ((int)lobby.Color).ToString());
data.Add("State_LastEdit", lobby.Data.State_LastEdit.ToString());
data.Add("Color_LastEdit", lobby.Data.Color_LastEdit.ToString());
return data;
}

4
Assets/Scripts/Lobby/ToLocalLobby.cs


MaxPlayerCount = lobby.MaxPlayers,
RelayCode = lobby.Data?.ContainsKey("RelayCode") == true ? lobby.Data["RelayCode"].Value : null, // By providing RelayCode through the lobby data with Member visibility, we ensure a client is connected to the lobby before they could attempt a relay connection, preventing timing issues between them.
State = lobby.Data?.ContainsKey("State") == true ? (LobbyState) int.Parse(lobby.Data["State"].Value) : LobbyState.Lobby,
Color = lobby.Data?.ContainsKey("Color") == true ? (LobbyColor) int.Parse(lobby.Data["Color"].Value) : LobbyColor.None
Color = lobby.Data?.ContainsKey("Color") == true ? (LobbyColor) int.Parse(lobby.Data["Color"].Value) : LobbyColor.None,
State_LastEdit = lobby.Data?.ContainsKey("State_LastEdit") == true ? long.Parse(lobby.Data["State_LastEdit"].Value) : 0,
Color_LastEdit = lobby.Data?.ContainsKey("Color_LastEdit") == true ? long.Parse(lobby.Data["Color_LastEdit"].Value) : 0
};
Dictionary<string, LobbyUser> lobbyUsers = new Dictionary<string, LobbyUser>();

29
Assets/Scripts/Relay/RelayUtpClient.cs


using System.Collections.Generic;
using Unity.Networking.Transport;
using UnityEngine;
using MsgType = LobbyRelaySample.relay.RelayUtpSetup.MsgType;
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.

protected bool m_hasSentInitialMessage = false;
private const float k_heartbeatPeriod = 5;
protected enum MsgType { Ping = 0, NewPlayer, PlayerApprovalState, ReadyState, PlayerName, Emote, StartCountdown, CancelCountdown, ConfirmInGame, EndInGame, PlayerDisconnect }
public virtual void Initialize(NetworkDriver networkDriver, List<NetworkConnection> connections, LobbyUser localUser, LocalLobby localLobby)
{

}
string id = System.Text.Encoding.UTF8.GetString(msgContents.GetRange(2, idLength).ToArray());
if (id == m_localUser.ID || !m_localLobby.LobbyUsers.ContainsKey(id)) // We don't need to hold onto messages if the ID is absent; users are initialized before they send events.
if (!CanProcessDataEventFor(conn, msgType, id))
if (msgType == MsgType.PlayerName)
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)
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup, "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());

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 id != m_localUser.ID && (m_localUser.IsApproved && m_localLobby.LobbyUsers.ContainsKey(id) || type == MsgType.PlayerApprovalState);
}
protected virtual void ProcessNetworkEventDataAdditional(NetworkConnection conn, MsgType msgType, string id) { }
protected virtual void ProcessDisconnectEvent(NetworkConnection conn, DataStreamReader strm)
{

private void SendInitialMessage(NetworkDriver driver, NetworkConnection connection)
{
WriteByte(driver, connection, m_localUser.ID, MsgType.NewPlayer, 0);
ForceFullUserUpdate(driver, connection, m_localUser); // Assuming this is only created after the Relay connection is successful.
}
private void OnApproved(NetworkDriver driver, NetworkConnection connection)
{
m_localUser.IsApproved = true;
ForceFullUserUpdate(driver, connection, m_localUser);
}
/// <summary>

39
Assets/Scripts/Relay/RelayUtpHost.cs


using System.Collections.Generic;
using Unity.Networking.Transport;
using MsgType = LobbyRelaySample.relay.RelayUtpSetup.MsgType;
namespace LobbyRelaySample.relay
{

}
/// <summary>
/// When a new client connects, they need to be updated with the current state of everyone else.
/// 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.
private void OnNewConnection(NetworkConnection conn)
private void OnNewConnection(NetworkConnection conn, string id)
{
new RelayPendingApproval(conn, NewConnectionApprovalResult, id);
}
private void NewConnectionApprovalResult(NetworkConnection conn, Approval result)
{
WriteByte(m_networkDriver, conn, m_localUser.ID, MsgType.PlayerApprovalState, (byte)result);
if (result == Approval.OK && conn.IsCreated)
{
foreach (var user in m_localLobby.LobbyUsers)
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)
foreach (var user in m_localLobby.LobbyUsers) // The host includes itself here since we don't necessarily have an ID available, but it will ignore its own messages on arrival.
ForceFullUserUpdate(m_networkDriver, conn, user.Value);
// 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 && (m_localLobby.LobbyUsers.ContainsKey(id) && m_connections.Contains(conn) || type == MsgType.NewPlayer);
}
protected override void ProcessNetworkEventDataAdditional(NetworkConnection conn, MsgType msgType, string id)

WriteByte(m_networkDriver, otherConn, id, msgType, value);
}
}
else if (msgType == MsgType.NewPlayer) // This ensures clients in builds are sent player state once they establish that they can send (and receive) events.
OnNewConnection(conn);
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.
{
conn.Disconnect(m_networkDriver);

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;
m_connections.Add(conn);
OnNewConnection(conn); // This ensures that clients in editors are sent player state once they establish a connection. The timing differs slightly from builds.
// 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.
}
}

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;
}
}

2
Assets/Scripts/Relay/RelayUtpSetup.cs


protected LobbyUser m_localUser;
protected Action<bool, RelayUtpClient> m_onJoinComplete;
public enum MsgType { Ping = 0, NewPlayer, ReadyState, PlayerName, Emote, StartCountdown, CancelCountdown, ConfirmInGame, EndInGame, PlayerDisconnect }
public void BeginRelayJoin(LocalLobby localLobby, LobbyUser localUser, Action<bool, RelayUtpClient> onJoinComplete)
{
m_localLobby = localLobby;

10
Assets/Scripts/UI/CountdownUI.cs


/// This countdown is purely visual, to give clients a moment if they need to un-ready before entering the game;
/// clients will actually wait for a message from the host confirming that they are in the game, instead of assuming the game is ready to go when the countdown ends.
/// </summary>
public class CountdownUI : LocalLobbyObserver
public class CountdownUI : ObserverBehaviour<Countdown.Data>
protected override void UpdateObserver(LocalLobby obs)
protected override void UpdateObserver(Countdown.Data data)
base.UpdateObserver(obs);
if (observed.CountDownTime <= 0)
base.UpdateObserver(data);
if (observed.TimeLeft <= 0)
m_CountDownText.SetText($"Starting in: {observed.CountDownTime:0}");
m_CountDownText.SetText($"Starting in: {observed.TimeLeft:0}"); // Note that the ":0" formatting rounds, not truncates.
}
}
}

472
Assets/TextMesh Pro/Resources/Fonts & Materials/Roboto-Bold SDF.asset
文件差异内容过多而无法显示
查看文件

66
Assets/Scripts/Game/Countdown.cs


using System;
using UnityEngine;
namespace LobbyRelaySample
{
/// <summary>
/// Runs the countdown to the in-game state. While the start of the countdown is synced via Relay, the countdown itself is handled locally,
/// since precise timing isn't necessary.
/// </summary>
[RequireComponent(typeof(UI.CountdownUI))]
public class Countdown : MonoBehaviour, IReceiveMessages
{
public class Data : Observed<Countdown.Data>
{
private float m_timeLeft;
public float TimeLeft
{
get => m_timeLeft;
set
{ m_timeLeft = value;
OnChanged(this);
}
}
public override void CopyObserved(Data oldObserved) { /*No-op, since this is unnecessary.*/ }
}
private Data m_data = new Data();
private UI.CountdownUI m_ui;
private const int k_countdownTime = 4;
public void OnEnable()
{
if (m_ui == null)
m_ui = GetComponent<UI.CountdownUI>();
m_data.TimeLeft = -1;
Locator.Get.Messenger.Subscribe(this);
m_ui.BeginObserving(m_data);
}
public void OnDisable()
{
Locator.Get.Messenger.Unsubscribe(this);
m_ui.EndObserving();
}
public void OnReceiveMessage(MessageType type, object msg)
{
if (type == MessageType.StartCountdown)
{
m_data.TimeLeft = k_countdownTime;
}
else if (type == MessageType.CancelCountdown)
{
m_data.TimeLeft = -1;
}
}
public void Update()
{
if (m_data.TimeLeft < 0)
return;
m_data.TimeLeft -= Time.deltaTime;
if (m_data.TimeLeft < 0)
Locator.Get.Messenger.OnReceiveMessage(MessageType.CompleteCountdown, null);
}
}
}

11
Assets/Scripts/Game/Countdown.cs.meta


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

57
Assets/Scripts/Relay/RelayPendingApproval.cs


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

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


fileFormatVersion: 2
guid: dbd38eacd3a3f464d8a9a1b69efe37db
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
正在加载...
取消
保存