其他部分
我把包括大厅,创建房间,选择颜色等功能放到其他部分进行阐述,因为这一部分属于Gameplay以外的部分,且随游戏不同改变的程度较小
玩家准备
玩家准备是指当所有玩家都载入游戏场景,每个人都按下互动键后,游戏才会正式进入倒计时开始。
这里要做的是创建额外的列表存储所有玩家的准备状态,同步游戏的状态,同步倒计时
在GameManager
中存储所有玩家的存储状态,并在客户端中使用ServerRPC
修改服务器中的玩家存储状态,在本地修改UI,服务器做逻辑判断,满足条件则更改游戏状态,游戏状态通过网络变量进行存储,在变量的OnNetworkChanged
上绑定原本的OnStateChanged
,保证原本的委托可以在网络中正常调用
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
state.OnValueChanged += State_OnValueChanged;
}
private void State_OnValueChanged(State previousValue, State newValue)
{
OnStateChanged?.Invoke(this, EventArgs.Empty); //如果转到开始则进行倒计时UI
}
private void GameInput_OnInteractAction(object sender, EventArgs e) {
if (state.Value == State.WaitingToStart) {
isLocalPlayerReady = true;
OnLocalPlayerReadyChanged.Invoke(this, EventArgs.Empty); //从教程UI转到等待UI
SetReadyServerRPC();
}
}
[ServerRpc(RequireOwnership = false)]
private void SetReadyServerRPC(ServerRpcParams serverRpcParams = default)
{
playerReadyDictionary[serverRpcParams.Receive.SenderClientId] = true;
bool allReady = true;
foreach (ulong clientId in NetworkManager.Singleton.ConnectedClientsIds)
{
if (!playerReadyDictionary.ContainsKey(clientId) || !playerReadyDictionary[clientId])
{
allReady = false;
break;
}
}
if (allReady)
{
state.Value = State.CountdownToStart;
}
}
暂停同步
直接在玩家暂停的地方调用ServerRPC
,让所有客户端暂停,可以分开设置多人暂停和本地暂停,但也可以直接让二者使用一套暂停
public void TogglePauseGame() {
TogglePauseGameServerRPC();
}
[ServerRpc(RequireOwnership = false)]
private void TogglePauseGameServerRPC()
{
TogglePauseGameClientRPC();
}
[ClientRpc]
private void TogglePauseGameClientRPC()
{
isGamePaused = !isGamePaused;
if (isGamePaused)
{
Time.timeScale = 0f;
OnGamePaused?.Invoke(this, EventArgs.Empty);
}
else
{
Time.timeScale = 1f;
OnGameUnpaused?.Invoke(this, EventArgs.Empty);
}
}
玩家退出
这里主要需要担心的是玩家退出时的不同情况
-
玩家持有东西退出时
调用服务器消息,服务器负责销毁持有的物品
public override void OnNetworkSpawn() //代替了Awake { if(IsServer) { Debug.Log("Server"); NetworkManager.Singleton.OnClientDisconnectCallback += NetworkManager_OnClientDisconnectCallback; } } private void NetworkManager_OnClientDisconnectCallback(ulong clientId) { if(OwnerClientId == clientId && HasKitchenObject()) { KitchenObject.DestroyKitchenObject(GetKitchenObject()); } }
-
主机退出时
为所有客户端提示主机已退出,重新进行游戏的UI
private void Start() { NetworkManager.Singleton.OnClientDisconnectCallback += NetworkManager_OnClientDisconnectCallback; Hide(); } private void NetworkManager_OnClientDisconnectCallback(ulong clientID) { if(clientID == NetworkManager.ServerClientId) { recipesDeliveredText.text = DeliveryManager.Instance.GetSuccessfulRecipesAmount().ToString(); Show(); } }
-
暂停时退出
暂停时退出是一个特殊的状态,如果像教程一样,允许一个或多个人暂停,并且只有所有人取消暂停时才会真正恢复游戏,那么如果有人在暂停时退出,其他人必须手动暂停恢复。因此需要在有人退出时额外进行一次刷新操作,确保没人暂停时恢复游戏
这个逻辑是教程的,我直接所有人都正常暂停,因此省去了这个情况
延迟加入
游戏时长比较短,且基本基于RPC进行同步,不能同步之前的状态,需要做大量的判断,因此如果延迟加入,应当不允许加入
连接场景
在游戏中,存在主场景,大厅场景,选择玩家场景和游戏场景,这些场景需要有效的链接和判断才能进入下一个场景
首先是连接这些场景的逻辑部分
- 进入Main场景
- Play进入Lobby场景
- 点击Create进入主机状态并使用NetworkManager的加载方法加载SelectionScene,点击Join则进入客户端状态并被动同步到SelectionScene
- 在SlectionScene有一个同步的NetworkObject,该物体负责同步每个机器上所有人的准备状态,一旦所有人准备完成,主机就会将所有人送入GameScene
- 退出到主界面时,玩家会清除残留的NetworkManager和KitchenGameMultiplayer
一些Debug部分
我做了很多DEBUG保证了重开,主机客户端换着开,退出,后都能保证全局变量的正确性
下面是关键部分处理
KitchenGameMultiplayer.cs
相关逻辑只能绑定一次,多次绑定会报错,因此断开连接后要将绑定解除
public void StartHost()
{
NetworkManager.Singleton.ConnectionApprovalCallback += Server_ConnectionApprovalCallback;
NetworkManager.Singleton.OnClientConnectedCallback += Server_OnClientConnectedCallback;
NetworkManager.Singleton.OnClientDisconnectCallback += Server_OnClientDisconnectCallback;
NetworkManager.Singleton.StartHost();
}
public void StartClient()
{
NetworkManager.Singleton.OnClientDisconnectCallback += Client_OnClientDisconnectCallback;
NetworkManager.Singleton.OnClientConnectedCallback += Client_OnClientConnectedCallback;
NetworkManager.Singleton.StartClient();
}
private void Client_OnClientDisconnectCallback(ulong obj)
{
NetworkManager.Singleton.OnClientDisconnectCallback -= Client_OnClientDisconnectCallback;
NetworkManager.Singleton.OnClientConnectedCallback -= Client_OnClientConnectedCallback;
}
private void Server_OnClientDisconnectCallback(ulong obj)
{
NetworkManager.Singleton.ConnectionApprovalCallback -= Server_ConnectionApprovalCallback;
NetworkManager.Singleton.OnClientConnectedCallback -= Server_OnClientConnectedCallback;
NetworkManager.Singleton.OnClientDisconnectCallback -= Server_OnClientDisconnectCallback;
}
NetworkManager
和KitchenGameMultiplayer
的单例问题
多次进入同一场景会出现关卡不会销毁物体变多的情况,并且在联网状况下二者哈希值一样也会报错,需要删除多余GameObject
public class MyNetworkManager : NetworkManager
{
private void Awake()
{
if (Singleton)
Destroy(gameObject);
}
}
public class KitchenGameMultiplayer : NetworkBehaviour
{
private void Awake()
{
if(Instance)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
HostDisconnectUI
中绑定了NetworkManager
的相关委托,在销毁UI时应进行解绑
private void Start() {
NetworkManager.Singleton.OnClientDisconnectCallback += NetworkManager_OnClientDisconnectCallback;
Hide();
}
private void OnDestroy()
{
NetworkManager.Singleton.OnClientDisconnectCallback -= NetworkManager_OnClientDisconnectCallback;
}
上面是我在不删除原本创建的DontDestroyOnLoad实例下做的,更简单的办法是和作者一样,在MainMenu直接创建一个类判断是否存在上述实例,存在的话就销毁掉,这样显然更加正确,因为在主界面不需要链接网络
角色选择场景
通过创建一个可被网络序列化的PlayerData结构体来同步玩家的状态(颜色,名称),这一块不使用RPC,而是使用NetworkList进行列表式的网络变量同步,将委托绑定到该列表改变时
改变时刷新所有选择玩家的视觉表现,如是否显示,名称,是否准备等
准备同步
首先在准备界面使用一个专门的网络同步物体CharacterSelectReady
来存储当前所有玩家的准备情况,服务器RPC会调用客户端RPC和对应委托,让各个客户端能够更新自己的屏幕显示状态,服务器端做完以上后会检查是否所有人都准备完成,准备完成则调用NetworkManager的加载场景函数加载进游戏场景,后面游戏场景内也是相似方式实现的
public void SetPlayerReady()
{
SetPlayerReadyServerRPC();
}
[ServerRpc(RequireOwnership = false)]
private void SetPlayerReadyServerRPC(ServerRpcParams serverRpcParams = default)
{
SetPlayerReadyClientRPC(serverRpcParams.Receive.SenderClientId);
playerReadyDictionary[serverRpcParams.Receive.SenderClientId] = true;
bool allClientsReady = true;
foreach (ulong clientId in NetworkManager.Singleton.ConnectedClientsIds)
{
if (!playerReadyDictionary.ContainsKey(clientId) || !playerReadyDictionary[clientId])
{
// This player is NOT ready
allClientsReady = false;
break;
}
}
if (allClientsReady)
{
Loader.LoadNetwork(Loader.Scene.GameScene);
}
}
[ClientRpc]
private void SetPlayerReadyClientRPC(ulong clientID)
{
playerReadyDictionary[clientID] = true;
OnReadyChanged?.Invoke(this, EventArgs.Empty);
}
颜色选择同步
从这开始就要保存不同玩家的状态了,为此需要一个专门的结构体记录玩家状态,并且使用List进行存储
基于以上需求,选择在Multiplayer类中使用NetworkList的泛型容器来进行存储,好处是服务器更改后,客户端会跟着同步,并调用OnListChanged的委托方法,非常方便
存储的内容是名为PlayerData的结构体,该结构体实现了网络序列化和相等的方法,这样才能被装入NetworkList中
以下是其定义
public struct PlayerData : IEquatable<PlayerData>, INetworkSerializable
{
public ulong clientId;
public int colorId;
public FixedString64Bytes playerName;
public FixedString64Bytes playerId;
public bool Equals(PlayerData other)
{
return
clientId == other.clientId &&
colorId == other.colorId &&
playerName == other.playerName &&
playerId == other.playerId;
}
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref clientId);
serializer.SerializeValue(ref colorId);
serializer.SerializeValue(ref playerName);
serializer.SerializeValue(ref playerId);
}
}
关于playerDataList的调用时机
首先是玩家加入和玩家退出时需要修改playerDataList,这样才能维护正确数量的玩家数据,并在准备界面正确更新玩家的状态,其次是设置玩家名称,颜色时修改
以上修改只在服务器端修改,之后会自动同步到客户端并调用OnListChanged委托,需要改变的其他地方只需要订阅该委托即可
颜色改变流程:
-
在Multiplayer中创建颜色数组作为原始数据,colorID就是对应颜色下标
-
单个颜色UI读取对应的颜色并记录到自身,设置颜色时向服务器发送RPC请求修改发送RPC的clinet的颜色
-
颜色修改会触发playerDataList的修改,绑定的委托会触发:
-
展示的玩家模型的颜色变化,显隐
-
颜色UI的选择框变化
-
按下按钮
//CharacterColorSelectSingleUI
GetComponent<Button>().onClick.AddListener(() =>
{
KitchenGameMultiplayer.Instance.ChangePlayerColor(colorId);
});
触发RPC
//KitchenGameMultiplayer
public void ChangePlayerColor(int colorID)
{
ChangePlayerColorServerRPC(colorID);
}
[ServerRpc(RequireOwnership = false)]
private void ChangePlayerColorServerRPC(int colorID, ServerRpcParams serverRpcParams = default)
{
if (!IsColorAvailable(colorID))
return;
int playerIndex = GetPlayerIndexByClientID(serverRpcParams.Receive.SenderClientId);
PlayerData playerData = GetPlayerDataByIndex(playerIndex);
playerData.colorId = colorID;
playerDataNetworkList[playerIndex] = playerData;
}
触发修改playerDataNetworkList
委托
//CharacterSelectPlayer
private void UpdatePlayer()
{
if(KitchenGameMultiplayer.Instance.IsPlayerConnected(playerIndex))
{
Show();
PlayerData data = KitchenGameMultiplayer.Instance.GetPlayerDataByIndex(playerIndex);
readyTxt.gameObject.SetActive(CharacterSelectReady.Instance.IsPlayerReady(data.clientId));
nameTxt.text = data.playerName.ToString();
playerVisual.SetPlayerColor(KitchenGameMultiplayer.Instance.GetPlayerColor(data.colorId));
}
else
{
Hide();
}
}
//CharacterColorSelectSingleUI
private void UpdateUI()
{
if (KitchenGameMultiplayer.Instance.GetLocalPlayerData().colorId == colorId)
{
selectedCircle.SetActive(true);
}
else
{
selectedCircle.SetActive(false);
}
}
对于游戏内的Player,只需要持有自身的PlayerVisual的引用,并在游戏开始时,根据自身clientID去获取对应color并设置在自身的PlayerVisual上即可
//Player
private void Start() {
GameInput.Instance.OnInteractAction += GameInput_OnInteractAction;
GameInput.Instance.OnInteractAlternateAction += GameInput_OnInteractAlternateAction;
PlayerData playerData = KitchenGameMultiplayer.Instance.GetPlayerDataByClientID(OwnerClientId);
playerVisual.SetPlayerColor(KitchenGameMultiplayer.Instance.GetPlayerColor(playerData.colorId));
}
踢人功能
在准备玩家头上添加一个Button,并且仅在房主的情况下显示,选择后可让对应玩家强制断开连接
在该界面简单复制一个HostDisconnectionUI,可以通知对应被踢出的玩家或者主机离开后剩下的玩家
//KitchenGameMultiplayer
public void kickPlayer(ulong clientId)
{
NetworkManager.Singleton.DisconnectClient(clientId);
}
//CharacterSelectPlayer
kickBtn.onClick.AddListener(() =>
{
PlayerData data = KitchenGameMultiplayer.Instance.GetPlayerDataByIndex(playerIndex);
KitchenGameMultiplayer.Instance.kickPlayer(data.clientId);
});
kickBtn.gameObject.SetActive(NetworkManager.Singleton.IsServer &&
KitchenGameMultiplayer.Instance.GetPlayerIndexByClientID(NetworkManager.ServerClientId) != playerIndex);
小注:
我遇到了一个关于NetworkList的同步问题,如果还有客户端连接,主机退出的话,主机会在断开连接后继续向其他客户端进行同步NetworkList,会报一个空指针异常。
为此,需要在对应的委托下做一个额外判断,确保服务器已经不再运行
//KitchenGameMultiplayer
private void Server_OnClientDisconnectCallback(ulong clientId)
{
if (NetworkManager.ShutdownInProgress)
{
Debug.LogWarning("Server is shutdowning...");
return;
}
for (int i = 0; i < playerDataNetworkList.Count; i++)
{
PlayerData playerData = playerDataNetworkList[i];
if (playerData.clientId == clientId)
{
// Disconnected!
playerDataNetworkList.RemoveAt(i);
break;
}
}
}
Lobby大厅
使用官方的Lobby + Relay方案
在原本的Multiplayer的StartHost和StartClient外再裹一层LobbyAPI
创建KitchenGameLobby对象,其关键的函数有创建,加入Lobby,初始化,维持心跳等
初始化
public void InitializeUnityAuthentication()
{
InitializationOptions options = new InitializationOptions();
options.SetProfile(UnityEngine.Random.Range(0, 10000).ToString());
Func<Task> needRetry = async () =>
{
await UnityServices.InitializeAsync(options);
await AuthenticationService.Instance.SignInAnonymouslyAsync();
};
RetryNetworkFunc(needRetry,
"Try Connect to Server...",
"Connect failed, do you want to retry ?"
);
}
主要代码都在Func对象中,这里这么写主要是我维护了一个通用的函数,这个函数RetryNetworkFunc
在尝试的不同阶段调用了不同的委托,外部的UI可以绑定这些委托来展示当前进度,主要目的是为了多次的尝试(因为国内网络环境限制,常常一次不能连接成功)。
这个通用函数适用于初始化,创建,加入,并且一套UI就可满足这三个的显示需求,调用也是上面的调用方法,顶层的函数接口不变。
这里说一下通用函数的内容
通用函数
通用函数内部主要是一个for循环和try…catch来进行运作,分为开始,失败和成功时,输入参数时给予开始时和失败时的话,在类中预先定义最大尝试次数和尝试间隔即可
public async void RetryNetworkFunc(Func<Task> needRetry, string onStart, string onFailed)
{
OnTryStart?.Invoke(onStart);
for (int attempt = 1; attempt <= maxRetryAttempts; attempt++)
{
try
{
await needRetry();
// 如果成功初始化和登录,跳出循环
break;
}
catch (System.Exception ex)
{
// 可以记录异常信息以便调试
Debug.LogError($"Attempt {attempt} failed: {ex.Message}");
OnTryFailedOnce?.Invoke($"Attempt {attempt} failed: {ex.Message}. \nWe will Try again, Please Wait...");
if (attempt == maxRetryAttempts)
{
// 如果达到了最大重试次数,抛出异常或执行其他处理
Debug.LogError("All attempts to initialize and sign in failed.");
// 这里可以根据需要选择抛出异常或进行其他处理
OnTryFailed?.Invoke(needRetry, onStart, onFailed);
return;
}
// 等待一段时间再进行重试
await Task.Delay(delayBetweenRetries);
}
}
OnTryComplete?.Invoke();
Debug.Log("All Complete.");
}
返回正题,初始化主要是初始化了Unity提供的在线服务和用户认证,前者用于服务,后者用于安全要求
然后是创建房间,加入房间,这些都直接由Lobby提供的API完成,代码也一目了然
创建,加入
public void CreateLobby(string lobbyName, bool isPrivate)
{
Func<Task> needRetry = async () =>
{
joinedLobby = await LobbyService.Instance.CreateLobbyAsync(
lobbyName, KitchenGameMultiplayer.MAX_PLAYER_COUNT, new CreateLobbyOptions { IsPrivate = isPrivate });
KitchenGameMultiplayer.Instance.StartHost();
Loader.LoadNetwork(Loader.Scene.SelectionScene);
};
RetryNetworkFunc(needRetry,
"Creating Lobby...",
"Create lobby Failed, do you want to retry?");
}
public void QuickJoin()
{
Func<Task> needRetry = async () =>
{
joinedLobby = await LobbyService.Instance.QuickJoinLobbyAsync();
KitchenGameMultiplayer.Instance.StartClient();
};
RetryNetworkFunc(needRetry,
"Quickly Joining...",
"Quickly Join failed, do you want to try again?");
}
public void JoinByCode(string lobbyCode)
{
Func<Task> needRetry = async () =>
{
joinedLobby = await LobbyService.Instance.JoinLobbyByCodeAsync(lobbyCode);
KitchenGameMultiplayer.Instance.StartClient();
};
RetryNetworkFunc(needRetry,
"Joining by code...",
"Join failed, do you want to try again?");
}
还有一个重要部分是关于设置心跳连接,如果长时间不启动心跳维持连接,那么其他客户端就无法发现这个Lobby,因此需要在Update中隔一段时间触发一次心跳才行
private void Update()
{
HandleHeartbeat();
}
private void HandleHeartbeat()
{
if(IsLobbyHost())
{
heartbeatTimer -= Time.deltaTime;
if(heartbeatTimer < 0)
{
float heartbeatTimerMax = 15f;
heartbeatTimer = heartbeatTimerMax;
LobbyService.Instance.SendHeartbeatPingAsync(joinedLobby.Id);
}
}
}
private bool IsLobbyHost()
{
return joinedLobby != null && joinedLobby.HostId == AuthenticationService.Instance.PlayerId;
}
并且这个过程由房主来完成,因此需要额外验证身份
改名
改名直接做在大厅上,在大厅修改即在本地修改,修改完成后建立房间连接时直接读这里的本地数据就行,连接后就需要通过RPC才能进行更改了。
不过由于PlayerData基本都是一个房间一个NetworkList不存在绑定于玩家的持久数据,因此进入一个房间时,仍然需要向其他玩家广播自己的名称,无论是Host还是Client。
在Server的ConnectCallBack和Client的ConnectCallBack上都添加这个函数,并将本地的名称变量传入。
[ServerRpc(RequireOwnership = false)]
private void SetPlayerNameServerRpc(string name, ServerRpcParams serverRpcParams = default)
{
PlayerData player = GetPlayerDataByClientID(serverRpcParams.Receive.SenderClientId);
player.playerName = name;
playerDataNetworkList[GetPlayerIndexByClientID(player.clientId)] = player;
}
离开房间
当所有玩家开始游戏时,房间就不再有意义了,因为游戏中玩家只能回到主菜单,然后再进入服务器,并且游戏不允许中途加入,所以所有玩家准备后,要删除房间,使用自带的异步删除API
//在所有玩家准备后,Network加载游戏地图前运行
public async void DeleteLobby()
{
if (joinedLobby != null)
{
try
{
await LobbyService.Instance.DeleteLobbyAsync(joinedLobby.Id);
joinedLobby = null;
}
catch(LobbyServiceException e)
{
Debug.Log(e);
}
}
}
当有玩家在准备前就离开,此时也需要在房间中清除对应玩家的ID,使用自带的异步函数完成
public async void LeaveLobby()
{
if (joinedLobby == null)
return;
try
{
await LobbyService.Instance.RemovePlayerAsync(joinedLobby.Id, AuthenticationService.Instance.PlayerId);
joinedLobby = null;
}
catch(LobbyServiceException e)
{
Debug.Log(e);
}
}
当玩家被踢时,也要有专门的函数离开,因为发起踢人的是房主,并非房主本人走,因此需要的id和玩家自己离开的id不一样,新开一个函数作为区分
这里需要Host获取到Client的本地的AuthenticationService.Instance.PlayerId,因此需要将其加入到PlayerData中,方便获取,和PlayerName一样,一旦玩家加入房间就广播一下。
在外部就是服务器调用这个函数,并传入其他玩家的playerId
public async void KickPlayer(string playerId)
{
if (!IsLobbyHost())
return;
try
{
await LobbyService.Instance.RemovePlayerAsync(joinedLobby.Id, playerId);
joinedLobby = null;
}
catch (LobbyServiceException e)
{
Debug.Log(e);
}
}
其他加入的人的广播自己ID的函数
[ServerRpc(RequireOwnership = false)]
private void SetPlayerIdServerRpc(string playerId, ServerRpcParams serverRpcParams = default)
{
PlayerData player = GetPlayerDataByClientID(serverRpcParams.Receive.SenderClientId);
player.playerId = playerId;
playerDataNetworkList[GetPlayerIndexByClientID(player.clientId)] = player;
}
查找房间列表
使用LobbyServices的API就可以做到异步查找,定义对应的委托,让UI接收并更新
由于需要定期更新,因此在Update中隔一段时间进行更新也是个好的选择
private async void RequestLobbyList()
{
try
{
QueryLobbiesOptions options = new QueryLobbiesOptions
{
Filters = new List<QueryFilter>
{
new QueryFilter(QueryFilter.FieldOptions.AvailableSlots, "0", QueryFilter.OpOptions.GT)
}
};
QueryResponse response = await LobbyService.Instance.QueryLobbiesAsync(options);
OnLobbyListChanged?.Invoke(response.Results);
}
catch(LobbyServiceException e)
{
Debug.Log(e);
}
}
Update中调用的函数
private void HandleLobbyListUpdate()
{
if(joinedLobby == null &&
UnityServices.State == ServicesInitializationState.Initialized &&
AuthenticationService.Instance.IsSignedIn &&
UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == Loader.Scene.LobbyScene.ToString())
{
lobbyListTimer -= Time.deltaTime;
if (lobbyListTimer < 0)
{
float lobbyListTimerMax = 5f;
lobbyListTimer = lobbyListTimerMax;
RequestLobbyList();
}
}
}
UI更新部分
private void UpdateLobbyList(List<Lobby> lobbyList)
{
foreach(Transform child in lobbyContainer)
{
if (child == lobbyTemplate)
continue;
Destroy(child.gameObject);
}
if(lobbyList.Count == 0)
{
noLobbyTips.SetActive(true);
return;
}
noLobbyTips.SetActive(false);
foreach(Lobby lobby in lobbyList)
{
Transform newLobbyEntry = Instantiate(lobbyTemplate, lobbyContainer);
newLobbyEntry.gameObject.SetActive(true);
newLobbyEntry.GetComponent<LobbyEntryUI>().SetLobby(lobby);
}
}
}
Relay连接
relay的使用非常简单,只需要添加额外的库,一些工具函数,并在创建和加入房间的时候配合Lobby调用它们即可
//KitchenGameLobby
private async Task<Allocation> AllocateRelay()
{
try
{
Allocation allocation = await RelayService.Instance.CreateAllocationAsync(KitchenGameMultiplayer.MAX_PLAYER_COUNT - 1);
return allocation;
}
catch(RelayServiceException e)
{
Debug.Log(e);
return default;
}
}
private async Task<string> GetRelayJoinCode(Allocation allocation)
{
try
{
string relayJoinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId);
return relayJoinCode;
}
catch (RelayServiceException e)
{
Debug.Log(e);
return default;
}
}
private async Task<JoinAllocation> JoinRelay(string joinCode)
{
try
{
JoinAllocation joinAllocation = await RelayService.Instance.JoinAllocationAsync(joinCode);
return joinAllocation;
}
catch (RelayServiceException e)
{
Debug.Log(e);
return default;
}
}
接下来在对应的创建,加入房间的地方使用它
创建
public void CreateLobby(string lobbyName, bool isPrivate)
{
Func<Task> needRetry = async () =>
{
joinedLobby = await LobbyService.Instance.CreateLobbyAsync(
lobbyName, KitchenGameMultiplayer.MAX_PLAYER_COUNT, new CreateLobbyOptions { IsPrivate = isPrivate });
//relay
if(useRelay)
{
//申请一个Relay并且获得其joinCode码,将该码塞入Lobby的Data中
Allocation allocation = await AllocateRelay();
string relayJoinCode = await GetRelayJoinCode(allocation);
await LobbyService.Instance.UpdateLobbyAsync(joinedLobby.Id, new UpdateLobbyOptions
{
Data = new Dictionary<string, DataObject>
{
{KEY_RELAY_JOIN_CODE, new DataObject(DataObject.VisibilityOptions.Member, relayJoinCode)}
}
});
//由allocation构建RelayServerData
NetworkManager.Singleton.GetComponent<UnityTransport>().SetRelayServerData(new RelayServerData(allocation, "dtls"));
}
KitchenGameMultiplayer.Instance.StartHost();
Loader.LoadNetwork(Loader.Scene.SelectionScene);
};
RetryNetworkFunc(needRetry,
"Creating Lobby...",
"Create lobby Failed, do you want to retry?");
}
加入
public void QuickJoin()
{
Func<Task> needRetry = async () =>
{
joinedLobby = await LobbyService.Instance.QuickJoinLobbyAsync();
//relay
if(useRelay)
{
//获取要加入的lobby的码,由此反向创建JoinAllocation,构建RelayServerData
string relayJoinCode = joinedLobby.Data[KEY_RELAY_JOIN_CODE].Value;
JoinAllocation joinAllocation = await JoinRelay(relayJoinCode);
NetworkManager.Singleton.GetComponent<UnityTransport>().SetRelayServerData(new RelayServerData(joinAllocation, "dtls"));
}
KitchenGameMultiplayer.Instance.StartClient();
};
RetryNetworkFunc(needRetry,
"Quickly Joining...",
"Quickly Join failed, do you want to try again?");
}