【Unity知识分享】Mirror实现房间等待功能(创建房间 / 搜索房间、加入房间、房间准备、房间内角色设置、返回房间)

1、功能展示

1.1 创建房间

1.2 搜索房间、加入房间

1.3 房间准备

1.4 房间内角色设置

1.5 返回房间

2、功能实现关键代码

UGUI UI结构

2.1 创建房间

cs 复制代码
    //创建主机,创建服务端和客户端
    private void OnStartHost()
    {
        discoveredServers.Clear();
        NetworkManager.singleton.StartHost();
        networkDiscovery.AdvertiseServer();
    }
    //只创建服务端
    private void OnStartServer()
    {
        discoveredServers.Clear();
        NetworkManager.singleton.StartServer();
        networkDiscovery.AdvertiseServer();
    }

2.2 搜索房间

cs 复制代码
    //点击搜索房间按钮回调
    private void OnFindServers()
    {
        discoveredServers.Clear();
        networkDiscovery.StartDiscovery();

        StopCoroutine("CreateRoomItems");
        StartCoroutine("CreateRoomItems");
    }
    //创建房间列表
    IEnumerator CreateRoomItems()
    {
        while(discoveredServers.Values.Count <= 0)
        {
            yield return null;
        }

        foreach (var item in roomItems)
        {
            GameObject.Destroy(item);
        }
        roomItems.Clear();
        for (int i = 0; i < discoveredServers.Values.Count; i++)
        {
            int temp = i;
            GameObject obj = GameObject.Instantiate(roomItemPrefab, itemParent);
            obj.SetActive(true);
            roomItems.Add(obj);
            obj.transform.GetChild(0).GetComponent<Text>().text = $"房间: {i}";
            obj.transform.GetChild(1).GetComponent<Text>().text = $"地址: {discoveredServers.Values.ElementAt(i).EndPoint.Address}";
            obj.transform.GetChild(2).GetComponent<Button>().onClick.AddListener(() => OnClickRoomItemBtn(temp));
        }
    }
    //房间列表,点击Item中的加入房间回调
    private void OnClickRoomItemBtn(int index)
    {
        Connect(discoveredServers.Values.ElementAt(index));
    }
    //加入房间
    void Connect(ServerResponse info)
    {
        networkDiscovery.StopDiscovery();
        NetworkManager.singleton.StartClient(info.uri);
    }

2.3 加入房间

cs 复制代码
    //房间列表,点击Item中的加入房间回调
    private void OnClickRoomItemBtn(int index)
    {
        Connect(discoveredServers.Values.ElementAt(index));
    }
    //加入房间
    void Connect(ServerResponse info)
    {
        networkDiscovery.StopDiscovery();
        NetworkManager.singleton.StartClient(info.uri);
    }

2.4 房间准备

NetwrkRoomPlayerExtZT中准备按钮和取消准备按钮绑定事件

cs 复制代码
            btn_ready.onClick.AddListener(() => CmdChangeReadyState(true));
            btn_readyCancel.onClick.AddListener(() => CmdChangeReadyState(false));
cs 复制代码
        [Command]
        public void CmdChangeReadyState(bool readyState)
        {
            readyToBegin = readyState;
            NetworkRoomManager room = NetworkManager.singleton as NetworkRoomManager;
            if (room != null)
            {
                room.ReadyStatusChanged();
            }
        }

2.5 房间内角色设置

在NetwrkRoomPlayerExtZT记录设置信息

cs 复制代码
    [SyncVar(hook = nameof(OnColorPaoTaiUpdated))]
    public Color selectedColorPaoTai = Color.white;
    [SyncVar(hook = nameof(OnColorJiTiUpdated))]
    public Color selectedColorJiTi = Color.white;

绑定颜色设置回调,使用Command将颜色修改同步到所有客户端

cs 复制代码
    NetworkDiscoveryUGUI.Instance.colorSet_paoTai.OnColorChange += OnPaoTaiColorChange;
    NetworkDiscoveryUGUI.Instance.colorSet_jiTi.OnColorChange += OnJiTiColorChange;



    private void OnPaoTaiColorChange(Color color)
    {
        ComSendToServerPaoTaiColorChange(color);
    }

    [Command]
    private void ComSendToServerPaoTaiColorChange(Color color)
    {
        selectedColorPaoTai = color;
    }

    private void OnJiTiColorChange(Color color)
    {
        ComSendToServerJiTiColorChange(color);
    }

    [Command]
    private void ComSendToServerJiTiColorChange(Color color)
    {
        selectedColorJiTi = color;
    }

在颜色修改回调OnColorPaoTaiUpdated和OnColorJiTiUpdated中实现UI逻辑展现

cs 复制代码
    private void OnColorPaoTaiUpdated(Color oldC, Color newC)
    {
        if(img_paoTai)
            img_paoTai.color = newC;
    }

    private void OnColorJiTiUpdated(Color oldC, Color newC)
    {
        if(img_jiTi)
            img_jiTi.color = newC;
    }

在NetworkRoomManagerExtZT中的OnRoomServerSceneLoadedForPlayer将颜色设置到创建的角色控制物体中

cs 复制代码
        /// <summary>
        /// Called just after GamePlayer object is instantiated and just before it replaces RoomPlayer object.
        /// This is the ideal point to pass any data like player name, credentials, tokens, colors, etc.
        /// into the GamePlayer object as it is about to enter the Online scene.
        /// </summary>
        /// <param name="roomPlayer"></param>
        /// <param name="gamePlayer"></param>
        /// <returns>true unless some code in here decides it needs to abort the replacement</returns>
        public override bool OnRoomServerSceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer, GameObject gamePlayer)
        {
            //PlayerScore playerScore = gamePlayer.GetComponent<PlayerScore>();
            //playerScore.index = roomPlayer.GetComponent<NetworkRoomPlayer>().index;
            var roomP = roomPlayer.GetComponent<NetwrkRoomPlayerExtZT>();
            var tankSurface = gamePlayer.GetComponent<TankSurface>();
            if (tankSurface != null)
            {
                tankSurface.playerColor = roomP.selectedColorJiTi;
                Debug.Log("设置坦克机体颜色成功");
            }
            return true;
        }

玩家物体预制体挂着脚本TankSurface,因为OnRoomServerSceneLoadedForPlayer中调用会将颜色同步到所有客户端的TankSurface,通过回调UpdateColor完成角色不同形象设置

cs 复制代码
using Mirror;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TankSurface : NetworkBehaviour
{
    [SyncVar(hook = nameof(UpdateColor))]
    public Color playerColor = Color.white;
    public SkinnedMeshRenderer mesh;

    private Material material;

    public void SetTankColor(Color color)
    {
        if (material == null)
            material = Instantiate(mesh.material);

        material.color = color;
        mesh.material = material;
    }

    // 同步触发:所有客户端都会自动改颜色
    void UpdateColor(Color oldColor, Color newColor)
    {
        SetTankColor(newColor);
        FindObjectOfType<TankOverviewCamera>().RefreshAllTanks();
    }
}

2.6 返回房间

NetworkRoomManagerExtZT中返回按钮绑定

cs 复制代码
        btn_returnRoom = NetworkDiscoveryUGUI.Instance.btn_returnRoom;          
        btn_returnRoom.onClick.AddListener(OnClickReturnRoom);
cs 复制代码
        private void OnClickReturnRoom()
        {
            ServerChangeScene(RoomScene);
        }

3、 完整代码

3.1 NetworkDiscoveryUGUI

cs 复制代码
using Mirror;
using Mirror.Discovery;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.PlayerLoop;
using UnityEngine.UI;

public class NetworkDiscoveryUGUI : MonoBehaviour
{
    public static NetworkDiscoveryUGUI Instance;

    public Button btn_FindServers;
    public Button btn_StartHost;
    public Button btn_StartServer;
    public Button btn_StopClient;
    public Button btn_StopHost;
    public Button btn_StopServer;

    public Button btn_ready;
    public Button btn_readyCancel;
    public Button btn_startGame;
    public Button btn_returnRoom;

    public ColorSettingUGUI colorSet_paoTai;
    public ColorSettingUGUI colorSet_jiTi;

    public GameObject roomItemPrefab;
    public Transform itemParent;
    public GameObject roomPlayerItemPrefab;
    public Transform roomPlayerItemParent;
    public GameObject roomListPanel;
    public GameObject roomPlayerListPanel;

    public NetworkDiscovery networkDiscovery;
    readonly Dictionary<long, ServerResponse> discoveredServers = new Dictionary<long, ServerResponse>();

    private List<GameObject> roomItems = new List<GameObject>();

    //#if UNITY_EDITOR
    //    void OnValidate()
    //    {
    //        if (Application.isPlaying) return;
    //        Reset();
    //    }

    //    void Reset()
    //    {
    //        networkDiscovery = GetComponent<NetworkDiscovery>();

    //        // Add default event handler if not already present
    //        if (!Enumerable.Range(0, networkDiscovery.OnServerFound.GetPersistentEventCount())
    //            .Any(i => networkDiscovery.OnServerFound.GetPersistentMethodName(i) == nameof(OnDiscoveredServer)))
    //        {
    //            UnityEditor.Events.UnityEventTools.AddPersistentListener(networkDiscovery.OnServerFound, OnDiscoveredServer);
    //            UnityEditor.Undo.RecordObjects(new UnityEngine.Object[] { this, networkDiscovery }, "Set NetworkDiscovery");
    //        }
    //    }
    //#endif

    private void Awake()
    {
        if(Instance == null)
        {
            Instance = this;
            //DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }

    // Start is called before the first frame update
    void Start()
    {
        btn_FindServers.onClick.AddListener(OnFindServers);
        btn_StartHost.onClick.AddListener(OnStartHost);
        btn_StartServer.onClick.AddListener(OnStartServer);
        btn_StopClient.onClick.AddListener(OnStopClient);
        btn_StopHost.onClick.AddListener(OnStopHost);
        btn_StopServer.onClick.AddListener(OnStopServer);

        roomListPanel.SetActive(false);
        roomPlayerListPanel.SetActive(false);

    }

    // Update is called once per frame
    void Update()
    {
        if (NetworkManager.singleton == null)
            return;
        if (!NetworkClient.isConnected && !NetworkServer.active && !NetworkClient.active)
        {
            btn_FindServers.gameObject.SetActive(true);
            btn_StartHost.gameObject.SetActive(true);
            btn_StartServer.gameObject.SetActive(true);

            roomListPanel.SetActive(true);
        }
        else
        {
            btn_FindServers.gameObject.SetActive(false);
            btn_StartHost.gameObject.SetActive(false);
            btn_StartServer.gameObject.SetActive(false);

            roomListPanel.SetActive(false);
        }

        if (NetworkServer.active || NetworkClient.active)
        {
            // stop host if host mode
            if (NetworkServer.active && NetworkClient.isConnected)
            {
                btn_StopHost.gameObject.SetActive(true);
                btn_StopClient.gameObject.SetActive(false);
                btn_StopServer.gameObject.SetActive(false);
                //if (GUILayout.Button("Stop Host"))
                //{
                //    NetworkManager.singleton.StopHost();
                //    networkDiscovery.StopDiscovery();
                //}
            }
            // stop client if client-only
            else if (NetworkClient.isConnected)
            {
                btn_StopHost.gameObject.SetActive(false);
                btn_StopClient.gameObject.SetActive(true);
                btn_StopServer.gameObject.SetActive(false);
                //if (GUILayout.Button("Stop Client"))
                //{
                //    NetworkManager.singleton.StopClient();
                //    networkDiscovery.StopDiscovery();
                //}
            }
            // stop server if server-only
            else if (NetworkServer.active)
            {
                btn_StopHost.gameObject.SetActive(false);
                btn_StopClient.gameObject.SetActive(false);
                btn_StopServer.gameObject.SetActive(true);
                //if (GUILayout.Button("Stop Server"))
                //{
                //    NetworkManager.singleton.StopServer();
                //    networkDiscovery.StopDiscovery();
                //}
            }
        }
        else
        {
            btn_StopHost.gameObject.SetActive(false);
            btn_StopClient.gameObject.SetActive(false);
            btn_StopServer.gameObject.SetActive(false);
        }

    }

    private void OnFindServers()
    {
        discoveredServers.Clear();
        networkDiscovery.StartDiscovery();

        StopCoroutine("CreateRoomItems");
        StartCoroutine("CreateRoomItems");
    }

    IEnumerator CreateRoomItems()
    {
        while(discoveredServers.Values.Count <= 0)
        {
            yield return null;
        }

        foreach (var item in roomItems)
        {
            GameObject.Destroy(item);
        }
        roomItems.Clear();
        for (int i = 0; i < discoveredServers.Values.Count; i++)
        {
            int temp = i;
            GameObject obj = GameObject.Instantiate(roomItemPrefab, itemParent);
            obj.SetActive(true);
            roomItems.Add(obj);
            obj.transform.GetChild(0).GetComponent<Text>().text = $"房间: {i}";
            obj.transform.GetChild(1).GetComponent<Text>().text = $"地址: {discoveredServers.Values.ElementAt(i).EndPoint.Address}";
            obj.transform.GetChild(2).GetComponent<Button>().onClick.AddListener(() => OnClickRoomItemBtn(temp));
        }
    }

    private void OnClickRoomItemBtn(int index)
    {
        Connect(discoveredServers.Values.ElementAt(index));
    }

    private void OnStartHost()
    {
        discoveredServers.Clear();
        NetworkManager.singleton.StartHost();
        networkDiscovery.AdvertiseServer();
    }

    private void OnStartServer()
    {
        discoveredServers.Clear();
        NetworkManager.singleton.StartServer();
        networkDiscovery.AdvertiseServer();
    }

    private void OnStopClient()
    {
        NetworkManager.singleton.StopClient();
        networkDiscovery.StopDiscovery();
    }

    private void OnStopHost()
    {
        NetworkManager.singleton.StopHost();
        networkDiscovery.StopDiscovery();
    }

    private void OnStopServer()
    {
        NetworkManager.singleton.StopServer();
        networkDiscovery.StopDiscovery();
    }

    void Connect(ServerResponse info)
    {
        networkDiscovery.StopDiscovery();
        NetworkManager.singleton.StartClient(info.uri);
    }

    public GameObject CreateRoomPlayer()
    {
        GameObject obj = GameObject.Instantiate(roomPlayerItemPrefab, roomPlayerItemParent);
        obj.SetActive(true);
        return obj;
    }

    public void OnDiscoveredServer(ServerResponse info)
    {
        Debug.Log($"Discovered Server: {info.serverId} | {info.EndPoint} | {info.uri}");

        // Note that you can check the versioning to decide if you can connect to the server or not using this method
        discoveredServers[info.serverId] = info;
    }
}

3.2 NetworkRoomManagerExtZT

cs 复制代码
using UnityEngine;
using UnityEngine.UI;

/*
	Documentation: https://mirror-networking.gitbook.io/docs/components/network-manager
	API Reference: https://mirror-networking.com/docs/api/Mirror.NetworkManager.html
*/

namespace Mirror.Examples.NetworkRoomZT
{
    [AddComponentMenu("")]
    public class NetworkRoomManagerExtZT : NetworkRoomManager
    {
        [Header("Spawner Setup")]
        [Tooltip("Reward Prefab for the Spawner")]
        public GameObject rewardPrefab;
        public byte poolSize = 10;

        public static new NetworkRoomManagerExtZT singleton => NetworkManager.singleton as NetworkRoomManagerExtZT;

        /// <summary>
        /// This is called on the server when a networked scene finishes loading.
        /// </summary>
        /// <param name="sceneName">Name of the new scene.</param>
        public override void OnRoomServerSceneChanged(string sceneName)
        {
            // spawn the initial batch of Rewards
            //if (sceneName == GameplayScene)
            //{
            //    Spawner.InitializePool(rewardPrefab, poolSize);
            //    Spawner.InitialSpawn();
            //}
            //else
            //    Spawner.ClearPool();
        }

        public override void OnRoomClientSceneChanged()
        {
            // Don't initialize the pool for host client because it's
            // already initialized in OnRoomServerSceneChanged
            //if (NetworkServer.active) return;

            //if (networkSceneName == GameplayScene)
            //    Spawner.InitializePool(rewardPrefab, poolSize);
            //else
            //    Spawner.ClearPool();
        }

        /// <summary>
        /// Called just after GamePlayer object is instantiated and just before it replaces RoomPlayer object.
        /// This is the ideal point to pass any data like player name, credentials, tokens, colors, etc.
        /// into the GamePlayer object as it is about to enter the Online scene.
        /// </summary>
        /// <param name="roomPlayer"></param>
        /// <param name="gamePlayer"></param>
        /// <returns>true unless some code in here decides it needs to abort the replacement</returns>
        public override bool OnRoomServerSceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer, GameObject gamePlayer)
        {
            //PlayerScore playerScore = gamePlayer.GetComponent<PlayerScore>();
            //playerScore.index = roomPlayer.GetComponent<NetworkRoomPlayer>().index;
            var roomP = roomPlayer.GetComponent<NetwrkRoomPlayerExtZT>();
            var tankSurface = gamePlayer.GetComponent<TankSurface>();
            if (tankSurface != null)
            {
                tankSurface.playerColor = roomP.selectedColorJiTi;
                Debug.Log("设置坦克机体颜色成功");
            }
            return true;
        }

        public override void OnRoomStopClient()
        {
            base.OnRoomStopClient();
        }

        public override void OnRoomStopServer()
        {
            base.OnRoomStopServer();
        }

        // 服务器:判断人数满就踢
        public override void OnServerConnect(NetworkConnectionToClient conn)
        {
            // 注意:必须用 > 判断,因为自己已经占了一个位置
            if (NetworkServer.connections.Count > maxConnections)
            {
                conn.Disconnect(); // 直接断开,旧版只能这样
                return;
            }

            base.OnServerConnect(conn);
        }

        // 客户端:断开时判断
        public override void OnClientDisconnect()
        {
            base.OnClientDisconnect();

            // ==============================================
            // 房间已满
            // ==============================================
            if (NetworkClient.isConnecting)
            {
                Debug.Log("【房间已满或游戏已开始,无法加入】");

                // ======================
                // 在这里写你的 UGUI 提示
                // ShowTip("房间已满!");
                // ======================
            }
            else
            {
                Debug.Log("正常退出房间");
            }
        }

        Button btn_startGame;
        Button btn_returnRoom;
        public override void Start()
        {
            base.Start();
            btn_startGame = NetworkDiscoveryUGUI.Instance.btn_startGame;
            btn_returnRoom = NetworkDiscoveryUGUI.Instance.btn_returnRoom;
            btn_startGame.onClick.AddListener(OnClickStartGame);
            btn_returnRoom.onClick.AddListener(OnClickReturnRoom);
        }

        public override void Update()
        {
            base.Update();

            if (btn_startGame == null) return;
            if (allPlayersReady && showStartButton)
            {
                btn_startGame.gameObject.SetActive(true);
            }
            else
            {
                btn_startGame.gameObject.SetActive(false);
            }

            if (NetworkServer.active && Utils.IsSceneActive(GameplayScene))
            {
                btn_returnRoom.gameObject.SetActive(true);
            }
            else
            {
                btn_returnRoom.gameObject.SetActive(false);
            }

            //显示房间内玩家列表
            if (Utils.IsSceneActive(RoomScene))
            {
                NetworkDiscoveryUGUI.Instance.roomPlayerListPanel.SetActive(true);
            }
            else
            {
                NetworkDiscoveryUGUI.Instance.roomPlayerListPanel.SetActive(false);
            }
        }

        private void OnClickStartGame()
        {
            showStartButton = false;

            ServerChangeScene(GameplayScene);
        }

        private void OnClickReturnRoom()
        {
            ServerChangeScene(RoomScene);
        }

        /*
            This code below is to demonstrate how to do a Start button that only appears for the Host player
            showStartButton is a local bool that's needed because OnRoomServerPlayersReady is only fired when
            all players are ready, but if a player cancels their ready state there's no callback to set it back to false
            Therefore, allPlayersReady is used in combination with showStartButton to show/hide the Start button correctly.
            Setting showStartButton false when the button is pressed hides it in the game scene since NetworkRoomManager
            is set as DontDestroyOnLoad = true.
        */

#if !UNITY_SERVER
        bool showStartButton;
#endif

        public override void OnRoomServerPlayersReady()
        {
            // calling the base method calls ServerChangeScene as soon as all players are in Ready state.
            if (Utils.IsHeadless())
                base.OnRoomServerPlayersReady();
#if !UNITY_SERVER
            else
                showStartButton = true;
#endif
        }

#if !UNITY_SERVER
        public override void OnGUI()
        {
            base.OnGUI();

            if (allPlayersReady && showStartButton && showRoomGUI && GUI.Button(new Rect(150, 300, 120, 20), "START GAME"))
            {
                // set to false to hide it in the game scene
                showStartButton = false;

                ServerChangeScene(GameplayScene);
            }
        }
#endif
    }
}

3.3 NetwrkRoomPlayerExtZT

cs 复制代码
using Mirror;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class NetwrkRoomPlayerExtZT : NetworkRoomPlayer
{
    public GameObject roomPlayerUI;
    public Button btn_remove;
    public Button btn_ready;
    public Button btn_readyCancel;
    public Text txt_name;
    public Text txt_readyState;
    public Image img_paoTai;
    public Image img_jiTi;

    // 同步颜色,所有人可见
    [SyncVar(hook = nameof(OnColorPaoTaiUpdated))]
    public Color selectedColorPaoTai = Color.white;
    [SyncVar(hook = nameof(OnColorJiTiUpdated))]
    public Color selectedColorJiTi = Color.white;

    public override void OnStartClient()
    {
        base.OnStartClient();

        Debug.Log($"OnStartClient {gameObject}");
        roomPlayerUI = NetworkDiscoveryUGUI.Instance.CreateRoomPlayer();
        btn_remove = roomPlayerUI.transform.GetChild(1).GetComponent<Button>();
        btn_remove.onClick.AddListener(OnClickRemove);

        txt_name = roomPlayerUI.transform.GetChild(0).GetComponent<UnityEngine.UI.Text>();
        txt_readyState = roomPlayerUI.transform.GetChild(2).GetComponent<UnityEngine.UI.Text>();
        img_paoTai = roomPlayerUI.transform.GetChild(3).GetComponent<UnityEngine.UI.Image>();
        img_jiTi = roomPlayerUI.transform.GetChild(4).GetComponent<UnityEngine.UI.Image>();

        SetReadyState(readyToBegin);
        txt_name.text = $"玩家: {index + 1}";

        //新玩家加入时,更新颜色显示
        // 安全调用,防止空引用
        if (this != null)
        {
            OnColorPaoTaiUpdated(selectedColorPaoTai, selectedColorPaoTai);
            OnColorJiTiUpdated(selectedColorJiTi, selectedColorJiTi);
        }

        if (NetworkClient.active && isLocalPlayer)
        {
            btn_ready = NetworkDiscoveryUGUI.Instance.btn_ready;
            btn_readyCancel = NetworkDiscoveryUGUI.Instance.btn_readyCancel;
            NetworkDiscoveryUGUI.Instance.colorSet_paoTai.OnColorChange += OnPaoTaiColorChange;
            NetworkDiscoveryUGUI.Instance.colorSet_jiTi.OnColorChange += OnJiTiColorChange;
            btn_ready.gameObject.SetActive(true);
            btn_readyCancel.gameObject.SetActive(false);
            btn_ready.onClick.AddListener(() => CmdChangeReadyState(true));
            btn_readyCancel.onClick.AddListener(() => CmdChangeReadyState(false));
        }
    }

    public override void OnClientEnterRoom()
    {
        Debug.Log(index);
        Debug.Log($"OnClientEnterRoom {SceneManager.GetActiveScene().path}");

        //if (NetworkClient.active && isLocalPlayer)
        //{
        //    NetworkDiscoveryUGUI.Instance.roomPlayerListPanel.SetActive(true);
        //}
    }

    public override void OnClientExitRoom()
    {
        Debug.Log($"OnClientExitRoom {SceneManager.GetActiveScene().path}");
        //Destroy(roomPlayerUI);

        //if (NetworkClient.active && isLocalPlayer)
        //{
        //    Debug.Log("isLocalPlayer" + index);
        //    NetworkDiscoveryUGUI.Instance.roomPlayerListPanel.SetActive(false);
        //}
    }

    private void OnPaoTaiColorChange(Color color)
    {
        ComSendToServerPaoTaiColorChange(color);
    }

    [Command]
    private void ComSendToServerPaoTaiColorChange(Color color)
    {
        selectedColorPaoTai = color;
    }

    private void OnJiTiColorChange(Color color)
    {
        ComSendToServerJiTiColorChange(color);
    }

    [Command]
    private void ComSendToServerJiTiColorChange(Color color)
    {
        selectedColorJiTi = color;
    }

    private void OnColorPaoTaiUpdated(Color oldC, Color newC)
    {
        if(img_paoTai)
            img_paoTai.color = newC;
    }

    private void OnColorJiTiUpdated(Color oldC, Color newC)
    {
        if(img_jiTi)
            img_jiTi.color = newC;
    }

    private void OnDestroy()
    {
        Destroy(roomPlayerUI);
    }

    public override void IndexChanged(int oldIndex, int newIndex)
    {
        //Debug.Log($"IndexChanged {newIndex}");
        if(roomPlayerUI != null)
        {
            txt_name.text = $"玩家: {newIndex + 1}";
        }
    }

    public override void ReadyStateChanged(bool oldReadyState, bool newReadyState)
    {
        Debug.Log($"ReadyStateChanged {newReadyState}");
        SetReadyState(newReadyState);
    }

    private void OnClickRemove()
    {
        GetComponent<NetworkIdentity>().connectionToClient.Disconnect();
    }

    private void Update()
    {
        if (((isServer && index > 0) || isServerOnly))
        {
            //服务端显示删除玩家按钮
            btn_remove?.gameObject.SetActive(true);
        }
        else
        {
            //玩家端隐藏删除玩家按钮
            btn_remove?.gameObject.SetActive(false);
        }

        if (NetworkClient.active && isLocalPlayer)
        {
            if (readyToBegin)
            {
                btn_ready.gameObject.SetActive(false);
                btn_readyCancel.gameObject.SetActive(true);
            }
            else
            {
                btn_ready.gameObject.SetActive(true);
                btn_readyCancel.gameObject.SetActive(false);
            }
        }
    }

    private void SetReadyState(bool ready)
    {
        if (ready)
        {
            txt_readyState.text = "已准备";
            txt_readyState.color = Color.green;
        }
        else
        {
            txt_readyState.text = "未准备";
            txt_readyState.color = Color.red;
        }
    }

#if !UNITY_SERVER
    public override void OnGUI()
    {
        base.OnGUI();
    }
#endif
}

3.4 TankSurface

cs 复制代码
using Mirror;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TankSurface : NetworkBehaviour
{
    [SyncVar(hook = nameof(UpdateColor))]
    public Color playerColor = Color.white;
    public SkinnedMeshRenderer mesh;

    private Material material;

    public void SetTankColor(Color color)
    {
        if (material == null)
            material = Instantiate(mesh.material);

        material.color = color;
        mesh.material = material;
    }

    // 同步触发:所有客户端都会自动改颜色
    void UpdateColor(Color oldColor, Color newColor)
    {
        SetTankColor(newColor);
        FindObjectOfType<TankOverviewCamera>().RefreshAllTanks();
    }
}

3.5 ColorSettingUGUI

cs 复制代码
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

public class ColorSettingUGUI : MonoBehaviour
{
    public Image img_showColor;
    public BtnColorItem[] BtnColorItems;
    public Slider slider_R;
    public Slider slider_G;
    public Slider slider_B;

    public Color curColor;
    public UnityAction<Color> OnColorChange;

    private void Awake()
    {
        foreach (var item in BtnColorItems)
        {
            item.button.onClick.AddListener(() => { OnClickColorBtn(item.color); });
        }

        slider_R.onValueChanged.AddListener(OnSliderRChange);
        slider_G.onValueChanged.AddListener(OnSliderGChange);
        slider_B.onValueChanged.AddListener(OnSliderBChange);

        SetShowColor(curColor);
    }

    private void OnClickColorBtn(Color color)
    {
        Debug.Log(color);
        SetShowColor(color);
    }

    private void OnSliderRChange(float value)
    {
        curColor.r = value;
        SetShowColor(curColor);
    }
    private void OnSliderGChange(float value)
    {
        curColor.g = value;
        SetShowColor(curColor);
    }
    private void OnSliderBChange(float value)
    {
        curColor.b = value;
        SetShowColor(curColor);
    }

    private void SetShowColor(Color color)
    {
        img_showColor.color = color;
        curColor = color;
        slider_R.SetValueWithoutNotify(color.r);
        slider_G.SetValueWithoutNotify(color.g);
        slider_B.SetValueWithoutNotify(color.b);

        OnColorChange?.Invoke(color);
    }
}

[Serializable]
public class BtnColorItem
{
    public Button button;
    public Color color;
}

3.6 TankController

cs 复制代码
using Mirror;
using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class TankController : NetworkBehaviour
{
    [Header("坦克底盘移动")]
    public float moveSpeed = 15f;       // 前进后退速度
    public float rotateSpeed = 90f;     // 底盘转向速度

    [Header("炮塔与炮管")]
    public Transform turret;            // 炮塔父物体
    public Transform cannon;            // 炮管
    public float turretRotateSpeed = 60f; // 炮塔旋转速度
    public float cannonRotateSpeed = 30f; // 炮管俯仰速度
    public float minCannonAngle = -10f;  // 炮管最低角度
    public float maxCannonAngle = 30f;   // 炮管最高角度

    [Header("开火设置")]
    public GameObject bulletPrefab;     // 炮弹预制体
    public Transform firePoint;         // 开火点
    public float bulletSpeed = 30f;     // 炮弹速度
    public float fireCooldown = 0.5f;   // 射击冷却

    private Rigidbody rb;
    private float currentCannonAngle = 0f;
    private float fireTimer = 0f;

    void Start()
    {
        rb = GetComponent<Rigidbody>();
        // 关闭刚体旋转,防止物理碰撞乱转
        rb.freezeRotation = true;
    }

    void Update()
    {
        // 冷却计时
        fireTimer += Time.deltaTime;

        // 炮塔鼠标控制
        //ControlTurretByMouse();
        if (!isLocalPlayer) return;
        // 开火
        if (Input.GetMouseButtonDown(0) && fireTimer >= fireCooldown)
        {
            CmdFire();
            fireTimer = 0f;
        }
    }

    void FixedUpdate()
    {
        if (!isLocalPlayer) return;
        // 底盘移动(物理更新)
        ControlChassis();
    }

    // 底盘移动控制
    void ControlChassis()
    {
        float vertical = Input.GetAxis("Vertical");
        float horizontal = Input.GetAxis("Horizontal");

        // 前进/后退
        Vector3 moveDir = transform.forward * vertical * moveSpeed;
        rb.velocity = moveDir;

        // 转向
        transform.Rotate(0, horizontal * rotateSpeed * Time.fixedDeltaTime, 0);
    }

    // 鼠标控制炮塔 + 炮管
    void ControlTurretByMouse()
    {
        // 炮塔水平旋转
        float mouseX = Input.GetAxis("Mouse X");
        turret.Rotate(0, mouseX * turretRotateSpeed * Time.deltaTime, 0);

        // 炮管俯仰(带角度限制)
        float mouseY = Input.GetAxis("Mouse Y");
        currentCannonAngle -= mouseY * cannonRotateSpeed * Time.deltaTime;
        currentCannonAngle = Mathf.Clamp(currentCannonAngle, minCannonAngle, maxCannonAngle);
        cannon.localEulerAngles = new Vector3(currentCannonAngle, 0, 0);
    }

    // 开火
    // 开火(改为子弹自己移动,这里只生成)
    [Command]
    void CmdFire()
    {
        if (bulletPrefab == null || firePoint == null) return;

        // 只生成子弹,不设置刚体速度
        GameObject bullet = Instantiate(bulletPrefab, firePoint.position, firePoint.rotation);
        NetworkServer.Spawn(bullet);
    }
}

3.7 Bullet

cs 复制代码
using Mirror;
using UnityEngine;

public class Bullet : NetworkBehaviour
{
    [Header("子弹参数")]
    public float moveSpeed = 30f;
    public float damage = 50f;
    public float lifeTime = 3f;

    public override void OnStartServer()
    {
        Invoke(nameof(DestroySelf), lifeTime);
    }

    void Update()
    {
        // 子弹自身 向前平移
        transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);
    }

    private void OnTriggerEnter(Collider other)
    {
        // 碰到物体立即销毁
        DestroySelf();
    }

    // destroy for everyone on the server
    [Server]
    void DestroySelf()
    {
        NetworkServer.Destroy(gameObject);
    }

}

3.8 场景设置

1)离线场景

NetworkRoomManager挂载以下脚本

NetworkRoomManagerExtZT设置

NetworkDiscovery绑定事件NetworkDiscoveryUGUI.OnDiscoveredServer

NetworkDiscoveryUGUI设置引用NetworkDiscovery

2)房间准备场景,空场景
3)游戏场景

需要设置玩家生成点

3.9 预制体

玩家预制体

房间准备玩家预制体

子弹预制体

4、经验总结

4.1 房间创建

先进入房间准备场景,并生成房间准备玩家,所有玩家准备完成再加载游戏场景生成游戏玩家

4.2 房间搜索实现原理

搜索IP和端口是否存在,有则为房间的IP和端口

4.3 房间角色不同形象

玩家设置角色形象数据,将设置数据保存到玩家房间物体NetworkRoomPlayer中,并将修改同步到所有客户端,游戏玩家生成时获取对应的NetworkRoomPlayer中的设置信息,完成不同角色形象设置

相关推荐
游乐码6 小时前
Unity坦克案例疑难记录(二)
unity·游戏引擎
小白学鸿蒙7 小时前
Funplay Unity MCP 接入 trae 实战
unity·游戏引擎·mcp
游乐码10 小时前
Unity基础(一)游戏中的数学Mathf函数
游戏·unity·游戏引擎
地狱为王1 天前
Unity实现猫脸关键点检测
unity·游戏引擎·猫脸关键点检测
598866753@qq.com1 天前
Unity Job System笔记
unity
winlife_1 天前
Funplay Unity MCP 与 Unity AI Assistant 详细对比:开源 MCP 工具集 vs 官方全栈 AI 产品
人工智能·unity·开源·ai编程·claude·mcp
御水流红叶1 天前
Android-Unity游戏逆向思路
android·游戏·unity
ellis19701 天前
Unity图集Atlas
unity
想不明白的过度思考者1 天前
Unity全局事件中心与新版输入架构实现练习——上帝模式与英雄模式的输入系统映射切换
java·unity·架构