由于mirror的概念和API同已经弃用的unet很相似。同时mirror的文档在基础概念比较少,而且全英文。所以可以先从unet的官方文档开始入门。UNet
mirror入门
1) 运行环境:
从AssetStore下载mirror插件。
注意.net版本要求。需要在playersetting1里将.net版本改成.net4.x。
2) 构建NetworkManager
建立一个空物体,将networkManager,Transport,networkManagerHUD组件挂在上面。
3) 制作playerPrefab
建立一个cube,在上面挂上networkIdentity作为在网络同步的唯一标识。之后加上networkTransform做位置的网络同步。勾选Client Authority,说明是客户端控制位移(因为是玩家)。在挂上一个随机移动的mono脚本。打包运行后发现server端上有跳跃的现象。
是因为server上物体随机位置计算了一次后,又同步了client上的位置一次造成闪烁。于是为了解决这个问题,就需要将脚本从继承MonoBehaviour改为继承NetworkBehaviour。
Start函数 ——>重写OnStartLocalPlayer函数(视脚本所挂物体而定(这个是只在客户端运行的函数)详看networkBehaviour的必然函数执行顺序)
还是要避免直接使用mono的start来初始化。因为在host下,mono的start只执行一次。但host在内部分为localclient和server。虽然共享场景,但依然可能导致其中一个没有被初始化(执行顺序不一定)。
4) client向server发送命令(command)
要将命令封装为一个方法,方法名以Cmd开头,给方法添加[Command]特性即可。
[Command] private void CmdSetRandomColor() { Debug.Log("CMD"); Color rc = Random.ColorHSV(); SetColor(rc); RpcSetRandomColor(rc); }
注意:1.实际上是client调用了server上的方法,显然command不能从server上发出。2.要在调用command方法时应判断是不是localPlayer,否则player即使不是本地玩家会也去调用command方法,导致弹出警告(command也不会执行,因为一个玩家不能在另一个玩家的游戏对象上调用命令,只能在自己的游戏对象上调用命令)。
由于是client——>server,会发现虽然server上的颜色改了,但client上并没有变化。
5) 于是需要一个server的回调,server——>client。
类似command用法,用一个方法封装回调。方法名以Rpc开头,添加[ClientRPC]特性即可。
[Command] private void CmdSetRandomColor() { Debug.Log("CMD"); Color rc = Random.ColorHSV(); SetColor(rc); RpcSetRandomColor(rc); } [ClientRpc] private void RpcSetRandomColor(Color color) { SetColor(color); }
于是client就能和server同步。
client—调用—>server的[command]方法—调用—>client的[ClientRPC]方法
clientRpc是会向所有client回调这个方法,有时候我们想让特定的client接受特定的回调。
于是就有了回调特定client的方法。同[clientRpc]类似。方法名以Target开头,添加[TargetRPC]特性,要注意的是该方法至少有一个NetworkConnection的形参,用来确定是回调哪一个client。
[Command] private void CmdSetRandomColor() { Debug.Log("CMD"); Color rc = Random.ColorHSV(); SetColor(rc); var nid = GetComponent<NetworkIdentity>(); TargetSetRandomColor(nid.connectionToClient,rc); } [TargetRpc] private void TargetSetRandomColor(NetworkConnection con, Color color) { Debug.Log("target"); SetColor(color); }
6) 值同步:SyncVal
如果想同步脚本中某一个值的话,可以给值添加[SyncVar]特性。
[SyncVar] private Color _color;
在Inspector面板上就会出现同步模式
默认广播给所有client,同步间隔0.1s
还可以指定值更新的时候,自动调用某个客户端上的方法。
[SyncVar(hook =nameof(SetColor))] private Color _color; private void SetColor(Color oldColor, Color newColor) { var sr = GetComponent<SpriteRenderer>(); sr.color = newColor; }
注意:1.调用的方法要有先前值和更新值两个形参。2.host下,所有object都中回调函数都会被调用,要用HasAuthority来限制。(因为host既是server又是client)
7) Object权限详解
在网络中权限是一个很重要的概念。拥有了一个物体的权限就表示有修改该物体的能力。在mirror框架中,server拥有绝大部分联网物体的权限,除了玩家角色和一些特定的物体。拥有了权限的玩家客户端就可以通过commands来同server交互,改变已有权限的物体。
获得权限的方式:
1) 玩家预制体在生成是自动赋予了对应的客户端权限。
2) 玩家可以用command用预制体生成object时可以设置该物体的权限。
如下:
[SerializeField] private GameObject _objPrefab; bool check = true; private void Update() { if (check && base.hasAuthority) { CmdSpawn(); check = false; } } [Command] private void CmdSpawn() { var obj = Instantiate(_objPrefab);//预制体要先实例化 NetworkServer.Spawn(obj, GetComponent<NetworkIdentity>().connectionToClient); }
3) 改变已有的物体的权限。
if (!hasAuthority) return; if (!Input.GetKeyDown(KeyCode.Mouse0)) return; RaycastHit hit; if (Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out hit, 100)) { NetworkIdentity id = hit.collider.GetComponent<NetworkIdentity>(); if (id != null && id.hasAuthority) { CmdRequestAuthority(id); } } } [Command] private void CmdRequestAuthority(NetworkIdentity otherId) { otherId.AssignClientAuthority(base.connectionToClient);//将该id的物体权限转到该客户端下 }
注意:1)一旦客户端掉线,客户端所拥有的所有物体都会被销毁。
2)networkBehaviour中的localplayer和 hasAuthority 。localplayer是判断当前脚本是不是在当前客户端的玩家角色上 。hasAuthority判断当前客户端是不是有权限控制这个物体。
8) 如何传递基本类型数据
在之前的client和server之间的交互(如command,clientRpc等)可以传递参数的。大部分基本类型如int,float等可以直接发送,部分mirror和unity类型也可以直接传输。如transform,vector3,netidentiy,GameObject等。注意Gameobject需要带有NetworkIdentity组件。可以通过Networkwriter类来确认那些类型被默认支持。
9) 如何传递自定义类型数据
对于没有支持的类型,可以通过自定义序列化器来支持。如下:
public class Item { public string Name; } public class Armour : Item { public int Protection; public int weight; } public class Potion : Item { public int Health; } /// <summary> /// item序列化器 /// </summary> public static class ItemSerializer { private const byte ITEM_ID = 0; private const byte POTION_ID = 1; private const byte ARMOUR_ID = 2; public static void WriteItem(this NetworkWriter writer, Item item) { if (item is Potion potion) { writer.WriteByte(POTION_ID); writer.WriteString(potion.Name); writer.WritePackedInt32(potion.Health); } if (item is Armour armour) { writer.WriteByte(ARMOUR_ID); writer.WriteString(armour.Name); writer.WritePackedInt32(armour.Protection); writer.WritePackedInt32(armour.weight); } else { writer.WriteByte(ITEM_ID); writer.WriteString(item.Name); } } public static Item ReadItem(this NetworkReader reader) { byte id = reader.ReadByte(); switch (id) { case POTION_ID: return new Potion { Name = reader.ReadString(), Health = reader.ReadPackedInt32() }; case ARMOUR_ID: return new Armour { Name = reader.ReadString(), Protection = reader.ReadPackedInt32(), weight = reader.ReadPackedInt32() }; case ITEM_ID: return new Item { Name = reader.ReadString() }; default: throw new Exception($"Unknown ItemType :{id}"); } } }
10) NetworkBehaviour的StartCycle
Awake() 最先无论client还是server。
OnstartServer() 仅在server上执行
OnstartAuthority() 仅在client执行,当物体生产时,同时在该客户端有权限时执行
OnStartClient()仅在client执行,用来初始化客户端
OnStartLocalPlayer()仅在client执行,当脚本所在物体为玩家角色时调用,用来设置跟踪相机等
OnStopAuthority()仅在client执行,当客户端失去该物体权限时调用
Start() 顺序不定,通常在最后但不保证每次都是,所以不建议将网络数据放这里处理。
————————————————
原文链接:https://blog.csdn.net/farcor_cn/article/details/110360424