Unity项目升级到Unity2021后,发现原来的UNet内容不可用了。网络搜索一番,可选的更新方案是NetCore,Mirror。而Mirror是从UNet发展而来,对其接口函数有比较好的兼容性,迁移的工作量会比较小。于是就决定将项目迁移到Mirror了!
整个迁移过程主要参考 Mirror官网文档 ,Mirror版本为 Mirror v96.6.6
主要改动
回顾整个迁移工作主要涉及两大改动:
- Mirror 版本中没有房间的概念了,其官方文档的意思就是,连接上某个server了就是等同于进入某个房间了;若要实现房间的概念,那么需要自行实现一个房间的系统: 需要自行实现一个ROOM SERVER来对接;若项目中没有房间的处理逻辑,则直接删除该部分代码即可!
- 带 [SyncVar] 标记的变量的处理,Mirror会自动生成相关的代码来同步该变量的变化
- 带 [SyncVar] 标记的变量 的GET/SET 数值设置和赋值函数会自动生成,不需要手动生成了;
- 序列化和反序列化函数中处理的都是带 [SyncVar] 标记的变量,那么这个 OnSerialize() / OnDeserialize() 可以直接删掉了,新版Mirror中 这两个函数处理的的是自定义数据内容的同步
代码改动列表
using UnityEngine.Networking;
// 修改为
using Mirror;
删除代码调用 base.SendCommandInternal / this.SendRPCInternal
, 此类函数调用转换为 根据命令发出方是服务器还是客户端,来选择 [Command] / [ClientRpc]
标记的函数调用
删除代码 NetworkBehaviour.RegisterCommandDelegate / NetworkCRC.RegisterBehaviour ,这个函数通常在静态类的构造函数中被调用,该静态构造函数都可以删掉
类似这样的函数 static void InvokeRpcXXX() / static void InvokeCmdXXX() ,它们遵循一定的规律,更像是自动生成的,对于这类函数可以直接删除
对于 void CallCmdXXX() 的函数则需要仔细判断,尤其是里面有 base.isServer / base.isLocalPlayer 这样的判断代码,base.isLocalPlayer为真时,执行 command 函数调用;而base.isServer为真时,执行 rpc 函数调用
NetworkInstanceId 修改为 uint
GameObject gameObject = ClientScene.FindLocalObject(objectid);
// ClientScene.FindLocalObject 修改为 NetworkClient.spawned.TryGetValue
if (NetworkClient.spawned.TryGetValue(objectid, out NetworkIdentity entry)) {
GameObject gameObject = entry.gameObject;
}
[SyncVar(hook = "OnHpChanged")]
public int hp;
void OnHpChanged(int newValue) {
// ...
}
// 修改为 如下形式:
[SyncVar(hook = nameof(OnHpChanged))]
public int hp;
// oldValue 是新加的参数
void OnHpChanged(int oldValue, int newValue) {
// ...
}
private void UNetVersion()
{
}
// UNetVersion 是一个占位函数,该函数下面所有的函数大概率都是UNet生成的,可以视情况删除掉
public int NetworkHungry
{
get
{
return this.Hungry;
}
set
{
base.SetSyncVar<int>(value, ref this.Hungry, 1u);
}
}
// NetworkHungry GET/SET属性设置 都删掉,所有对NetworkHungry的访问都改为对Hungry
protected static void InvokeCmdCmdEatUpdate(NetworkBehaviour obj, NetworkReader reader)
{
if (!NetworkServer.active)
{
UnityEngine.Debug.LogError("Command CmdEatUpdate called on client.");
return;
}
((CharacterHero)obj).CmdEatUpdate((int)reader.ReadPackedUInt32());
}
// InvokeCmdXXX 这样简短的函数直接删除掉
public void CallCmdEatUpdate(int num)
{
if (!NetworkClient.active)
{
return;
}
if (base.isServer)
{
this.CmdEatUpdate(num);
return;
}
NetworkWriter networkWriter = new NetworkWriter();
networkWriter.Write(0);
networkWriter.Write((short)((ushort)5));
networkWriter.WritePackedUInt32((uint)CharacterLiving.kCmdCmdEatUpdate);
networkWriter.Write(base.GetComponent<NetworkIdentity>().netId);
networkWriter.WritePackedUInt32((uint)num);
base.SendCommandInternal(networkWriter, 0, "CmdEatUpdate");
}
// 注意 服务端调用函数和客户端调用函数区分,调用参数的传递Mirror都自动出来了,修改为:
public void CallCmdEatUpdate(int num)
{
if (!NetworkClient.active)
{
return;
}
if (base.isLocalPlayer)
{
this.CmdEatUpdate(num);
return;
}
}
[TargetRpc]
public void TargetReciveItem(NetworkConnection target, string itemid) { ... }
// TargetRpc 函数的第一个参数修改为 NetworkConnectionToClient 类型, 因此原函数修改为
[TargetRpc]
public void TargetReciveItem(NetworkConnectionToClient target, string itemid) { ... }
if (base.GetComponent<NetworkTransform>())
// NetworkTransform 修改为 NetworkTransformHybrid, 在Mirror中 NetworkTransformHybrid 的功能 与 UNet的 NetworkTransform 功能比较接近
if (base.GetComponent<NetworkTransformHybrid>())
public override void OnNetworkDestroy() { }
// Mirror 这个函数没有了,它被拆分成了以下两个函数
public override void OnStopClient() { }
public override void OnStopServer() { }
bool bReady = this.network.clientLoadedScene;
// 修改为
bool bReady = NetworkClient.ready;
public override bool OnSerialize(NetworkWriter writer, bool forceAll)
{
...
return true;
}
// 修改函数返回类型
public override void OnSerialize(NetworkWriter writer, bool forceAll)
{
...
}
public override void OnServerReady(NetworkConnection conn) { ... }
// 修改为:
public override void OnServerAddPlayer(NetworkConnectionToClient conn) { ... }
NetworkManager.singleton.networkPort = 12345;
// 修改为如下代码:
// 设置端口 Mirror 把端口设置挪动到传输协议类中去了
PortTransport transport = (PortTransport)Transport.active;
transport.Port = 12345;
其他代码注意事项
在 Start()
里为什么不安全调用 RPC?
在 Start()
调用时:
- 对象可能还没被
NetworkServer.Spawn
(服务端); - 或者客户端还没接收到该对象并注册到
NetworkClient.spawned
中; - 对象的
connectionToClient
还未绑定(null
); - 还没完成权限设置,比如
hasAuthority
还没确定; - 如果是
TargetRpc
,还找不到目标连接。
Host 模式下 = 同时是客户端 + 服务端
Host 模式下的状态:
属性 | 结果 | 说明 |
---|---|---|
NetworkServer.active | ✅ true | 你是服务端 |
NetworkClient.active | ✅ true | 你也是客户端 |
isServer | ✅ true (对于你自己创建的对象) | 当前对象是服务端副本 |
isLocalPlayer | ✅ true (对于本地玩家对象) | 当前对象由你控制 |
脚本资源替换
以上部分内容只是把代码从UNet迁移到了Mirror,并解决了编译问题。但整个迁移过程并没有完成,此时项目并不能正常跑起来。还需要把对应的脚本引用修改过来,例如:
UnityEngine.Networking.NetworkIdentity
修改为 :
Mirror.NetworkIdentity
Mirror中的类名都沿用了UNet中的类名,甚至字段都保留了(NetworkTransform 除外)。因此,这个过程就很直接了:找出原来的脚本引用,修改为新的。例如:
UnityEngine.Networking.NetworkIdentity 的引用为:
{fileID: 372142912, guid: dc443db3e92b4983b9738c1131f555cb, type: 3}
搜索整个项目,把关卡、prefab中的引用修改为 Mirror.NetworkIdentity 的引用即可!
找出UNet中类对应的脚本引用
项目设置为’Force Text’资源模式,新建一个空场景,写一个简单的编辑器脚本执行以下代码
using UnityEngine.Networking;
new GameObject("NetworkManager").AddComponent<NetworkManager>();
new GameObject("NetworkIdentity").AddComponent<NetworkIdentity>();
new GameObject("NetworkLobbyManager").AddComponent<NetworkLobbyManager>();
new GameObject("NetworkMigrationManager").AddComponent<NetworkMigrationManager>();
new GameObject("NetworkStartPosition").AddComponent<NetworkStartPosition>();
new GameObject("NetworkDiscovery").AddComponent<NetworkDiscovery>();
new GameObject("NetworkProximityChecker").AddComponent<NetworkProximityChecker>();
new GameObject("NetworkLobbyPlayer").AddComponent<NetworkLobbyPlayer>();
new GameObject("NetworkManagerHUD").AddComponent<NetworkManagerHUD>();
new GameObject("NetworkTransformChild").AddComponent<NetworkTransformChild>();
new GameObject("NetworkTransform").AddComponent<NetworkTransform>();
new GameObject("NetworkBehaviour").AddComponent<NetworkBehaviour>();
new GameObject("NetworkTransformVisualizer").AddComponent<NetworkTransformVisualizer>();
代码执行完之后,保存场景,再用文本编辑器打开场景文件,这时可以找出 UNet类名 和 脚本引用了。我找到这个对应关系如下:
UnityEngine.Networking.NetworkManager=>{fileID: -822479833, guid: dc443db3e92b4983b9738c1131f555cb, type: 3}
UnityEngine.Networking.NetworkIdentity=>{fileID: 372142912, guid: dc443db3e92b4983b9738c1131f555cb, type: 3}
UnityEngine.Networking.NetworkLobbyManager=>{fileID: -859942077, guid: dc443db3e92b4983b9738c1131f555cb, type: 3}
UnityEngine.Networking.NetworkMigrationManager=>{fileID: 1180575928, guid: dc443db3e92b4983b9738c1131f555cb, type: 3}
UnityEngine.Networking.NetworkStartPosition=>{fileID: -1050975500, guid: dc443db3e92b4983b9738c1131f555cb, type: 3}
UnityEngine.Networking.NetworkDiscovery=>{fileID: -1553936188, guid: dc443db3e92b4983b9738c1131f555cb, type: 3}
UnityEngine.Networking.NetworkAnimator=>{fileID: 1590678366, guid: dc443db3e92b4983b9738c1131f555cb, type: 3}
UnityEngine.Networking.NetworkProximityChecker=>{fileID: 1205285110, guid: dc443db3e92b4983b9738c1131f555cb, type: 3}
UnityEngine.Networking.NetworkLobbyPlayer=>{fileID: 246975512, guid: dc443db3e92b4983b9738c1131f555cb, type: 3}
UnityEngine.Networking.NetworkManagerHUD=>{fileID: 227461547, guid: dc443db3e92b4983b9738c1131f555cb, type: 3}
UnityEngine.Networking.NetworkTransformChild=>{fileID: -1267208747, guid: dc443db3e92b4983b9738c1131f555cb, type: 3}
UnityEngine.Networking.NetworkTransform=>{fileID: -1768714887, guid: dc443db3e92b4983b9738c1131f555cb, type: 3}
UnityEngine.Networking.NetworkBehaviour=>{fileID: -1355867648, guid: dc443db3e92b4983b9738c1131f555cb, type: 3}
UnityEngine.Networking.NetworkTransformVisualizer=>{fileID: -837897634, guid: dc443db3e92b4983b9738c1131f555cb, type: 3}
找出新旧脚本的对应表,对整个项目进行 脚本引用 替换
根据上面的UNet类和引用的对应表:
- 根据类名,例如根据 UnityEngine.Networking.NetworkIdentity ,在项目的Mirror目录下 寻找 NetworkIdentity.cs 文件,并从对应的 NetworkIdentity.cs.meta中读取 guid值
- UnityEngine.Networking.NetworkTransform 修改为找Mirror目录下的
NetworkTransformHybrid.cs 文件
上面的过程结束后,可以得到 一个如下对应关系
{fileID: xxx, guid: xxx_old, type: 3} -> { fileID: xxx, guid: xxx_new, type: 3 } 字典,这个时候需要把项目中 所有关卡、prefab资源对前者的引用,修改为对后者的引用。这个修正可以采用python脚本自动执行:
## 修正对prefab的引用
def correct_guid_ref(asset_folder, guid_mappings):
files = Helper.FileUtil.list_files_in_folder_with_exts(asset_folder, None, ['.unity', '.prefab', '.asset'])
for file in files:
lines = Helper.FileUtil.read_asset_as_YAML_lines(file)
bChanged = False
for i in range(len(lines)):
line = lines[i]
index = line.find(', guid: dc443db3e92b4983b9738c1131f555cb,')
if index != -1:
old_part = line[line.find('{'):line.rfind('}')+1]
if old_part in guid_mappings:
line = line.replace(old_part, guid_mappings[old_part])
lines[i] = line
bChanged = True
else :
print('can not find ' + old_part)
if bChanged:
print('correct guid ref: ' + file)
Helper.FileUtil.save_lines_to_file(file, lines)
执行脚本之后,可以检查下项目中是否还存在对UNet脚本的引用,简单一点的话,就用 Total Commander 搜索 以下文本是否存在:
guid: dc443db3e92b4983b9738c1131f555cb
有关 迁移的资源修正的完整python代码,请查看这里:unity-unet-migrated-to-mirror
Mirror端口设置
在 NetworkManager 对象上添加脚本 KcpTransport (选择传输层协议,以及设置端口),并把它设置为前者的Transport,示例图如下:

到此,迁移过程完成。如没有什么其他问题的话,项目基本可以跑起来了!