• Unity + Mirror实现原创卡牌游戏局域网联机


    资源下载地址

    局域网联机插件 Mirror:Mirror | 网络 | Unity Asset Store

    本地客户端测试多人游戏(不用打包)插件 : ParrelSync

    Mirror官方文档:General - Mirror (gitbook.io)

    Mirror使用

    前置准备

    1. 导入Mirror Package
    2. 创建空物体,添加 Network ManagerNetwork Manager HUD以及 KCP Transport(也可以选择其他网络连接方式)
    3. 导入ParrelSync, 并为其clone当前项目,在此之后clone后的项目能同步你在本项目的所有修改

    演示场景

    点击开始之后会看到如下界面,需要其中一台电脑作为Host,其他玩家点击Client就可以直接连接

    在这里插入图片描述

    Mirror演示场景连接成功效果:

    在这里插入图片描述

    同步

    首先需要注意的事情:

    • 在你需要用到联网功能的脚本中都要添加 use Mirror来使用相应Api,并且继承自 NetWorkBehavior而不是 MonoBehavior
    • 在涉及到玩家输入时,首先先要进行 isLocalPlayer的判断
    • 如果游戏玩家有 Prefab,需要在prefab添加 NetworkIdentity组件并将其拖入到 NetworkManager组件的 PlayerPrefab

    服务端与客户端通信

    概念含义

    Network Identity

    该组件控制着游戏物体在网络中的独特ID,他的 Server Only选项表示是否确保物体只生成在服务端。

    注意:Mirror不支持嵌套的 Network Identity,确保父物体是唯一一个具有该组件的物体,子物体通过 GetComponentInParent去查找

    在每一个运行过程中生成的预制体都需要添加该组件。

    Network Authority

    Authority(权限)决定着谁拥有并控制着这个物体,默认情况下服务器拥有所有物体的权限

    但有时候我们需要客户端拥有权限,比如玩家输入,我们有以下方法将权限给到客户端:

    • NetworkServer.Spawn: 在创建物体时给出权限

    • NetworkServer.AddPlayerForConnection,生成玩家物体时自动添加权限

      GameObject go = Instantiate(prefab);
      NetworkServer.Spawn(go, connectionToClient);
      
      • 1
      • 2
    • AssignClientAuthority: 在任何时候添加权限

      identity.AssignClientAuthority(conn);
      
      • 1

    给物体赋予权限后,我们就能在ClientRpc中根据IsOwn来根据是否是自己的物体来执行不同函数,比如本游戏中,将卡牌放置在不同区域

    回调函数

    在需要使用服务端、客户端开始结束时回调的脚本中继承自NetworkBehavior,本例子继承自Network Manager是为了方便不再添加一个Network Manager组件

    仅在server上执行

    Server Only ——

    • OnStartServer:在服务端上生成时调用
    • OnStopServer:在服务器上销毁或者取消生成时调用
    • OnSerialize:在他在发送到客户端序列化之前调用, 同时确保调用 base.OnSerialize
    仅在Client 上执行

    Client Only ——

    • OnStartClient: 在客户端上生成时调用
    • OnStartLocalPlayer: 仅在client执行,当脚本所在物体为玩家角色时调用,用来设置跟踪相机等
    • OnStopClient: 当对象在客户端上被ObjectDestroyMessageObjectHideMessage消息销毁时调用
    • OnstartAuthority() 仅在client执行,当物体生产时,同时在该客户端有权限时执行
    • OnStopAuthority()仅在client执行,当客户端失去该物体权限时调用

    Awake() 最先无论client还是server。
    Start() 顺序不定,通常在最后但不保证每次都是,所以不建议将网络数据放这里处理。

    在这里插入图片描述

    能够传输的数据类型

    • C#基本类型——int, char, float 等
    • Untiy 数学类型——Vector3, Rect等
    • NetworlIdentity ——这就是为什么要给预制体添加这个组件
    • 只包含上述类型的class、ScriptableObject(这两个会在接收端重新实例化从而产生垃圾)以及数组

    特性

    知道何时以及如何使用以下特性,首先要明确你要同步什么变量,应该服务端执行还是客户端执行,以及传递的参数(尤其是涉及到GameObject的isOwned)在不同客户端下的不同执行情况

    [Command]

    拥有这个特性下的函数从客户端发送,由服务端执行。函数开头需要加上 Cmd前缀

    避免每一帧都调用Cmd方法,这会产生巨大的流量

    [ClientRpc]

    服务端发送该函数,到客户端执行函数。开头需要加上 Rpc前缀

    public class Player : NetworkBehaviour
    {
        int health;
    
        public void TakeDamage(int amount)
        {
            if (!isServer) return;
    
            health -= amount;
            RpcDamage(amount);
        }
    
        [ClientRpc]
        public void RpcDamage(int amount)
        {
            Debug.Log("Took damage:" + amount);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    使用本地客户端作为主机运行游戏时,将在本地客户端上调用 ClientRpc 调用,即使它与服务器处于同一进程中

    [TargetRpc]

    clientRpc是会向所有client回调这个方法,有时候我们想让特定的client接受特定的回调,于是就有了回调特定client的方法

    TargetRpc 函数由服务器上的用户代码调用,然后在指定网络连接的客户端上的相应客户端对象上调用。函数开头需要加上 Target前缀

    public class Player : NetworkBehaviour
    {
        public int health;
    
        [Command]
        void CmdMagic(GameObject target, int damage)
        {
            target.GetComponent().health -= damage;
    
            // 重点,尽管target没有在TargetDoMagic中使用,但必须传入
            NetworkIdentity opponentIdentity = target.GetComponent();
            TargetDoMagic(opponentIdentity.connectionToClient, damage);
        }
    
        [TargetRpc]
        public void TargetDoMagic(NetworkConnection target, int damage)
        {
            // 这会出现在对手的客户端中,而不是攻击者的
            Debug.Log($"Magic Damage = {damage}");
        }
    
        // 治疗自己
        [Command]
        public void CmdHealMe()
        {
            health += 10;
            TargetHealed(10);
        }
    
        [TargetRpc]
        public void TargetHealed(int amount)
        {
            // 没有指定NetworkConnection变量,因此出现在物体拥有者中
            Debug.Log($"Health increased by {amount}");
        }
    }
    
    • 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
    [SyncVar]

    SyncVar 是从 NetworkBehavior 继承的类的属性,这些类从服务器同步到客户端。当生成游戏对象或新玩家加入正在进行的游戏时,将向他们发送对他们可见的网络对象上所有 SyncVar 的最新状态。使用[SyncVar]指定脚本中要同步的变量。

    能用Hook指定变量发生变量时将要调用的函数

    [SyncVar(hook = nameof(OnHolaCountChanged))]
    int holaCount = 0;
    
    void OnHolaCountChanged(int oldCount, int newCount)
    {
        Debug.Log($"We had {oldCount} holas, but now we have {newCount} holas!");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    假设您正在制作一个库存系统。假设玩家 A、B 和 C 位于同一区域。整个网络中总共有 12 个对象:

    • 客户端 A 有玩家 A(他自己)、玩家 B 和玩家 C
    • 客户端 B 有玩家 A、玩家 B(他自己)和玩家 C
    • 客户端 C 有玩家 A、玩家 B 和玩家 C(他自己)
    • 服务器有玩家 A、玩家 B、玩家 C

    除了服务器和客户端A之外,其他人没必要也不应该知道A的库存,典型用例包括任务、玩家在纸牌游戏中的手牌、技能、经验或您不需要与其他玩家共享的任何其他数据。

    演示

    void Update() {
        if (!isLocalPlayer) return;
        HandleMovement();
    
        if (Input.GetKeyDown(KeyCode.X))
        {
            Debug.Log("Sending Hola to Server!");
            CmdHola();
        }
    }
    
    [Command]
    void CmdHola()
    {
        Debug.Log("Received Hola from Client!");
        TargetReplyHola();
    }
    
    [TargetRpc]
    void TargetReplyHola()
    {
        Debug.Log("Received Hola from Server!");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    客户端点击X:

    在这里插入图片描述

    Host端点击X

    在这里插入图片描述

    游戏编写

    模型锚点

    网上找到的.fbx模型,一般里面的模型中心不在锚点上,这种一般的处理方法就是创建一个空的父物体,父物体的锚点控制在模型中心,但这样一个个改太慢了

    写了一个脚本自动化改,放在Assets\Editor文件夹下

    具体代码:点击查看详细内容
    using UnityEngine;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEditor;
    public static class PivotEazier
    {
    	[MenuItem("GameObject/Pivot/Create Pivot", false, 0)]
    	static void CreatePivotObject()
    	{
    		if (Selection.activeGameObject != null)
    		{
    			var pivot = CreatePivotObject(Selection.activeGameObject);
    			Selection.activeGameObject = pivot;
    		}
    	}
    	[MenuItem("GameObject/Pivot/Create Pivot (Local Zero)", false, 0)]
    	static void CreatePivotObjectAtParentPos()
    	{
    		if (Selection.activeGameObject != null)
    		{
    			var pivot = CreatePivotObjectAtParentPos(Selection.activeGameObject);
    			Selection.activeGameObject = pivot;
    		}
    	}
    	[MenuItem("GameObject/Pivot/Delete Pivot", false, 0)]
    	static void DeletePivotObject()
    	{
    		GameObject objSelectionAfter = null;
    
    	if (Selection.activeGameObject != null)
    	{
    		if (Selection.activeGameObject.transform.childCount > 0)
    		{
    			objSelectionAfter = Selection.activeGameObject.transform.GetChild(0).gameObject;
    		}
    		else if (Selection.activeGameObject.transform.parent != null)
    		{
    			objSelectionAfter = Selection.activeGameObject.transform.parent.gameObject;
    		}
    
    		DeletePivotObject(Selection.activeGameObject);
    
    		Selection.activeGameObject = objSelectionAfter;
    	}
    }
    private static GameObject CreatePivotObjectAtParentPos(GameObject current)
    {
    	if (current == null)
    	{
    		return null;
    	}
    
    	int siblingIndex = current.transform.GetSiblingIndex();
    
    	GameObject newObject = new GameObject(current.name);
    	newObject.transform.SetParent(current.transform.parent);
    
    	newObject.transform.localPosition = Vector3.zero;
    	newObject.transform.localScale = Vector3.one;
    	newObject.transform.localRotation = Quaternion.identity;
    
    	newObject.transform.SetSiblingIndex(siblingIndex);
    
    	current.transform.SetParent(newObject.transform);
    
    	return newObject;
    }
    private static GameObject CreatePivotObject(GameObject current)
    {
    	if (current == null)
    	{
    		return null;
    	}
    
    	int siblingIndex = current.transform.GetSiblingIndex();
    
    	GameObject newObject = new GameObject("Pivot");
    	newObject.transform.SetParent(current.transform.parent);
    
    	newObject.transform.position = current.transform.position;
    	newObject.transform.localScale = current.transform.localScale;
    	newObject.transform.rotation = current.transform.rotation;
    
    	newObject.transform.SetSiblingIndex(siblingIndex);
    
    	current.transform.SetParent(newObject.transform);
    
    	return newObject;
    }
    private static GameObject DeletePivotObject(GameObject current)
    {
    	Transform parent = current.transform.parent;
    	int childrenCount = current.transform.childCount;
    	int siblingIndex = current.transform.GetSiblingIndex();
    
    	Transform[] children = new Transform[childrenCount];
    	for (int i = 0; i < childrenCount; i++)
    	{
    		children[i] = current.transform.GetChild(i);
    	}
    
    	for (int i = 0; i < childrenCount; i++)
    	{
    		children[i].SetParent(parent);
    		children[i].SetSiblingIndex(siblingIndex + i);
    	}
    
    	if (Application.isPlaying)
    	{
    		GameObject.Destroy(current);
    	}
    	else
    	{
    		GameObject.DestroyImmediate(current);
    	}
    
    	if (children.Length > 0)
    	{
    		return children[0].gameObject;
    	}
    	else
    	{
    		return null;
    	}
    }
    
    • 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
    • 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

    }

    使用方法是将模式中心对齐其父物体中心,在Hierarchy窗口中右键Create Pivot (Local Zero)就可以

    PlayPrefab

    我们如果想要获取当前玩家的一些参数,需要按照以下代码获取——PlayerManger挂载在playerPrefab上

    // 获取玩家预制体的NetworkIdentity
    NetworkIdentity networkIdentity = NetworkClient.connection.identity;
    playerManager = networkIdentity.GetComponent();
    
    • 1
    • 2
    • 3

    这样我们调用 PlayerManager.Function时就是调用当前玩家的方法,记住这个方法在 ClientRpc中会很有用,比如给特定ID的玩家设定称号,就需要获取当前ID是否与特定ID相等

    因为你必须确定是当前玩家的PlayerManager触发的函数,所以Cmd方法最好写在PlayerManager中,这个肯定会涉及到耦合。如果不想这样,可以自己新建一个类,但必须确认你调用的时候是当前玩家控制下的对应实例

    是否添加NetworkBehavior

    不是所有的物体都添加该组件,本游戏中比如玩家手牌,以及一些Manger管理器都是不用通信交互的,因为他们没必要让其他玩家知道。

    但是呢,我们可以通过网络通信方法,TargetRpc、ClientRpc来改变这些没有networkBehavior的实例。

    需要添加的一般有其中一个特征:

    • 连接之后创建的物体
    • 需要进行同步的物体,比如玩家本身或者跟随其的宠物

    如果添加了这个组件都要拖拽到NetworkManager组件中的 Registered Spawnable Prefab中,这代表NetworkManager会同步这个物体的状态

    创建物体

    在服务器上“生成”游戏对象意味着在连接到服务器的客户端上创建游戏对象,并由生成系统管理。

    使用此系统生成游戏对象后,只要服务器上的游戏对象发生更改,状态更新就会发送到客户端。当 Mirror 销毁服务器上的游戏对象时,也会销毁客户端上的游戏对象。服务器将生成的游戏对象与所有其他联网游戏对象一起管理,以便在其他客户端稍后加入游戏时,服务器可以在该客户端上生成游戏对象。这些生成的游戏对象具有称为“netId”的唯一网络实例 ID,该 ID 在每个游戏对象的服务器和客户端上都是相同的。

    游戏演示视频

    Good Lock 演示视频_哔哩哔哩_bilibili

  • 相关阅读:
    RocketMQ存储设计的奥妙
    ACM第三周---周训---题目合集.
    TS泛型的使用
    CodeForces 570D Tree Requests
    【Python数据科学快速入门系列 | 09】Matplotlib数据关系图表应用总结
    SpringSecurity(八)【会话管理】
    Ubuntu 16.4虚拟机 配置Hadoop集群
    2023 10月最新Vmd 下载安装教程,Windows&Linux
    做软件测试一定要注意的细节
    机器学习入门教学——决策树
  • 原文地址:https://blog.csdn.net/jkkk_/article/details/128190389