概念 The Server & Client are ONE project in order to achieve an order of magnitude gain in productivity.
vis2k/Mirror: #1 Open Source Unity Networking Library (github.com)
AssetStore
官方文档
QuickStart(单机转多人) 场景中添加一个空物体名为NetworkManager
,添加组件NetworkMananger
、NetworkManagerHUD
、KCPTransport
对于player-objects
,添加NetworkTransform
(会自动添加NetworkIdentity
),勾选ClientAuthority
做成预制体,并设置到NetworkManager
的PlayerPrefab
场景中添加空物体,添加NetworkStartPosition
组件,作为出生点,并在NetworkManager
选择出生点循环方式(RoundRobin
是轮流)
相关脚本改为继承自NetworkBehaviour
在脚本的输入和控制部分添加判断isLocalPlayer
(否则直接return
)
覆写OnStartLocalPlayer
绑定主相机和其它组件
重要的成员变量添加属性[SyncVar]
标记,重要的成员函数使用[Command]
标记
对于non-player-objects
,添加NetworkTransform
(会自动添加NetworkIdentity
)
在NetworkManager
的SpawnablePrefab
一栏注册预制体
对于实例化对象的脚本,也需要继承自NetworkBehaviour
在实例化之前检查isServer
(或者在OnStartServer
调用)
实例化之后调用NetworkServer.Spawn()
实例 当需要同步某些表现时,首先需要向服务器发送命令,然后服务器通过某种机制为所有客户端调用相关函数来进行更改。这种机制可能是[SyncVar]
绑定的hook函数,hook函数会在变量改变时在所有客户端上进行执行,又或者是[ClientRpc]
同步显示玩家头顶名字 在OnStartLocalPlayer
里调用CmdSetupPlayer
,CmdSetupPlayer
为playerName
字段赋值,触发绑定的OnPlayerNameChanged
,此时才设置真正显示的文字。
CmdSetupPlayer
在服务器上执行,而绑定的hook
函数OnPlayerNameChanged
会在所有客户端对象上调用,从而实现同步显示
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Player : NetworkBehaviour { public override void OnStartLocalPlayer ( ) { var randomName = "Player #" + Random.Range(100 , 999 ).ToString(); CmdSetupPlayer(randomName); } [Command ] private void CmdSetupPlayer (string name ) { playerName = name; } private void OnPlayerNameChanged (string oldValue, string newValue ) { playerNameText.text = newValue; } public Text playerNameText; [SyncVar(hook = nameof(OnPlayerNameChanged)) ] public string playerName; }
同步玩家开枪 先调用CmdShoot
,然后到服务器上执行CmdShoot
具体逻辑,而CmdShoot
调用RpcShoot
,为每个客户端都生成子弹
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void Update ( ) { if (Input.GetButtonDown("Fire" )){ CmdShoot(); } } [Command ] void CmdShoot ( ) { RpcShoot(); } [ClientRpc ] void RpcShoot ( ) { }
同步显示非玩家物体的状态 非玩家物体要调用Cmd,要么出生时带有ClientAuthority
,要么调用了NetworkIdentity.AssignClientAuthority
,要么[Command]
使用参数为false
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [SyncVar ] DoorState doorState; void Update ( ) { if (Input.GetButtonDown("OpenDoor" )){ OpenDoor(); } } [Client ] void OpenDoor ( ) { CmdOpenDoor(); } [Command(requireAuthority = false) ] void CmdOpenDoor (NetworkConnectionToClient = null ) { doorState = DoorState.Open; }
场景切换 在NetworkManager
中指定OfflineScene
和OnlineScene
,前者是客户端断开后自动切换到的场景
1 2 3 4 5 if (isServer){ NetworkManager.singleton.ServerChangeScene("xxxSceneName" ); }
原理 编写TestNetworkBehaviour
脚本
原代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 using System.Collections.Generic;using UnityEngine;using Mirror;public class TestNetworkBehaviour : NetworkBehaviour { [SyncVar ] int testSyncVar; [SyncVar(hook = nameof(OnTestSyncVarWithHookChanged)) ] int testSyncVarWithHook; void OnTestSyncVarWithHookChanged (int oldValue, int newValue ) { Debug.Log($"Test Sync var with hook changed from {oldValue} to {newValue} " ); } [ClientRpc ] void RpcTestClientRpc (int testParam ) { Debug.Log("Test ClientRpc" ); } [TargetRpc ] void RpcTestTargetRpc (int testParam ) { Debug.Log("Test TargetRpc" ); } [Command ] void CmdTestCmd (int testParam ) { Debug.Log("Test Cmd" ); } [Client ] void TestClient (int testParam ) { Debug.Log("Test Client" ); } [Server ] void TestServer (int testParam ) { Debug.Log("Test Server" ); } void Awake ( ) { testSyncVar = 1 ; testSyncVarWithHook = 1 ; } }
编译后在项目根目录找到Library/ScriptAssemblies/Assembly-CSharp.dll
,使用dnspy
进行反编译
反编译代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 using System;using System.Runtime.InteropServices;using Mirror;using Mirror.RemoteCalls;using UnityEngine;public class TestNetworkBehaviour : NetworkBehaviour { [SyncVar ] private int testSyncVar; [SyncVar(hook = "OnTestSyncVarWithHookChanged" ) ] private int testSyncVarWithHook; private void OnTestSyncVarWithHookChanged (int oldValue, int newValue ) { Debug.Log(string .Format("Test Sync var with hook changed from {0} to {1}" , oldValue, newValue)); } [ClientRpc ] private void RpcTestClientRpc (int testParam ) { PooledNetworkWriter writer = NetworkWriterPool.GetWriter(); writer.WriteInt(testParam); this .SendRPCInternal(typeof (TestNetworkBehaviour), "RpcTestClientRpc" , writer, 0 , true ); NetworkWriterPool.Recycle(writer); } [TargetRpc ] private void RpcTestTargetRpc (int testParam ) { PooledNetworkWriter writer = NetworkWriterPool.GetWriter(); writer.WriteInt(testParam); this .SendTargetRPCInternal(null , typeof (TestNetworkBehaviour), "RpcTestTargetRpc" , writer, 0 ); NetworkWriterPool.Recycle(writer); } [Command ] private void CmdTestCmd (int testParam ) { PooledNetworkWriter writer = NetworkWriterPool.GetWriter(); writer.WriteInt(testParam); base .SendCommandInternal(typeof (TestNetworkBehaviour), "CmdTestCmd" , writer, 0 , true ); NetworkWriterPool.Recycle(writer); } [Client ] private void TestClient (int testParam ) { if (!NetworkClient.active) { Debug.LogWarning("[Client] function 'System.Void TestNetworkBehaviour::TestClient(System.Int32)' called when client was not active" ); return ; } Debug.Log("Test Client" ); } [Server ] private void TestServer (int testParam ) { if (!NetworkServer.active) { Debug.LogWarning("[Server] function 'System.Void TestNetworkBehaviour::TestServer(System.Int32)' called when server was not active" ); return ; } Debug.Log("Test Server" ); } private void Awake ( ) { this .NetworktestSyncVar = 1 ; this .NetworktestSyncVarWithHook = 1 ; } private void MirrorProcessed ( ) { } public int NetworktestSyncVar { get { return this .testSyncVar; } [param: In ] set { if (!base .SyncVarEqual<int >(value , ref this .testSyncVar)) { int num = this .testSyncVar; base .SetSyncVar<int >(value , ref this .testSyncVar, 1U L); } } } public int NetworktestSyncVarWithHook { get { return this .testSyncVarWithHook; } [param: In ] set { if (!base .SyncVarEqual<int >(value , ref this .testSyncVarWithHook)) { int oldValue = this .testSyncVarWithHook; base .SetSyncVar<int >(value , ref this .testSyncVarWithHook, 2U L); if (NetworkServer.localClientActive && !base .getSyncVarHookGuard(2U L)) { base .setSyncVarHookGuard(2U L, true ); this .OnTestSyncVarWithHookChanged(oldValue, value ); base .setSyncVarHookGuard(2U L, false ); } } } } protected void UserCode_RpcTestClientRpc (int testParam ) { Debug.Log("Test ClientRpc" ); } protected static void InvokeUserCode_RpcTestClientRpc (NetworkBehaviour obj, NetworkReader reader, NetworkConnectionToClient senderConnection ) { if (!NetworkClient.active) { Debug.LogError("RPC RpcTestClientRpc called on server." ); return ; } ((TestNetworkBehaviour)obj).UserCode_RpcTestClientRpc(reader.ReadInt()); } protected void UserCode_RpcTestTargetRpc (int testParam ) { Debug.Log("Test TargetRpc" ); } protected static void InvokeUserCode_RpcTestTargetRpc (NetworkBehaviour obj, NetworkReader reader, NetworkConnectionToClient senderConnection ) { if (!NetworkClient.active) { Debug.LogError("TargetRPC RpcTestTargetRpc called on server." ); return ; } ((TestNetworkBehaviour)obj).UserCode_RpcTestTargetRpc(reader.ReadInt()); } protected void UserCode_CmdTestCmd (int testParam ) { Debug.Log("Test Cmd" ); } protected static void InvokeUserCode_CmdTestCmd (NetworkBehaviour obj, NetworkReader reader, NetworkConnectionToClient senderConnection ) { if (!NetworkServer.active) { Debug.LogError("Command CmdTestCmd called on client." ); return ; } ((TestNetworkBehaviour)obj).UserCode_CmdTestCmd(reader.ReadInt()); } static TestNetworkBehaviour ( ) { RemoteCallHelper.RegisterCommandDelegate(typeof (TestNetworkBehaviour), "CmdTestCmd" , new CmdDelegate(TestNetworkBehaviour.InvokeUserCode_CmdTestCmd), true ); RemoteCallHelper.RegisterRpcDelegate(typeof (TestNetworkBehaviour), "RpcTestClientRpc" , new CmdDelegate(TestNetworkBehaviour.InvokeUserCode_RpcTestClientRpc)); RemoteCallHelper.RegisterRpcDelegate(typeof (TestNetworkBehaviour), "RpcTestTargetRpc" , new CmdDelegate(TestNetworkBehaviour.InvokeUserCode_RpcTestTargetRpc)); } public override bool SerializeSyncVars (NetworkWriter writer, bool forceAll ) { bool result = base .SerializeSyncVars(writer, forceAll); if (forceAll) { writer.WriteInt(this .testSyncVar); writer.WriteInt(this .testSyncVarWithHook); return true ; } writer.WriteULong(base .syncVarDirtyBits); if ((base .syncVarDirtyBits & 1U L) != 0U L) { writer.WriteInt(this .testSyncVar); result = true ; } if ((base .syncVarDirtyBits & 2U L) != 0U L) { writer.WriteInt(this .testSyncVarWithHook); result = true ; } return result; } public override void DeserializeSyncVars (NetworkReader reader, bool initialState ) { base .DeserializeSyncVars(reader, initialState); if (initialState) { int num = this .testSyncVar; this .NetworktestSyncVar = reader.ReadInt(); int num2 = this .testSyncVarWithHook; this .NetworktestSyncVarWithHook = reader.ReadInt(); if (!base .SyncVarEqual<int >(num2, ref this .testSyncVarWithHook)) { this .OnTestSyncVarWithHookChanged(num2, this .testSyncVarWithHook); } return ; } long num3 = (long )reader.ReadULong(); if ((num3 & 1L ) != 0L ) { int num4 = this .testSyncVar; this .NetworktestSyncVar = reader.ReadInt(); } if ((num3 & 2L ) != 0L ) { int num5 = this .testSyncVarWithHook; this .NetworktestSyncVarWithHook = reader.ReadInt(); if (!base .SyncVarEqual<int >(num5, ref this .testSyncVarWithHook)) { this .OnTestSyncVarWithHookChanged(num5, this .testSyncVarWithHook); } } } }
[SyncVar] 编译之后生成对应Network
开头的属性,set
方法里设置对应dirtyBit
,如果有hook就调用hook函数,代码里用到原变量的地方全部替换成了编译生成的属性
编译生成的SerializeSyncVars
方法里,测试dirtyBit
,返回变量值是否发生更新;DeserializeSyncVars
方法里,如果是初始化,则全部用生成的属性赋值一遍,否则测试dirtyBit
,有需要更新的才赋值对应的属性
[ClientRPC] 用户代码被移动到UserCode_
开头的方法里,还生成了对应的InvokeUserCode_
开头的方法。而原方法改为调用SendRPCInternal
发送网络消息,让clients调用InvokeUserCode_
开头的方法
调用参数被序列化到了NetworkWriter
对象中,调用函数名对应的字符串在静态构造函数中使用RemoteCallHelper
类的相关方法进行了注册
[TargetRPC] 基本同上,换成了SendTargetRPCInternal
[Command] 基本同上,换成了SendCommandInternal
[Client] 编译后添加检查NetworkClient.active
,否则警告
[Server] 编译后添加检查NetworkServer.active
,否则警告
Serialization序列化 Mirror
执行序列化和反序列化使用的是Weaver
,而Weaver
是在Unity用Mono.Cecil
编译之后修改dll
内置 Mirror
本身支持序列化的类型包括C#的基本数据类型、Unity中的结构类型(比如Vector3
、Quaternion
、Rect
、Ray
、Guid
)以及NetworkIdentity
、GameObject、Transform
。
这些类型的序列化方法定义在在Mirror/Runtime/NetworkReader.cs
和Mirror/Runtime/NetworkWriter.cs
中,可以看到NetworkIdentity
、GameObject、Transform
的序列化只是简单地传递了一个int32
类型的netId
自动生成 只要自定义类型中的字段属于内置类型,Weaver
会自动为public
字段(不包括属性)生成读写函数,比如
1 2 3 4 public struct MyData{ public int someVal; public float anotherVal; }
用[System.NonSerialized]
标记public
字段后就不会自动生成读写函数
自定义 主要是通过对NetworkWriter
和NetworkReader
定义扩展方法来实现
1 2 3 4 5 6 public static void WriteMyType (this NetworkWriter writer, MyType value ) { } public static MyType ReadMyType (this NetworkReader reader ) { }
具体例子可以参考之前的文章Unity Mirror 自定义序列化器传输数据
ParrelSync VeriorPies/ParrelSync: (Unity3D) Test multiplayer without building (github.com)
使用ParrelSync不用打包即可同时运行客户端和服务器
用git安装 或者直接复制仓库里的Editor
文件夹到项目中。在顶部栏选择ParrelSync > CloneManager
,选择创建新的克隆,然后在新的编辑器里打开克隆的项目。
它的原理是为原项目的Asset
、Packages
、ProjectSettings
建立符号链接,而Library
之类的文件夹才会独立复制一份。所以素材资源只需要放到原项目中,克隆的项目打开后只需要另外编译脚本(生成的dll在Library
文件夹)