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

这篇具有很好参考价值的文章主要介绍了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 unity,unity,游戏,游戏引擎

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

mirror unity,unity,游戏,游戏引擎

同步

首先需要注意的事情:

  • 在你需要用到联网功能的脚本中都要添加 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() 顺序不定,通常在最后但不保证每次都是,所以不建议将网络数据放这里处理。

mirror unity,unity,游戏,游戏引擎

能够传输的数据类型

  • 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:

mirror unity,unity,游戏,游戏引擎

Host端点击X

mirror unity,unity,游戏,游戏引擎

游戏编写

模型锚点

网上找到的.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文章来源地址https://www.toymoban.com/news/detail-816443.html

到了这里,关于Unity + Mirror实现卡牌游戏局域网联机的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 公网连接内网工具推荐(还在为局域网联机发愁?)

     下载地址 Windows 64位 Mac OS X 64位 Linux 64位 Linux/ARM 32位 64位 一、FastNat可为您解决的问题 1.没有公网服务器,需要发布本地的站点或网络程序到公网上,供他人访问; 此项功能大大方面开发人员进行远程调试,微信小程序等开发工作进行。 2.需要远程到在其他网络中的设备,

    2024年02月11日
    浏览(30)
  • 阿里云服务器开服教程,用于我的世界等局域网联机

    想要和好朋友在家玩我的世界局域网联机?又不想用frp(花一元巨款)?还没有公网IP?完全可以联机,还不用和frp抢带宽,相当方便。 首先,我们需要注册一个阿里云账号 然后选择ECS 然后选择立即购买,这里也可以点击免费试用,同样系统选择CentOS就可以了 其它配置不需

    2024年02月09日
    浏览(54)
  • Unity 基于Netcode for gameObjects实现局域网同步

    1.需要将unity升级到2021.3及以后的版本的稳定版本,使用不稳定的2022版本测试过,存在打包问题: 1. Netcode for gameObjects 2. Multiplayer Tools 该package是附带Netcode for GameObjects手动安装的 3. Burst 该Package同样也是附带安装,如果在不稳定的2022版本中打包问题也源于这个package,猜测可能

    2024年02月11日
    浏览(31)
  • 【Unity】(Socket)TCP 实现同一局域网 一台主机控制多台主机

    前几天博主接到一个任务:5台HTCVIVEPro设备,其中一台设备进行演示,另外四台设备画面同步。 在设备没到之前,博主进行了一下前期准本工作: 同一局域网 一台主机控制多台主机 PS:博主参考了其它博主大大的文章,感觉很有用!!!!!! 如果需要其它的一些TCP操作流程

    2024年02月10日
    浏览(31)
  • 和朋友一起玩游戏怎么创建局域网

    大家有没有遇到过想和朋友一起玩游戏却不知道怎么创建局域网的情况,其实非常的简单,下面小编教大家怎么做。 点击电脑右下角的网络图标,然后选择打开网络和共享中心。 在网络共享中心中选择设置新的连接或网络。 在设置网络或连接中选择设置无线临时网络,然后

    2024年02月06日
    浏览(74)
  • UE4/5多人游戏详解(一、基础理论与局域网内部的连接)

    目录 多人游戏理论简单讲解 点对点: 于是,服务端和客户端的概念出现了: 局域网连接: 从第三人称c++项目开始 创建项目: 以一个客户端作为监听服务器 3个客户端,在场景后方使用服务器: 局域网连接: 蓝图 C++ 什么是点对点? 比如两个玩家,其移动和攻击,将数据

    2023年04月19日
    浏览(30)
  • 路由模式怎么实现局域网中三层交换机互联?实现局域网中三层交换机互联方法教程

    复习: 交换机有关vlan的配置: switch#configure terminal !进入全局模式 switch(config)# vlan 10 !创建vlan 10 switch(config-vlan)# name test10 !改名为test10 switch#show vlan !显示vlan配置 switch(config-if)# interface fastethernet 0/5 !进入f0/5的接口配置模式 switch(config-if)#switchport trunk encapsulation dot1q swit

    2024年02月06日
    浏览(32)
  • 虚拟局域网VLAN的实现机制

    IEEE802.1Q帧(也称Dot One Q帧)对以太网的MAC帧格式进行了扩展,插入了4字节的 VLAN 标记。 Access Trunk Hybrid Access端口一般用于连接用户计算机 Access端口只能属于一个VLAN Access端口的PVID值与端口所属VLAN的ID相同(默认为1) Access端口接收处理方法: 一般只接受“未打标签”的普通

    2024年02月10日
    浏览(27)
  • 局域网实现PC、Pad、Android互联

    在我们使用多设备时,往往会因为多设备的文件不能同步而造成很多麻烦(QQ等软件不能传输大文件)。这里我们使用计算机网络的知识来实现多设备的互联,使得多设备的文件更方便传输。 注意,这里的前提条件都是所有的设备连接在同一个 WIFI 上面。 设备版本: Android:

    2024年02月11日
    浏览(29)
  • 利用Nginx正向代理实现局域网电脑访问外网

    在网络环境中,有时候我们需要让局域网内的电脑访问外网,但是由于网络策略或其他原因,直接访问外网是不可行的。这时候,可以借助 Nginx 来搭建一个正向代理服务器,实现局域网内电脑通过 Nginx 转发访问外网的需求。 在工作中我遇到了一个类似的情况:在公司网络中

    2024年03月27日
    浏览(36)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包