阅读本文需要基本的网络游戏架构知识的了解,比如常见的网络同步方案如帧同步,状态同步是怎么运作的。

什么是mirror?

mirror是一款联机游戏解决方案插件,和unity的Unet十分的类似,但是修了很多bug。其中mirror的网络方案是状态同步。

如何获取mirror?

需要在App Store中获取,然后再packagemanager中导入。

mirror快速上手

https://mirror-networking.gitbook.io/这里是mirror的官方文档

Network Manager

首先mirror最核心的是networkmanager,我们应该先在开始场景创建一个空对象作为networkmanager,然后挂载Network Manager组件

image.png

首先在意Scene Management下的offline Scene 和 Online Scene

其次注意这个Player Object,其中的Player Prefab就是玩家操控的对象(下文会介绍)auto create player就是让组件自动帮你生成这个对象(当有玩家加入链接,或者主机开通链接)method的选项就是生成方式,有随机,有依次,出生点需要创建一个spawnpoint的空对象,挂载上network start position。如果你希望自定义出生方法,需要重写networkmanager脚本,然后在OnServerAddPlayer进行修改

最后,我们还要再这个对象上添加一个传输组件,默认可以使用kcp 组件(采用的是udp传输协议)将其拖拽到Network Info 的Transport上

NetworkManageHUD

这个组件是一个快速构建多人游戏的工具组件,主要是用来提供一个ui,省去自己设计ui的功夫,快速测试自己的游戏。

可以将这个组件添加到NetworkManager对象上,或者新建一个空对象。进入场景后会有如下ui

image.png

host就是建立一个主机(包括服务端和客户端)

client就是以客户端进入游戏,默认进入localhost地址(本地计算机),也就是说,你在一台电脑开两个游戏窗口,其中一个选择host另一个选择client即可以进行联机

NetworkBehaviour

Network Behaviour - Mirror (gitbook.io)

networkbehaviour用来取代monobehaviour,所有要同步的脚本都应该继承networkbehaviour而不是monob,注意挂载继承该类的组件还需要同时挂载network identity component组件。

isLocalPlayer:这个是一个变量,当是本地客户端拥有的player时返回真,否则返回假,用于区分和其他客户端拥有的player对象(一场游戏中,有多个player比如说控制的英雄,他们身上都挂载了相同的脚本,如果只想对本客户端拥有的英雄进行操作,则需要用到这个变量)

Commands:是一个特性,用来标记某个函数是在服务器端执行,比如说你希望在客户端上收集输入,然后交给服务端来处理则需要用着个特性来标记这么一个函数。这对需要在所有客户端之间同步某些玩家操作(如射击)非常有用。Command特性保证了该方法只在服务器上执行,并且将结果发送回所有客户端以进行适当的同步。同时建议使用Command特性的函数以Cmd作为函数名前缀,同时调用cmd函数时要判断是不是localPlayer(不然会报错,并且不执行)

Client RPC Calls:有了向服务器发出指令的command自然有服务器的回调,我们使用ClientRpc这个特性(同样,命名建议以Rpc为前缀),在状态同步中,服务器端是唯一有权限修改状态的,rpc则是让所有客户端都按照服务器的意义来同步服务器修改的状态。比如说开枪事件,服务器就需要告知所有客户端需要播放开枪声音和显示枪口。

对于Command和rpc的调用流程可以简单理解为:

client—调用command方法—>server执行command方法—调用rpc方法—>client执行ClientRPC方法

比如说我们如果要给进入游戏的玩家一个随机的颜色,那么生成随机颜色必须在服务器端,否则各个玩家看到的颜色是不同的,比如你觉得你是蓝色,但是在比人看来你的颜色是他的客户端随机生成的颜色的颜色,而如果是由服务器来生成那么大家看到的颜色就是服务器随机生成的颜色。而服务器生成了颜色还需要告知所有客户端,这个新进来的玩家的颜色是什么,这个时候就是调用rpc。

		[Command]
    private void CmdSetRandomColor()
    {
        Color c = Random.ColorHSV();
        RpcSetRandomColor(rc);
    }
    [ClientRpc]
    private void RpcSetRandomColor(Color color)
    {
        SetColor(color);
    }

如果希望特定的客户端调用则可以使用[TargetRpc],注意使用这个特性的函数至少要有一个NetworkConnection的形参(就是NetworkIdentity组件)

SyncVar:是一个特性,用于标记需要同步的变量,面板上的Sync Setting就是用来设置这个的,分别是模式和同步间隔时间

20201130014957195.png

Network Transfrom

用于同步transfrom的组件,并且提供间隔的插值

spaces_-MGmQrf2z6FL0ZpExPAn_uploads_cwKghd0kaUglT18AS0Ws_image.webp

一个物体可以添加多个networkTransfrom(来同步子物体的transfrom)使用networkTransfrom必须要有network Identity组件。

主要注意Target,要同步的对象,Sync Direction同步方向,如果是Client To Server的话才可以在客户端修改Transfrom位置然后同步到服务器端,否则修改Transfrom仅仅在本地生效,不在其他客户端上生效。括号内的Unreliable和reliable是两个版本,前者传输快,但是不可靠,后者反之。

同步变量并且监听

普通变量:

[SyncVar(hook = nameof(PlayerNumberChange))]
	public int PlayerNumber = 0;
	int nowPlayer = 0;

	public void PlayerNumberChange(int oldValue,int newValue) {
		if(newValue == 2) {
			Debug.Log("游戏人数已够");
		}
	}
注意这个是默认从服务器端同步到客户端,修改了后就是双通,在脚本的Inspector上修改
回调函数是所有客户端都执行的

SyncList:

private void Start() {
		playerControlsSync.Callback += OnScoreListChanged;
	}

	private void OnDestroy() {
		playerControlsSync.Callback -= OnScoreListChanged;
	}

	private void OnScoreListChanged(SyncList<PlayerControl>.Operation op, int index,PlayerControl oldItem , PlayerControl newItem ) {
		switch (op) {
			case SyncList<PlayerControl>.Operation.OP_ADD:
				Debug.Log("Added item at index " + index);
				break;
			case SyncList<PlayerControl>.Operation.OP_CLEAR:
				Debug.Log("Cleared the list");
				break;
			case SyncList<PlayerControl>.Operation.OP_INSERT:
				Debug.Log("Inserted item at index " + index);
				break;
			case SyncList<PlayerControl>.Operation.OP_REMOVEAT:
				Debug.Log("Removed item at index " + index);
				break;
			case SyncList<PlayerControl>.Operation.OP_SET:
				Debug.Log("Changed item at index " + index);
				break;
			default:
				break;
		}
	}

运行时创建网络对象

代码实例

移动

private void Update() {
        if (!isLocalPlayer) { return; }
        float horizontal = Input.GetAxisRaw("Horizontal");
        float vertical = Input.GetAxisRaw("Vertical");
        Vector3 move = new Vector3(horizontal, vertical, 0) *Time.deltaTime* speed;
        transform.Translate(move);
				//注意要使用networktransfrom,并且传输方向是客户端到服务器端。
    }

射击

		void Shoot() {
        restCd += Time.deltaTime;
        if (Input.GetMouseButton(0) && restCd > shootCd) {
            restCd = 0;
            Vector3 mousePos = Input.mousePosition;
            mousePos = Camera.main.ScreenToWorldPoint(mousePos);
            Vector2 dir = mousePos - battery.transform.position;
            CmdShoot(dir);
        }
    }
    [Command]
    void CmdShoot(Vector3 dir) {
        RpcFire(dir);
    }
    [ClientRpc]
    void RpcFire(Vector3 dir) {
        GameObject b = Instantiate(m_Bullet);
        b.transform.position = transform.position;
        var bs = b.GetComponent<BulletControl>();
        bs.dir = dir.normalized;
        bs.own = this.gameObject;

    }
文章作者: 妖白与茶色
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 妖白与茶色的博客
喜欢就支持一下吧