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

news2024/11/17 3:23:44

资源下载地址

局域网联机插件 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/63638.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

UNet 网络做图像分割DRIVE数据集

目录 1. 介绍 2. 搭建 UNet 网络 3. dataset 数据加载 4. train 训练网络 5. predict 分割图像 6. show 7. 完整代码 1. 介绍 项目的目录如下所示 DRIVE 存放的是数据集predict 是待分割的图像result 里面放分割predict 的结果dataset 是处理数据的文件、model存放une…

day5_redis学习

文章目录秒杀优化阻塞队列实现消息队列Redis实现消息队列List实现消息队列PubSub实现消息队列Stream实现消息队列发布以及查看探店笔记点赞以及点赞排行榜秒杀优化 上面的过程中&#xff0c;我们进行秒杀操作的基本步骤为: 所以这时候整个过程就耗费较长的时间&#xff0c;因…

【1687. 从仓库到码头运输箱子】

来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 描述&#xff1a; 你有一辆货运卡车&#xff0c;你需要用这一辆车把一些箱子从仓库运送到码头。这辆卡车每次运输有 箱子数目的限制 和 总重量的限制 。 给你一个箱子数组 boxes 和三个整数 portsCount, maxBoxes 和 m…

python大数据毕业设计选题题目大全

文章目录0 前言1 大数据相关题目2 开题指导2.1 起因2.2 如何避坑(重中之重)2.3 为什么这么说呢&#xff1f;2.4 难度把控2.5 题目名称3 最后0 前言 这是学长亲手整理的&#xff0c;大数据毕设选题系列第二篇&#xff0c;都是经过学长精心审核的题目&#xff0c;适合作为毕设&a…

CPP 核心编程6-多态

#include "iostream" using namespace std;//多态 class Animal { public:void speak(){cout << "动物在说话" << endl;} };class Cat : public Animal { public:void speak(){cout << "cat在说话" << endl;} };//地址早…

【C语言航路】第七站:结构体初阶

目录 一、结构体的声明 1.结构的基础知识 2.结构的声明 3.结构体成员的类型 4.结构体变量的定义和初始化 二、结构体成员的访问 三、结构体传参 总结 一、结构体的声明 1.结构的基础知识 结构是一些值的集合&#xff0c;这些值称为成员变量&#xff0c;结构的每个成员可…

《少有人走的路:心智成熟的旅程》笔记

几乎人人都有心理问题&#xff0c;只是程度不同而已。 几乎人人都有横渡不同的心里疾病&#xff0c;只是得病的时间不同而已。 ps : 许多人都没有付出足够的时间和精力&#xff0c;去解决知识、社交、心理方面的问题 作者序言&#xff1a; 目录 一、痛苦的价值 二、对待痛苦…

Linux操作系统粘滞位(解决上篇文章提出的问题)

前言 &#xff1a; 在上一篇Linux操作系统的博客中提出了&#xff0c;一个问题就是在一个公共目录里&#xff0c;假如我们有了对目录写的权限&#xff0c;我们就能进行创建属于我们自己的文件&#xff0c;并且给这个文件进行设置他的权限&#xff0c;我们发现虽然是我的文件&am…

抗疫行动题材网页设计 大学生最美逆行者感动人物网页代码 众志成城万众一心抗击疫情HTML网页设计

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

【GD32F427开发板试用】+DHT11温湿度监测

本篇文章来自极术社区与兆易创新组织的GD32F427开发板评测活动&#xff0c;更多开发板试用活动请关注极术社区网站。作者&#xff1a;四季的温度 在完成所有项目后会开源&#xff0c;本次依旧是想做一个通过DHT11采集信息&#xff0c;OLED显示&#xff0c;ESP8266上传&#xff…

第二十四章《学生信息管理系统》第1节:学生信息管理系统简介

学生信息管理系统用于管理学生基本信息,该系统除能够大大的帮助学籍管理人员提高工作效率。本小节将从软件功能、数据库系统设计和项目结构几个方面介绍该软件系统的设计方案。 24.1.1系统功能简介 学生信息管理系统集信息展示、查询、增删和修改多种功能为一体,该系统的主…

1552_AURIX_TC275_时钟分发

全部学习汇总&#xff1a; GreyZhang/g_TC275: happy hacking for TC275! (github.com) 这一页文件我没写什么批注&#xff0c;但是还是留下来了。从这个图中能够看到各个模块的时钟源是可以来自于什么地方。 1. CCU的输入主要是来自于两个PLL、备份时钟以及晶振。 2. 对大多数…

SpringBoot引入外部jar包,项目打包成war包发布(亲测有效) - 第453篇

历史文章&#xff08;文章累计450&#xff09; 《国内最全的Spring Boot系列之一》 《国内最全的Spring Boot系列之二》 《国内最全的Spring Boot系列之三》 《国内最全的Spring Boot系列之四》 《国内最全的Spring Boot系列之五》 利用Spring扩展点对敏感信息加密解密&a…

【小f的刷题笔记】(JS)数组 - 前缀和 LeetCode303 LeetCode34

【数组】 前缀和&#xff1a; &#x1f31f; 原始数组不会被修改的情况下&#xff0c;频繁查询某个区间的累加和 ✔ 一次把所有从一开始到本数的累加值计算出来存在一个新数组里&#xff0c;区间的累加值通过减法得出 LeetCode303 链接&#xff1a; 303.区域和检索 - 数组…

刷爆力扣之至少是其它数字两倍的最大数

刷爆力扣之至少是其它数字两倍的最大数 HELLO&#xff0c;各位看官大大好&#xff0c;我是阿呆 &#x1f648;&#x1f648;&#x1f648; 今天阿呆继续记录下力扣刷题过程&#xff0c;收录在专栏算法中 &#x1f61c;&#x1f61c;&#x1f61c; 该专栏按照不同类别标签进行刷…

一文带你了解【深度学习】中CNN、RNN、LSTM、DBN等神经网络(图文解释 包括各种激活函数)

觉得有帮助请点赞关注收藏~~~ 一、深度学习概述 深度学习算法属于机器学习算法的范畴&#xff0c;深度学习一般具有自主学习能力 基于深度学习的自然语言处理基本操作步骤包括&#xff1a; 将原始信息输入神经网络模型&#xff0c;通过自主学习算法识别输入特征&#xff1b;…

Redis 发布订阅

Redis 发布订阅 Redis 发布/订阅 (publish/subscribe) 是一种消息通信模式&#xff1a;发送者 (publish) 发送消息&#xff0c;订阅者 (subscribe) 接收消息。 Redis 客户端可以订阅任意数量的频道。 下图展示了频道 channel1&#xff0c;以及订阅这个频道的三个客户端 ——…

李沐论文精度系列之八:视频理解论文串讲

文章目录一 、前言二、 DeepVideo&#xff08;IEEE 2014&#xff09;2.1 模型结构2.2 实验结果2.3 总结三、双流网络及其变体3.1 Two-Stream&#xff08;NeurIPS 2014&#xff09;3.1.1 简介3.1.2 改进工作3.2 Two stream LSTM&#xff08;CVPR 2015 &#xff09;3.2.1 模型结构…

Django的学习笔记

Django初笔记一、认识Django1.基本原理2.框架二、建立一个简单的项目1.建立一个HelloWord&#xff08;1&#xff09;进入虚拟环境&#xff08;2&#xff09;建立项目三、基本应用结构&#xff08;1&#xff09;配置文件setting&#xff08;2&#xff09;URL&#xff08;路由系统…

【C语言进阶(NEW)】一、数据储存详解|数据类型介绍|整形在内存中的存储|浮点型在内存中的存储

目录 一、数据类型介绍 1.1 基本内置类型 1.2 类型的基本归类 1.3 有符号&#xff08;signed&#xff09;与无符号&#xff08;unsigned&#xff09;的区别 二、整形在内存中的存储 2.1 原码、反码、补码 2.2 大小端 2.2.1 什么是大小端 2.2.2 为什么有大端和小端 2.…