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中的设置信息,完成不同角色形象设置
