【推荐100个unity插件之8】实现多人在线联机游戏——Mirror插件的使用介绍(附项目源码)

线上365bet体育 2025-10-01 00:35:33 admin

最终效果

文章目录

最终效果前言关于Mirror导入Mirror插件

注意事项基本使用一、创建场景的NetworkManager网络管理器1、NetworkManager组件2、NetworkManagerHUD组件3、KcpTransport组件

二、创建一个玩家1、为玩家添加NetworkIdentity组件2、为玩家添加NetworkTransform组件

三、添加玩家初始生成位置四、玩家控制1、NetworkBehaviour2、书写人物控制脚本

五、同步摄像机六、同步不同角色的名字和颜色修改七、同步动画八、同步子弹方法一方法二方法三

九、聊天功能十、场景同步切换十一、重新绘制一个HUD界面十二、查找服务器十三、角色死亡复活十四、自己编一个network manager十五、AOI迷雾效果实现十六、开房间的功能

连接线上服务器(待续)1、linux服务器2、然后就可正常打包客户端,连接服务器游玩了

后续源码参考完结

前言

终于来了,之前很多人私信我,想看关于如何实现多人游戏的流程,这不就来了。关于Mirror插件其实我已经关注很久了,最近才有时间把它整理出来。

有些同学做了一个单机版的小Demo,想改成局域网多人联机版,要处理好多复杂的同步问题,比如物理碰撞、状态同步等等,这个对于Unity萌新来说,不大友好。有没有什么好用的网络库可以让开发更高效呢?有,那就是:Mirror!

注:在Unity 5.1 ~ Unity2018中你可以使用UNet(全称Unity Networking),到Unity 2019之后UNet就被废弃了,Mirror就是来替代UNet的。你在网上搜到的Unity Netwoking的教程就是UNet,它已经过时了,不要再使用UNet了!

关于Mirror

Mirror是Unity的高级网络 API,支持不同的低级传输(UDP、TCP、KCP等等)。 使用 Mirror,客户端、服务端是在同一个工程中的,这就是为什么它叫Mirror。也就是说它没有一个独立的服务端,而是由一台客户端作为Host,它既是客户端又是服务端,其他客户端连接这台Host客户端。

Mirror是一个简单高效的开源的unity多人游戏网络框架,Mirror在Unity商店中是免费的。

官方文档地址:https://mirror-networking.gitbook.io/docs

导入Mirror插件

建议从Asset Store上下载Mirror版本,因为GitHub的版本不一定稳定。

Asset Store地址:https://assetstore.unity.com/packages/tools/network/mirror-129321 将Mirror插件添加到自己的账号中,然后回到Unity,在Package Manager中就可以下载了

注意事项

场景内所有挂在了你的代码的物体都会默认添加NetworkIdentity,但network manager组件与network identity组件放在一个物体上会报错。一般游戏内除了包含NetworkManager组件的物体都要挂载NetworkIdentity组件,包括即将孵化的。

如果物体内有network manager组件但是没有Kcp transport组件,会报错。

如果场景内有多个network manager组件,会报错。一个场景中只能有一个激活的NetworkManager(它是单例模式的)。

如果角色预制体拖不进player prefab栏,可能是没有挂在network identity组件。

角色代码必须有if (!isLocalPlayer) return;否则。。。后果自己知道。

基本使用

一、创建场景的NetworkManager网络管理器

在起始场景中创建一个空游戏对象,然后添加新创建的网络管理器组件(NetworkManager、NetworkManagerHUD、KcpTransport组件)。 KcpTransport组件挂载在networkManager的transport上 并配置Scene场景,offline和online是离线界面和游戏界面,比如说我们新建一个offline场景,在里面放一个network manager的network managerHUD,然后再新建一个online场景,把他们都注册到build setting(生成设置)里的build里的场景栏中(拖进去),offline在上。然后我们进入offline场景,运行,点击host,便会进入online(在线场景)。

1、NetworkManager组件

NetworkManager是管理多个客户端连接的组件。它是多人联机游戏的核心控制组件。一个场景中只能有一个激活的NetworkManager(它是单例模式的)。

连接的服务端IP地址在NetworkManager中进行设置,Max Connections是最大连接数。(注意:任何一个客户端都可以同时是一个服务端)

2、NetworkManagerHUD组件

NetworkManagerHUD组件是下面这个GUI的逻辑,通过它我们可以方便地进行测试。

3、KcpTransport组件

Mirror帮我们封装了各种不同等级的传输协议(各种Transport组件),常用的是KcpTransport和TelepathyTransport。 KcpTransport是使用可靠UDP协议,TelepathyTransport是使用TCP协议。 Transport组件中可以设置端口号、最大延迟等等参数:

二、创建一个玩家

1、为玩家添加NetworkIdentity组件

创建一个玩家物体Player,为玩家添加networkIdentity组件,NetworkIdentity组件提供了游戏物体在网络中的唯一标识(ID)。

游戏运行过程中,我们在Inspector视图中预览到NetworkIdentity的信息。 一般游戏内除了包含NetworkManager组件的物体都要挂在此组件,包括即将孵化的。 这东西只有俩选项:

一个勾选框Server Only,意思是只有服务端能操作,大家根据自己的需要勾选。第二个是visible,里面有三个选项:默认(Default)、强制隐藏(Force Hidden)、强制显示(ForceShown),个人感觉没啥用,大家默认就行。

只有挂载了networkIdentity,网络中枢才能识别到这个物件,并对之进行同步。接下来将Player作为一个预制体保存,并在场景中删除,后拖拽预制体到网络中枢(networkManager)的Player Prefab插槽中,以后它的产生就完全依靠网络中枢在连接到主机后自动生成。 自动创建播放器(Auto Create Player):默认勾选,勾选的话当连接服务器时会自动生成上面的“玩家预制件”。

注:如果角色预制体托不进player prefab栏,可能是没有挂在NetworkIdentity组件

2、为玩家添加NetworkTransform组件

为玩家添加NetworkTransform,NetworkTransform组件会通过网络自动同步position、rotation和scale。带NetworkTransform组件的物体必须也带NetworkIdentity组件。

Mirror 目前提供2种Network Transform:

Reliable:低带宽,与Rpcs/Cmds/等相同的延迟。

Unreliable:高带宽,极低延迟

使用Reliable,除非需要超低延迟。

添加并勾选networkTransport的Client Authority属性。 我们可以设置Positon、Rotation、Scale同步的敏感度(新版本貌似只能设置Rotation了) 为了让同步有一个平滑效果(不会一卡一卡的),我们可以勾选平滑差值(当然默认就是勾选的)

注:后面我们会利用玩家body的Scale进行翻转,这里给body也加上Network Transform代码,记勾选SyncScale.

三、添加玩家初始生成位置

创建几个空物体作为玩家的初始生成位置,添加Network Start Position脚本,并将该物体拖动到合适的位置。 并在NetworkManager中选择随机(Random)或者轮询(Round Robin)的出生点选择方式。

1.Random:生成为随机(可能相同的生成位置将被两个或更多玩家使用)

2.Round Robin:循环(使用每个可用位置,直到客户端数超过生成点数)。

效果

四、玩家控制

1、NetworkBehaviour

玩家控制脚本需要继承NetworkBehaviour。在书写玩家控制脚本之前,我觉得有必要介绍一下NetworkBehaviour。

NetworkBehaviour脚本处理具有NetworkIdentity组件的游戏对象,NetworkBehaviour的子类中可以处理高级API功能,例如Commands、ClientRpc's、SyncEvents、SyncVars。

NetworkBehaviour组件具有以下功能:

Synchronized variables:同步变量Network callbacks:网络回调Server and client functions:服务端和客户端函数Sending commands:发送命令Client RPC calls:客户端远程过程调用Networked events:网络事件

NetworkBehaviour提供了一些 网络回调:

OnStartServer回调 这个回调函数只在服务端调用,当在服务端生成一个游戏对象,或者服务端启动时被回调。OnStopServer回调 这个回调函数只在服务端调用,当在服务端销毁一个游戏对象,或者服务端停止时被回调。OnStartClient回调 这个回调函数只在客户端调用,当客户端生成一个游戏对象,或者客户端连接到服务端时被回调。OnStopClient回调 这个回调函数只在客户端调用,当服务端销毁一个游戏对象时被回调。OnStartLocalPlayer回调 这个回调函数只在客户端调用,当客户端生成一个玩家对象时被回调。OnStartAuthority回调 这个回调函数只在客户端调用,当游戏对象拿到控制权时。OnStopAuthority回调 这个回调函数只在客户端调用,当游戏对象失去控制权时。

标记服务端函数或客户端函数: 在NetworkBehaviour中,我们可以使用下面这些注解对函数进行标注。

[Server]、[ServerCallback]表示函数为服务端函数,只在服务端执行;[ServerCallback],它与[Server]一样,只能在服务端调用,只是没有Warning输出而已。[Client]、[ClientCallback]表示为客户端函数,只在客户端执行。

Command 命令: 使用[Command]注解对函数进行标记,表示这个函数是由客户端调用,由服务端来执行。 被[Command]标记的函数约定以Cmd开头。

Client RPC 客户端远程过程调用: 使用[ClientRpc]注解对函数进行标记,表示这个函数是由服务端调用,在所有与服务端连接的客户端执行。 被[ClientRpc]标记的函数约定以Rpc开头。

TargetRpc在指定客户端远程过程调用: TargetRpc在服务端调用,在指定的与服务端连接的客户端执行,该方法至少有一个,NetworkConnection的形参,用来确定是在哪一个客户端执行,并且方法名称以"Target"开头。

比如某个客户端角色得分加分

NetworkIdentity netIdentity = GetComponent

TargetShowMessage(netIdentity.connectionToClient, 1);

[TargetRpc]

private void TargetShowMessage(NetworkConnection target, int count)

{

sumCount += count;//加分

}

Networked Events 网络事件(观察者模式): 类似于Client RPC调用,不同之处是它触发的是事件。 使用[SyncEvent]对事件进行标记。被[SyncEvent]标记的事件变量必须以Event开头,例EventTakeDamage。例子可以参见官方手册:https://mirror-networking.gitbook.io/docs/guides/synchronization/syncevent

Mirror提供的函数注解如下(部分注解我们上面已做了介绍),具体的注解可以参见Mirror官方手册:https://mirror-networking.gitbook.io/docs/guides/attributes

2、书写人物控制脚本

网络同步需要注意的一些事情:

1.需要用到联网功能的脚本中都要添加using Mirror来使用相应API,并且继承NetworkBehaviour而不是MonoBehaviour。

2.涉及到玩家输入时,首先先要进行isLocalPlayer的判断,通过islocalplayer来判断是否具有当前对象的权限

为控制游戏对象,添加一个简单的人物控制脚本为PlayerControl.cs,继承NetworkBehaviour。

其中移动的同步会自动通过NetworkTransform进行同步,所以我们只需对本地坦克进行控制即可。

using UnityEngine;

using Mirror;

public class PlayerControl : NetworkBehaviour //MonoBehaviour --> NetworkBehaviour

{

private Rigidbody2D rb; // 刚体组件

void Start()

{

rb = GetComponent(); // 获取刚体组件

}

//速度:每秒移动5个单位长度

public float moveSpeed = 5;

void Update()

{

// isLocalPlayer是父类NetworkBehaviour的属性,用于判断当前NetworkBehaviour对象是否为本地对象;

if (!isLocalPlayer) return; //不应操作非本地玩家

Move();

}

void Move()

{

//通过键盘获取水平轴的值,范围在-1到1

float horizontal = Input.GetAxisRaw("Horizontal");

rb.velocity = new Vector2(horizontal * moveSpeed, rb.velocity.y); // 设置刚体速度

if (horizontal != 0)

{

transform.GetChild(0).localScale = new Vector3(-horizontal, 1, 1); // 翻转角色

}

}

}

效果

五、同步摄像机

对于在每个客户端独立生成的对象(这里以每位玩家的camera为例),需要将start方法修改为OnStartLocalPlayer(),这样可以避免多个客户端的摄像机被修改为同一台。

OnStartLocalPlayer:仅在client执行,当脚本所在物体为玩家角色时调用,用来设置跟踪相机,角色初始化等

public override void OnStartLocalPlayer()

{

rb = GetComponent(); // 获取刚体组件

//摄像机与角色绑定

Camera.main.transform.SetParent(transform);

Camera.main.transform.localPosition = new Vector3(0, 0, Camera.main.transform.position.z);

}

效果

六、同步不同角色的名字和颜色修改

同步变量需要添加同步变量的标记[SyncVar(hook=nameof(FunctionExecOnClient))],当同步变量发生变化时就会调用后面的FunctionExecOnClient方法

当服务器的场景中的一个SyncVar的值发生变化时,就同步给其它所有客户端。

对于同步变量的修改,使用[Command]标记(针对方法的标记,方法名以Cmd开头)

using TMPro;

public TMP_Text nameText;

//需要把name和颜色同步给其他玩家,添加同步变量的标记[SyncVar(hook=nameof(FunctionExecOnClient))]

[SyncVar(hook = nameof(OnPlayerNameChanged))]

public string playerName;

[SyncVar(hook = nameof(OnPlayerColorChanged))]

private Color playerColor;

//申明OnPlayerNameChanged和OnPlayerColorChanged这两个方法

//第一个变量(oldstr)是同步变量修改前的值,第二个(newstr)是同步变量修改后的值

private void OnPlayerNameChanged(string oldstr, string newstr)

{

nameText.text = newstr;

}

private void OnPlayerColorChanged(Color oldCor, Color newCor)

{

nameText.color = newCor;

}

void Update()

{

if (!isLocalPlayer) return; //不应操作非本地玩家

Move();

if (Input.GetKeyDown(KeyCode.Space))

{

//随机生成颜色和名字

ChangedColorAndName();

}

}

public override void OnStartLocalPlayer()

{

//。。。

//开始就随机生成颜色和名字

ChangedColorAndName();

}

//player 的随机名称和颜色

private void ChangedColorAndName()

{

//随机名称和颜色

var tempName = $"Player{Random.Range(1, 999)}";

var tempColor = new Color(Random.Range(0, 1f), Random.Range(0, 1f), Random.Range(0, 1f), 1);

//同步变量进行修改

CmdSetupPlayer(tempName, tempColor);

}

//对于同步变量的修改,使用[Command]标记(针对方法的标记,方法名以Cmd开头)

//通过这个方法同时对name和颜色进行修改

[Command]

private void CmdSetupPlayer(string name, Color color)

{

playerName = name;

playerColor = color;

}

效果

七、同步动画

挂载Network Animator组件

private Animator anim; // 动画组件

anim = gameObject.GetComponentInChildren(); // 获取动画组件

public override void OnStartLocalPlayer()

{

//。。。

anim = gameObject.GetComponentInChildren(); // 获取动画组件

}

void Update()

{

if (!isLocalPlayer) return; //不应操作非本地玩家

//。。。

//攻击动画控制

if (Input.GetMouseButtonDown(0))

{

anim.SetTrigger("isAttack");

anim.SetBool("isIdle", false);

}else{

anim.SetBool("isIdle", true);

}

}

八、同步子弹

bomb就是普通的炸弹预制体

方法一

[ClientRpc]关键字,服务端可以向所有的连接的客户端发送同步指令,方法名也需要Rpc开头。

public GameObject bomb;//炸弹预制体

void Update()

{

if (!isLocalPlayer) return; //不应操作非本地玩家

//。。。

//生成炸弹

if (Input.GetMouseButtonDown(1))

{

Cmdshoot();

}

}

[Command]

private void Cmdshoot()

{

RpcWeaponFire();

}

[ClientRpc]

private void RpcWeaponFire()

{

GameObject b = Instantiate(bomb, transform.position, Quaternion.identity);

b.transform.Translate(1, 0, 0);//防止子弹撞到角色

b.GetComponent().AddForce(Vector2.up * 500f);

}

效果

方法二

NetworkManager最下面有个列表(Registered Spawnable Prefab),他是用来放游戏中需要孵化的物体的,比如说enemy(敌人),bullet(子弹)啊,都给它拖进去 ps:记得给炸弹添加Network Identity组件,不然拖不进去

public GameObject bomb;//炸弹预制体

void Update()

{

if (!isLocalPlayer) return; //不应操作非本地玩家

//。。。

//生成炸弹

if (Input.GetMouseButtonDown(1))

{

Cmdshoot();

}

}

[Command]

private void Cmdshoot()

{

GameObject b = Instantiate(bomb, transform.position, Quaternion.identity);

b.transform.Translate(1, 0, 0);//防止子弹撞到角色

b.GetComponent().AddForce(Vector2.up * 500f);

Destroy(b, 2.0f);//两秒后删除

NetworkServer.Spawn(b);//服务器孵化,同步客户端

}

效果 问题

你会发现客户端给炸弹施加的AddForce力并没有效果,原因是我们没有添加同步刚体的组件,给炸弹添加Network Rigidbody 2D组件

效果

方法三

子弹预设上挂以下脚本 NetworkIdentity:因为炮弹也是一个网络对象,所以它需要NetworkIdentity组件; 炮弹的Transform信息不使用NetworkTransform进行同步,而是通过Rigibody刚体组件的力来使炮弹飞行,所以只需要同步一下力即可,在Projectile脚本中实现炮弹的逻辑。

开炮需要由服务端来执行,

void Update()

{

// ...

// 检测玩家是否按下射击键

if (Input.GetKeyDown(shootKey))

{

// 调用射击命令

CmdFire();

}

}

// 这个方法在服务器端执行

[Command]

void CmdFire()

{

// 在发射点位置和方向实例化炮弹预制体

GameObject projectile = Instantiate(projectilePrefab, projectileMount.position, transform.rotation);

// 在网络上生成炮弹对象,使其在所有客户端同步

NetworkServer.Spawn(projectile);

// 调用客户端RPC方法,在所有客户端上播放射击动画

RpcOnFire();

}

// 这个方法在所有观察此坦克的客户端上执行

[ClientRpc]

void RpcOnFire()

{

// 设置动画器的"Shoot"触发器,播放射击动画

animator.SetTrigger("Shoot");

}

炮弹脚本 炮弹也是一个网络对象,它的行为脚本也必须继承NetworkBehaviour,

// Projectile.cs

public class Projectile : NetworkBehaviour

{

}

炮弹预设实例化后,需要给Rigibody一个力,从而让炮弹向前飞行,

// Projectile.cs

void Start()

{

rigidBody.AddForce(transform.forward * force);

}

炮弹需要有一个生命周期控制,超过5秒自动销毁,执行NetworkServer.Destroy(gameObject)来销毁对象,前面介绍了OnStartServer回调函数只在服务端调用,当在服务端生成一个游戏对象,或者服务端启动时被回调。

// Projectile.cs

public override void OnStartServer()

{

Invoke(nameof(DestroySelf), destroyAfter);

}

[Server]

void DestroySelf()

{

NetworkServer.Destroy(gameObject);

}

我们看到这里有一个[Server]注解,它表示只有服务端可以调用此函数。

九、聊天功能

新增ChatController脚本

using UnityEngine;

using Mirror;

using TMPro;

using UnityEngine.UI;

public class ChatController : NetworkBehaviour

{

public TMP_InputField chatText;//输入框

public Button chatBtn;//发送按钮

public GameObject chatInfo;//聊天框内容预制体

public GameObject chatFrame;//聊天框

public PlayerController playerController ;

[SyncVar(hook = nameof(OnChatTextStringChanged))]

public string chatTextString;

private void OnChatTextStringChanged(string oldstr, string newstr)

{

//添加聊天内容

GameObject ci = Instantiate(chatInfo);

ci.GetComponent().text = newstr;

ci.transform.SetParent(chatFrame.transform);

}

void Awake()

{

chatBtn.onClick.AddListener(SendBtn);

}

public void SendBtn()

{

if (player != null)

{

playerController.CmdSendPLayerMessage(chatText.text);

}

}

}

修改PlayerController,调用传送人物名字

private ChatController chatController;

void Awake()

{

chatController = FindObjectOfType();

}

public override void OnStartLocalPlayer()

{

//。。。

chatController.playerController = this;

}

[Command]

public void CmdSendPLayerMessage(string message)

{

if (chatController != null)

{

chatController.chatTextString = playerName + "说:" + message;

}

}

绘制UI页面,记得添加Network Identity组件 记得给聊天的UI canvas挂载Network Identity脚本

效果

十、场景同步切换

新建三个场景NetworkManager对象上 新增ScenceController 代码,控制NetworkManagerHUD的显隐

using UnityEngine;

using UnityEngine.UI;

using Mirror;

using UnityEngine.SceneManagement;

public class ScenceController : MonoBehaviour

{

private void Update()

{

Scene scene = SceneManager.GetActiveScene();

//控制NetworkManagerHUD的显隐

if(scene.name == "Main"){

GetComponent().enabled = false;

}else{

GetComponent().enabled = true;

}

}

//开始游戏,场景切换

public void ButtonLoadScene()

{

SceneManager.LoadScene("SampleScene1");

}

}

Main场景为游戏开始页面,默认就放一个按钮,按钮调用ButtonLoadScene方法,Network Manager只需要在初始场景挂载即可(及Main场景),前面代码已经控制了NetworkManagerHUD的显隐,报错HUD视图你不在主场景显示 挂载对应的场景

SampleScene1和SampleScene2场景基本没啥区别,更前面的游戏页面一样,删除原本的NetworkManager对象,防止与主界面Main场景的冲突 新增ButtonChangeScene方法,控制游戏内的场景切换,方法挂载在SampleScene1和SampleScene2场景的场景切换按钮上

//同步切换场景

public void ButtonChangeScene()

{

if (isServer)

{

var scene = SceneManager.GetActiveScene();

NetworkManager.singleton.ServerChangeScene

(

scene.name == "SampleScene1" ? "SampleScene2" : "SampleScene1"

);

}

else

{

Debug.Log("你不是host");

}

}

效果

十一、重新绘制一个HUD界面

NetworkManagerHUD(需要配合Network Manager组件),他会自动绘制一个GUI: Host(主机):相当于又是服务器又是客户端。

Client:连接服务端,后面是服务端IP地址,localhost为本地端口,相当于自己连接自己。

Server Only:只当服务端。

但是,这个UI界面不太好看,所以我们一般不用这个组件,都会自己制作GUI。

在场景中新增三个按钮

新增MyNetworkManagerHUD 代码,挂载在游戏页面,实例代码

using UnityEngine;

using UnityEngine.UI;

using Mirror;

public class MyNetworkManagerHUD : MonoBehaviour

{

private NetworkManager networkManager; // 创建 NetworkManager 对象

public GameObject btn;

public GUISkin mySkin;

private GameObject startHost;//启动网络主机按钮

private GameObject startClient;//启动网络客户端按钮

private GameObject stopHost;//停止网络主机或客户端按钮

void Awake()

{

networkManager = FindObjectOfType();

startHost = GameObject.Find("StartHost");

startClient = GameObject.Find("StartClient");

stopHost = GameObject.Find("StopHost");

startHost.GetComponent