Unity UNet迁移到Mirror

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.activetrue你是服务端
NetworkClient.activetrue你也是客户端
isServertrue(对于你自己创建的对象)当前对象是服务端副本
isLocalPlayertrue(对于本地玩家对象)当前对象由你控制

脚本资源替换

以上部分内容只是把代码从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,示例图如下:

mirror transport choose and port setup

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

留下评论

您的邮箱地址不会被公开。 必填项已用 * 标注