MirrorNetworking

概念

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(单机转多人)

  1. 场景中添加一个空物体名为NetworkManager,添加组件NetworkManangerNetworkManagerHUDKCPTransport

  2. 对于player-objects,添加NetworkTransform(会自动添加NetworkIdentity),勾选ClientAuthority

    做成预制体,并设置到NetworkManagerPlayerPrefab

    场景中添加空物体,添加NetworkStartPosition组件,作为出生点,并在NetworkManager选择出生点循环方式(RoundRobin是轮流)

  3. 相关脚本改为继承自NetworkBehaviour

    在脚本的输入和控制部分添加判断isLocalPlayer(否则直接return

    覆写OnStartLocalPlayer绑定主相机和其它组件

  4. 重要的成员变量添加属性[SyncVar]标记,重要的成员函数使用[Command]标记

  5. 对于non-player-objects,添加NetworkTransform(会自动添加NetworkIdentity)

    NetworkManagerSpawnablePrefab一栏注册预制体

  6. 对于实例化对象的脚本,也需要继承自NetworkBehaviour

    在实例化之前检查isServer(或者在OnStartServer调用)

    实例化之后调用NetworkServer.Spawn()

实例

当需要同步某些表现时,首先需要向服务器发送命令,然后服务器通过某种机制为所有客户端调用相关函数来进行更改。这种机制可能是[SyncVar]绑定的hook函数,hook函数会在变量改变时在所有客户端上进行执行,又或者是[ClientRpc]

同步显示玩家头顶名字

OnStartLocalPlayer里调用CmdSetupPlayerCmdSetupPlayerplayerName字段赋值,触发绑定的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
// player脚本,继承自NetworkBehaviour
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; // 玩家头顶UI

[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中指定OfflineSceneOnlineScene,前者是客户端断开后自动切换到的场景

1
2
3
4
5
if(isServer){
NetworkManager.singleton.ServerChangeScene("xxxSceneName");
}

// 在离线场景中的脚本继承自MonoBehaviour,仍然是SceneManager.LoadScene("xxx")

原理

编写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, 1UL);
}
}
}

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, 2UL);
if (NetworkServer.localClientActive && !base.getSyncVarHookGuard(2UL))
{
base.setSyncVarHookGuard(2UL, true);
this.OnTestSyncVarWithHookChanged(oldValue, value);
base.setSyncVarHookGuard(2UL, 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 & 1UL) != 0UL)
{
writer.WriteInt(this.testSyncVar);
result = true;
}
if ((base.syncVarDirtyBits & 2UL) != 0UL)
{
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中的结构类型(比如Vector3QuaternionRectRayGuid)以及NetworkIdentityGameObject、Transform

这些类型的序列化方法定义在在Mirror/Runtime/NetworkReader.csMirror/Runtime/NetworkWriter.cs中,可以看到NetworkIdentityGameObject、Transform的序列化只是简单地传递了一个int32类型的netId

自动生成

只要自定义类型中的字段属于内置类型,Weaver会自动为public字段(不包括属性)生成读写函数,比如

1
2
3
4
public struct MyData{
public int someVal;
public float anotherVal;
}

[System.NonSerialized]标记public字段后就不会自动生成读写函数

自定义

主要是通过对NetworkWriterNetworkReader定义扩展方法来实现

1
2
3
4
5
6
public static void WriteMyType(this NetworkWriter writer, MyType value){
// 把value的字段写入writer
}
public static MyType ReadMyType(this NetworkReader reader){
// 从reader读取字段,构造并返回一个实例
}

具体例子可以参考之前的文章Unity Mirror 自定义序列化器传输数据

ParrelSync

VeriorPies/ParrelSync: (Unity3D) Test multiplayer without building (github.com)

使用ParrelSync不用打包即可同时运行客户端和服务器

用git安装或者直接复制仓库里的Editor文件夹到项目中。在顶部栏选择ParrelSync > CloneManager,选择创建新的克隆,然后在新的编辑器里打开克隆的项目。

它的原理是为原项目的AssetPackagesProjectSettings建立符号链接,而Library之类的文件夹才会独立复制一份。所以素材资源只需要放到原项目中,克隆的项目打开后只需要另外编译脚本(生成的dll在Library文件夹)