【Unity】案例 —— 胡闹厨房联机案例(玩家,大厅与连接部分)

其他部分

我把包括大厅,创建房间,选择颜色等功能放到其他部分进行阐述,因为这一部分属于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;
}

NetworkManagerKitchenGameMultiplayer的单例问题

多次进入同一场景会出现关卡不会销毁物体变多的情况,并且在联网状况下二者哈希值一样也会报错,需要删除多余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?");
}
  • 28
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值