资源下载地址

局域网联机插件 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);
    
  • AssignClientAuthority: 在任何时候添加权限

    identity.AssignClientAuthority(conn);
    

给物体赋予权限后,我们就能在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);}
}

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

[TargetRpc]

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

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

public class Player : NetworkBehaviour
{public int health;[Command]void CmdMagic(GameObject target, int damage){target.GetComponent<Player>().health -= damage;// 重点,尽管target没有在TargetDoMagic中使用,但必须传入NetworkIdentity opponentIdentity = target.GetComponent<NetworkIdentity>();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}");}
}

[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!");
}

假设您正在制作一个库存系统。假设玩家 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!");
}

客户端点击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;}
}

}

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

PlayPrefab

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

// 获取玩家预制体的NetworkIdentity
NetworkIdentity networkIdentity = NetworkClient.connection.identity;
playerManager = networkIdentity.GetComponent<PlayerManager>();

这样我们调用 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

Unity + Mirror实现原创卡牌游戏局域网联机相关推荐

  1. 基于QT开发的开源局域网联机UNO卡牌游戏报告(附github仓库地址)

    源代码: https://github.com/yunwei37/UNO-game-oop 目录 1. 需求分析 1.1. UNO卡牌游戏的基本功能 1.2. UNO卡牌游戏的规则 2. 总体设计 3 ...

  2. 卡牌游戏源代码(原创)(控制台)

    游戏预览: 完成度90%,约3000行,过年这几天全用在这上面了 由于尚未学到QT等,因此只能在黑窗口下面制作了 未完成的部分: ①战斗代码未优化(800行,精简后应该能降到200行左右) ②关卡掉落 ...

  3. 本科课程【虚拟现实引擎Unity3D】实验4 - 卡牌游戏完善

    大家好,我是[1+1=王], 热爱java的计算机(人工智能)渣硕研究生在读. 如果你也对java.人工智能等技术感兴趣,欢迎关注,抱团交流进大厂!!! Good better best, never ...

  4. unity5.6回合制战斗卡牌游戏源码支持安卓+IOS双端 C#语言开发

    unity5.6回合制战斗卡牌游戏源码.支持安卓+IOS双端 C#语言开发.拿来学习研究和二次开发都很不错. 说明:使用Unity 5.6.0f3开发,插件使用Dotween 源码下载 (1条消息) ...

  5. 卡牌游戏的基本市场分析

    一.背景分析 当前这类卡牌是桌游,其最流行的代表就是"三国杀","三国杀"严格来说分为两个产品:一个是实体的卡牌游戏,另一个是网络版的游戏,这两个产品的关系是网 ...

  6. 一个卡牌游戏的DEMO(0)

    最近有一个卡牌游戏的创意,打算用Unity把Demo实现出来练练手.我尽量尝试每天记录一下开发的进度. 打算在这里主要记录游戏设计和开发的过程,作为自己的一个摸索的记录. 虽然只有一个人在做,但还是打 ...

  7. [Unity3D]卡牌游戏中有关卡牌类的制作

    文章目录 展示卡牌 卡牌展示效果 打出卡牌 敌人释放技能 需求 制作 流程 抽牌: 弃牌: 一些问题 抽牌 查看卡牌 查看抽牌堆 查看弃牌堆 卡牌效果的实现 还没学到设计模式,所以自己和同学捣鼓了一个 ...

  8. 【概率DP】$P2059$ 卡牌游戏

    [概率DP]P2059 卡牌游戏 链接 题目描述 N个人坐成一圈玩游戏.一开始我们把所有玩家按顺时针从1到N编号.首先第一回合是玩家1作为庄家.每个回合庄家都会随机(即按相等的概率)从卡牌堆里选择一张 ...

  9. 天池 在线编程 卡牌游戏(01背包)

    文章目录 1. 题目 2. 解题 1. 题目 你跟你的朋友在玩一个卡牌游戏,总共有 n 张牌. 每张牌的成本为 cost[i] 并且可以对对手造成 damage[i] 的伤害. 你总共有 totalM ...

最新文章

  1. 索引( index )
  2. 山东大学 2020级数据库系统 实验三
  3. 【Servlet】请求转发与重定向
  4. php 转义字符处理,PHP转义与反转义字符串函数详解
  5. 谷粒商城:07. pms_catelog.sql
  6. Oracle执行计划使用分析SQL执行效率
  7. 计算机上怎么计算x的n次方,计算x的n次方
  8. 五菱宏光MINI EV,重走“小米”路
  9. 借助 PowerVR 开发工具,让 iOS 应用在苹果 M1 电脑上实现更好体验
  10. Jetpack Room
  11. 计算机u盘读不出来,详细教你解决电脑读不出u盘
  12. 从一道面试题掌握ES6的综合运用(有彩蛋)
  13. MySQL 8.0 drop table恢复
  14. [OHIF-Viewers]医疗数字阅片-医学影像-cornerstone-core-Cornerstone.js-Cornerstone Examples-基石实例-上...
  15. 安装anaconda时需要卸载python么_怎么卸载python 安装 anaconda
  16. 仿掌阅实现书籍打开动画
  17. 【路径规划】基于matlab蚁群算法机器人栅格地图最短路径规划【含Matlab源码 119期】
  18. 计算机毕业设计Java星星电影购票网站(源码+系统+mysql数据库+lw文档)
  19. Android用yasea 推流异常AmfString cannot be cast to com.github.faucamp.simplertmp.amf.AmfNumber捕获和解决
  20. 计算机网络 (2)标准化工作、性能指标、分层结构

热门文章

  1. 网站推荐-极简壁纸网站
  2. vue实现用户登录验证 + 权限验证 + 动态路由(左侧菜单栏)
  3. 搭建SSH,SSZ架构需注意数据库的编码问题
  4. Houdini图文笔记:VEX知识点小结(一)
  5. 电子设计大赛-微电网模拟系统
  6. scara机器人dh参数表_SCARA机器人技术参数.docx
  7. image图片大小调整和方向调整(UIImageOrientation)
  8. macbook pro 连接无线鼠标卡顿问题解决
  9. 【Stream流学习】Java 8 新特性|Collectors.joining() 案例详解
  10. 宿舍管理系统之登录功能