虚幻-5-多人游戏开发指南-全-

虚幻 5 多人游戏开发指南(全)

原文:zh.annas-archive.org/md5/af7ceaf2d2d6d76392ab1a8859cb5d28

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

自从首次出现以来,多人游戏已经彻底改变了游戏行业,为玩家提供了一种全新的游戏体验方式。与传统的单玩家游戏不同,多人游戏允许玩家在实时互动和参与,提供一种动态和吸引人的社交体验。

近年来,技术的进步使得多人游戏比以往任何时候都更容易接触,因此,它已经成为世界上最受欢迎的游戏类型之一,每天都有数百万玩家在各种平台上参与多人游戏。

游戏引擎,如 Unreal Engine,在游戏开发者中越来越受欢迎,因为它们提供了许多专门设计来支持高质量多人体验的工具和功能,例如跨平台多人支持和高级延迟补偿技术。

Unreal Engine 也有一个庞大且活跃的游戏开发者社区,提供支持、资源和合作与网络的机会,这意味着,如果你对网络化游戏开发感兴趣,没有理由不尝试一下!

本书面向对象

如果你是一名游戏程序员,特别是 Unreal Engine 开发者,对视频游戏网络系统知之甚少,并希望深入了解这个主题,那么这本书就是为你准备的。

精通其他游戏引擎并对理解 Unreal 多人系统原理感兴趣的开发者,也将从这本书中受益;然而,强烈建议具备 Unreal Engine 和 C++ 的基础知识。

对多人游戏的热情将帮助你充分利用这本书。

本书涵盖内容

第一章从开发者角度开始多人游戏开发,你将温和地被引入多人游戏开发的世界。

第二章理解网络基础,你将探索网络编程的基本概念,以便开始使用 Unreal Engine 进行多人开发。

第三章使用项目原型测试多人系统,你将指导创建一个简单的多人原型,从项目模板设置开始,测试一些基本的多人功能。

第四章设置您的第一个多人环境,你将获得在 Unreal Engine 中开发多人 C++ 项目的基石。

第五章在多人环境中管理演员,你将开始创建一个多人角色,并了解如何在多人环境中处理它。

第六章 在网络中复制属性 中,你将学习如何在多人环境中处理属性并将它们同步到客户端。

第七章 使用远程过程调用 (RPCs) 中,你将开始在一个网络环境中调用函数,从服务器到客户端,以及从客户端到服务器。

第八章 将人工智能引入多人环境 中,你将创建一个敌人角色,并为其添加简单的 AI 以使其在多人系统中工作。

第九章 扩展 AI 行为 中,你将为 AI 添加更多功能,使其更具吸引力。

第十章 增强玩家体验 中,你将为游戏添加更多功能,例如动画和非玩家角色。

第十一章 调试多人游戏 中,你将学习调试和性能分析网络游戏的基本原则。

第十二章 管理多人会话 中,你将了解游戏会话及其特性。

第十三章 会话期间处理数据 中,你将学习如何在多人会话中处理数据。

第十四章 部署多人游戏 中,你将了解为多人游戏构建专用服务器的基础知识。

第十五章 添加 Epic Online Services (EOS) 中,你将全面了解 Epic Games 开发者门户和 Epic Online Services,这是一套强大的服务套件,旨在帮助你创建尽可能沉浸式的在线体验。

要充分利用本书

要充分利用本书,强烈建议你熟悉 Unreal Engine 及其主要功能。一些 C++ 编程经验也将是一个优势。

对游戏,特别是多人游戏,有着强烈的热情将极大地帮助你理解最先进的话题。建议你通过玩像 英雄联盟堡垒之夜使命召唤 这样的游戏来熟悉多人游戏。

本书涵盖的软件/硬件 操作系统要求
Unreal Engine 5.1 Windows 10 64-bit version 1909 revision .1350 或更高版本/版本 2004 和 20H2 revision .789 或更高版本,Ubuntu 22.04,或最新的 macOS Ventura
Visual Studio 2019 或 2022 和 JetBrain Rider 2023+

由于本书专注于网络环境而非图形,你不需要高性能的电脑来跟随所有章节。然而,为了正确运行 Unreal Engine,一台配备良好显卡的好电脑是强烈推荐的。

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件 github.com/PacktPublishing/Multiplayer-Game-Development-with-Unreal-Engine-5。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有其他来自我们丰富图书和视频目录的代码包,可在 github.com/PacktPublishing/ 获取。查看它们!

使用的约定

本书使用了多种文本约定。

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“接下来,您将找到一些声明,例如 UCLASS()GENERATED_BODY()UPROPERTY()UFUNCTION(),这些是由 UE 使用的,每个都有精确的功能。”

代码块设置如下:

#include "US_GameState.h" 
AUS_GameMode::AUS_GameMode() 
{ 
   GameStateClass = AUS_GameState::StaticClass();
} 

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

#include "US_Character.h" 
#include "Components/SphereComponent.h"

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“从 游戏 部分选择 空白 模板。”

小贴士或重要提示

它看起来像这样。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。

勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将不胜感激,如果您能向我们报告,请访问 www.packtpub.com/support/errata 并填写表格。

盗版:如果您在互联网上发现我们作品的任何形式的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过电子邮件发送至 copyright@packtpub.com 并附上材料的链接。

如果您想成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为本书做出贡献,请访问 authors.packtpub.com

分享您的想法

读完 使用 Unreal Engine 5 的多玩家游戏开发 后,我们非常乐意听到您的想法!请 点击此处直接进入本书的亚马逊评论页面 并分享您的反馈。

您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。

下载本书的免费 PDF 副本

感谢您购买本书!

您喜欢在路上阅读,但又无法携带您的印刷书籍到处走吗?

你的电子书购买是否与您选择的设备不兼容?

别担心,现在每购买一本 Packt 书籍,你都可以免费获得该书的 DRM 免费 PDF 版本。

在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

优惠远不止于此,您还可以获得独家折扣、时事通讯和每日邮箱中的精彩免费内容。

按照以下简单步骤获取优惠:

  1. 扫描二维码或访问下面的链接

packt.link/free-ebook/9781803232874

  1. 提交你的购买证明

  2. 就这些!我们将直接将免费的 PDF 和其他优惠发送到您的邮箱

第一部分:介绍多人游戏

在本书的第一部分,你将获得一个面向初学者的多人游戏开发领域的介绍。一旦你对它的关键概念有了扎实的理解,你将开始构建一个多人游戏的功能原型。

本部分包括以下章节:

  • 第一章开始多人游戏开发

  • 第二章理解网络基础知识

  • 第三章使用项目原型测试多人游戏系统

第一章:开始多人游戏开发

欢迎来到 Unreal Engine 多人游戏开发的奇妙世界!我非常激动您选择了我和我的书作为您在这个有时令人畏惧的技术领域的指南;我保证我会尽我所能使这次旅程尽可能轻松和有趣。

在本书中,您将学习如何从头开始创建 Unreal Engine 多人游戏,处理客户端/服务器逻辑,管理 AI 对手,测试和配置网络,并利用可用的云服务。到结束时,您将精通创建网络视频游戏,并对许多陷阱以及如何避免它们有深入的了解。

在本章中,您将了解多人游戏是什么,其起源以及今天可用的不同类型的多人游戏。此外,您还将探索用于多人游戏的技术如何在游戏之外的应用场景中得到应用。

在本章中,我将讨论以下主题:

  • 介绍多人游戏

  • 理解多人游戏类别

  • 探索游戏玩法变化

  • 多人技术仅仅是针对游戏吗?

技术要求

如您所知,Unreal Engine 编辑器在硬件要求方面可能非常严格,但请不要害怕。幸运的是,这本书更侧重于游戏编程,而不是实时视觉效果。

在这里,让我们看看硬件和软件要求,以及您在跟随本书时需要具备的一些先决知识。

先决知识

在您开始之前,我有责任提醒您,这本书是为那些已经对 Unreal Engine 开发有一定了解的人准备的。因此,您应该已经熟悉以下主题:

  • Epic Games 启动器和 Unreal Engine 编辑器

  • Blueprint 类和 Blueprint 编程

  • 使用您选择的 IDE 进行 C++编程

  • 对使用 Unreal Engine 的 C++编程有基本的理解。

硬件要求

以下是在撰写本书时 Epic Games 官方推荐的一些基本要求;如果您至少拥有以下硬件,您应该在整个章节中保证有一个良好的体验:

  • Windows 操作系统:

    • 操作系统: Windows 10 64 位版本 1909 修订版.1350 或更高版本,或版本 2004 和 20H2 修订版.789 或更高版本

    • 处理器: 四核 Intel 或 AMD,2.5 GHz 或更快

    • 内存: 8 GB RAM

    • 显卡: 兼容 DirectX 11 或 12 的图形卡

  • Linux:

    • 操作系统: Ubuntu 22.04

    • 处理器: 四核 Intel 或 AMD,2.5 GHz 或更快

    • 内存: 32 GB RAM

    • 显卡: NVIDIA GeForce 960 GTX 或更高版本,并安装最新的 NVIDIA 二进制驱动程序

    • 显存: 8 GB 或更多

  • macOS:

    • 操作系统: 最新 macOS Ventura

    • 处理器: 四核 Intel,2.5 GHz 或更快

    • 内存: 8 GB RAM

    • 显卡: 兼容 Metal 1.2 的图形卡

在我编写这本书的时候,我使用的以下硬件:

  • 桌面

    • 操作系统:Windows 10 64 位版本

    • 处理器:Intel Core i9 9900K @3.60GHz

    • 内存:64 GB RAM

    • 显卡:NVIDIA GeForce RTX 3090ti

  • 笔记本电脑

    • 操作系统:Windows 10 64 位版本

    • 处理器:Intel Core i7 9750H @ 2.60GHz

    • 内存:16 GB RAM

    • 显卡:NVIDIA GeForce RTX 2070

软件要求

本书假设您已经在计算机上安装并完全启用了 Epic Games Launcher 和 Unreal Engine 5。

注意

在本书编写时,Unreal Engine 的最新版本是 5.1.1,但您将能够跟随任何比 5.1.1 更新的版本。

此外,您还需要一个支持 C++ 和 Unreal Engine 的 IDE。如果您有一些过去的经验,那么您可能已经安装了 Visual Studio 2019/2022 或 JetBrains Rider;如果没有,您需要在 第四章 开始之前安装其中之一,设置您的第一个 多人游戏环境

注意

本书假设您已经安装了 Visual Studio 2019 或 2022 并完全可用;然而,使用 JetBrains Rider 也是完全可以接受的。

设置 Visual Studio 以进行 Unreal Engine 开发

安装 Visual Studio 后,您还需要以下额外组件才能使其与 Unreal Engine 正确工作:

  • C++ 性能分析工具

  • C++ AddressSanitizer

  • Windows 10 SDK

  • Unreal Engine 安装程序

要包含这些工具,请按照以下步骤操作:

  1. 打开 Visual Studio 安装程序

  2. 从您的 Visual Studio 安装中选择 修改,选择您将要使用的版本,如图 图 1.1* 所示:

图 1.1 – Visual Studio 安装程序

图 1.1 – Visual Studio 安装程序

  1. 修改 模态窗口打开时,在顶部栏中,确保您处于 工作负载 部分。

  2. 然后,通过点击复选标记激活 使用 C++ 进行游戏开发 选项。

  3. 接下来,如果它已关闭,请从右侧边栏打开 安装详情 | 使用 C++ 进行游戏开发 | 可选

  4. 选择 C++ 性能分析工具C++ AddressSanitizer、可用的最新 Windows 10 SDK 版本和 Unreal Engine 安装程序,如图 图 1.2* 所示。

图. 1.2 – 激活了 Unreal Engine 安装程序的 Visual Studio

  1. 点击 下载时安装 按钮(或 全部下载,然后安装 按钮)以开始安装过程。

下载和安装过程完成后,您将准备好使用 Unreal Engine 开发自己的 C++ 游戏。

Unreal Engine 的 IDE 支持

微软最近为 Visual Studio 2022 引入了一个名为 IDE Support for Unreal Engine 的新 Unreal Engine 集成扩展。此工具在 Unreal Engine 类、函数和属性之上添加了一些新功能,例如 Blueprint 引用、Blueprint 资产和 CodeLens 提示。

要包含此工具,请按照以下步骤操作:

  1. 如果已关闭,请打开工作负载部分。

  2. 通过点击复选标记激活使用 C++进行游戏开发选项。

  3. 如果它已关闭,请从右侧边栏打开安装详情 | 使用 C++进行游戏开发 | 可选

  4. 选择虚幻引擎的 IDE 支持,如图图 1.3所示。

图 1.3 – 安装 IDE 支持

图 1.3 – 安装 IDE 支持

  1. 现在安装此工具。

现在您的 IDE 已经正确配置,是时候了解多人游戏从何而来。在下一节中,您将了解一些视频游戏的历史。

引入多人游戏

多人游戏可能是当今最受欢迎的娱乐形式之一。这些类型的游戏为什么在多年里变得如此受欢迎和吸引人,有几个原因。

首先,与其他人一起玩增加了竞争(或合作)的元素,这可以非常激励人心且有趣。无论是像超逼真的反恐精英:全球攻势(www.counter-strike.net/)这样的游戏,还是像橡皮筋大盗(www.rubberbandits.game/)这样的疯狂设定,在虚拟环境中游玩都有一种神奇的魅力,使得游戏既令人兴奋又愉快。

有其他人一起玩也意味着引入了许多创造性的问题解决机会,以及来自不同背景和国家的许多人之间的许多社交互动机会,否则他们可能永远不会有交集!

但这一切是如何开始的?

在 20 世纪 70 年代初,伊利诺伊大学和控制数据公司开发的PLATO 分时系统使多个地点的学生能够访问在线课程。在 PLATO IV 推出后不久,学生们开始利用新引入的图形功能来创建多人视频游戏。到 70 年代末,PLATO 推出了不同类型的游戏,从地下城探险到太空战斗再到坦克战斗。

然而,多人游戏直到 90 年代中期才真正兴起:在这个时候,互联网接入变得广泛可用,玩家们终于可以连接到世界各地的彼此。

流行的Doom被认为是第一款在线游戏,因为它允许同时最多四名玩家,并具有一种死亡竞赛模式,你可以为此竞争分数。

几十年来,技术得到了极大的改进,我们现在能够在全球范围内一起在令人惊叹的沉浸式虚拟世界中游玩,而且(希望!)没有任何延迟或连接问题:难怪这么多人被这些类型的体验所吸引!

此外,玩家们在游戏过程中还发展出了创造性的沟通方式,例如使用 Skype 和 Discord 等在线服务,这些服务提供了更加沉浸式的体验。

随着实时流媒体平台如 Twitch 和 YouTube 的出现,多人游戏的新阶段已经开始。玩家可以在玩游戏的同时进行直播,让数百万人享受他们的体验。

成为视频游戏中的网络程序员

如果你正在阅读这本书,那么你很可能会想要了解网络的基本原理,并将你即将拥有的强大知识应用于多人视频游戏编程,以创造下一个大热门。作为一名多人程序员,你将能够为他人创造有趣和互动的游戏:这将是一次非常有益的经历!

但请注意——网络视频游戏编程可能相当具有挑战性,工作时间长,压力可能很大。在追求这种职业之前,了解这一点非常重要。

为了避免这些类型的陷阱,了解网络的工作原理至关重要,以便让玩家拥有愉快且无瑕疵的体验。

理解多人游戏也意味着理解如何解决计算机问题并在它们出现时处理它们。

请放心……它们迟早会出现!

在下一节中,你将了解主要的多人游戏类型,以及将它们区分开来的特点。

理解多人游戏类别

可供选择的游戏类别非常丰富,从第一人称射击到角色扮演;通常,一款游戏会融合多种类型,为玩家提供他们所寻找的精确内容。在这里,你可以找到目前最流行的游戏类型的非详尽列表,从多人角度描述它们。

第一人称射击游戏

第一人称射击FPS)游戏可能是市场上最激动人心和沉浸式的视频游戏之一,正如其名所示,它涉及从第一人称视角使用各种类型的武器进行游戏。

玩家将通过角色的机制,如跑步、躲避、瞄准、射击(以及经常需要重新装填武器!)来体验虚拟世界;这意味着你需要有快速的反射能力,否则你很快就会出局!

多人 FPS 游戏提供了多种游戏模式,可以以合作战役的形式进行,例如夺旗或死亡竞赛。

可用的多人 FPS 游戏不计其数,但Apex Legendswww.ea.com/games/apex-legends)和使命召唤www.callofduty.com/)是这种流行类型的良好例子。

第三人称射击游戏

第三人称射击TPS)游戏与 FPS 游戏非常相似,但玩家可以从第三人称视角进行对战。虽然 FPS 游戏往往更专注于射击对手和完成目标,但 TPS 游戏为玩家提供了更广阔的周围世界视角,使它们成为更注重策略的用户的好选择。

在 FPS(第一人称射击)和 TPS(第三人称射击)游戏中,最受欢迎的功能之一是能够更改你角色的皮肤,允许创建独特的头像,使其在人群中脱颖而出。

《堡垒之夜》www.fortnite.com/) 由 Epic Games(Unreal 的开发者)开发,是世界上最受欢迎的 TPS 游戏之一,每天都有数百万玩家在世界各地玩这款游戏。

实时策略

实时策略(RTS)游戏将竞争和策略元素结合在多人体验中。通常,玩家必须建立一支军队,并在网上与其他玩家互动,这通常涉及管理资源、与其他人结盟(同时试图智胜他们!),当然,当明显只剩下一个玩家能站到最后时,显然必须攻击他们!

有史以来最成功和最受欢迎的 RTS 游戏之一是 《星际争霸》starcraft.com/),玩家控制一个种族,必须与其他种族战斗以争夺权力和统治权。

大型多人在线角色扮演游戏

大型多人在线角色扮演游戏(MMORPG)中,经典的角色扮演游戏通过网络功能得到了增强,允许成千上万的(甚至数百万)玩家实时互动。

MMORPG(大型多人在线角色扮演游戏)中最令人兴奋的事情之一就是故事情节的持续变化,因为玩家们会通过自己的行动来影响它;他们在寻找下一个冒险的同时,会购买装备、磨练技能,并形成联盟。

《魔兽世界》worldofwarcraft.blizzard.com/) 无疑是这一类型中最受欢迎且运行时间最长的游戏之一。

多用户地下城

多用户地下城(MUD)游戏可以被认为是 MMORPG 的先驱,它们是基于文本的冒险游戏,每个玩家在虚拟世界中扮演一个冒险者的角色。MUD 通常包括角色扮演、策略和砍杀游戏的元素。

虽然它们可能看起来有点过时,但由于它们活跃的社区,MUD(多用户地下城)仍然被广泛地玩,玩家们在这些社区中经过多年的相处形成了牢固的关系。

有时,MUD 是由一群虚构体裁的粉丝群体创建的,例如 DiscWorldMUDdiscworld.starturtle.net/),这是一款基于特里·普拉切特创作的 DiscWorld 系列的游戏。

多人在线战斗竞技场

多人在线战斗竞技场(MOBA)游戏是一种策略子类型,其中两个团队相互竞争:每个玩家控制一个角色,试图击败对方团队,通常是通过摧毁敌方基地。竞技场是预定义的,让团队能够提前制定策略。

MOBAs 通常还具备由非玩家角色(NPC)小兵组成的人工智能(AI)控制,这些小兵将帮助角色实现他们的目标。

最受欢迎的 MOBA 游戏之一是英雄联盟(www.leagueoflegends.com/),自 2009 年以来一直存在,并且仍然是全球玩家的最爱。

注意

我在这里提供的游戏示例显然是由数十(如果不是数百)人开发的,显然,创建这类游戏超出了本书的范围。此外,一些类型——如 MMORPG——是制作起来最复杂和最具雄心的视频游戏之一。考虑到这一点,在尝试创建复杂的多人游戏之前,你应该谨慎行事——首先,确保你拥有处理其广泛范围所需的必要技能和信心。

现在我们已经了解了可用的不同多人游戏类型,在下一节中,你将发现这些类型可以通过创意添加来增强,从而完全改变它们被玩的方式。

审查游戏玩法变体

考虑到所有上述选项,玩家选择哪种多人游戏类型纯粹是个人喜好问题。然而,作为一名开发者,你需要了解不同类型的游戏玩法和技术,以确保你的项目能够正常运行。在本节中,我将介绍一些应用于常规游戏玩法的变体。

非对称游戏

非对称多人游戏中,两个或更多玩家团队在游戏玩法中竞争,每个团队的机制都不同。这些游戏通常要求玩家根据他们选择的阵营来制定策略。

最好的例子之一是Among Us(www.innersloth.com/games/among-us/),这是一款设定在太空船上的游戏,其中一些玩家扮演冒充者,他们的目标是让所有其他船员在他们的真实身份揭露之前因某些“意外”事故而死亡。

捉迷藏游戏玩法

有些游戏旨在开放式,让玩家有相当大的自由探索世界,并比其他更线性的冒险实现更多目标。

在多人游戏中,这导致了某些捉迷藏变体,玩家试图避免彼此(或主线故事),无论游戏是否官方支持他们。

这种类型变体的一个例子是秘密邻居(www.secretneighbor.com/),这是一款多人社交游戏,一群冒险的孩子试图潜入神秘邻居的家中,以揭露他可能囚禁儿童的证据。

异步游戏玩法

异步多人游戏将允许玩家在不需同时连接的情况下相互互动。这些游戏通常轮流进行,每个玩家将移动一步,然后等待对手完成下一步。

而且是的...在线国际象棋是一种异步游戏!

现在你已经对主要的网络游戏类型有了深入的理解,你可能想知道游戏引擎的多玩家技术是否仅限于游戏。在下一节中,我将提供一些例子来证明相反的情况。

多玩家技术仅仅是针对游戏吗?

如你所读到的,多玩家网络是创建沉浸式和娱乐性游戏的一个极其强大的工具。然而,你可能想知道这项技术是否仅限于游戏。

简短的回答是否定的。

在如 Unreal Engine 等游戏引擎中可用的网络技术可以是一个极其有用的工具,而不仅仅局限于游戏。实时远程协作可以极大地提高生产力,并且可以应用于几乎任何类型的项目,从教育目的到建筑,直至电影制作。

电影制作

随着虚拟制作VP)的出现,多玩家技术在电影制作中变得越来越重要。

VP 是一种工作流程,它将计算机生成图像CGI)、动作捕捉以及真实和虚拟资产结合在一个实时可视化中。VP 使内容创作者(通常是电影制作人)能够更高效、成本效益更高地进行制作。

在 VP 管道中使用多玩家技术的最佳例子之一是虚拟侦察,这是一个在不出自己工作室的情况下探索和评估潜在拍摄地点的过程。这通常是通过增强现实AR)和虚拟现实VR)的混合使用来实现的。

在流媒体服务 Disney+的《曼达洛人》系列中,可以看到使用 Unreal Engine 在 VP 中的良好例子。

建筑

Unreal Engine 可以用于建筑,以创建建筑项目的实时交互式可视化。这些模拟可以在提供在线协作能力的多玩家网络系统中运行,使建筑师之间能够进行实时协作。在设计过程中,建筑师可以在网络系统中使用建筑信息模型BIM)技术来访问相同的模型并同时进行更改,从而更加协作和高效。

具有多用户功能的实时渲染软件帮助设计师和建筑师与建筑、工程和施工AEC)生命周期中的利益相关者保持联系,从而实现更快、更好的沟通。

Reflect (unity.com/products/unity-reflect),由 Unity Technologies 开发的服务,是一个很好的例子,它让用户能够通过 3D 渲染、VR 和 AR 的混合体验虚拟环境。

教育

Unreal Engine 的多玩家功能可以成为教育者和学生的一个惊人的工具,提供沉浸式和交互式的学习体验。任何对游戏引擎有一定实际知识的教师都可以创建一个虚拟教室,让学生能够实时互动。

最近,像Fortnite及其Fortnite Creative版本(www.fortnite.com/creative)这样的游戏被许多学校用作教育工具,让学生发展解决问题的技能,并以有趣的方式吸引他们。

Unreal 的 Collab Viewer 模板

Unreal Engine 编辑器有几个非常优秀的模板,可以立即应用于网络协作环境:Collab Viewer for ArchitectureCollab Viewer for Automotive, Product Design 和 Manufacturing

图 1.4中,你可以看到已选择并准备生成的Collab Viewer for Architecture

图 1.4 – 在 Unreal 编辑器中选择的 Collab Viewer 模板

图 1.4 – 在 Unreal 编辑器中选择的 Collab Viewer 模板

这些项目模板为协作工业和建筑项目提供了一个惊人的起点;一切都已经设置好了(甚至有一个登录面板来访问应用程序),所以你只需要添加自己的环境并导出应用程序。

作为一名教师,我经常使用这些 Unreal Engine 模板作为创建多人教育体验(如虚拟博物馆或展示学生项目)的快捷方式。

图 1.5展示了在单台机器上模拟三个用户的应用情况(别担心,我们很快就会回到这个话题!)。

图 1.5 – Collab Viewer 在行动中

图 1.5 – Collab Viewer 在行动中

了解如何使用 Collab Viewer 及其许多功能(VoIP、实时注释、VR 模式等)超出了本书的范围,但如果你对这个主题感兴趣,Epic Games 在 Unreal Engine 文档中有一个详尽的章节:docs.unrealengine.com/5.1/en-US/collab-viewer-templates-in-unreal-engine/

摘要

在本章中,你已经了解了多人游戏的主要类别以及为什么在当今时代对网络有一个坚实的理解如此重要。更重要的是,你还简要概述了一些实际例子,这些例子扩展了传统上仅被视为游戏领域的概念。

在下一章中,我将指导你了解网络的基本原理。虽然你将遇到大量的信息(这可能看起来有些理论化),但请放心,当你进展到第三章使用项目原型测试多人游戏系统时,一切都会变得清晰,在那里你将开始创建你的第一个多人原型。

第二章:理解网络基础知识

拥有强大的网络知识是任何成功多玩家游戏开发的基础,因为它提供了对构成网络的不同组件如何工作的基本理解。网络可以分为三个主要领域:逻辑架构、协议和标准,以及物理基础设施。

由于对网络游戏感兴趣的任何人至少都应该对这些概念有一些基本了解,本章的主要目标是向您介绍这些组件以及在多玩家应用程序开发过程中可能出现的重大问题。到本章结束时,您还将了解虚幻引擎多玩家框架的组织方式,以便为下一步:创建您的第一个多玩家游戏原型做好准备。

因此,在本章中,我们将涵盖以下主题:

  • 什么是计算机网络?

  • 介绍网络协议

  • 理解网络问题

  • 介绍虚幻引擎的多玩家系统

技术要求

本章没有技术要求需要遵循。

什么是计算机网络?

我们生活在一个通过计算机、智能手机、智能家居和各种不同设备不断相互沟通的世界。计算机网络是现代技术的支柱。

大多数人可能甚至不关心设备通信及其工作方式;最重要的是……它确实工作。但“网络”究竟是什么意思?而且,最重要的是,作为多玩家游戏开发者,我们需要了解网络系统哪些内容?

计算机网络涉及两个或更多(大多数情况下,更多!)设备,这些设备通过连接在一起,共同目标是尽可能快、尽可能可靠地共享数据和资源。

网络可以使用电缆(有线)或无线电波(无线)进行通信,可以覆盖一个封闭区域或更大规模。尽管所有这些技术都拥有相同的目的,但它们的结构和能力将存在显著差异。

一旦设备在物理上连接,它们必须建立一个“合同”以便安全通信;没有这样的合同,数据可能会发送给错误的人,从而将重要信息置于风险之中。

合同建立后,每个设备都可以使用一个“数字信封”向另一个设备发送数据,该信封可以识别发送者和接收者。这确保了数据只被预期的接收者接收。

在网络上传输数据可能非常复杂,尤其是在处理大文件时(想想一个带有 150 MB 3D 模型附件的电子邮件,你可能就会明白这个意思了!)因此,数据必须由发送方“分割”成小块,一旦到达目的地,接收方将重新打包,以便使用。

这将我们引向最后一个阶段:信息丢失。尽管技术不断进步,但在我们这个不完美的世界中,数据丢失仍然是一种可能性。数据在传输过程中可能会被损坏或丢失;因此,采取措施确保数据安全并防止此类损失是非常重要的。

如果事情听起来很困难,那是因为它们确实如此!但不要害怕,这正是我开始学习这类技术时的感受;随着你通过这本书的内容,你会发现许多主题已经被 UE 多人游戏系统处理,让你有更多机会探索更高级的主题。

在下一节中,你将了解网络或一组网络是如何从结构和操作角度进行组织的。

计算机网络类型

正如你所知,计算机网络通过共享信息的方式为多个设备提供了相互通信的途径。以下是一些著名的例子:

  • 发送电子邮件

  • 流媒体视频

  • ...猜猜看?在线玩游戏!

但这些设备是如何连接的呢?有几种不同类型的计算机网络可供选择,它们具有不同的功能和目的;根据你的需求(以及你的预算!),你必须找到一个既能提供可靠性又能提供信任的解决方案。

局域网

局域网LAN)可能是最常见的计算机网络类型。局域网通常由一个电信网络组成,该网络连接着位于近距离的设备(通常不超过 1 公里)。这类网络通常利用以太网电缆在短距离内提供的快速连接。

局域网可以分为两种类型:

  • 客户/服务器局域网,其中多个设备(即客户端)连接到一个中央计算机(即服务器)

  • 对等局域网,其中每个设备平等地共享网络功能

图 2**.1 展示了一个典型的通过 Wi-Fi 或以太网电缆连接的小型局域网。

图 2.1 – 局域网设置

图 2.1 – 局域网设置

广域网WAN)相比,局域网的主要优势是维护水平较低和实施成本较低。此外,由于局域网限制在相对较小的距离和有限数量的连接设备内,它确保了更高的安全性。

局域网的一些良好应用实例包括学术校园(有时称为校园网)、医院或拥有多个部门的办公室。

当设备之间的连接仅通过无线方式进行时,局域网可以成为无线局域网WLAN),它通常用于家庭或公共场所(例如图书馆、机场和咖啡馆)等地方,在这些地方设备必须无需物理电缆即可连接。

广域网

局域网(LAN)和广域网(WAN)的主要区别在于,广域网覆盖的面积要大得多,本质上是由连接在一起的多个局域网组成的集合。

对于局域网(LAN)来说,广域网(WAN)可以如下所示:

  • 客户/服务器广域网,其中多个设备连接到一台中央计算机

  • 一种对等广域网,其中每个设备在网络功能中承担平等的责任

由于在长距离传输中通常会有速度损失,广域网(WAN)通常比局域网(LAN)慢。然而,广域网的主要优势在于其可以被公营:一个很好的例子是互联网,其中没有任何实体拥有完全的所有权。

图 2**.2 展示了通过广域网连接在一起的几个局域网:

图 2.2 – 一个广域网配置

图 2.2 – 一个广域网配置

城域网MAN)是一种较小的广域网,通常由城市、地区或政府管理;它通常包括一个高速骨干网络,该网络连接多个局域网。例如,它可能连接属于同一学术机构的多个校园(这正是我现在教学的地方的工作方式!)。

虚拟专用网络

图 2**.3 展示了一个示例,其中连接到局域网的设备通过 VPN 与另一台设备在互联网上进行通信。

图 2.3 – VPN 示例

图 2.3 – VPN 示例

在本节中,您已经了解了计算机网络的基本原理。您已经探讨了计算机网络是如何构建和组织的。在下一节中,您将更深入地研究这些主题,以了解设备如何通过规则和协议进行通信,以及这些规则如何保证交换数据的可靠性和准确性。

介绍网络协议

为了管理两个或更多计算机之间的通信,我们需要一些规则来规定数据如何发送和接收,以及为确保可靠性需要采取的安全措施。这些规则被称为协议

简单来说,协议就像一门国际通用语言:每个人都需要知道相同的词汇才能相互沟通。更具体地说,网络协议是一套指令,它规定了如何格式化传输接收数据,以便同一网络中的设备能够相互交互。

但在通过网络传输数据之前,它应该被仔细打包和结构化,以便接收者能够识别并重新组装它。

分组交换

分组交换是指将数据以小部分(或数据包)的形式发送到网络的方法。这个过程涉及将数据分成小段,并添加关于数据包内容、来源和目的地的额外信息。这个额外信息称为报头,通常放在每个数据包的前面。在某些情况下,数据包的末尾也可能包含额外的信息,称为尾部

这些数据包随后通过网络发送,并在目的地重新组装。在途中,它们被中间节点处理,这些节点可以存储传入数据并将其转发到下一个更接近最终接收者的节点。

使用分组交换的一些优点包括带宽效率高和可靠性高,因为接收者可以检测到丢失的数据包。另一个优点是由于相对较低的实施成本,具有成本效益。

例如,图 2.4显示了由其报头和数据部分组成的传输控制协议TCP)数据包:

  • 报头由一系列比特块组成,每个块都有其特定的含义(例如,报头长度指示报头数据偏移,以便指定实际数据在序列中的起始位置)

  • 数据可以有不同的长度,并包含数据包中包含的实际内容

图 2.4 – 一个 TCP 数据包

图 2.4 – 一个 TCP 数据包

规定数据如何打包和通过网络传输的协议集合现在被称为互联网协议套件TCP/IP 套件

TCP/IP 套件

TCP/IP 套件由不同的逻辑层,或组成,它们一个叠一个。这些包括以下内容:

  • 应用层

  • 传输层

  • 网络层

  • 数据链路层

图 2.5显示了 TCP/IP 套件中的层以及一些最常用的协议,还包括我们将在本节后面讨论的物理层:

图 2.5 – TCP/IP 层

图 2.5 – TCP/IP 层

每一层都在支持其上层需求方面发挥着重要作用。例如,这可能涉及从上层接收数据块,根据当前层的协议处理它,然后将它发送到下层。

现在我们来逐一查看这些层。

应用层

应用层为网络中不同机器上运行的应用程序之间的通信提供了接口。它位于 TCP/IP 套件的最高层,并允许通过使用协议(如文件传输协议FTP)和超文本传输协议HTTP))共享数据,这些协议用于将文件上传到互联网并在您喜欢的浏览器上下载网页。

传输层

传输层负责提供可靠的数据交付和流量控制,并提供允许应用程序在网络中安全通信的服务。

虽然 TCP 因其可靠性而被广泛使用,但在某些情况下,其他协议因其速度而被优先考虑,例如用户数据协议UDP),它不会保证数据包的交付,但因为它不需要建立和维护连接的开销,所以会更快。

此层通常负责检测问题和验证数据完整性;一种方式是通过校验和,即用于检查数据错误的数字和字母序列。

网络层

网络层(或更通用地,TCP/IP 套件中的网络层)负责提供在不同网络之间路由数据的方法,并确保数据包正确交付。此层还提供各种服务,如寻址、路由、拥塞控制和流量控制。

网络层负责提供一种逻辑寻址系统,该系统允许主机易于替换,将主机组组织成子网,并使远程子网能够相互通信。

用于实现这些功能的最常见协议是互联网协议版本 4IPv4),它是一个 32 位地址,用于标识网络上的设备。最新的版本是互联网协议版本 6IPv6),它基于 128 位地址,旨在取代 IPv4,因为 IPv4 的可用地址正在耗尽。

数据链路层

数据链路层负责在物理连接的主机之间提供通信方法。这意味着该层必须提供一种方法,使源主机能够打包信息并通过物理层传输。

图 2.6 展示了两个设备(客户端和服务器)之间典型的通信流程以及数据通过不同层传递的情况:

图 2.6 – 客户端与服务器之间的通信

图 2.6 – 客户端与服务器之间的通信

物理层

在所有上述层之下是物理层,它负责在物理介质(如电缆、光纤、Wi-Fi 连接或甚至蓝牙通信)上发送原始数据(比特)。

此层定义了信号的发送和接收方式,以及如何将这些信号调制和解调为可用的数字信息。

尽管我们在这里讨论了物理层,但它实际上并不包含在 TCP/IP 套件中,这就是为什么它在图 2.5中显示的其他层中稍微分离的原因。

现在你已经了解了数据是如何打包的以及 TCP/IP 套件的组织方式,你将在下一节学习网络中可能发生的主要问题和陷阱。

理解网络问题

在涉及大量数据并在物理媒体中移动的情况下,遇到严重问题的风险是相当大的。因此,了解这些潜在问题以及如何最好地避免它们或限制其影响是很重要的。

安全

处理计算机网络时首先要考虑的问题是安全。如果没有适当的网络安全协议,恶意人员可能获得访问敏感信息的权限。

在应用内购买您角色的最新皮肤或您的 Steam 账户登录凭证被盗,这绝对不是您想经历的事情!

数据包丢失

数据包丢失发生在从一个设备发送到另一个设备的数据包丢失或损坏时,这是一个可能导致任何网络出现重大中断的主要问题。这种中断可能导致通信缓慢甚至完全中断,从而给玩家带来负面的游戏体验。

例如,考虑一下,如果您对试图向您跳来的狂暴对手的完美射击在网络中丢失:那时您会遇到很大的麻烦!

延迟

计算机网络中的延迟表示数据包从一个指定点传输到其目的地所需的时间,通常以毫秒为单位。这可能是由于各种因素造成的,例如缓慢的互联网连接、过时的硬件或拥挤的网络。

在多人游戏中,应尽可能降低延迟,因为它会对您所玩游戏的性能产生重大影响。例如,在一个需要快速反应的第一人称射击游戏中,如果您的角色因为连接缓慢而被击败,这可能会破坏所有的乐趣!

注意

虽然本书没有讨论安全和数据丢失问题,但您将在本书的第四部分中了解延迟以及如何处理它。然而,如果您对深入探讨网络安全和数据丢失主题感兴趣,Packt 提供了大量关于该主题的书籍。

现在您已经对通过网络管理数据时的主要问题有了基本的了解,是时候进入 Unreal Engine 并了解该软件如何处理网络了。

介绍 Unreal Engine 多玩家系统

正如我们在前面的章节中已经看到的,在网络化系统中,考虑发送什么数据以及如何发送数据是至关重要的,因为这可以极大地影响游戏的性能和整体体验。

Unreal Engine 拥有一个强大的网络框架,该框架被用于世界上一些最受欢迎的在线游戏中。本节提供了关于驱动 Unreal Engine 多玩家框架的概念概述,以及它们在多人游戏中的使用。

网络模式和服务器类型

在虚幻引擎中,计算机与多人会话的关系被称为网络模式。虚幻游戏可以设置为以下网络模式之一:

  • 客户端:在此模式下,计算机将充当客户端,连接到网络多人会话中的服务器。

  • 独立模式:此模式严格用于非联网游戏(单人游戏或本地多人游戏),并且不会接受来自远程客户端的任何连接。

  • 专用服务器:在此模式下,计算机将作为服务器托管网络多人会话,并接受来自远程客户端的连接。作为专用服务器,一切都将优化以实现持久和安全的托管,因此将忽略任何以玩家为导向的功能,例如图形、音频或输入。

  • 监听服务器:在此模式下,计算机将作为服务器运行,接受远程客户端,同时也接受本地玩家。这意味着它将牺牲一些性能,但允许计算机同时作为服务器和客户端。这种模式可以被视为客户端和专用服务器模式的组合,允许你在作为客户端参与游戏的同时,同时托管网络。

注意

监听服务器因其设置简单和能够提供局域网中的休闲和竞技多人游戏而受到欢迎。由于托管会话的玩家将直接在服务器上玩游戏,他们通常会比其他玩家有优势。然而,由于服务器也作为客户端运行,因此它们不适合高度竞技的游戏或涉及大量数据的游戏。此外,允许客户端托管游戏并让其他客户端加入游玩的监听服务器,为网络上的所有客户端创造了潜在的安全漏洞。这是因为托管网络的客户端可能会进行恶意行为,例如作弊或给自己带来不公平的优势。

复制系统

在 UE 中,服务器和客户端之间复制游戏状态信息的过程称为复制。复制系统允许高级抽象和低级定制,使得管理在创建面向多个用户同时使用的虚幻引擎项目时可能出现的任何场景变得更加简单。

如果在 Actor 上启用了复制,则运行在不同机器上的游戏的所有实例都将同步。另一方面,如果禁用了复制,Actor 将只在其被创建的机器上更新其功能。

在多人游戏开发过程中,你可能需要复制的最常见元素可能包括创建/销毁、移动、变量和组件。

然而,还有一些元素不应该复制,因为它们将在客户端单独运行,例如骨骼和静态网格、材质、粒子系统和声音发射器。通常,服务器不需要了解这些元素的性质(即,纯粹的美学性质)。

网络角色

在多人在线游戏中,了解每个 Actor 由哪个设备控制非常重要。这由 Actor 自身的网络角色决定。

拥有授权Actor 角色的设备是控制 Actor 状态的设备,它负责实时向其他玩家复制关于 Actor 的信息。

位于非授权远程机器上的相同 Actor 的副本定义为远程代理,并将接收来自授权者的所有复制信息。

在 UE 中,权威通常由服务器持有,这意味着信息通常是服务器客户端的。这种模型被称为服务器授权

Pawn 和 PlayerControllers

如你所知,在 UE 中,Pawn(或更常见的是角色)可以由 PlayerController 拥有。这在多人游戏中也是如此,为每个连接的玩家创建一个 PlayerController。

在游戏过程中,任何被分配给特定 Pawn 的 Actor 会自动与该 Pawn 的所有者客户端关联。例如,一个 Pawn 可能拥有一个如步枪或剑 Actor 这样的物品,而这个物品将由拥有 Pawn 的相同连接持有。

相关性和优先级

为了确定在多人游戏中复制 Actor 是否有益,会考虑相关性。被认为不相关的 Actor 将在复制过程中被排除。这种方法用于减少通过网络发送的数据量,从而提高数据复制的效率。

当带宽有限时,在复制数据时首先选择最重要的 Actor。每个 Actor 都有一个分配的优先级值,用于确定复制的顺序。

远程过程调用

在多人会话期间,可以通过远程过程调用RPC)复制一个函数。RPC 可以从连接到网络的任何机器调用,但它们的实现将在网络会话的一部分特定机器上完成。

RPC 可以从服务器、客户端或多个客户端(多播)发送。RPC 要么保证到达目的地(可靠),要么不保证(不可靠)。

在本节中,我提供了一些关于 UE 多人系统的关键定义。这里的信息可能看起来相当密集,事情可能看起来有点令人不知所措。但不要害怕——你刚刚完成了第二章,整本书都可供你学习所有内容!

概述

在本章中,你学习了计算机网络的基本概念和参与成功网络通信的关键参与者。此外,你被介绍了互联网协议以及构成 TCP/IP 套件的层级,以及网络连接过程中可能出现的重大问题。最后,你了解了虚幻引擎的多玩家系统和框架提供的关键功能。

在下一章中,你将通过构建一个多人游戏的原型并在你的电脑上测试其功能来开始使用 UE 获得实际经验。

致谢

本章中的图表是在 Flaticon 的 Made Lineal 图标(www.flaticon.com/)的帮助下创建的。

第三章:使用项目原型测试多人系统

现在你已经了解了网络的工作原理以及计算机如何远程通信,是时候测试 Unreal Engine 网络框架的一些基本功能了。了解 UE 环境中不同元素如何交互的最好方法是通过使用可用的项目模板并启用其多人功能。

本章的主要目标是作为对 UE 主要多人框架功能和如何在单个设备(如您的电脑)上测试它们的温和介绍。到结束时,你将创建你的第一个多人原型,并准备好进行下一步,即从头开始创建一个完整工作的网络游戏。

因此,在接下来的几节中,我将向您介绍以下主题:

  • 创建多人游戏原型

  • 在本地测试多人游戏

  • 在网络上更新属性

  • 在网络上执行函数

技术要求

对于这个第一个原型,你只需要安装 UE 5。对于本章,你不需要用 C++编程,因为原型将是蓝图。

为了让事情更有趣,我将使用集成插件中的 Quixel Megascans 的一些资源,但这不是强制性的。

完成的项目可以在本书的 GitHub 项目模板中找到,在第三章部分:github.com/PacktPublishing/Multiplayer-Game-Development-with-Unreal-Engine-5

注意

从本章开始,我将交替使用“蓝图”和“蓝图类”这两个术语。如果需要区分,我将使用适当的术语,例如“动画蓝图”。

创建多人游戏原型

对于这个项目,游戏玩法将不是主要焦点;我希望你专注于多人框架的核心组件。因此,游戏将非常简单,并遵循以下基本规则:

  • 每个玩家应该控制他们的角色

  • 服务器将在随机位置生成物品拾取

  • 玩家将捕获拾取并获得分数

  • 游戏将无限进行

使用预制的项目,如模板,是您获得 UE 主要多人功能实战经验的好机会,而无需花费时间从头创建项目。在这里,你将使用俯视角模板创建自己的多人游戏原型。

从模板设置项目

当你准备好时,从 Epic Games Launcher 启动 UE 5。然后,按照以下步骤操作:

  1. 从可用的模板中选择游戏 | 俯视角

  2. 将项目设置为蓝图

  3. 为您的项目命名(例如,TopDown_Multiplayer)。

  4. 将其他设置保留为默认值。

  5. 点击创建按钮。

图 3**.1显示了项目的完成设置:

图 3.1 – 项目设置

图 3.1 – 项目设置

一旦创建项目,你就可以导入一些内容了。

添加 Quixel Megascans

在这个项目中,我想为我的拾取使用一些疯狂的东西:我将从 Quixel Megascans 库中选择一些水果和蔬菜(quixel.com/megascans)!

Quixel Megascans 是一个包含高分辨率 3D 扫描的免费库:它与 UE 完全集成;你只需使用你的 Epic Games 账户。要从 UE 访问模型,你需要使用 Quixel Bridge,这是一个已安装在 Unreal Engine 编辑器中的插件。

要将一些 Megascans 添加到项目中,只需这样做:

  1. 通过点击 快速添加到项目 | Quixel Bridge 打开 Quixel Bridge。

  2. 3D 资产 部分中,寻找一些水果或蔬菜——或者任何能激发你想象力的东西!

  3. 点击 下载 按钮下载资产并将它们添加到你的项目中。

图 3**.2 展示了在下载阶段,Quixel Bridge 中的一些我选定的模型:

图 3.2 – Quixel Bridge

图 3.2 – Quixel Bridge

一旦你获得了你的资产,它们可能大小不一。然而,你需要调整它们的大小以创建一个视觉上吸引人且功能性的拾取。为此,请按照以下步骤操作:

  1. 从主菜单中,通过选择 内容 | 角色 | 人体模型 | 网格 文件夹创建一个空关卡。

  2. 将你的模型放在参考旁边。

  3. 将它们调整到大约是参考大小的三分之一。

图 3**.3 展示了调整大小后的模型:

图 3.3 – 调整大小的模型

图 3.3 – 调整大小的模型

由于拾取将从一个共同的蓝图父级创建,最好的做法是将模型的比例设置为 1,这可以通过 UE 中的 模型工具 实现。通过在 主工具栏 区域的 模式选择 下拉菜单中选择它来打开 模型工具 面板,如图 图 3**.4 所示:

图 3.4 – 激活模型工具面板

图 3.4 – 激活模型工具面板

一旦启用 模型工具 面板,为每个模型执行以下步骤:

  1. 选择模型。

  2. 选择 变换 | BakeRS 以激活 旋转和缩放 烘焙工具。

  3. 在工具底部的 新资产位置 下拉菜单中,选择 AutoGen 文件夹(全局)

  4. 点击蓝色 接受 按钮以开始烘焙过程。

图 3**.5 展示了 BakeRS 工具已打开并准备好处理模型:

图 3.5 – BakeRS 工具

图 3.5 – BakeRS 工具

在这个过程结束时,所有三个模型将具有相同的大小,但每个都缩放到 1 的值。

你必须做的最后一件事是为每个模型生成碰撞:

  1. 打开项目中的每个静态网格。

  2. 打开碰撞下拉菜单。

  3. 选择添加 26DOP 简化碰撞以向网格添加碰撞区域。

  4. 保存修改后的资产以应用更改。

这个场景可以安全关闭,因为你不会再使用它。打开内容 | TopDown | Maps文件夹。通过这样做,你的资产将准备好在游戏中作为拾取物品使用。

下一步将是修改玩家控制器,以便你可以处理 Pawn 的移动。

修改玩家控制器

模板中的玩家控制器已经可以正常工作,但为了使游戏更有趣(毕竟这是一个多人游戏!),你需要做一些小的调整。

目前,角色移动是通过在地图上的一个点上单击来控制的。然而,我们希望玩家能够通过按住鼠标按钮并在关卡周围移动来移动他们的角色。

按照以下步骤修改玩家控制器蓝图:

  1. 导航到内容 | TopDown | 蓝图,并打开BP_TopDownController蓝图。

  2. 然后,通过单击事件****图选项卡打开事件图。

  3. 删除设置目的地输入 – 触摸组,因为你不会使用它。

  4. 设置目的地输入 – 游戏手柄和鼠标组中,删除连接到已取消完成执行引脚的所有节点。

  5. Ongoing执行引脚连接到与Triggered执行引脚相同的Branch节点。

修改后的图应如下所示:

图 3.6 – 玩家控制器修改后的图

图 3.6 – 玩家控制器修改后的图

现在玩家控制器已经被修改,如果你尝试以当前状态玩游戏,你应该能够在按住左鼠标按钮时移动你的角色。这是在玩独立游戏时预期的正常行为。

在下一节中,你将学习如何设置 UE,使其在计算机上模拟一个多人游戏会话。

本地测试多人游戏

测试多人游戏可能会遇到问题,因为它需要在多个设备上提供游戏。幸运的是,UE 允许你在单个计算机上模拟这种场景,这使得开发者创建和测试多人游戏变得更加容易。在本节中,你将学习如何将你的编辑器用作服务器并在本地启动其他游戏实例。

作为监听服务器玩游戏

是时候开始测试游戏在多人环境中的工作了。你将通过使用 UE 的网络****模式功能来完成这项工作:

  1. 通过单击播放按钮旁边的汉堡按钮打开更改播放模式和播放设置菜单,如图3.7所示:

图 3.7 – 汉堡按钮

图 3.7 – 汉堡按钮

  1. 3

  2. 然后,选择Net Mode | Play As Listen Server,如图3.8所示:

图 3.8 – 网络模式设置

图 3.8 – 网络模式设置

注意

当你在编辑器中以 监听服务器 方式测试游戏时,编辑器充当服务器和客户端。根据所选玩家的数量,将打开额外的实例进行测试。作为旁注,以 新编辑器窗口(PIE) 模式启动游戏将指定一个屏幕作为服务器,其他屏幕作为客户端。这种区别可以通过检查启动窗口的标题栏来识别。

现在,你可以点击 播放 按钮,之后编辑器将为每个额外的玩家打开一个窗口,如图 图 3.9* 所示:

图 3.9 – 以监听服务器方式测试游戏

图 3.9 – 以监听服务器方式测试游戏

将焦点放在每个窗口上,你可以玩每个独立的角色。

注意

如果你在播放模式下检查 大纲 窗口,你会注意到有三个 BP_TopDownCharacter 实例(每个玩家一个),但只有一个 BP_TopDownController – 这是你为本地玩家需要的那个。

通过网络更新

在这一点上,你可能想知道在没有做任何与网络相关的事情的情况下,如何在不同客户端之间同步角色。

答案是 复制,我在上一章中解释了它,并在本原型中演示了它:角色演员是启用了复制的,因此在游戏过程中,它们的一些属性,如 位置旋转,将在客户端之间更新。

要查看一个演员是否复制,请执行以下操作:

  1. 通过访问 内容 | 自顶向下 | 蓝图 来打开 BP_TopDownCharacter 蓝图。

  2. 通过点击 类默认 选项卡来打开 详情 面板。

  3. 找到 复制 类别,并注意 复制 属性已被选中。此外,注意 复制移动 已被选中,如图 图 3.10* 所示:

图 3.10 – 复制类别

图 3.10 – 复制类别

激活 复制 属性的最好部分之一是属性将在网络上自动更新:你不需要做任何事情。稍后,你将学习如何通过网络调用执行函数。

添加额外的角色生成点

如你所注意到的,三个玩家被生成在彼此旁边:这是因为我们在关卡中有一个单独的玩家开始演员。

为了解决问题,你将添加更多的生成点:

  1. 添加几个 玩家开始 对象,数量最多为你要测试的玩家数量 – 你可以通过点击 快速添加到项目 按钮,然后选择 基本 | 玩家开始 来完成此操作。

  2. 将它们放置在地图上的任何你认为适合你游戏的位置。在 图 3.11* 中,你可以看到我为三个生成点所做的选择:

图 3.11 – 添加更多生成点

图 3.11 – 添加更多生成点

如果你此时测试你的游戏,你会注意到玩家现在会在Player Start位置随机生成,但有时,两个或更多角色仍然会出现在同一个生成点上。这可以通过检查已经被占用的位置并在新玩家加入会话时从选择中排除它们来轻松解决。为此,请按照以下步骤操作:

  1. 通过转到Content | Topdown | Blueprints打开BP_TopDownGameMode蓝图。

  2. 然后,打开事件图。

  3. 我的蓝图 | 函数中,通过点击覆盖选项为ChoosePlayerStart函数添加一个覆盖。

  4. 添加一个获取所有类别的演员节点,并将其输入执行引脚连接到Choose Player Start节点的执行引脚。然后,将演员类别下拉属性设置为Player Start

  5. 添加一个For Each Loop节点来遍历你在上一个节点中找到的所有Out Actor属性。

  6. Loop Body执行引脚连接到一个分支节点。

  7. 从循环的Array Element引脚点击并拖动以获取一个Player Start Tag节点,并将其输出引脚连接到一个不等于(!=)节点。将此节点的比较值设置为Used。将此检查的结果连接到Branch节点的Condition引脚。

  8. Branch节点的True执行引脚连接到一个值为UsedSet Player Start Tag节点。目标引脚应连接到循环的Array Element区域。

  9. Set节点的输出执行引脚连接到图的返回节点

  10. Return NodeReturn Value引脚应设置为循环的Array Element属性。

生成的蓝图如图 3.12所示:

图 3.12 – 游戏模式图

图 3.12 – 游戏模式图

此图的作用是遍历关卡中的所有Player Start对象,并寻找一个尚未标记为Used(即尚未被占用)的对象。一旦找到一个合适的候选对象,它就会被标记为Used,其值将被返回,准备好用作角色的生成点。

运行游戏 – 每个角色现在应该会在独特的位置生成。有了这个,你的玩家现在可以与关卡进行交互了!

试试玩并测试你的游戏:确保一切按预期运行,并且玩家在所有客户端上正确同步。

在下一节中,你将学习如何通过添加一些拾取物并将分数分配给角色来更新多个客户端的属性。

在网络上更新属性

现在,是时候添加一些游戏玩法并在运行时正确同步元素了。在接下来的几个小节中,你将致力于以下功能:

  • 创建拾取物蓝图

  • 添加拾取物变体

  • 为角色添加一个点计数器

现在让我们添加这些功能。

创建拾取蓝图

我们将要创建的第一个东西是一个可拾取的项目,它将通过发送消息授予拾取它的角色的点数。

要创建此类通信,您需要创建一个接口:

  1. 在您的Blueprint文件夹中,右键单击并选择蓝图 | 蓝图接口

  2. 将接口命名为PointsAdder

  3. 打开蓝图接口

  4. 将默认函数重命名为AddPoints

  5. 添加一个整数类型的Value

您刚刚创建的接口应该与图 3.13中显示的相同:

图 3.13 – PointsAdder 接口与 AddPoints 声明

图 3.13 – PointsAdder 接口与 AddPoints 声明

一旦界面准备就绪,您需要创建一个将使用它的拾取蓝图:

  1. 在您的Blueprints文件夹中,添加一个继承自BP_BasePickup的蓝图类。

  2. 打开蓝图。然后,选择类默认值选项卡,并将你选择的网格添加到静态网格字段中。

  3. 物理部分,启用模拟物理属性,并检查启用重力属性是否已启用。

  4. SphereCollision组件添加到蓝图组件层次结构中。

  5. 将组件命名为Trigger,并确保生成重叠事件属性已被启用。

  6. SphereCollision组件的球体半径属性设置为比您将要使用的静态网格稍大的值(例如,50)。

现在,您需要将一些代码逻辑添加到蓝图。首先,让我们为拾取角色添加一个点值:

  1. 打开事件图。

  2. 添加一个Points类型的变量。

  3. 通过单击变量类型旁边的眼睛按钮将其设置为实例可编辑

  4. 编译后,将变量的默认值设置为1

蓝图视口应该看起来与图 3.14中显示的相同:

图 3.14 – 拾取视口

图 3.14 – 拾取视口

现在是时候为蓝图设置重叠事件行为:

  1. 删除Event BeginPlayEvent Tick节点,因为您不会使用它们。

  2. 添加一个Cast To Character节点,并将其输入执行引脚连接到Event ActorBeginOverlap的输出引脚,以检查演员是否为所需的类型(即,角色)。

  3. 如果检查成功,则添加一个AddPoints (消息)节点:这是您之前在接口中声明的函数。

  4. As Character引脚连接到函数节点的目标引脚。

  5. 在图中添加一个获取点节点,并将引脚连接到添加点函数节点的Value引脚。

  6. 最后,将添加点节点的输出执行引脚连接到销毁演员节点,以便在拾取后删除拾取物。

最终的图应该类似于图 3.15所示:

图 3.15 – 拾取事件图

图 3.15 – 拾取事件图

这个图所做的是相当直接的:每当一个演员与拾取重叠时,拾取将通过接口发送一个AddPoints消息,然后销毁自己。如果重叠的演员没有实现接口,消息将简单地丢失,不会引发任何错误。

现在,是时候采取这个过程中最关键的一步:启用复制。为此,请执行以下操作:

  1. 组件选项卡中,选择BP_BasePickup (self)元素。

  2. 然后,在细节面板中,查找复制类别并启用Replicates属性。

现在你有一个基础拾取,你可以创建变体,准备在游戏中使用。

添加拾取变体

为拾取蓝图创建一个变体相当简单:

  1. 在内容浏览器中右键单击你的BP_BasePickup项目。

  2. 选择创建子蓝图类,给你的新拾取命名,并打开它。

  3. 打开类默认值选项卡。然后,将网格分配给静态网格字段(在我的例子中,是一个水果 Megascan)。

  4. 根据图 3.16 所示,为Points属性分配一个你选择的值:

图 3.16 – 橙色拾取的设置

图 3.16 – 橙色拾取的设置

为你的每个拾取重复这些步骤,你就可以准备出发了!

在你为蓝图角色实现AddPoints接口之前,你可以自由地添加一些全新的拾取并作为监听服务器多人游戏测试游戏。

注意

如果你的拾取在客户端似乎有不同的旋转,这意味着你可能没有启用复制。请确保你的蓝图中的ReplicatesReplicate Movement字段被勾选!

为角色添加点数计数器

现在你已经知道了如何在网络上复制演员,是时候学习如何复制单个变量以及如何在运行时拦截更改了。

你将通过跟踪每个玩家获得的成绩,并在游戏演员旁边显示它们来实现这一点。按照以下步骤操作:

  1. 打开蓝图项目文件夹。

  2. 添加一个整数类型的Points变量。

  3. 点数属性的细节面板中,查找复制字段,并从下拉菜单中选择RepNotify

你会注意到,一旦选择了RepNotify字段,就会在你的蓝图上添加一个名为OnRep_Points的函数 – 这个函数将在每次由权威演员更新变量时在客户端被调用。

注意

RepNotifyReplicated值之间的区别在于,在第二种情况下,变量将在网络上更新,而无需执行任何通知函数。还应注意的是,OnRep_XXX函数在每个客户端由服务器调用,而不会在服务器本身上执行。

您现在将向角色添加一个文本组件,以显示他们在比赛中获得的分数:

  1. 添加一个PointsLabel

  2. 将组件放置在您认为合适的位置。我选择了这些变换值:位置 (-120, 0, -80)旋转 (0, 90, 180)

  3. 根据您的意愿增强组件的特性。我选择了图 3.17中显示的设置:

图 3.17 – PointsLabel 组件

图 3.17 – PointsLabel 组件

现在是时候让拾取物与角色进行通信了。我们将通过实现我们之前定义的接口来完成此操作:

  1. BP_TopDownCharacter打开的情况下,选择 设置标签页。

  2. 详细信息面板中,单击实现接口字段上的添加下拉按钮并选择PointsAdded接口。

  3. 将名为AddPoints的新函数添加到MyBlueprint标签页的接口部分。右键单击函数名称并选择实现事件 – 这将在事件图中添加相应的节点并选择它。

  4. Points变量拖入事件图,并选择设置选项。

  5. 再次拖动Points变量,这次选择获取选项。

  6. 使用添加+)节点将事件的输出引脚添加到获取点节点。

  7. 事件节点的执行引脚连接到设置节点。

  8. 添加节点的结果引脚连接到设置节点的Points引脚。

注意

您会注意到,您添加到图中的设置获取节点现在在右上角都有一个图标。此外,设置节点上还装饰有表示带有通知的文本:这意味着Points变量是通过带有函数通知进行复制的。

图 3.18显示了AddPoints事件的最终图:

图 3.18 – 添加点事件

图 3.18 – 添加点事件

我们最后需要做的是实现通知函数,以便我们可以更新显示给角色的分数:

  1. 双击OnRep_Points函数以打开它。

  2. 从事件图中的PointsLabel组件拖动一个获取节点。

  3. 从其输出引脚添加一个设置文本节点,并将其输入执行引脚连接到On Rep Points节点的输出执行引脚。

  4. 从事件图中的Points变量拖动一个获取节点,并将其引脚连接到设置文本节点的引脚。Unreal 将自动添加一个转换为文本转换节点。

最终的图应该与图 3.19 中描述的非常相似:

图 3.19 – On Rep Points 图

图 3.19 – On Rep Points 图

现在,如果你测试游戏,你应该能看到每当玩家在关卡中获得拾取物时,所有客户端都会更新。

通过这样,你已经了解了在多人游戏会话期间对象是如何更新的。具体来说,你获得了关于演员是如何复制的以及如何通过复制通知检测变量变化的见解。在下一节中,你将通过添加拾取物的生成区域并对你的人物进行一些美学改进来增强你的原型,以便它们可以轻松识别。你将通过在网络上调用函数来实现这一点。

在网络上执行函数

在本节中,你将学习如何正确地在网络上调用函数,以及“权限”一词对 UE 多人游戏系统真正意味着什么。特别是,你将了解在函数被调用时,哪个实体应该执行函数:客户端或服务器。

生成演员

是时候在运行时开始添加拾取物了。你将通过向关卡中添加一个生成区域蓝图来实现这一点。

此蓝图应该能够做到以下几点:

  • 每次生成东西时都选择一个随机位置

  • 在预定义的时间间隔内随机生成拾取物

  • 并且显然...在网络上的行为要正确!

让我们开始吧。

选择一个随机的生成位置

让我们从创建蓝图并设置其参数开始:

  1. 创建一个新的演员蓝图,并将其命名为 BP_Spawner

  2. 添加一个 SpawnArea,并通过将其拖放到默认的 Scene Root 组件上,使其成为 Scene Root 组件。

  3. 添加一个 SpawnableObjects,使其成为 Instance Editable

一旦编译并保存了蓝图,打开其事件图。然后,执行以下操作:

  1. 创建一个名为 Spawn 的函数并打开它。

  2. 将函数的执行节点连接到 SpawnActor from Class 节点。

  3. SpawnableObjects 变量添加一个 Get 节点,并将其输出引脚连接到 Random Array Item 节点。

  4. Random Node 的输出 Actor Class Reference 引脚连接到 Spawn 节点的 Class 引脚。

要为生成的物品获取位置,我们将从 Box Collision 组件内部获取一个随机位置:

  1. 右键单击 SpawnActor 节点的 Spawn Transform 引脚,并选择 Split Struct Pin

  2. SpawnArea 组件拖入函数图,并将其引脚连接到 Get Scaled Box Extent 节点。

  3. 添加一个 Get Actor Location 节点,并将其输出引脚连接到 Random Point in Bounding Box 节点。

  4. Get Scaled Box Extent 节点的输出引脚连接到 Random Point in Bounding Box 中的一个 Half Size 引脚。

  5. Random Point in Bounding Box 节点的 Return Value 引脚连接到 SpawnActorSpawn Transform Location 引脚。

最终的图示可以在图 3.20中看到:

图 3.20 – 生成函数图

图 3.20 – 生成函数

生成函数从给定元素列表中选择一个随机的蓝图类,并在定义区域内随机位置生成其实例。

在预定间隔随机生成拾取器

你现在将要添加一个计时器,以预定间隔生成拾取器:

  1. 打开蓝图的事件图部分并删除Actor BeginOverlapTick事件。

  2. 添加一个通过事件设置计时器节点,并将其输入执行引脚连接到BeginPlay事件的输出引脚。

  3. 时间值设置为1并勾选循环框。

  4. 连接OnTimer

  5. 将自定义事件的执行引脚连接到生成函数。

你刚才所做的一切可能看起来非常直接且正确,但实际上是错误的...或者至少,它遗漏了一些东西:代码在每个客户端上的行为将不同。请稍等片刻,让我们测试一下这种错误行为:

  1. 删除你之前可能添加到游戏关卡中的所有拾取器。

  2. BP_Spawner实例添加到关卡中。

  3. 将实例放置在大约场景中心,并更改生成区域组件的Box Extent值,以便盒子将覆盖整个游戏区域;如果你使用的是默认场景,则类似于(1300,1600,32)应该就足够了。

  4. 生成区域组件放置在地面上方 – 拾取器应该从上方掉落并落到地面上。

  5. 将你创建的所有拾取器添加到可生成 对象数组中。

  6. 运行多人模拟。

你会注意到客户端之间的事物看起来完全不同步:特别是,UE 实例(即服务器)将在每个间隔生成一个拾取器,而额外的客户端将生成两个。

我们做错了什么?简单来说,目前每个客户端中的每个生成器都在生成物品,但只有服务器(拥有权限的那个)在网络上生成物品。这意味着服务器每次只会得到一个拾取器,但客户端会得到两个:一个由服务器生成,另一个由客户端自己创建,而服务器对此一无所知。

使用演员权限正确生成拾取器

为了修复生成问题,我们只需告诉生成器只有在它们有权限这样做时才生成拾取器(即,它是服务器):

  1. BeginPlay事件的执行引脚和通过事件设置计时器 by Event之间添加一个Switch Has Authority节点。

  2. 权限执行引脚连接到计时器节点的输入引脚。

修正后的图示在图 3.21中:

图 3.21 – 生成计时器图

图 3.21 – 生成计时器图

当你现在测试游戏时,它应该表现正确:相同的对象应该在每个客户端实例中同时生成。

原型几乎完成了,但我希望你在创建一个最后的网络功能:为你的角色创建一个个性化皮肤。

角色皮肤化

使用两个或更多相同角色的游戏可能会很快变得非常混乱。对于原型,最佳选项是快速创建一些彩色材料,并在角色被生成时立即分配给它们。我们将在网络环境中这样做。

让我们先创建一些材料实例:

  1. 在内容浏览器中,导航到 Content | Characters | Mannequins | Materials | Instances | Manny

  2. MI_Manny_01 重复几次,以等于你在 Play as a Listen Server 部分设置的连接数(即,3)。

  3. 使用你自己的首选约定命名新材料;我使用了 MI_Manny_01_[ColorName]

  4. 打开每个新实例,并将 Tint 属性更改为你喜欢的颜色。

  5. 保存所有材料实例并关闭它们。

现在,让我们打开角色蓝图并添加另一个复制的变量:

  1. 打开 BP_TopDownCharacter

  2. 添加一个名为 SkinMaterial 的新变量,其类型为 Material Interface Object Reference,并使其 Instance Editable

  3. Replication 字段的下拉菜单设置为 RepNotify。这将创建一个名为 OnRep_SkinMaterial 的函数。

接下来,打开 OnRep_SkinMaterial 函数以添加皮肤更改逻辑。然后,执行以下操作:

  1. 从事件图中拖动 Mesh 参考到 Components 面板。

  2. 拖动一个用于 SkinMaterial 变量的 Get 节点。

  3. 将函数执行插针连接到 Set Material 节点。

  4. Mesh 参考连接到 Target 插针。

  5. SkinMaterial 参考连接到 Material 插针。

该函数的图示见 图 3.22

图 3.22 – 复制的 SkinMaterial 变量的函数

图 3.22 – 复制的 SkinMaterial 变量的函数

  1. 每当 SkinMaterial 变量更改时,OnRep_SkinMaterial 将负责将其分配给角色的第一个材料。

现在,你需要更改每个角色在添加到关卡后的材料:

  1. Blueprints 文件夹中,打开 BP_TopDownGameMode

  2. 添加一个 SkinMaterials

  3. 一旦编译了蓝图,将之前创建的所有材料添加到 Details 面板中的 Default Value 字段。

  4. 添加一个 SkinCount 变量;这个变量将用作选择皮肤的索引计数器。

你可能已经注意到我们没有复制 SkinCount 变量;在这种情况下,我们不需要这样做,因为这个变量只存在于服务器上,并且用于在游戏角色被生成时立即处理角色的皮肤。了解何时以及复制哪些变量是一个我将在 第六章通过网络复制属性 中讨论的主题。

接下来,你必须获取已使用皮肤的计数。然后,每次创建新的连接时,你将分配下一个可用的皮肤给角色。为了实现这一点,你将使用一个名为OnRestartPlayer的事件,该事件在每次玩家重新启动时执行(包括第一次生成时):

  1. 在事件图中添加一个Event OnRestartPlayer节点。

  2. New Player连接一个Get Controlled Pawn节点。

  3. 将其输出的引脚连接到BP_TopDownCharacter节点。

  4. 将事件执行引脚连接到Cast节点。

  5. 将输出的As BP Top Down Character连接到一个Set Skin Material节点(注意添加的w/Notify标签,以指示值更改时的通知调用)。

  6. 将转换节点的成功执行引脚连接到Set Skin Material节点。

  7. SkinMaterials数组添加一个Get节点,并为SkinCount变量添加一个Get节点。

  8. 将输出的Skin Materials引脚连接到Get (a copy)节点。

  9. Skin Count引脚连接到Get索引。

  10. Get节点的输出引脚连接到Set Skin Material节点的Skin Material引脚。

  11. 最后,将Set Skin Material的输出执行引脚连接到一个Increment (++)节点。Skin Count x变量应该增加;这将跟踪数组中选定的皮肤。

图 3.23 描述了游戏模式图:

图 3.23 – 游戏模式图

图 3.23 – 游戏模式图

现在原型已经创建,是时候测试(并玩)它了!

测试游戏

以监听服务器运行游戏 – 每个角色应该在其网络中同步其彩色皮肤,并且每个玩家的得分应该正确显示在角色本身上。图 3.24 显示了游戏原型的实际运行情况:

图 3.24 – 测试游戏原型

图 3.24 – 测试游戏原型

因此,你最终测试了你的多人游戏原型:玩家可以四处奔跑并拾取下落的物品,准备获得分数。虽然看起来很有趣,但这只是多人游戏开发的开始!

概述

在本章中,你创建了你的第一个多人游戏的原型,并了解了如何在网络上同步 Actors 和变量。此外,你开始通过 UE 系统测试原型,该系统并发模拟多个连接,所有这些通过使用 Blueprints 实现。

然而,向前推进,你将过渡到使用 C++编程语言,这可能看起来有点令人畏惧,但我向你保证,我会努力使这种转变尽可能轻松!

是时候让你的原型走向它的命运了...我们还有更重要的事情要做!在下一章中,你将从头开始创建一个游戏。你将使用 C++来开发它,并利用这种开发方式的所有优势。

第二部分:虚幻引擎中的网络和多人游戏

在本书的第二部分,你将为使用虚幻引擎的完整多人项目打下基础。从那里,你将深入研究虚幻引擎游戏框架的基本功能,以及它们如何在多人环境中实现。

本部分包括以下章节:

  • 第四章设置您的第一个多人环境

  • 第五章在多人环境中管理演员

  • 第六章在网络中复制属性 O**ver the Network

  • 第七章使用远程过程调用 (RPCs)

第四章:设置您的第一个多人游戏环境

在 UE5 中设置完整的联网游戏可能是一项令人畏惧的任务。这需要了解网络、编码以及引擎本身,所有这些都可能让经验丰富的开发者感到不知所措。但是,有了正确的指导和一些努力,您可以在很短的时间内(好吧,有点儿...)创建一个引人入胜的多人游戏体验!

为了避免在重新思考和修改过程中出现多个问题,第一步应该是清楚地理解项目的主题。这可以避免从一开始就产生混淆,并使工作流程更加顺畅。之后,您需要创建一个虚幻项目并正确设置一切。这包括创建您的游戏框架GF)类,以便您能够访问开发所需的所有必要功能,以及配置项目设置以使用这些类。

到本章结束时,您将对使用 C++在 UE 中进行编程有一个扎实的理解,并为您的多人游戏奠定了基础。

在接下来的几节中,我将向您介绍以下主题:

  • 介绍虚幻阴影 - 骷髅领主的遗产

  • 在虚幻引擎中理解 C++

  • 开始您的虚幻多人游戏项目

  • 添加玩家类

技术要求

为了跟随本章内容,您应该已经按照第一章中解释的,设置了包含所有虚幻依赖项的 Visual Studio(或 JetBrains Rider)。

您将使用本书配套仓库中提供的起始内容,该仓库位于github.com/PacktPublishing/Multiplayer-Game-Development-with-Unreal-Engine-5

通过此链接,找到本章的相应部分并下载以下.zip文件:Unreal Shadows – Starter Content。如果在本章的进展过程中您迷路了,在仓库中您也会在这里找到最新的项目文件:Unreal Shadows – Chapter 04 End

此外,为了完全理解本章内容,在您跟随我了解 UE 框架主要特性的关键特性时,您需要对 C++编程有一些基本知识。

介绍虚幻阴影 - 骷髅领主的遗产

考虑以下来自(从未出版)奇幻小说《虚幻阴影 - 骷髅领主的遗产》的段落:

“空气中弥漫着腐朽的恶臭,三个盗贼走进了骷髅领主的地下城。他们的任务很明确:潜入堡垒,找到国王的骑士,并把他活着带回来。他们找到的任何东西都可以带回家。”

当他们悄悄穿过阴暗的走廊时,他们都清楚自己并不孤单:成群的亡灵小兵潜伏在每一个角落,他们那绿色的眼睛空洞地 staring blankly.

公会之前已经遇到了不少危险的敌人,但从未遇到过如此强大的不死军队。他们静静地穿过走廊,小心翼翼地不引起任何注意。他们最不想做的就是吸引整个僵尸群向他们扑来。

恭喜你 – 你刚刚被雇佣来制作这部畅销书的视频游戏改编版!而且更棒的是,它将是一款多人游戏!

解释项目概述

你将要开发的游戏将是一款第三人称射击游戏,它将使用捉迷藏游戏玩法变化,正如在第一章中介绍的那样,开始多人游戏开发。这意味着这将是一款潜行游戏,玩家只能通过安静和谨慎地移动来生存。

在这款多人游戏中,每个参与者将扮演盗贼公会的一员,这是一个由游民和盗贼组成的秘密组织,潜入一个不死巫师的领域。主要目标将是营救被关押在地下监狱中的非玩家角色(希望它们还活着!)。此外,玩家还将收集来自过去和不太幸运的冒险者的宝藏和装备。

每个角色都将具备以下能力:

  • 通过行走或跑步移动

  • 操纵武器

  • 获取增强道具

  • 装备新武器

  • 通过经验点提高技能

这款游戏的主要焦点将是潜行移动,因为不死军队将证明对角色来说直接交战过于强大。因此,移动(尤其是奔跑)和挥舞武器会产生噪音,使得之前未察觉的敌人立刻对玩家的存在警觉起来。

敌人将由 Lichlord 的随从代表,一群无意识的骷髅僵尸,它们将在关卡中四处游荡,对玩家角色一无所知。

角色发出的过多噪音或掉入陷阱会惊动附近的敌人,使得完成游戏变得几乎不可能。遗憾的是,玩家角色获得经验点只能通过击败敌人来实现,这进一步增加了整体体验的参与度!

注意

由于这本书是关于多人游戏编程而不是游戏设计,因此游戏机制的平衡将不会是游戏玩法的主要焦点;相反,重点将在于使事物有效运作。

开始项目

我们希望游戏在视觉上吸引人,但我猜你们大多数人可能没有 3D 建模的背景(我没有!)。这就是为什么我们将使用 Kay Lousberg(kaylousberg.com/)提供的某些惊人的资源,这些资源可以免费用于个人和商业用途。

图 4**.1 展示了我们将要使用的一种包装:

图 4.1 – KayKit 地牢包

图 4.1 – KayKit 地牢包

你将从 UE5 中可用的空白模板创建一个全新的项目开始,然后你将添加上述套件中的某些资产;然而,为了避免正确导入它们的繁琐任务,我已经为你打包好了。

项目文件以及每一章的代码都可以在这个书的配套项目页面上找到,位于此处:github.com/PacktPublishing/Multiplayer-Game-Development-with-Unreal-Engine-5

在确定了你要做什么之后,现在是时候了解如何在 UE5 中使用 C++了。以下部分将简要介绍引擎框架提供的核心功能。

理解虚幻引擎中的 C++

如果你和我一样热爱游戏开发和编程,你可能会发现,在 UE5 中编写 C++代码相当有趣,而且实际上并不太难入门。Epic Games 在添加让 C++变得容易使用的功能方面做得非常出色。

虽然在 UE5 中可以编写标准的 C++代码,但要实现更好的游戏性能,建议使用引擎最常用的功能,如内置的反射系统和内存管理。

蓝图和 C++

如你所知,UE 提供了两种编程游戏逻辑的方法:蓝图可视化脚本和 C++。

蓝图可视化脚本使得没有丰富编码经验的开发者能够创建复杂的游戏机制,而无需编写任何代码。另一方面,C++是一种面向对象编程OOP)语言,它需要更多的技术知识,但比蓝图提供了对游戏引擎的更多控制。

应该注意的是,C++和蓝图是严格相连的,因为蓝图提供了底层 C++代码的视觉表示,并遵循其原则,包括继承和多态。虽然蓝图不需要高级编码技能,但它们在数据类型、指针、引用和其他规则方面遵循与编程语言相同的原理。

这两种语言都可以在 UE5 项目中一起使用,大多数情况下,你可以用 C++实现的事情同样可以用蓝图实现;然而,C++在定制 UE 的核心功能或创建扩展其功能(超出开箱即用的功能)的插件方面表现更出色。

尽管蓝图可视化脚本和 C++在处理 UE 项目时都提供了强大的工具集,但 C++通过面向对象编程技术提供了更底层的访问权限——这就是为什么一旦开始开发多人游戏,对它的深入了解非常重要。

理解 C++类

一个虚幻引擎 C++类,嗯,就是一个普通的 C++类!

如果你已经对 C++ 中的 OOP 有很好的了解,你在这里会感到很自在:创建新的 UE C++ 类的过程首先是通过定义它将表示的对象类型开始的,例如 Actor 或组件。一旦定义了类型,变量和方法就在头文件(.h)中声明,代码逻辑在源文件(.cpp)中实现。

虽然源文件的行为类似于其种类的常规 C++ 文件,但头文件将允许你为将用于继承自你的类的 Blueprints 的变量和函数声明附加信息。此外,它将减轻在运行时管理内存的痛苦(我稍后会回到这一点)。

随着 UE5 的发布,Epic Games 引入了一个令人惊叹的检查工具,称为 BP_BasePickup,这是在上一个章节中创建的。

图 4.2 – C++ 头文件预览工具的实际应用

图 4.2 – C++ 头文件预览工具的实际应用

在 UE 中,有三个主要的类类型,你将在开发过程中从中派生:

  • UObject 是 UE 的基类,提供了 UE 中大部分主要功能,例如垃圾回收GC)(是的,UE 提供了它!),网络支持,属性和方法反射等。AActor 是一种可以添加到游戏关卡中的 UObject,可以从编辑器或运行时添加:在后一种情况下,我们说 Actor 被已生成。在多人环境中,AActor 是可以在网络中复制的基类型,它将为需要同步的任何组件提供信息。

  • UActorComponent 是定义将附加到 Actor 或 Actor 自身的另一个组件的组件的基本类。

此外,你还将使用以下实体:

  • UStruct 用于创建平面数据结构,并且不扩展自任何特定类

  • UEnum 用于表示元素枚举

最后一点,在这本书中,你会发现类名以一些字母开头,一旦在编辑器中使用这些类,这些字母将不可见。UE 使用前缀来指明类类型。主要使用的前缀如下:

  • UObject(例如,组件)

  • AActor)并且可以添加到关卡中

  • FColor 结构

  • TArrayTMap

  • I 用于接口

  • E 用于枚举

  • booluint8(可以用来代替 bool

注意

请记住,大多数这些前缀是强制性的;如果你尝试命名一个没有 A 前缀的从 Actor 继承的类,你会得到一个错误。UE 会在编辑器中隐藏前缀。此规则仅适用于 C++ 类;Blueprints 可以没有这样的前缀命名。

现在你已经熟悉了 UE 中可用的主要类型,是时候探索类头文件的主要功能,以便理解其核心功能了。

UE C++ 头文件的解剖

UE5 中 Actor 的 C++头文件将类似于以下代码片段:

#include "Engine/StaticMEshActor.h"
#include "APickup.generated.h"
UCLASS(Blueprintable, BlueprintType)
class APickup : public AStaticMeshActor
{
  GENERATED_BODY()
public:
  APickup();
  UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category="Default")
  TObjectPtr<class USphereComponent> Trigger;
  UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Default")
  int32 Points;
  UFUNCTION(BlueprintCallable)
  void Reactivate();
};

如你所见,这里有很多事情在进行中。

首先,你会注意到一个#include "APickup.generated.h"声明。这一行代码是自动生成的,由你的头文件中声明的include文件或编译器抛出错误。

注意

UHT 是一个自定义解析和代码生成工具,它支持 Unreal Engine 中的 UObject 系统。UHT 用于解析 Unreal 的 C++头文件,并生成引擎与用户创建的类一起工作所需的样板代码。

类构造函数(在这种情况下,APickup())用于设置属性默认值,就像你在常规 C++类中做的那样;更重要的是,你将使用它来创建和添加组件到 Actor 本身。

接下来,你会找到一些声明,例如UCLASS()GENERATED_BODY()UPROPERTY()UFUNCTION(),这些是由 UE 使用的,并且每个都有精确的功能。如果你想知道名为BlueprintReadOnlyVisibleAnywhere等属性的含义,不要害怕!我将在第五章中解释它们的含义,在多人游戏环境中管理 Actor

在下一个子节中,我将向你展示它们的精确含义。

Unreal Engine 反射系统

术语反射指定了允许程序在运行时检查其自身结构的特性。这个特性非常有价值,并作为 UE 的核心技术之一,支持编辑器中的详细面板、序列化、垃圾回收、网络复制以及蓝图与 C++之间的通信等众多系统。

由于 C++没有对反射的原生支持,Epic Games 为其在 UE 中开发了自己的系统,用于收集、检查和修改与 C++类、结构等相关数据。

为了使用反射,你需要使用注释(如UCLASS()UFUNCTION()UPROPERTIES())来标记任何你希望使系统可见的类型或属性。

为了启用这些注释,你将使用我在上一个子节中介绍的#include "APickup.generated.h"声明(当你使用 Unreal Class Wizard 创建类时,此声明会自动生成,因此你不必担心它)。

以下列表提供了在反射系统中可访问的基本标记元素:

  • UCLASS(): 用于为需要从UObject派生的类生成反射数据

  • USTRUCT(): 用于为结构生成反射数据

  • GENERATED_BODY(): 此标记将被替换为所需的所有类型样板代码

  • UPROPERTY(): 用于通知引擎,关联的成员变量将具有一些附加功能,例如蓝图可访问性或跨网络复制(这对你来说在以后会非常重要!)

  • UFUNCTION(): 允许(以及其他事情)从扩展的 Blueprint 类或从 Blueprint 本身覆盖此函数

反射系统也被垃圾回收器使用,所以你不必担心内存管理,正如你将在下一小节中看到的。

内存管理和垃圾回收

在游戏级别中有成千上万个(有时是数万个!)活跃对象时,GC 是编程的一个基本部分。它就像你运行代码的清洁工——通过自动收集和处置不再需要的对象,帮助保持一切整洁有序。这是一个确保你的程序运行顺畅、没有内存泄漏或性能问题的好方法,这样你就可以专注于创建令人惊叹的功能。

C++ 没有原生实现垃圾回收(GC),因此 UE 实现了自己的系统:你只需确保对象的合法引用得到维护。为了使你的类启用 GC,你需要确保它们继承自 UObject;然后系统会保留一个对象列表(也称为 root),这些对象不应该被垃圾回收。只要对象在 root 列表中,它就不会被删除;一旦它从列表中移除,它将在下一次垃圾回收器被调用时(即,在特定间隔)从内存中删除。

注意

除非你调用它们的 Destroy() 方法,否则演员仅在级别关闭时被销毁:在这种情况下,它们将立即从游戏中移除并由垃圾回收器删除。

在本节中,我向你介绍了区分 Unreal Engine 项目和常规 C++ 项目的核心功能。在下一节中,你将通过创建空白项目并扩展主要的 Unreal GF 类来开始应用这些知识。

开始你的 Unreal 多人游戏项目

在本节中,你终于要开始开发多人游戏项目了(我知道你迫不及待地想要开始它!)!你将创建一个 Unreal C++ 空白项目,并添加我提供的已打包的资产。然后,你将创建管理多人会话所需的 GF 类。那么,让我们开始吧。

创建你的项目文件

让我们先创建一个空白项目:

  1. 打开 Epic Games Launcher 并启动 Unreal 编辑器。

  2. Games 部分选择 Blank 模板。

  3. Project Defaults 中,选择 C++ 作为项目类型。

  4. 确保未选中 Starter Content 字段,因为你不会使用它。

  5. 给项目一个有意义的名称(例如,UnrealShadows_LOTL)。

  6. 点击 Create 按钮。

  7. 创建 UE 项目后,获取你在本章开头下载的 UnrealShadows-StarterContent.zip 文件,并将其解压到你的电脑上。

  8. 导航到你的项目 Content 文件夹,位于 [Your Project Path] | UnrealShadows_LOTL | Content

  9. 将解压文件的文件内容(_ExternalActors_BlueprintsKayKitMaps 文件夹)复制到 Content 文件夹中,以将所有需要的资产添加到你的项目中。

一旦复制了文件,它们应该出现在 UE 编辑器中,并在你的项目中可用。如果文件没有在内容浏览器中弹出,只需关闭编辑器并重新打开,让 UE 更新 Content 文件夹。

你会注意到我已经添加了两个级别(Maps 文件夹:这些级别将在本书中使用,并且为了便于开发而创建。你可以自由创建自己的地图或添加额外的资产,这些资产可以位于 内容 | KayKit | Dungeon Elements)。

只是为了确认一切如预期进行,打开 Level_01 地图,你应该能看到 图 4**.3 中显示的关卡:

图 4.3 – Level 01 地图

图 4.3 – Level 01 地图

是时候添加任何 UE5 项目中使用的某些主要类了,这些类扩展了 GF 元素。

创建项目游戏实例

如你可能已经知道的,在 UE5 中,GameInstance 是一个负责管理需要在关卡变化或游戏会话之间持久化的高级数据的类。它本质上是一个全局可访问的 UObject,可以存储你想要在整个游戏中保持的数据,例如玩家得分,以及其他需要在不同关卡或游戏会话之间共享的信息。

可以创建一个扩展 GameInstance 的类作为蓝图类或 C++,它在游戏启动时实例化,并且仅在游戏关闭时销毁。

重要提示

与大多数 C++ Unreal Engine 项目一样,你将使用 C++ 类和蓝图混合工作。C++ 类位于 所有 | C++ 类 | UnrealShadows_LOTL 文件夹中,并且只能添加到那里(或子文件夹中)。如果你找不到这个文件夹,你可能创建了一个仅蓝图的项目。不要绝望,一旦创建了第一个 C++(稍后将有更多介绍),Unreal Engine 编辑器将负责将其转换为 C++ 项目,一切都将就绪!

要创建你的项目 GameInstance,请按照以下步骤操作:

  1. 在主菜单中,选择 工具 | 新建 C++ 类...

  2. 游戏实例

  3. 选择 GameInstance 类,如图 图 4**.4

图 4.4 – 类创建向导

图 4.4 – 类创建向导

  1. 点击 下一步 进入 命名新游戏 实例 面板。

  2. US_GameInstance。你可以保留其他字段不变,这样面板看起来就像 图 4**.5

图 4.5 – 命名你的类

图 4.5 – 命名你的类

  1. 点击 创建类 按钮以生成你的类文件。

注意

我将在项目中扩展主要 GF 元素的大部分类中使用 US_ 前缀:这只是一个 UnrealShadows 的简称,将使我们能够看到这些文件来自我们的项目。

一旦创建过程结束,您将获得两个新文件:US_GameInstance.hUS_GameInstance.cpp。恭喜您——您刚刚创建了您的第一个 Unreal C++ 类!

打开头文件,您将看到以下代码:

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "US_GameInstance.generated.h"
/**
 *
*/
UCLASS()
class UNREALSHADOWS_LOTL_API UUSGameInstance : public UGameInstance
{
  GENERATED_BODY()
};

源文件将保持为空,除了头文件中的 #``include 声明:

// Fill out your copyright notice in the Description page of Project Settings.
#include "US_GameInstance.h"

如您所见,这个类扩展了基础 GameInstance 类(即 UGameInstance),目前除了在上一节中引入的宏声明之外,它没有任何功能。然而,随着项目的进展,将添加新的功能,例如数据收集或在线服务管理。

注意

如果您将项目命名为不同于我的名称(例如,UnrealShadows_LOTL),您将在类声明中获得不同的 API 导出名称;正如您从前面的代码中看到的,我的名称是 UNREALSHADOWS_LOTL_API。请记住这一点,因为我的代码将引用此名称,您可能会遇到编译错误。为了修复这个问题,您应该将 UNREALSHADOWS_LOTL_API 文本更改为 YOUR_PROJECT_NAME(全部大写),并带有 _API 后缀。

这个游戏实例需要添加到项目设置中,以便在运行时被实例化。为此,请按照以下步骤操作:

  1. 从主菜单打开 项目设置。然后点击 项目 | 地图与模式 部分。

  2. 游戏实例类 下拉菜单中,选择 UG_GameInstance,如图 4**.6 所示:

图 4.6 – 分配给项目的游戏实例

图 4.6 – 分配给项目的游戏实例

现在我们已经将游戏实例分配给了项目,是时候为菜单和大厅关卡创建一个游戏模式了。

创建游戏模式和游戏状态

在 UE 中,游戏模式 是一个控制游戏规则的类,例如玩家如何加入游戏,如何在不同关卡之间过渡,以及其他游戏特定设置。游戏模式通常与 游戏状态 类配对,该类管理游戏的当前状态,例如得分、剩余时间和其他重要信息。游戏模式和游戏状态类一起允许开发者创建复杂和定制的游戏机制。

如果您检查您的 C++ Classes 文件夹,您会注意到已经有一个名为 UnrealShadowsLOTLGameModeBase 的游戏模式(如果您的项目名称与我的不同,名称可能会有所不同)。这是一个从 GF 自动生成的类,扩展了 AGameModeBase

你将不会使用这个,因为你需要从AGameMode创建一个类;这个类通过添加一些增强多人游戏系统的功能来扩展AGameModeBase,例如游戏规则和胜负条件。要使用自己的设置扩展游戏模式,请按照以下步骤操作:

  1. 创建一个与之前章节中为游戏实例创建的类相同的新的 C++类。

  2. 所有类部分选择GameMode并点击下一步

  3. 将你的类命名为US_GameMode并点击创建类按钮。

  4. 一旦创建了类,就需要将其设置为所有级别的默认游戏模式。为此,打开项目设置并选择地图与模式部分。

  5. 然后,点击默认游戏模式下拉菜单并选择US_GameMode,如图 4.7 所示:

图 4.7 – 默认游戏模式

图 4.7 – 默认游戏模式

  1. 关闭项目设置窗口。

现在游戏模式已经定义,是时候创建一个游戏状态了:

  1. 创建另一个从GameState(来自所有类部分)扩展的 C++类。

  2. 在点击US_GameState后,点击创建类按钮。

  3. 现在,通过在 Unreal 编辑器的C++类文件夹中双击相应的图标来声明US_GameMode文件。这将打开 IDE 内的头文件和源文件。

  4. US_GameMode.h中声明一个构造函数,添加以下两行代码:

    public:
    
       AUS_GameMode();
    
  5. US_GameMode.cpp中实现构造函数,添加以下代码:

    #include "US_GameState.h"
    
    AUS_GameMode::AUS_GameMode()
    
    {
    
       GameStateClass = AUS_GameState::StaticClass();
    
    }
    

之前的代码实际上声明了US_GameMode的 Game State 类为之前创建的US_GameState。请注意,此声明也可以在子蓝图中进行;这将允许在编辑器中通过下拉菜单切换类。最终,这取决于个人喜好,你可能更倾向于代码导向,并偏好代码解决方案,或者你可能想利用原生代码和编辑器之间令人印象深刻的交互。

在本节中,你已创建了系统将用于处理多人会话的主要类。目前,这些只是等待添加一些游戏逻辑的空容器;请耐心等待,我们还有许多章节要填补这个空白!

在下一节中,你将创建处理游戏中的角色输入和存在的类。

添加玩家类

现在,你已经准备好创建一些几乎任何 UE 游戏都会使用的主要类:那些用于管理玩家输入并在游戏中显示角色及其状态的类。

玩家控制器负责管理来自玩家输入设备(如键盘和鼠标)的输入,并向玩家的角色发送命令,使其执行相应的动作。玩家控制器类通常用于控制Pawn类或Character类,这些类代表游戏中玩家的角色。

最后,玩家状态 是一个包含有关玩家游戏状态信息的类,例如经验点、分数和其他相关数据。它在服务器和客户端上存在,以确保所有玩家都能访问相同的信息。

让我们创建这三个类:

  1. 创建一个扩展 PlayerController 的 C++ 类,并将其命名为 US_PlayerController

  2. 创建另一个扩展 Character 的 C++ 类,并将其命名为 US_Character

  3. 最后,创建一个扩展 PlayerState 的 C++ 类,并将其命名为 US_PlayerState

这三个类应该像添加游戏状态一样添加到游戏模式中,但为了给我们的 Character 类提供更多灵活性,你将从中创建一个 Blueprint。要从新创建的 C++ 类中获取 Blueprint,你需要编译项目。

你现在将第一次编译你的源代码,以检查一切是否已正确设置。

编译你的源代码

在 Unreal Engine 项目中编译是指将用 C++ 编写的可读代码转换为计算机可以理解和运行的可执行代码的过程 – 这是开发过程中的一个重要步骤。UE 提供了工具来简化编译过程并提高开发体验。

在 Unreal Engine 中,你可以利用 Live Coding 功能,该功能允许在 UE 引擎运行时重建应用程序的 C++ 代码并修补其二进制文件。

使用 Live Coding,你可以修改 C++ 类,编译它们,并在编辑器运行时观察更改生效 – 所有这些都不需要中断游戏测试会话或正在进行的工作。这个特性为迭代开发带来了巨大的优势,尤其是在使用 C++ 运行时逻辑,如游戏代码或前端用户交互时。

Live Coding 默认启用,当使用 IDE 或 Unreal Engine 时,你可以通过按键盘上的 Ctrl + Alt + F11 来启动 Live Coding 构建。

或者,要禁用 Live Coding 并开始编译过程,你可以点击 Unreal Engine 编辑器右下角的 编译 按钮,如图 图 4.8 所示:

图 4.8 – 编译按钮

图 4.8 – 编译按钮

一旦源代码编译完成,你应该会收到成功消息。否则,你将收到失败的编译产生的通常错误或警告;在这种情况下,确保你的代码编写正确且无错误至关重要。

一旦你的项目成功编译,就是时候从你的 Character 类创建一个 Blueprint 了。

创建角色 Blueprint 类

由于你将在稍后对角色进行一些定制,因此拥有由 Blueprint 类提供的额外灵活性是至关重要的。

由于您已成功编译代码,您可能会期望您创建的类现在将准备好在蓝图创建向导中可用。这是一个正确的假设,您现在将测试它。让我们这样做:

  1. 导航到内容 | 蓝图文件夹。

  2. 创建一个继承自US_Character的蓝图类,命名为BP_Character

  3. 保存并关闭蓝图:您现在不会对它做任何事情。

这个新的蓝图应该被添加到游戏模式中,作为游戏会话期间使用的默认 Pawn。不幸的是,蓝图类不能直接在 C++类中引用。这意味着您必须通过ConstructorHelpers实用类中可用的FClassFinder方法来找到它。

将玩家类添加到游戏模式

您现在将声明新创建的类到游戏模式中。让我们再次打开US_GameMode.cpp文件并添加一些代码逻辑。在声明部分,添加以下代码块:

#include "US_PlayerController.h"
#include "US_PlayerState.h"
#include "US_Character.h"
#include "UObject/ConstructorHelpers.h"

这将声明您将要声明的所有 GF 类以及ConstructorHelpers实用类。

然后,在构造函数的闭合括号之前,添加以下代码块:

PlayerStateClass = AUS_PlayerState::StaticClass();
PlayerControllerClass = AUS_PlayerController::StaticClass();
static ConstructorHelpers::FClassFinder<APawn> PlayerPawnBPClass(TEXT("/Game/Blueprints/BP_Character"));
if (PlayerPawnBPClass.Class != nullptr)
{
   DefaultPawnClass = PlayerPawnBPClass.Class;
}

如您所见,代码的前两行以与您在上一节中为GameStateClass所做的类似方式声明了PlayerStateClassPlayerControllerClass

同时,从 C++类中检索蓝图引用(例如PlayerPawnBPClass)的方式与常规 C++类不同:您需要硬编码一个路径到您的项目。这可能不是一个理想的解决方案,因为文件可能会被移动或删除,但...它确实有效!

请记住,我的文件路径(即"/Game/Blueprints/BP_Character")可能因您的文件夹组织方式而略有不同。

现在游戏模式类已经被修改,点击 Unreal 编辑器中的编译按钮。

一旦得到成功的结果,就是时候查看游戏模式实例,以确保一切正确。为此,请按照以下步骤操作:

  1. 打开项目设置 | 地图与模式部分。

  2. 定位到选定的游戏模式字段,通过点击其旁边的箭头将其展开。

  3. 检查我们创建的 GF 类是否都正确分配,如图图 4.9所示:

图 4.9 – 更新的默认游戏模式

图 4.9 – 更新的默认游戏模式

在本节最后,您已经通过添加下一章中将要扩展的所有 GF 类,完成了游戏模式的设置。

摘要

在本章中,您被简要介绍了您将在本书的其余部分开发的项目:一款涉及盗贼、秘密宝藏和大量不死小弟的多人潜行游戏。道路仍然漫长,但必须阻止巫妖王!

之后,你了解了 Unreal Engine C++“方言”的主要主题。涉及了许多额外的功能,书中剩余部分还将发现更多。其中最令人兴奋的事情之一是,如果设置得当,你不必担心内存管理:Unreal Engine 完美地处理它。更重要的是,通过向你的类、变量和函数添加装饰,你可以将它们暴露给 Blueprint 系统,让你的项目对非代码导向的开发者更加灵活和易于访问。

最后,你创建了将用于你的游戏的主要类,这些类扩展了 GF 提供的类。从持久的 GameInstance 开始,你进入了 Game Mode,然后是所有面向玩家的元素。你现在有一个坚实的基础来开始开发你的多人游戏项目。

在下一章中,我将通过展示如何在多人环境中管理它来引导你创建玩家角色。

第五章:在多人环境中管理 Actor

在 UE5 中正确设置多人环境时,了解 Actor 的连接管理方式以及其属性在游戏会话中的相关性非常重要。

在本章中,您将开始增强玩家角色(目前只是一个空壳),以全面理解之前提到的概念。为此,您将为 Character 类添加更多组件(相机是您绝对需要的!)并实现玩家输入逻辑。

此外,您还将了解在多人环境中知道谁拥有 Actor 的重要性,以及它如何根据其在关卡中的相关性而表现不同。

到本章结束时,您将具备如何管理 Unreal 多人游戏中的 Actor 的扎实知识,这将使您能够创建更稳健和高效的多人体验。

因此,在本章中,我将向您介绍以下主题:

  • 设置角色

  • 控制 Actor 的连接

  • 理解 Actor 的相关性

  • 介绍权限

技术要求

要跟随本章介绍的主题,您应该已完成上一章,并理解其内容。

此外,如果您希望从本书的配套仓库开始编写代码,您可以下载.zip项目文件,链接为github.com/PacktPublishing/Multiplayer-Game-Development-with-Unreal-Engine-5

您可以通过点击Unreal Shadows – 第四章 结束链接下载与上一章结尾同步的文件。

设置角色

在我开始撰写关于连接、权限和角色等主题之前,我需要您正确设置玩家角色——目前,我们可怜的英雄只是一个空类!

因此,在本节中,您将添加一个相机和一些用户输入,并设置允许盗贼角色在关卡中移动以寻找宝藏和金子的主要功能!

为角色添加基本设置

在接下来的几个步骤中,您将添加构成第三人称相机行为的组件,并实现它们的逻辑。之后,您将为 Character 类中已存在的组件设置一些默认值:箭头、胶囊和骨骼网格组件。

为角色添加相机组件

要开始,打开US_Character.h头文件。您将添加一个相机组件和一个弹簧组件,将相机连接到 Character 类中可用的胶囊组件。为此,在GENERATED_BODY()宏之后添加这两个组件声明:

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
TObjectPtr<class USpringArmComponent> CameraBoom;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UCameraComponent> FollowCamera;

在前面的代码块中,我们正在声明一个相机组件和一个弹簧组件,这将创建相机系统。首先,你会注意到两个变量上都有UPROPERTY()声明和一些属性指定符。让我来解释一下:

  • VisibleAnywhere属性表示这个属性在所有相关的虚幻引擎编辑器窗口中都是可见的,但不能被编辑

  • BlueprintReadOnly属性表示这个属性可以被蓝图读取但不能修改

  • Category属性指定了当在蓝图****详情面板中显示属性时的类别

你还会注意到一个meta声明,它让你可以控制属性如何与虚幻引擎和编辑器的各个方面交互:在这种情况下,AllowPrivateAccess表示私有成员应该可以从蓝图访问。我们需要这个声明,因为这些属性的访问性没有明确声明,因此它们默认为private

注意

对于属性指定符的详尽列表,请查看官方 Epic Games 文档,可以在以下链接找到:docs.unrealengine.com/5.1/en-US/unreal-engine-uproperty-specifiers/

接下来,看看类型之前的class关键字——这是一个 C++的类前向声明。如果你不熟悉这个概念,它是一种在不提供完整类定义的情况下声明类名及其成员的方法。这在你想在头文件中使用一个类但不想包含整个类定义的情况下很有用,这可能会使编译变慢并创建不必要的依赖。

最后,你会注意到TObjectPtr<T>模板——这是 UE5 中的一个新特性,它被引入来替换头文件中的原始指针(例如,USpringComponent*)与 UProperties。TObjectPtr<T>模板仅适用于在代码头文件中声明的成员属性。对于.cpp文件中的函数和短生命周期范围,使用TObjectPtr<T>与使用原始指针相比没有提供额外的优势。

由于相机和弹簧组件是私有的,你需要为它们添加两个 getter 方法。在头文件的public声明中,找到以下代码行:

virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;

然后,在这行代码下面添加以下内容:

FORCEINLINE USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
FORCEINLINE UCameraComponent* GetFollowCamera() const { return FollowCamera; }

这两种方法将允许你访问指针组件,并且FORCEINLINE宏强制代码内联;这将给你的代码带来一些性能优势,因为当你使用这种方法时,你将避免函数调用。

实现相机行为

现在你的属性已经添加好了,是时候添加一些代码逻辑来处理它们了。打开.cpp文件,并在其顶部添加以下包含:

#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/SpringArmComponent.h"

然后,在构造函数(即AUS_Character::AUS_Character())中添加以下代码:

CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->TargetArmLength = 800.0f;
CameraBoom->bUsePawnControlRotation = true;

在这里,CreateDefaultSubobject()<T>是一个用于创建一个新子对象的功能,该子对象将由另一个对象拥有。子对象本质上是一个对象的组件或成员变量,该方法通常在对象的构造函数中调用以初始化其子对象(在这种情况下,是组件)。

SetupAttachment()方法会将一个组件重新父化到另一个组件。在这种情况下,你将相机组件附加到RootComponent,实际上它是胶囊组件。

让我们给相机同样的处理。在之前的代码行之后添加此代码块:

FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
FollowCamera->bUsePawnControlRotation = false;

这里的唯一真正区别是,你将相机重新父化到弹簧组件而不是根组件。

你刚刚创建了一种“命令链”,其中相机连接到与 Actor 根连接的弹簧组件——这将在相机撞击障碍物时让相机以“弹簧”行为跟随角色,并为玩家提供更好的感觉。

设置默认组件属性

作为最后一步,你将修改一些属性以创建 Character 类的默认设置。在构造函数中添加以下代码行:

bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
GetCapsuleComponent()->InitCapsuleSize(60.f, 96.0f);
GetMesh()->SetRelativeLocation(FVector(0.f, 0.f, -91.f));
static ConstructorHelpers::FObjectFinder<USkeletalMesh> SkeletalMeshAsset(TEXT("/Game/KayKit/Characters/rogue"));
if (SkeletalMeshAsset.Succeeded())
{
   GetMesh()->SetSkeletalMesh(SkeletalMeshAsset.Object);
}
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f);
GetCharacterMovement()->MaxWalkSpeed = 500.f;
GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;

在这里,你只是更改了角色 Actor 及其组件的一些默认值。需要注意的是,你通过FObjectFinder()实用方法(在之前使用的ConstructorHelpers类中可用)获取角色模型并将其分配给SkeletalMeshComponent

更新角色蓝图

现在是时候编译你的项目了,只是为了检查你没有语法错误,并且角色已经正确设置。为此,保存你的文件,回到 Unreal 编辑器,并点击编译按钮。

一旦编译阶段完成,打开BP_Character蓝图,你应该注意到你的更改没有显示出来。这是因为蓝图尚未更新。要修复这个问题,选择文件 | 刷新所有节点。现在你应该在组件面板中看到添加到层次结构中的相机吊杆跟随相机元素,如图图 5.1所示:

图 5.1 – 新增的角色组件

图 5.1 – 新增的角色组件

你可能仍然看不到更新后的网格在骨骼网格组件中。要修复这个问题,请执行以下步骤:

  1. 组件面板中选择网格元素,并在细节面板中查找骨骼网格资产字段。

  2. 如果骨骼网格资产显示为None,点击它旁边的重置属性箭头,如图图 5.2所示:

图 5.2 – 重置属性按钮

图 5.2 – 重置属性按钮

  1. 仔细检查使用控制器旋转偏航 Y属性,因为它可能也需要重置。

现在,你应该能够看到视口已经更新,并添加了选定的网格,如图 图 5**.3 所示:

图 5.3 – 更新的角色蓝图

图 5.3 – 更新的角色蓝图

当你的角色设置完成后,就是时候通过添加一些用户交互来让它动起来了。

为角色添加交互

现在我们将为角色添加输入设置。为此,我们将使用 UE5 中引入的新 增强输入系统。这个新系统为开发者提供了比之前一个(简单地称为输入系统)更高级的功能,例如复杂的输入处理和运行时控制重映射。由于旧系统正在被弃用,它可能迟早会被从 Unreal Engine 中移除,因此最好保持对变化的关注。

关于增强输入系统,最重要的是了解它与你的代码如何通信:这是通过表示角色在游戏过程中可以做什么的 输入动作 来实现的(即,行走、跳跃或攻击)。一组输入动作可以收集在一个 输入映射上下文 中,它代表了一组触发包含动作的规则。

在运行时,UE5 将检查一个输入触发器列表,以确定用户输入如何激活输入动作,验证如长按、释放事件或双击等模式。在触发输入之前,系统可以通过一系列 Input Modifiers 预处理原始输入,这些修改器将改变数据,例如为摇杆设置自定义的死区或从输入本身获取负值。

在本节中,你将为你的角色创建一些基本的交互,包括移动、冲刺和与物体交互(我们将攻击动作留到后面的章节)。冲刺和交互动作将通过按下一个按钮来激活,而移动动作将由键盘/鼠标组合或控制器摇杆控制。

注意

如果你想要探索增强输入系统提供的全部可能性,你可以通过访问这个网页来查看官方文档:docs.unrealengine.com/5.1/en-US/enhanced-input-in-unreal-engine/

创建输入动作

要开始创建输入动作,请按照以下步骤操作:

  1. 打开你的 Input

  2. 在文件夹内,右键单击并选择 Input | Input Action 来创建一个输入动作资产。

  3. 将其命名为 IA_Interact

  4. 创建另外三个输入动作,并将它们命名为 IA_LookIA_MoveIA_Sprint

让我们开始编辑 IA_Interact 动作 – 我们需要它通过单次按钮(或按键)按下激活,并且这个动作应该在按钮按下的一瞬间被触发。为此,双击资产以打开它,并执行以下操作:

  1. 点击 Triggers 字段旁边的 + 按钮,添加一个触发器。

  2. 点击创建的下拉菜单并选择 按下 – 此选项将避免在玩家按住按钮时触发多个事件。

  3. 其他保持不变 - 只需确保 值类型 已设置为默认值 数字(bool)

交互动作资产的最终结果如图 图 5**.4 所示:

图 5.4 – 交互动作设置

图 5.4 – 交互动作设置

IA_Sprint 动作与交互动作非常相似,但需要在角色开始冲刺时触发一个按下事件,在角色停止冲刺时触发一个释放事件。

双击 IA_Sprint 资产以打开它并按以下说明更改设置:

  1. 通过点击 Triggers 字段旁边的 + 按钮两次添加两个触发器。

  2. 点击创建的第一个下拉菜单并选择 按下

  3. 点击第二个下拉菜单并选择 已释放

  4. 其他保持不变,确保 值类型 已设置为默认值 数字(bool)

冲刺动作资产的最终结果如图 图 5**.5 所示:

图 5.5 – 冲刺动作设置

图 5.5 – 冲刺动作设置

是时候设置 IA_Move 资产了,所以打开它并将 值类型 更改为如图 图 5**.6 所示:

图 5.6 – 移动动作设置

图 5.6 – 移动动作设置

作为最后一步,打开 AI_Look 资产并将 值类型 更改为 Axis2D (Vector2D),如图 图 5**.7 所示:

图 5.7 – 查看动作设置

图 5.7 – 查看动作设置

现在基本动作已经定义,是时候创建映射上下文并设置其属性了。

设置输入映射上下文

如前所述,映射上下文指的是一组输入动作,它标识玩家可能遇到的具体情况。这里需要创建的是角色可以执行的基本动作(移动、环顾四周和交互),因此是时候打开 内容浏览器 并创建此资产:

  1. 右键单击 Input 文件夹并选择 Input | Input Mapping Context

  2. 将新创建的资产命名为 IMC_Default 并双击它以开始编辑。

  3. 点击 Mappings 字段旁边的 + 按钮。在下拉菜单中,选择 IA_Interact

  4. 重复此步骤三次以添加 IA_SprintIA_MoveIA_Look

到这些步骤结束时,你应该会有类似于 图 5**.8 的内容:

图 5.8 – 输入映射上下文面板

图 5.8 – 输入映射上下文面板

现在上下文已经创建,是时候映射玩家将使用的输入了。正如我之前所述,我们将允许他们使用控制器或键盘和鼠标交互的混合。目前,所有映射都应该设置为;这意味着没有输入将通过此上下文。

让我们通过从IA_Interact映射开始解决这个问题:

  1. 点击IA_Interact下面的键盘图标,并按下键盘上的I(交互)键。

  2. 然后点击IA_Interact字段右侧的+按钮以添加另一个映射。

  3. 从下拉菜单中选择游戏手柄 | 游戏手柄面按钮底部。或者,如果你有一个连接到你的 PC 的游戏控制器,你可以简单地点击键盘图标,然后按下相应的按钮(例如,Xbox 控制器的A按钮)。

现在我们将设置IA_Sprint的映射:

  1. 如果玩家使用键盘,则设置左 Shift

  2. 如果玩家使用控制器,则设置游戏手柄 | 游戏手柄左摇杆按钮(此第二个选项将允许玩家按下摇杆以冲刺)。

接下来,IA_Move将允许玩家使用左摇杆控制器或常用的 WASD 键——这意味着你需要添加五个交互:一个用于摇杆,然后是上、下、左和右方向的四个交互。让我们将它们添加到映射上下文中,从摇杆设置开始:

  1. 游戏手柄 | 游戏手柄左摇杆 2D 轴添加到映射中。此外,从修饰符列表中添加一个具有值Dead Zone的修饰符。

  2. 接下来是方向,对于右方向(在键盘上映射为D),添加一个没有修饰符的键盘 | D 映射。

  3. 对于左方向(在键盘上映射为A),添加一个键盘 | A 映射。然后添加一个具有值Negate的修饰符。这将从这个交互中赋予负值(即,向右移动是正值,而向左移动是负值)。

  4. 对于前进方向(在键盘上映射为W),添加一个键盘 | W 映射。然后,添加一个具有值Swizzle Input Axis Values的修饰符,这将把x值转换为y(反之亦然),因此你会为你的角色获得一个“前进”值。

  5. 最后,对于后退方向(在键盘上映射为S),添加一个键盘 | S 映射。然后,添加一个具有值Swizzle Input Axis Values的修饰符和一个具有值Negate的附加修饰符。这将以与A键中解释的左移动类似的方式,从这个交互中赋予负值。

最后,IA_Look映射将由控制器的右摇杆或鼠标的移动控制。要添加这些设置,请执行以下步骤:

  1. 从下拉菜单中选择控制器为 Gamepad | Gamepad Right Thumbstick 2D-Axis。此外,从 Modifiers 列表中添加一个值为 Dead Zone 的修饰符,这样摇杆在静止位置时不会发送数据。

  2. 为鼠标选择 Mouse | Mouse XY 2D-Axis 交互。然后从 Modifiers 列表中添加一个值为 Negate 的修饰符,并取消选择 XZ 复选框,只保留 Y 值被选中。这将赋予鼠标交互负值 – 例如,向前移动将使角色向下移动相机,向后移动将使相机向上移动。

你现在应该有一个类似于 图 5.9 中所示的映射上下文:

图 5.9 – 完整的映射上下文

图 5.9 – 完整的映射上下文

现在映射上下文已经定义好了,是时候设置角色以便它能接收来自玩家的输入了。

导入增强型输入模块

让我们回到 IDE,因为你现在已经准备好为你的角色添加一些组件和代码逻辑了。由于我们使用的是增强型输入系统,你首先需要做的是将它添加到 Build.cs 文件中的模块声明中。

要这样做,打开你的 C++ 项目的 Source 文件夹中的 UnrealShadows_LOTL.Build.cs 文件(如果你的项目名称不同,名称可能会有所不同)。然后找到以下代码行:

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });

通过添加增强型输入模块来更改它:

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput" });

这将使增强型输入模块可用于你的项目,你将准备好开始实现用户交互,这正是你现在要做的。

向角色添加用户交互

要向角色添加用户交互,你需要声明你刚刚创建的增强型输入资产。

在接下来的步骤中,你将声明映射上下文和动作引用到你的代码中,以及相应的函数。之后,你将实现处理所有动作所需的代码逻辑。最后,你将在角色蓝图内部声明这些动作。

声明输入属性和函数

首先要做的是为映射上下文和要添加到 US_Character.h 头文件中的动作添加所需的资产引用。打开头文件,它应该已经包含了以下代码行(如果没有,则添加为公共声明):

virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;

接下来,声明一个指向输入映射上下文的指针以及每个输入动作的指针。为此,在类的隐式 private 部分中(即 GENERATED_BODY() 宏之后,组件声明之后)添加以下代码:

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input", meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UInputMappingContext> DefaultMappingContext;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input", meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UInputAction> MoveAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UInputAction> LookAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UInputAction> SprintAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UInputAction> InteractAction;

作为头声明步骤的最后一步,在 protected 部分中,在 BeginPlay() 方法声明之后添加以下方法:

void Move(const struct FInputActionValue& Value);
void Look(const FInputActionValue& Value);
void SprintStart(const FInputActionValue& Value);
void SprintEnd(const FInputActionValue& Value);
void Interact(const FInputActionValue& Value);

如你所见,你为之前定义的每个交互添加了一个方法。只需记住,在 SprintStart()SprintEnd() 中。

实现角色的映射上下文

在接下来的步骤中,你将通过初始化它并将每个输入动作绑定到相应的方法来实现映射上下文。

打开US_Character.ccp并添加以下代码块,其中包含了你在接下来的步骤中将使用的所有类:

#include "Components/InputComponent.h"
#include "GameFramework/Controller.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"

然后,查找BeginPlay()方法,并在Super声明之后添加此代码块:

if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
{
   if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
   {
      Subsystem->AddMappingContext(DefaultMappingContext, 0);
   }
}

第一行代码通过Cast<T>模板检查控制器是否为PlayerController

注意

在 Unreal Engine 中工作,频繁地对特定类进行转换是很常见的(就像你在之前的章节中用蓝图所做的那样)。你可能已经习惯了纯 C++中的转换,但你应该知道 Unreal 的行为略有不同,因为它可以安全地将类型转换为可能不合法的类型。如果你习惯于在这种情况下出现常规 C++崩溃,你可能会很高兴地知道 Unreal 将简单地返回一个更安全的nullptr

然后,代码将尝试从玩家那里获取增强输入子系统,如果成功,将映射上下文添加到它上面。从这一点开始,上下文中声明的所有动作都将由输入系统“跟踪”。

当然,你需要将这些动作绑定到相应的方法实现(即移动、冲刺、交互等)。为此,查找SetupPlayerInputComponent()方法,并在Super()声明之后添加此代码块:

if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
   EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AUS_Character::Move);
   EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AUS_Character::Look);
   EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &AUS_Character::Interact);
   EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Started, this, &AUS_Character::SprintStart);
   EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Completed, this, &AUS_Character::SprintEnd);
}

如你所见,我们正在调用输入组件指针上的BindAction()方法来将每个动作绑定到相应的方法。

实现动作

现在你已经准备好实现每个动作的方法。让我们从Move方法开始。添加以下代码块:

void AUS_Character::Move(const FInputActionValue& Value)
{
   const auto MovementVector = Value.Get<FVector2D>();
   GEngine->AddOnScreenDebugMessage(0, 5.f, FColor::Yellow, FString::Printf(TEXT("MovementVector: %s"), *MovementVector.ToString()));
   if (Controller != nullptr)
   {
      const auto Rotation = Controller->GetControlRotation();
      const FRotator YawRotation(0, Rotation.Yaw, 0);
      const auto ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
      const auto RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
     AddMovementInput(ForwardDirection, MovementVector.Y);
      AddMovementInput(RightDirection, MovementVector.X);
   }
}

如你所见,这段代码首先做的事情是从Value参数获取一个二维向量。这个向量包含了左摇杆(或键盘)的xy方向,并指示角色应该移动的方向。我添加了一个屏幕上的消息来跟踪这个值。

接下来,如果有控制器拥有这个 Actor,我们将计算角色的前向和右向方向,并将其移动到相应的方向(如果你曾经尝试过 Unreal 第三人称模板,你应该已经熟悉这个过程)。

你接下来要实现的方法是Look(),所以就在Move()函数之后添加这些行:

void AUS_Character::Look(const FInputActionValue& Value)
{
   const auto LookAxisVector = Value.Get<FVector2D>();
   GEngine->AddOnScreenDebugMessage(1, 5.f, FColor::Green, FString::Printf(TEXT("LookAxisVector: %s"), *LookAxisVector.ToString()));
   if (Controller != nullptr)
   {
      AddControllerYawInput(LookAxisVector.X);
      AddControllerPitchInput(LookAxisVector.Y);
   }
}

如你所见,我们正在从Value参数获取一个二维向量,这次它将来自右摇杆或鼠标。之后,我们向控制器添加偏航/俯仰;这将导致弹簧组件以及随之而来的相机组件围绕角色旋转。

对于冲刺动作,角色有两个可用方法——一个用于开始冲刺,另一个用于结束冲刺。在之前的函数之后添加此代码块:

void AUS_Character::SprintStart(const FInputActionValue& Value)
{
   GEngine->AddOnScreenDebugMessage(2, 5.f, FColor::Blue, TEXT("SprintStart"));
   GetCharacterMovement()->MaxWalkSpeed = 3000.f;
}
void AUS_Character::SprintEnd(const FInputActionValue& Value)
{
   GEngine->AddOnScreenDebugMessage(2, 5.f, FColor::Blue, TEXT("SprintEnd"));
   GetCharacterMovement()->MaxWalkSpeed = 500.f;
}

此代码简单地在角色冲刺时增加其最大速度。

注意

步行和冲刺的值是硬编码的;随着我们通过本书的进展,我们将在稍后从数据集中获取这些值。

你需要实现的最后一个方法是Interact(),但截至目前,我们还没有任何可以与之交互的东西!所以,你只需在函数内添加一个屏幕上的消息:

void AUS_Character::Interact(const FInputActionValue& Value)
{
   GEngine->AddOnScreenDebugMessage(3, 5.f, FColor::Red, TEXT("Interact"));
}

要使角色完全功能化,你需要做的最后一件事是将输入资产添加到蓝图。

更新角色蓝图

要更新蓝图,请执行以下步骤:

  1. 保存你修改的所有文件,并返回到 Unreal 编辑器。

  2. 点击编译按钮,等待成功消息。

  3. 打开你的BP_Character蓝图,并选择类****默认部分。

  4. 详细信息面板中搜索输入类别。你应该会得到默认映射上下文属性以及创建的四个动作。

  5. 点击默认映射上下文下拉按钮,并选择相应的资产(应该只有一个可供选择)。

  6. 对于每个动作属性,从下拉菜单中选择相应的动作资产。

前一步骤的结果在图 5.10中展示:

图 5.10 – 蓝图输入设置

图 5.10 – 蓝图输入设置

角色终于完成了!虽然很艰难,但你现在可以开始测试它了。

测试角色的移动

现在基础用户交互已经实现,是时候开始在可玩级别上测试它了。打开Level_01地图,并执行以下操作:

  1. 在级别中寻找SP 1(代表SpawnPoint 1)标签,并在其附近添加一个玩家开始演员。

  2. 网络模式设置为监听服务器,并包含3名玩家。

  3. 点击播放按钮来测试游戏。

你应该能够移动角色,并让他们冲刺和四处张望。

你可能想知道,尽管你没有添加任何多人游戏代码逻辑,你为什么已经玩起了网络游戏。答案在角色类中,它已经被设置为可复制的——只需打开true

你可能也注意到了,虽然服务器窗口中的角色移动和冲刺很顺畅,但在客户端窗口中,当你跑步时,动作看起来有点跳跃。这是因为在客户端尝试执行冲刺动作,但服务器实际上是控制者——结果,客户端会让角色移动得更快,但服务器会将其带回移动位置。基本上,目前我们正在尝试在客户端“作弊”,但权威的服务器会禁止你这样做。显然,这是我们代码中的错误,但我们仍然需要理解复制的完整含义以及如何从服务器执行函数。

要修复这个错误,你需要了解更多关于复制的知识。请耐心等待——我将在第六章通过网络复制属性 O**ver the Network,和第七章使用远程过程调用 Calls (RPCs)中提供更多关于这个主题的详细信息。

现在你已经从头开始创建了自己的英雄角色,是时候了解如何控制 Actor 连接了:我将在下一节介绍这个主题。

控制 Actors 的连接

现在你已经创建了一个完全工作的角色,是时候了解在虚幻引擎内部如何处理连接了(为了快速刷新你对连接工作原理的理解,你可以参考第二章理解 网络基础)。

每个连接都有自己的PlayerController,它是专门为该连接创建的;在这种情况下,我们说PlayerController“属于”该连接。

在虚幻引擎中,Actors 可以有一个PlayerController,然后PlayerController就成为了该 Actor 的所有者。这意味着第一个 Actor 也由拥有PlayerController的相同连接拥有。

在 Actor 复制过程中使用所有权的概念来确定哪些连接接收每个 Actor 的更新:例如,一个 Actor 可能会被标记,以便只有拥有该 Actor 的连接才会收到其属性更新。

例如,让我们想象一下你的盗贼角色(基本上是一个 Actor)被PlayerController控制——这个PlayerController将是角色的所有者。在游戏过程中,盗贼获得了一个可以赋予魔法匕首的拾取物品:一旦装备,这件武器将属于角色。这意味着PlayerController也将拥有这把匕首。最终,盗贼 Actor 和匕首都将由PlayerController连接拥有。一旦盗贼 Actor 不再被 Player Controller 控制,它将不再由连接拥有,武器也是如此。

如果你已经开发过独立游戏,你可能习惯于通过使用诸如Get Player ControllerGet Player Character(或它们对应的 C++版本,UGameplayStatics::GetPlayerController()UGameplayStatics::GetPlayerCharacter())这样的节点来检索玩家控制器或角色。如果你不知道自己在做什么,在网络环境中使用这些函数可能会导致许多问题,因为根据上下文,你将得到不同的结果。

例如,当Player Index等于0时调用Get Player Controller函数将给出以下结果:

  • 如果你是从监听服务器调用它,监听服务器的PlayerController

  • 如果你是从专用服务器调用它,第一个客户端的PlayerController

  • 如果你是从客户端调用它,客户端的PlayerController

如果事情看起来很混乱,考虑到索引在服务器和不同的客户端之间可能不一致,它们将变得更加混乱。

正因如此,当在 Unreal Engine 中开发多人游戏时,你很可能会使用以下一些函数(或它们相应的节点):

  • AActor::GetOwner(),它返回一个 Actor 实例的所有者

  • APawn::GetController(),它返回 Pawn 或角色实例的控制器

  • AController::GetPawn(),它返回控制器所拥有的 Pawn

  • APlayerState::GetPlayerController(),它将返回创建 Player State 实例的 Player Controller(远程客户端将返回 null 值)

关于组件,你应该知道它们有自己确定拥有连接的方式——它们将从遵循组件的外部链开始,直到找到拥有它们的 Actor。从那里开始,系统将像之前解释的那样继续确定该 Actor 的拥有连接。要获取组件的所有者,你将使用UActorComponent::GetOwner()方法。

在本节中,我们刚刚“触及”了所有者的概念以及如何获取有关它的信息,但你应该知道连接所有权非常重要,它将在本书的其余部分无处不在:换句话说,拥有连接的想法被认为足够重要,以至于将在我们正在开发的多玩家项目中得到处理。

在下一节中,我将介绍一个与连接所有权紧密相关的话题:相关性。

理解 Actor 相关性

相关性是确定场景中哪些对象应该根据其对玩家的重要性可见或更新的过程。这是 Unreal Engine 中的一个重要概念,通过理解它是如何工作的,你可以确保你的游戏运行得高效。在本节中,我们将探讨这个主题,并展示一个根据其设置如何工作的示例。

理解相关性

在 Unreal Engine 中,相关性这个术语指的是引擎如何根据 Actor 在游戏世界中的当前位置来确定应该将哪些 Actor 复制到哪些客户端,以及哪些 Actor 对玩家的当前视图或区域是相关的。

一个游戏关卡的大小可以从非常小到非常大不等。这可能会在更新网络上所有内容以及连接到服务器的每个客户端时引起问题。由于游戏角色可能不需要知道关卡中发生的每一件事,大多数时候,只需让它知道附近发生的事情就足够了。

因此,引擎使用几个因素来让玩家知道一个 Actor 上是否发生了变化:这些因素包括 Actor 本身的距离、其可见性以及 Actor 是否在游戏世界中当前处于活动状态。被认为无关紧要的 Actor 将不会被复制到玩家的客户端,这将减少网络流量并提高游戏性能。

Unreal 使用一个名为AActor::IsNetRelevantFor()的虚函数来测试演员的相关性。这个测试评估一组旨在提供对能够真正影响客户端的演员的可靠估计的属性。测试可以总结如下:

  • bAlwaysRelevant标志设置为true

  • 或者,它属于PawnPlayerController

  • 或者,它是Pawn对象

  • 或者,Pawn对象是诸如噪音或伤害等动作的发起者

  • bNetUseOwnerRelevancy属性设置为true,并且演员本身有一个所有者,所有者的相关性将被使用。* bOnlyRelevantToOwner属性设置为true并且未通过第一次检查,那么它就不相关。* 第四次检查:如果演员附着在另一个演员的骨骼上,那么其相关性由其父级的相关性决定。* bHidden属性设置为true并且根组件没有与检查演员发生碰撞,那么演员就不相关。* AGameNetworkManager设置为使用基于距离的相关性,如果演员比网络裁剪距离更近,则演员是相关的。

注意

Pawn/CharacterPlayerController类在相关性检查上略有不同,因为它们需要考虑额外的信息,例如运动组件。

应该注意的是,这个系统并不完美,因为在处理大型演员时,距离检查可能会给出错误的否定结果。此外,该系统没有考虑声音遮挡或其他与环境声音相关的复杂性。尽管如此,这个近似值足够精确,在游戏过程中可以得到良好的结果。

在介绍完所有这些理论之后,现在是时候将我们的注意力转回到项目上,开始实现一个有形的示例。在下面的子节中,你将通过测试你的角色来看到相关性在实际中的应用。

测试相关性

要测试游戏过程中的相关性效果,你将创建一个简单的拾取并对其设置进行操作。

创建拾取演员

首先,创建一个新的从US_BasePickup继承的 C++类。然后,打开生成的头文件,在private部分添加这两个组件声明:

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components",  meta = (AllowPrivateAccess = "true"))
TObjectPtr<class USphereComponent> SphereCollision;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UStaticMeshComponent> Mesh;

你应该熟悉之前的代码——我们只是在声明用于触发拾取的碰撞组件和用于其视觉外观的网格组件。

接下来,在protected部分,在BeginPlay()声明之后,添加一个将处理角色与演员重叠的声明:

UFUNCTION()
void OnBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

紧接着,添加拾取动作的声明:

UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Pickup", meta=(DisplayName="Pickup"))
void Pickup(class AUS_Character* OwningCharacter);

我们需要这个函数在蓝图内部可调用,所以我们使用BlueprintCallable指定符。

然后,BlueprintNativeEvent指定符表示该函数可以被蓝图覆盖,但它也具有一个默认的本地 C++实现,如果蓝图没有实现任何内容,则会调用该实现。

为了原生实现方法,在US_BasePickup.cpp文件中,我们需要实现一个与主函数同名但末尾添加_Implementation的 C++函数。

最后,进入public部分 - 在相应的属性之后,为了避免前向声明 - 为之前声明的组件添加两个获取器:

FORCEINLINE USphereComponent* GetSphereCollision() const { return SphereCollision; }
FORCEINLINE UStaticMeshComponent* GetMesh() const { return Mesh; }

现在已经完全声明了头文件,打开US_BasePickup.cpp文件以开始向 Actors 添加代码逻辑。首先,在文件顶部添加必要的包含:

#include "US_Character.h"
#include "Components/SphereComponent.h"

然后,在构造函数中添加以下代码块,它创建两个组件并将它们附加到 Actors 上:

SphereCollision = CreateDefaultSubobject<USphereComponent>("Collision");
RootComponent = SphereCollision;
SphereCollision->SetGenerateOverlapEvents(true);
SphereCollision->SetSphereRadius(200.0f);
Mesh = CreateDefaultSubobject<UStaticMeshComponent>("Mesh");
Mesh->SetupAttachment(SphereCollision);
Mesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);

立即之后,将bReplicates设置为true(因为默认情况下 Actors 不会复制):

bReplicates = true;

BeginPlay()函数内部,为重叠事件添加一个动态多播委托:

SphereCollision->OnComponentBeginOverlap.AddDynamic(this, &AUS_BasePickup::OnBeginOverlap);

注意

为了对复制给予适当的关注和重点,我已将第六章通过网络复制属性 O**ver the Network,指定为对这一主题的深入探讨。

现在,在BeginPlay()函数的括号关闭后添加重叠处理程序:

void AUS_BasePickup::OnBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
                                    UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    if (const auto Character = Cast<AUS_Character>(OtherActor))
    {
        Pickup(Character);
    }
}

之前的代码块相当直接:在检查重叠 Actors 是AUS_Character(即我们的多人游戏英雄)之后,我们简单地调用Pickup()方法。

为了完成拾取逻辑,你现在将添加Pickup()的 C++实现:

void AUS_BasePickup::Pickup_Implementation(AUS_Character * OwningCharacter)
{
   SetOwner(OwningCharacter);
}

此方法的代码逻辑可以在继承的蓝图中实现,但为了演示,我们只是将此 Actors 的所有者设置为重叠的 Actors:这是在下一个相关性测试中使事物正常工作的一个重要步骤。

现在是时候回到虚幻引擎编辑器并做一些“魔法”了 - 毕竟,这是一本关于创建幻想游戏的书!

创建拾取蓝图类

为了测试相关性在实际操作中的效果,你将创建一个蓝图拾取... 嗯,差不多吧。初步检查时,相关性可能会表现出一些奇特的趋势。这正是我们将召唤一本神奇神秘的书籍悬浮在半空中的原因!

打开虚幻引擎编辑器,按照以下步骤操作:

  1. 编译你的项目以将拾取添加到蓝图的可用类中。

  2. 在你的Blueprints文件夹中,创建一个新的BP_SpellBook

  3. 蓝图详情面板中,选择静态网格属性的网格 - 我选择了spellBook模型。

要使书本漂浮,我们将通过使用时间轴节点上下移动网格。为此,请按照以下步骤操作:

  1. 打开蓝图事件图,在画布上右键单击,并添加一个Float

  2. 双击节点以打开相应的编辑器。

  3. 点击Alpha。按钮在图 5**.11中显示:

图 5.11 – 跟踪按钮

图 5.11 – 跟踪按钮

  1. 点击循环按钮以启用循环模式。按钮在图 5**.12中显示:

图 5.12 – 循环按钮

图 5.12 – 循环按钮

  1. 右键单击曲线面板,并选择添加键到...选项。然后,将时间设置为0,将设置为0

  2. 创建另一个键,但这次将时间设置为2.5,将设置为0.5

  3. 创建最后一个键,这次将时间设置为5,将设置为0

  4. 右键单击每个键,并将键插值值设置为自动

时间轴节点的最终结果在图 5.13中显示:

图 5.13 – 时间轴节点设置

图 5.13 – 时间轴节点设置

你刚刚创建了一个在 0 和 1 之间无限循环的正弦值;你将使用这个浮动值来移动书籍上下。要实现这种浮动运动,返回到事件图,并执行以下操作:

  1. 事件开始播放节点连接到时间轴节点。

  2. 组件面板中的网格组件拖动到事件图画布上。从其输出引脚点击并拖动以添加一个设置相对位置节点。

  3. 设置相对位置的输入执行引脚连接到时间轴节点的更新执行引脚。

  4. 时间轴节点的Alpha引脚连接到一个乘法节点,并将此最后一个节点的第二个参数设置为100

  5. 右键单击设置相对位置节点的新位置引脚,并选择拆分结构引脚以暴露 X、Y 和 Z 值。

  6. 乘法节点的结果引脚连接到设置相对位置节点的新位置 Z

完整的图示在图 5.14中显示:

图 5.14 – 浮动书籍图

图 5.14 – 浮动书籍图

请注意,这个浮动动画纯粹是视觉效果,所以我们不会担心它是否在网络中同步。

现在蓝图项目已经创建,是时候将其添加到关卡中并测试其拾取功能了——我们将在下一小节中这样做。

测试相关性设置

现在是时候测试当相关性设置更改时,魔法书在多人环境中的行为如何了。

首先,将BP_SpellBook蓝图的一个实例拖动到关卡中,靠近PlayerStart演员,以便玩家在它被生成时处于视线范围内。

打开PB_SpeelBook蓝图,并选择类默认值面板,查找复制类别。默认设置应类似于图 5.15中显示的设置:

图 5.15 – 复制类别

图 5.15 – 复制类别

尝试以监听服务器的形式玩游戏,有三个玩家,每个玩家都应该看到预期的书籍。接下来事情会变得有点复杂...

停止应用程序播放,回到BP_SpellBook蓝图。查找Net Load on Client属性并取消选中。由于这个属性将在地图加载期间加载角色,我们需要禁用它,以便角色仅在它对客户端相关时才被加载。

现在,你准备好根据你在下一步中更改的属性测试不同的情况。

设置网络剔除距离

你将要测试的第一个情况是关于距离剔除 - 目前,你的对象被设置为在非常远的距离上相关。为了检查这一点,再次运行游戏,你应该看到与上次游戏没有区别。但是,如果你将Net Cull Distance Squared降低到一个非常低的数字,例如,500,会发生什么?你将得到非常“奇怪”的行为:服务器窗口将显示这本书,而两个客户端则不会!

在一个客户端窗口激活的情况下,尝试走到书应该所在的区域附近,它将立即弹出!我没有已经警告过你这本书几乎就是魔法般的存在吗?

你刚才更改的属性设置了用于使角色对客户端相关的距离的平方。这意味着法术书将在角色位于这个距离的平方根内时“神奇”地出现。由于服务器是权威的(即,知道一切),它将始终显示角色。

现在我们来测试一种设置角色始终相关并且始终可见的方法。

设置角色始终相关

返回到法术书蓝图,并将Always Relevant属性设置为True,其余的保持与上一个示例相同。当你玩游戏时,你会注意到每个客户端都会从开始就能看到这本书。这是因为这本书现在被标记为无论角色在关卡中的任何位置都应该是相关的;因此,它将立即被客户端加载并对玩家可见。

这显然不是我们想要的情况 - 考虑到我们的游戏中可能有无数移动元素,我们不想对关卡中的每个角色都进行连续更新。但你可能已经想象到了这一点,不是吗?

让我们通过设置基于角色的所有者来避免这个问题。

设置所有者的相关性

你可能记得,Pickup()函数的 C++代码将拾取的所有者的所有权分配给与之重叠的角色。相反,在这个蓝图里,我们将看到如果角色只对所有者相关会发生什么:

  1. Only Relevant to Owner属性设置为True

  2. Always Relevant属性设置为False

  3. Net Cull Distance Squared设置为一个非常低的数字,比如说10

在最后一步,我们正在设置法术书,使其不会对任何客户端相关,除非它直接位于对象上;这将使我们能够测试谁是该角色的所有者。

客户端除非进入其碰撞区域,否则无法看到这本书,这时角色成为拾取物的所有者。一旦另一个角色进入拾取区域,它将成为新的所有者,这本书将变得相关。几分钟后,第一个客户端会看到这本书消失,因为角色不再是拾取物的所有者,因此它不再与它相关!

作为最后的注意事项,还有一个属性你应该知道:Net Use Owner Relevancy将根据其所有者的相关性返回 Actor 的相关性。当你将武器分配给角色或敌人时,这将会很有用!

在本节中,你已经揭开了相关性的神秘秘密,并见证了它的实际应用。当你开始优化游戏时,这个概念将非常有价值,但始终最好从一开始就打下坚实的基础,并设定正确的方向。下一节将介绍另一个重要的概念,即权限。

介绍权限

如我们所述的第二章理解网络基础,术语权限指的是游戏状态中哪个实例具有对某些方面的最终决定权。在虚幻引擎的多玩家环境中,服务器对游戏状态具有权限:这意味着服务器对诸如玩家移动、伤害计算和其他游戏机制等事项做出最终决定。

当客户端请求执行影响游戏状态的操作时,它会向服务器发送一条消息,请求执行该操作的权限。服务器随后确定该操作是否有效,如果是的话,相应地更新游戏状态。一旦服务器更新了游戏状态,它就会向所有客户端发送消息,告知他们更新的状态。

在虚幻引擎中,Actor 可以是本地控制或远程控制,权限的概念在确定哪些控制有效时很重要。本地控制的 Actor 对其自己的动作具有权限,而远程控制的 Actor 则从服务器接收命令并遵循这些命令。

总体而言,权限的概念确保所有玩家看到一致的游戏状态,并且没有玩家拥有不公平的优势。

使用 Actor 的 Role 和 Remote Role 属性控制权限

在虚幻引擎中,有两个属性返回关于 Actor 复制的 重要信息:角色远程角色。这两个属性提供了有关谁对 Actor 具有权限、Actor 是否被复制以及复制方式的信息。

在虚幻引擎中,Actor 在网络游戏中可以具有四种可能的角色之一:

  • ROLE_Authority:运行实例对 Actor 具有权威控制

  • ROLE_AutonomousProxy:运行实例是 Actor 的自主代理

  • ROLE_SimulatedProxy:运行实例是 Actor 的本地模拟代理

  • ROLE_None:在这种情况下,角色无关紧要

总体而言,RoleRemoteRole 属性用于控制 Actor 在 Unreal Engine 网络游戏中的行为,它们的值可能根据 Actor 的所有权和复制设置而有所不同。特别是,Role 属性指定了 Actor 在本地机器上的角色,而 RemoteRole 属性指定了 Actor 在远程机器上的角色。

例如,如果将 Role 设置为 ROLE_Authority 并且 RemoteRole 设置为 ROLE_SimulatedProxyROLE_AutonomousProxy 中的任何一个,那么当前游戏实例将负责将此 Actor 复制到远程连接。

应该注意的是,只有服务器将 Actor 复制到连接的客户端,因为客户端永远不会将 Actor 复制到服务器。这意味着只有服务器会将 Role 设置为 ROLE_Authority,并将 RemoteRole 设置为 ROLE_SimulatedProxyROLE_AutonomousProxy

自主和模拟代理

在测试法术书拾取(好吧,这严格来说不是一个“拾取”,但你应该明白这个意思)时,你可能注意到,一旦 Actor 的所有者发生变化,这本书似乎在一段时间内对旧所有者和新所有者都保持相关。为了避免使用过多的 CPU 资源和带宽,服务器不会在每次更新时复制 Actor,而是在由 AActor::NetUpdateFrequency 属性确定的频率下进行复制。

在更新任何 Actor 的移动过程中,也会发生同样的事情,客户端将在预定义的间隔内接收数据;因此,玩家可能会在 Actor 上收到看似不规律更新。为了避免这些问题,引擎将尝试根据最新的数据外推移动。

默认行为依赖于预测移动,并由 ROLE_SimulatedProxy 管理。在这种模式下,客户端会根据从服务器接收到的最新速度持续更新 Actor 的位置。

当一个 Actor 由 PlayerController 对象控制时,你可以使用 ROLE_AutonomousProxy。在这种情况下,系统将直接从人类玩家那里接收额外信息,使预测未来动作的过程更加平滑。

在本节中,你对权限和 Actor 角色领域有了一些了解。这些概念无疑将在未来的章节中派上用场,尤其是在你深入研究诸如角色武器和敌人 AI 等复杂主题时。

摘要

在本章中,你通过更新你的角色所需的移动和交互功能,进一步推进了你的多人项目开发——这得益于 Unreal Engine 提供的增强输入系统。

接下来,你通过理解什么是所有者,对多人环境中谁在操纵 Actor 有了一些清晰的认识。

之后,你开始意识到在游戏中相关性至关重要的角色。正如你亲自发现的,了解属性是如何设置的至关重要,否则事情将开始出现大转变,变得奇怪起来。

最后,你对组成虚幻引擎多人游戏的不同角色以及为什么它们在跨多个客户端复制演员行为中扮演关键角色有了宝贵的见解。

这引出了最后一个问题:究竟“复制”一个对象意味着什么?嗯,我想是时候散步或享受一杯咖啡来充电了。你需要调动你所有的能量和注意力,因为我在下一章将揭露(几乎)所有复制的秘密!

第六章:在网络上复制属性

当使用虚幻引擎创建多人游戏时,复制是一个重要的概念。特别是,属性复制允许在多个玩家之间同步对象,使他们能够在共享环境中交互。此功能还处理诸如角色移动和物理计算等问题,确保每个人都能获得一致的游戏世界体验和视角,无论平台类型如何,并且没有人因作弊或延迟问题而获得优势。

在本章中,你将开始处理复制,主要关注角色技能的属性复制。接下来,从上一章中创建的基础拾取开始,你将实现一个硬币拾取,这将授予角色经验点,在游戏过程中使角色升级。最后,你将通过更新一个简单的用户界面来应用复制,该界面将显示角色的经验点和等级。

在本章结束时,你将很好地掌握在多人设置中 Actor 如何复制以及与之相关的属性。基本上,你将了解 Actor 在多人环境中的行为和操作。

在接下来的几节中,我将介绍以下主题:

  • 添加角色统计数据

  • 理解属性复制

  • 处理角色等级提升

  • 为游戏添加 HUD

技术要求

要跟随本章中介绍的主题,你应该已经完成了前面的内容,并理解了它们的内容。

此外,如果你希望从本书的配套仓库开始编写代码,你可以下载本书配套项目仓库中提供的.zip项目文件:

github.com/PacktPublishing/Multiplayer-Game-Development-with-Unreal-Engine-5.

你可以通过点击Unreal Shadows – 第五章``结束链接下载与最后一章结尾同步的文件。

添加角色统计数据

在介绍属性复制并在项目中实现它之前,我们的盗贼英雄需要为这样一个大步做好准备:这就是为什么我会引导你创建一组将被插入到角色类中的统计数据。

首件事是定义你的角色统计数据。特别是,你需要以下数据:

  • 行走和冲刺速度,以处理游戏过程中角色的不同步伐

  • 一个伤害倍数,用于管理角色升级时的更强大打击

  • 检查角色是否达到下一个等级时需要提升的等级值

  • 一个潜行倍数,将处理角色在行走或冲刺时产生的噪音

你可能已经注意到你的角色没有生命值 – 这是因为这是一个潜行游戏,玩家将不得不小心翼翼地穿过地牢。一旦被发现,他们在这个特定游戏中将没有面对一群不死随从的选项!因此,游戏玩法将更多地集中在从远处击败敌人或悄悄地从他们身边溜走。

根据前面的信息,你将创建一个包含初始化角色所需的所有数据点的数据结构,然后你将创建一个数据表,让你可以管理玩家在游戏过程中获得的经验。所以,让我们开始吧。

创建统计结构

首先,你需要创建一个结构,它将包括所有上述统计数据。由于这不是一个类,你不需要从 Unreal Engine 编辑器创建它,而是从 IDE 创建。

注意

无法在 Unreal 编辑器内部直接创建非类实体。

打开你的集成开发环境(IDE),在你的UnrealShadows_LOTL | Source | UnrealShadows_LOTL文件夹中创建一个名为US_CharacterStats.h的文件(因为这是一个数据结构,你不需要.cpp文件)。然后,打开该文件并插入以下代码:

#pragma once
#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "US_CharacterStats.generated.h"
USTRUCT(BlueprintType)
struct UNREALSHADOWS_LOTL_API FUS_CharacterStats : public FTableRowBase
{
 GENERATED_BODY()
 UPROPERTY(BlueprintReadWrite, EditAnywhere)
 float WalkSpeed = 200.0f;
 UPROPERTY(BlueprintReadWrite, EditAnywhere)
 float SprintSpeed = 400.0f;
 UPROPERTY(BlueprintReadWrite, EditAnywhere)
 float DamageMultiplier = 1.0f;
 UPROPERTY(BlueprintReadWrite, EditAnywhere)
 int32 NextLevelXp = 10.0f;
 UPROPERTY(BlueprintReadWrite, EditAnywhere)
 float StealthMultiplier = 1.0f;
};

include部分是自解释的 – 之后,除了标准的 C++ struct关键字来声明结构外,你还会注意到一个USTRUCT()声明而不是UCLASS(),以及结构名称前的F前缀(即FUS_CharacterStats)。这是在 Unreal Engine 中声明结构的标准方法。然后,为了使 Unreal 编辑器能够从该结构创建数据表(稍后将有更多介绍),扩展了FTableRowBase类型。

在结构声明内部,我们只是添加了一个属性列表 – 所有这些属性都被标记为BlueprintReadWrite,以便蓝图访问和修改数据,以及EditAnywhere,以便你可以在下一步创建的数据表中编辑值。

创建统计数据表

现在你已经为你的角色创建了一个数据结构,并准备好从它创建实际的数据。在 Unreal Engine 中,我们将使用UObject属性 – 包括来自项目的资产引用,例如材料或纹理。

要创建你的角色数据表,请按照以下步骤操作:

  1. 在内容浏览器中打开你的Blueprints文件夹。

  2. 编译你的项目,以便在编辑器中可用 C++结构。

  3. 在内容浏览器中右键单击并选择杂项 | 数据表

  4. 选择行结构弹出窗口中,从下拉菜单中选择US_CharacterStats,如图图 6.1所示:

图 6.1 – 数据表创建面板

图 6.1 – 数据表创建面板

  1. 点击US_CharacterStats

  2. 双击新创建的资产以打开它。你将得到一个如图 6.2所示的空数据集:

图 6.2 – 空数据表

图 6.2 – 空数据表

注意

您也可以通过将.csv.json文件导入到项目中生成数据表。此外,虚幻引擎将允许您轻松地将项目表导出为.csv.json格式。有关导入和导出过程的更多信息,请参阅此处链接的官方文档:docs.unrealengine.com/5.1/en-US/data-driven-gameplay-elements-in-unreal-engine/

当您的表格打开时,是时候添加一些按角色级别组织的数据行 - 您当然希望角色在获得足够经验时能够成长,不是吗?

让我们先为角色的基础级别添加一行:

  1. 表格面板中点击添加按钮。

  2. level_01

  3. 您现在可以为角色的第一个经验级别设置一些统计数据。寻找250,0

  4. 800,0

  5. 1,0

  6. 10

  7. 1,0

最终结果应与图 6**.3中显示的设置相同:

图 6.3 – level_01 角色的设置

图 6.3 – level_01 角色的设置

我们将添加几个更多级别来处理游戏中的角色经验增长。重复之前的步骤,但将两个新行分别命名为level_02level_03。然后为**level_02**行使用以下值:

  • 275,0

  • 850,0

  • 1,1

  • 25

  • 1,5

**level_03**行添加以下值:

  • 300,0

  • 900,0

  • 1,0

  • 50

  • 2

这些纯粹是指示性值 - 您可以根据自己的需求进行调整,并添加尽可能多的附加级别。

现在您已经为角色的经验添加了数据集,您就可以直接从代码中读取包含的信息了。这就是为什么我需要您回到US_Character.h头文件中添加数据表声明的原因。

从角色读取数据表

在本节中,您将向角色添加数据表,以便根据经验水平读取其值。首先要做的事情是添加对US_Character.h头文件的引用。因此,在头文件的private部分,在所有现有声明之后,添加以下代码:

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Character Data", meta = (AllowPrivateAccess = "true"))
class UDataTable* CharacterDataTable;
struct FUS_CharacterStats* CharacterStats;

第一项声明将允许您直接从子蓝图类中引用数据表,而结构声明将允许您从数据表中引用单行并将其用作角色统计数据。

之后,您需要创建一个函数,允许系统更新角色的当前级别。在public部分,添加以下方法声明:

void UpdateCharacterStats(int32 CharacterLevel);

您需要添加到类头文件中的最后一件事是用于统计结构的获取器函数。仍然在public部分,在最后一个闭合括号之前,添加以下代码行:

FORCEINLINE FUS_CharacterStats* GetCharacterStats() const { return CharacterStats; }

现在,你可以保存此文件并打开US_Character.cpp以处理数据检索。在文件顶部,添加你将要使用的类的include声明:

#include "US_CharacterStats.h"
#include "Engine/DataTable.h"

接下来,通过在文件末尾添加以下代码来实现UpdateCharacterStats()方法:

void AUS_Character::UpdateCharacterStats(int32 CharacterLevel)
{
 if(CharacterDataTable)
 {
  TArray<FUS_CharacterStats*> CharacterStatsRows;
  CharacterDataTable->GetAllRows<FUS_CharacterStats>(TEXT("US_Character"), CharacterStatsRows);
  if(CharacterStatsRows.Num() > 0)
  {
   const auto NewCharacterLevel = FMath::Clamp(CharacterLevel, 1, CharacterStatsRows.Num());
   CharacterStats = CharacterStatsRows[NewCharacterLevel - 1];
   GetCharacterMovement()->MaxWalkSpeed = GetCharacterStats()->WalkSpeed;
  }
 }
}

如你所见,首先我们检查数据表是否被引用(你稍后将从角色蓝图添加它),然后使用GetAllRows<T>()方法将所有表行检索到本地数组中(即CharacterStatsRows变量)。如果数据表中至少有一行,我们获取对应于角色等级减 1 的行(即对于 1 级角色,我们将获取行号 0)。请注意,还有FMath::Clamp()方法,它保证我们不会尝试获取高于数据集中可用行数的等级值。

之后,我们从行中检索WalkSpeed列并将其值分配给角色的移动组件的MaxWalkSpeed属性——这意味着,如果有数据表分配,你的角色将以数据集中的值开始游戏,而不是从构造函数开始。

现在,你准备好将角色的统计数据更新到 1 级——你将在BeginPlay()函数中执行此操作。为此,在BeginPlay()函数中,并在括号关闭之前,添加以下代码:

UpdateCharacterStats(1);

你需要做的最后一件事是更新两个使用硬编码值但需要使用数据表统计信息的冲刺方法。为此,搜索SprintStart()方法并找到以下行:

GetCharacterMovement()->MaxWalkSpeed = 3000.f;

然后,将其更改为以下代码:

if (GetCharacterStats())
{
 GetCharacterMovement()->MaxWalkSpeed = GetCharacterStats()->SprintSpeed;
}

让我们用SprintEnd()方法做同样的事情,它应该位于上一个方法之后。找到以下行:

GetCharacterMovement()->MaxWalkSpeed = 500.f;

然后使用以下代码块进行更改:

if(GetCharacterStats())
{
 GetCharacterMovement()->MaxWalkSpeed = GetCharacterStats()->WalkSpeed;
}

在这两种情况下,代码都是自解释的——我们只是检查在角色统计数据中是否有有效的数据引用,并将冲刺或行走速度分配给角色移动组件。

现在保存你的文件并编译项目,只是为了确保一切正常且为下一步做好准备。

你的角色现在准备好接受我们在本章开头创建的数据表。

将数据表添加到角色中

要将数据表资产添加到角色中,切换回 Unreal 编辑器并按照以下步骤操作:

  1. 打开BP_Character蓝图。

  2. 选择类默认值选项卡,然后在详细信息面板中查找字符****数据类别。

  3. 角色数据表属性中,点击下拉菜单并选择DT_CharacterStats

你的角色现在可以使用数据集中的统计数据了——即使这个可怜的小偷被锁定在 1 级经验等级中,稍后你将在地牢中释放他们,看看他们的表现如何!

测试游戏以检查一切是否正常工作。只需记住我在上一章中提到的话:由于客户端和服务器试图强制角色符合不同的速度值,移动仍然存在 bug,但你正接近解决方案。

因此,在本节中,你通过添加从数据表中检索的一些统计数据并使用它们来初始化一些属性来改进了角色。目前,你只使用了移动属性,但不要害怕!一旦角色完成,一切都将各就各位。

在接下来的章节中,我们将深入研究虚幻中的属性复制主题——当需要提升角色等级时,这将很有用,你将在本章结束时完成这项工作。

理解属性复制

如前所述,属性复制允许在虚幻多人环境中同步对象。需要注意的是,由于服务器是权威的,更新永远不会由客户端发送。显然,客户端可能会(礼貌地)请求服务器更改属性值,服务器将相应地行事。此外,属性复制充当一个可靠的服务:因此,客户端的 Actor 最终将与服务器上的值相同。

这意味着如果你试图从客户端修改一个复制的属性,你对该属性所做的任何更改都将是临时的。你应该已经熟悉这个主题,因为目前角色的移动逻辑有点 buggy——我们试图让客户端的角色运行,但一旦网络更新,服务器就会阻止我们的命令。

这是因为,一旦服务器向客户端发送更新,包含该属性新值的更新,你在客户端本地所做的任何更改都将被覆盖并替换为来自服务器的新、正确的值。因此,如果服务器更新不频繁,客户端可能需要一段时间才能通知到新的、正确的值。

修复那个讨厌的 bug 是我们将在第七章中进行的,使用远程过程调用(RPCs),在那里你需要学习如何从客户端调用函数到服务器。然而,本章的主要焦点是理解如何复制属性。所以,无需多言,让我们来看看内部是如何运作的!

启用属性复制

为了使属性能够复制,你需要设置一些事情。首先,在将包含属性的 Actor 构造函数中,你需要将bReplicates标志设置为true

注意

APawnACharacter扩展的类或蓝图将默认将bReplicates属性设置为true,而常规 Actor 则不会。

然后,需要复制的属性需要在UPROPERTY()宏中添加Replicated指定符。例如,你可以使用以下代码来复制你角色的分数:

UPROPERTY(Replicated)
int32 Score;

如果你需要一个在属性更新时执行的回调函数,你可以使用ReplicatedUsing=[FunctionName]代替——这个属性将允许你指定一个在更新发送到客户端时将被执行的函数。例如,如果你想在你角色的分数被复制时执行名为OnRep_Score()的方法,你将编写类似于以下代码的内容:

UPROPERTY(ReplicatedUsing="OnRep_Score")
int32 Score;

接下来,你需要在同一类中实现OnRep_Score()方法;这个函数必须声明UFUNCTION()宏。

一旦所有复制属性都通过之前的属性正确装饰,它们需要在AActor::GetLifetimeReplicatedProps()函数内部使用DOREPLIFETIME()宏进行声明。使用之前的分数示例,你需要使用以下代码来声明Score属性:

DOREPLIFETIME(AMyActor, Score);

属性注册复制后,无法取消注册,因为虚幻引擎将优化数据存储以减少计算时间:这意味着默认情况下,你将无法对属性的复制有太多控制。

幸运的是,你可以使用DOREPLIFETIME_CONDITION()宏代替,这将允许你添加一个额外的条件以更精确地控制复制。这些条件的值是预定义的——一个例子是COND_OwnerOnly,它只会将数据发送给 Actor 的所有者(我们将在本章后面使用这个值)。作为另一个例子,如果你需要在属性复制中拥有更精细的控制,你可以使用DOREPLIFETIME_ACTIVE_OVERRIDE()宏,这将允许你使用 Actor 内部定义的自己的条件。

使用额外的条件进行复制的最大缺点是性能,因为引擎在复制属性之前需要执行额外的检查——这意味着在没有任何迫切要求指定使用替代选项的情况下,建议使用DOREPLIFETIME()宏。

现在你已经了解了如何复制一个对象,是时候介绍如何在网络上引用对象了。

在网络上引用 Actor 和组件

总有一天,你需要从你的代码中引用一个 Actor 或组件——这意味着在多人游戏中,你需要知道这个引用是否可以被复制。简单来说,一个 Actor 或组件只有在支持网络的情况下才能在网络上被引用。

有一些简单的规则可以帮助你确定你的对象是否可以在网络上被引用:

  • 如果一个 Actor 被复制,它也可以作为引用被复制

  • 如果一个组件被复制,它也可以作为引用被复制

  • 需要稳定命名的非复制的 Actors 和组件,以便作为引用进行复制

注意

一个稳定命名的对象意味着一个实体将在服务器和客户端都存在,并且具有相同的名称。例如,如果一个 Actor 在游戏过程中没有被生成,而是直接从包中加载到级别中,那么它就是一个稳定命名的 Actor。

本节为您提供了关于 Unreal Engine 中网络复制的根本概念的介绍,解释了它是如何与 Actors 和组件交互的。如果你觉得理论太多而感到有些迷茫,不要害怕!你将通过创建角色的等级提升系统,将所有这些理论转化为一个具体、可工作的示例。

处理角色等级提升

如我之前提到的,在本节中,你将提升你英雄的经验值和技能。像往常一样,你将运用代码魔法来实现它!毕竟,你正在编写一个幻想游戏。

我知道你可能觉得在 Character 类内部编写代码是个好主意,但请相信我,实际上有一个更好的地方。那就是PlayerState类,我们碰巧已经为这次场合设置了它——之前,我让你创建US_PlayerState类,现在就是时候在其中添加一些有价值的代码了。

如在第四章中介绍的,设置您的第一个多人游戏环境PlayerState是一个包含玩家游戏状态信息的类,存在于服务器和客户端上。由于我们需要同步角色的经验值和等级,这是放置所有内容的理想位置。

我们在这里需要做的是跟踪经验值,一旦角色达到新的等级,就通过网络广播信息并更新角色统计数据。

但首先,最重要的是要有一个清晰的思路,知道我们打算做什么。

提前规划

由于PlayerState类将保存有关角色的重要信息,因此必须提前考虑你想要实现的目标以及如何达到那个目标——这意味着我们必须确切地规划我们将要添加到这个类中的内容。

下面是这个游戏框架类将实现的一些主要功能:

  • 跟踪角色的当前等级和经验值

  • 在网络上同步上述属性

  • 当玩家等级提升时更新角色类

  • 当角色获得一些经验值或等级提升时广播事件

作为起点,在下一个子节中,我们将首先声明所需的属性和函数。

声明PlayerState属性和函数

在以下步骤中,我们将定义主要属性,以便角色在获得足够经验时能够升级——这意味着我们需要跟踪盗贼的经验点和等级。此外,每当值发生变化时,我们将在网络上复制这些属性,并将此事件通知游戏中注册的每个 Actor。

因此,让我们首先打开US_PlayerState.h文件,并在protected部分添加以下代码:

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, ReplicatedUsing="OnRep_Xp", Category = "Experience")
int Xp = 0;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, ReplicatedUsing="OnRep_CharacterLevelUp", Category = "Experience")
int CharacterLevel = 1;
UFUNCTION()
void OnRep_Xp(int32 OldValue) const;
UFUNCTION()
void OnRep_CharacterLevelUp(int32 OldValue) const;

如你所见,我们首先声明了两个属性Xp(代表经验点)和CharacterLevel;它们都可以在 Unreal 的EditDefaultsOnly属性中进行修改,但BlueprintsReadOnly使它们在蓝图不可修改,以保持所有升级逻辑都在 C++源代码中。

作为附加属性,我们使用ReplicatedUsing属性,我在上一节中介绍了它。这将使我们能够在属性更新时执行函数——在这种情况下,我们为Xp属性设置了OnRep_Xp,为CharacterLevel设置了OnRep_CharacterLevelUp

接下来,在你的头文件中创建一个public部分,并添加以下代码:

UFUNCTION(BlueprintCallable, Category="Experience")
void AddXp(int32 Value);

此函数将使我们能够为新PlayerState分配新的经验点。我们需要将其设置为BlueprintCallable,以便从我们的蓝图(例如,从拾取物品)中使用此函数。

在此之后,添加以下声明:

virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

如前所述,我们需要重写此方法以声明将要复制的属性(更多内容将在稍后介绍)。

实现我们两个属性复制的所有必要设置已经完成,但仍需添加一些额外的元素以确保一切正常工作。我们需要在属性更改时广播一些信息——当你在本章的稍后部分实现用户界面时,这将很有用。

要实现此类功能,你将使用委托。你可能已经熟悉这个话题在 C++中的使用,但你应该知道,在 Unreal Engine 中,委托提供了一种以通用、类型安全的方式通过专用宏在 C++对象上调用成员函数的方法。

注意

如果你想要了解更多关于 Unreal Engine 支持的类型和如何在项目中使用它们的信息,请查看官方文档,可以在以下链接找到:docs.unrealengine.com/5.1/en-US/delegates-and-lamba-functions-in-unreal-engine/

由于我们想要为两个属性广播事件,我们将声明两个委托——一个用于每个属性。在头文件的开头,在UCLASS()声明之前,添加以下代码:

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnXpChanged, int32, NewXp);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCharacterLevelUp, int32, NewLevelXp);

这两行代码非常相似——它们都声明了一个Broadcast()方法来通知系统中的每个监听器发生变化。我们将在我们的蓝图类中使用这些功能来绑定事件并相应地做出反应。

让我们声明我们的delegate函数。创建一个protected部分,并添加以下两行代码,这些代码将用于广播事件:

UPROPERTY(BlueprintAssignable, Category = "Events")
FOnXpChanged OnXpChanged;
UPROPERTY(BlueprintAssignable, Category = "Events")
FOnCharacterLevelUp OnCharacterLevelUp;

由于它们的目的不言自明,我想现在是时候停止说话,开始写下实现代码了!

实现玩家状态(PlayerState)逻辑

现在所有属性和方法都已声明,你将实现玩家状态(PlayerState)逻辑——每当角色获得一些经验时,你应该检查它是否达到了升级所需的足够点数。获得的经验点和升级应该广播到系统中,以保持一切同步。

首先,打开US_PlayerState.cpp文件并添加所需的include声明:

#include "US_Character.h"
#include "US_CharacterStats.h"
#include "Net/UnrealNetwork.h"

接下来,为GetLifetimeReplicatedProps()方法添加实现:

void AUS_PlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
 Super::GetLifetimeReplicatedProps(OutLifetimeProps);
 DOREPLIFETIME_CONDITION(AUS_PlayerState, Xp, COND_OwnerOnly);
 DOREPLIFETIME_CONDITION(AUS_PlayerState, CharacterLevel, COND_OwnerOnly);
}

如你所见,我们正在使用上一节中引入的DOREPLIFETIME_CONDITION()宏来声明XpCharacterLevel属性应该被复制——在这种情况下,我们只想让属性在拥有角色的玩家(即玩家的客户端)上被复制,我们通过使用COND_OwnerOnly标志来实现这一点。

接下来,使用以下代码为AddXp()方法添加实现:

void AUS_PlayerState::AddXp(const int32 Value)
{
 Xp += Value;
 OnXpChanged.Broadcast(Xp);
 GEngine->AddOnScreenDebugMessage(0, 5.f, FColor::Yellow, FString::Printf(TEXT("Total Xp: %d"), Value));
 if (const auto Character = Cast<AUS_Character>(GetPawn()))
 {
  if(Character->GetCharacterStats()->NextLevelXp < Xp)
  {
   GEngine->AddOnScreenDebugMessage(3, 5.f, FColor::Red, TEXT("Level Up!"));
   CharacterLevel++;
   Character->UpdateCharacterStats(CharacterLevel);
   OnCharacterLevelUp.Broadcast(CharacterLevel);
  }
 }
}

在这里,每当收到经验点更新时,我们只需将值添加到角色池(即Xp属性)中。接下来,我们通过强制转换为AUS_Character类型来确认角色类型,如果转换成功,我们检索其统计数据以检查是否应该升级。如果检查成功,我们简单地增加角色等级并调用UpdateCharacterStats()方法来让盗贼更新技能行。当我们更改属性值时,然后向所有听众广播新的值。一些(暂时的)调试信息完成了代码。

玩家状态(PlayerState)现在几乎完成了——我们只需要在服务器端更新值时向客户端广播这些值。为此,将以下代码块添加到文件中:

void AUS_PlayerState::OnRep_Xp(int32 OldValue) const
{
 OnXpChanged.Broadcast(Xp);
}
void AUS_PlayerState::OnRep_CharacterLevelUp(int32 OldValue) const
{
 OnCharacterLevelUp.Broadcast(CharacterLevel);
}

广播调用是显而易见的——每个注册的 Actor 都会收到通知,包括XpCharacterLevel属性的新的值。

因此,在之前的步骤中,你已经成功开发了一个完全可操作的复制系统,该系统能够有效管理获得的角色经验和技能发展。我知道手头的任务可能感觉令人畏惧,甚至有时感觉反直觉,但随着时间和实践,你可以放心,一切都会变得更容易、更易于管理!

在我们的游戏中还有一些东西缺失:实际的经验点。让我们不要浪费时间,着手添加一个角色可以使用以获得经验点的物品。在接下来的步骤中,你将创建一些金币,从之前创建的US_BasePickup类开始,以赋予你的盗贼渴望的经验。

将金币收集添加到关卡中

因此,我们准备好创建一些将在游戏中使用以增加角色经验值的硬币 – 这将是一个简单的蓝图,每当敌人被杀死或该级别可用时都会生成。

要这样做,请返回 Unreal 编辑器并编译项目,以便更新所有改进。然后,导航到Blueprints文件夹并完成以下步骤:

  1. 在内容浏览器中右键点击并选择蓝图类 | US_BasePickup,从它创建一个新的蓝图。

  2. 将蓝图命名为BP_GoldCoinPickup,双击它以打开它。

  3. 组件面板中,选择网格组件,并将其分配给静态网格属性,设置为硬币静态网格。将其比例改为2,以便在游戏中更明显。

您的蓝图现在应该类似于图 6.4所示:

图 6.4 – 金币蓝图

图 6.4 – 金币蓝图

现在拾取项已经有了基础形状,是时候添加一些代码逻辑来使它完全功能化了。打开事件图选项卡,按照以下步骤操作:

  1. 创建一个类型为整数的变量,命名为获得经验值,并为其设置默认值5

  2. 在画布上右键点击,查找事件拾取,并将其添加到图中。

  3. 添加一个转换为 US_PlayerState节点,并将其输入执行引脚连接到事件的输出执行引脚。

  4. 拥有角色事件引脚点击并拖动,在释放按钮后,从出现的选项中选择添加一个获取玩家状态节点。

  5. PlayerState的输出引脚连接到Cast节点的对象引脚。

  6. As Us PlayerState的输出引脚点击并拖动以创建一个添加经验值节点。

  7. Cast节点的成功执行引脚连接到添加经验值节点的输入执行引脚。

  8. 变量部分拖动一个获取获得经验值节点到画布中,并将其引脚连接到添加经验值节点的引脚。

  9. 最后,添加一个销毁演员节点并将其连接到添加经验值节点的输出执行引脚。

图的最终结果如图 6.5所示:

图 6.5 – 硬币事件图

图 6.5 – 硬币事件图

如您所见,视觉脚本代码相当简单 – 每当角色捡起硬币时,其 PlayerState 将更新为它授予的经验值。

要测试游戏,只需将一堆硬币拖放到您的级别中并玩游戏。每次角色捡起硬币时,您应该看到一个显示消息,当角色获得足够的经验时,您应该得到另一个消息,即升级消息。

应该注意的是,在之前的代码中,拾取事件将在客户端和服务器上同时调用——这是不应该做的事情,因为它可能会在你的游戏中引发问题。幸运的是,在这种情况下,PlayerState 将正确处理数据,所以我们不必担心。你将在 第六章 中学习如何处理更复杂的情况,在网络中复制属性

作为额外的练习,你可以给硬币添加一个浮动动画,就像你在 第五章 中为法术书所做的,在多人环境中管理演员

添加硬币子类

作为可选步骤,你可以创建具有不同经验值的不同硬币拾取。以下是这样做的方法:

  1. 右键点击 BP_SilverCoinPickup

  2. Earned XpMI_Metal 的值设置为 3 作为网格材质。

为了让你的角色有各种物品去寻找,你可以重复此步骤多次。这将赋予你的角色一个多样化的宝藏去寻找。

在本节中,你已经为你的盗贼英雄创建了一个升级系统。借助复制的帮助,当角色达到足够经验值时,将获得正确的升级通知。目前,这可以通过收集关卡周围的硬币拾取来实现——稍后,你将在击败那些讨厌的巫妖领主小兵时生成宝藏!

在下一节中,我将指导你创建一个简单的用户界面,该界面将显示角色的等级和获得的经验值;你将通过监听玩家状态通知并相应地做出反应来完成此任务。

向游戏中添加 HUD

在本节中,你将为游戏创建一个 抬头显示 (HUD),以帮助在游戏过程中监控玩家角色的进度。正如你可能已经知道的,创建此类信息的最佳方式是通过 Unreal 动作图形 (UMG) 系统——这是一个基于 GUI 的编辑器,允许开发者为他们游戏创建用户界面元素,如菜单、HUD 和其他显示屏幕。你将使用此系统来创建带有相对信息的 HUD 小部件。

目前我们需要显示的内容相当简单——一组显示角色经验值的文本和另一组显示等级的文本。

首先,让我们创建蓝图和视觉元素。

创建小部件蓝图

要创建小部件蓝图,在 Unreal 编辑器中,请执行以下步骤:

  1. 打开你的 Blueprints 文件夹,右键点击内容浏览器,选择 WB_HUD 并双击资产以打开它。

  2. Palette 选项卡拖动一个 Canvas 元素到 Designer 视图中。这个画布将作为你的视觉元素的主要容器。

  3. 将一个Text元素拖入之前添加的Canvas中,并将其命名为XpLabel。确保在Details面板中勾选Is Variable字段,以便在稍后使用的图中暴露此元素。

  4. 将标签放置在画布上适合你需求的位置;在我的情况下,我选择了屏幕的左上角。

  5. 将另一个Text元素拖入之前添加的Canvas实例中,并将其命名为CharacterLevelLabel。再次确保在Details面板中勾选Is Variable字段,以便在稍后使用的图中暴露此元素。

  6. 将标签放置在画布上适合你需求的位置;在我的情况下,我选择了屏幕的右上角。

你的 HUD 最终结果应该类似于图 6**.6

图 6.6 – HUD 设计面板

图 6.6 – HUD 设计面板

现在你已经创建了小部件,是时候添加一些 Visual Scripting 代码来使其完全功能化了。

向 Widget 蓝图添加代码逻辑

在以下步骤中,你将在蓝图上添加一些代码逻辑,以便监听 PlayerState 的事件并相应地做出反应。

为经验值标签创建自定义事件

让我们先创建一个自定义事件,该事件将更新经验值标签。为此,打开你的小部件的Graph面板并执行以下步骤:

  1. 创建一个自定义事件,并将其命名为OnXpChanged_Event

  2. 选择它,在NewXp

  3. MyBlueprint面板中,拖动一个XpLabel的获取节点。

  4. XpLabel输出引脚,点击并拖动,添加一个SetText (****Text)节点。

  5. OnXpChanged_Event执行引脚连接到传入的SetText (Text)执行引脚。

  6. Event节点的New Xp引脚连接到SetText (Text)节点的In Text引脚。此操作将自动添加一个To Text (Integer)节点转换器。

这段代码的最终结果显示在图 6**.7中:

图 6.7 – Xp 自定义事件

图 6.7 – Xp 自定义事件

作为额外的、可选步骤,你可能想要添加一个经验值(例如,经验 值: 150)。

现在你有一个自定义事件来处理经验值标签,是时候为角色等级做同样的事情了。

为角色等级标签创建自定义事件

现在让我们创建一个自定义事件,该事件将更新角色等级标签:

  1. 创建一个自定义事件,并将其命名为OnCharacterLevelUp_Event

  2. 选择它,在NewLevel

  3. MyBlueprint面板中,拖动一个CharacterLevelLabel的获取节点。

  4. CharacterLevelLabel输出引脚,点击并拖动,在释放鼠标按钮后,从出现的选项中选择一个SetText (Text)节点。

  5. OnLevelLabelChanged_Event执行引脚连接到传入的SetText (Text)执行引脚。

  6. 事件节点的新关卡引脚连接到setText (Text)节点的In Text引脚。此操作将自动添加一个To Text (Integer)节点转换器。

这段代码的最终结果显示在图 6.8中:

图 6.8 – 角色等级自定义事件

图 6.8 – 角色等级自定义事件

就像之前的标签一样,你可能想使用Level:(例如,Level: 1)。

现在你已经有了处理角色等级标签的自定义事件,是时候将这些事件绑定到 PlayerState 广播的通知上了。

绑定到 PlayerState 事件

在 Widget 蓝图的这个最终步骤中,你将绑定之前创建的事件到 PlayerState,以便在每次更新通知分发时更新 HUD:

  1. 在图中添加一个初始化事件节点。此节点在游戏过程中仅执行一次(即,当对象已初始化时),是添加绑定操作的最佳位置。

  2. 将事件连接到一个Delay节点,其Duration设置为0,2。由于 PlayerState 在初始化时不可用,等待它可用是快速解决问题的方法。

  3. 添加一个Branch节点,并将其输入执行引脚连接到Delay节点的Completed执行引脚。将Branch节点的False执行引脚连接到Delay节点的输入执行引脚;这将创建一个循环,直到 PlayerState 被正确初始化。

现在我们将从这个 widget 的拥有者恢复 PlayerState。

  1. 在图中添加一个获取拥有玩家节点。此节点返回控制(即,拥有)HUD 的玩家。

  2. 从此节点的返回值引脚,点击并拖动以创建一个Get PlayerState节点。

  3. US_PlayerState类,我们相当确信我们将恢复那种类型的 PlayerState,因此我们不需要担心验证。

  4. Cast To US_PlayerState节点的Success引脚连接到Branch节点的Condition引脚。

  5. 从输出的PlayerState

  6. Branch节点的True执行引脚连接到Set PlayerState的输入执行引脚。

到目前为止创建的可视化脚本代码显示在图 6.9中:

图 6.9 – PlayerState 绑定的第一部分

图 6.9 – PlayerState 绑定的第一部分

现在你已经拥有了PlayerState的引用,是时候将自定义事件绑定到之前章节中创建的委托上了。

  1. US_PlayerState类的输出引脚。

  2. Set PlayerState的输出执行引脚连接到Bind Event to On Xp Changed节点的输入执行引脚。

  3. 从绑定节点的Event引脚,点击并拖动以添加一个Create Event节点。此节点有一个下拉菜单 – 在这里,选择OnXpChanged_Event (NewXp),这将执行OnXpChanged_Event自定义事件,每当系统从 PlayerState 接收到相应的通知时。

  4. Bind Event to On Xp Changed节点的输出执行引脚连接到一个On Xp Changed Event节点;这将初始化时调用事件,以更新 HUD。

  5. Variables部分,拖动一个Get PlayerState节点,并从它创建一个Get Xp节点。将Get Xp节点的输出引脚连接到On Xp Changed Event节点的New Xp引脚。

此部分的 Visual Scripting 代码如图 6.10所示:

图 6.10 – PlayerState 绑定的第二部分

图 6.10 – PlayerState 绑定的第二部分

绑定阶段的最后部分几乎与您刚刚采取的步骤相同,只是我们现在为玩家等级创建绑定。

  1. On Xp Changed Event节点的输出引脚,点击并拖动以创建一个Bind Event to On Character Level Up Up节点。

  2. Variables部分拖动一个Get PlayerState节点并将其连接到Bind Event to On Character Level Up节点的Target引脚。

  3. Bind Event to On Character Level Up节点的Event引脚,点击并拖动以添加一个Create Event节点。从下拉菜单中选择OnCharacterLevelUp_Event (NewLevel)。此选择将在系统从 PlayerState 接收到相应的通知时执行OnCharacterLevelUp_Event自定义事件。

  4. Bind节点的输出执行引脚连接到一个On Character Level Up Event节点;这将初始化时调用事件,以更新 HUD。

  5. Variables部分,拖动一个Get PlayerState节点以创建一个Get Character Level节点。将Get Character Level节点的输出引脚连接到On Character Level Up Event节点的New Level引脚。

此图的最后一部分如图 6.11所示:

图 6.11 – PlayerState 绑定的最后一部分

图 6.11 – PlayerState 绑定的最后一部分

你已经创建好了所有用于监听任何 PlayerState 通知并相应更新 HUD 的绑定。现在是时候添加最后一步 – 在游戏中显示 HUD。

将 HUD 添加到角色上

现在将添加 HUD 到玩家视图中。如果你已经熟悉独立游戏中的 Unreal Engine 用户界面,你可能已经知道如何操作。

然而,你应该意识到,在多人环境中,只有当角色由本地控制(即拥有客户端)时,用户界面小部件才应该附加到游戏视图中。如果你没有检查创建小部件的角色是否由本地控制,你将为级别中生成的每个角色创建一个小部件——包括由其他玩家控制并在客户端复制的角色。显然,在游戏中出现杂乱无章的叠加 HUD 不是你想要看到的情况!

要将 HUD 添加到角色,请按照以下步骤操作:

  1. 首先,找到BP_Character蓝图并打开它。

  2. 在事件图中,找到开始播放事件。然后,将一个分支节点添加到事件的执行引脚。

  3. 分支节点的条件引脚连接到一个本地控制节点——这将确保我们只将 HUD 附加到由客户端控制的角色。

  4. 分支节点的True执行引脚创建一个创建小部件节点。从下拉菜单中选择WB_HUD以选择我们的 HUD。

  5. 创建小部件节点的输出执行引脚连接到添加到视图节点。将返回值引脚连接到目标引脚。

图表的最终结果可以在图 6**.12中看到:

图 6.12 – 将 HUD 添加到视图中

图 6.12 – 将 HUD 添加到视图中

之前的视觉脚本代码相当容易理解,但重要的是要提到,视口只添加到由客户端控制的角色,因为多个 HUD 重叠并不是理想的情况!

现在一切都已经设置妥当,你将测试你的游戏以查看它的工作情况!

测试游戏

要测试游戏,请以监听服务器的形式开始玩游戏,并检查一切是否正常工作。特别是,你应该看到以下行为:

  • 游戏开始时,HUD 应显示 0 经验值和角色等级等于 1

  • 每当角色捡起一枚硬币时,HUD 应更新总经验值

  • 如果达到目标经验值,玩家应升级,并且 HUD 将显示新的等级

最终结果应该与图 6**.13中显示的相当相似:

图 6.13 – 最终 HUD

图 6.13 – 最终 HUD

如果一切按计划进行,你就可以开始进入 Lichlord 多人史诗的下一章:客户端-服务器通信了!

摘要

在本章中,我向您介绍了虚幻引擎多人框架中最重要的话题之一:复制。

作为第一步,你为玩家创建了一些统计数据,以便使你的游戏玩法更加灵活。你是通过结构和数据表来做到这一点的——即使你在开发独立游戏,这个话题也会很有用。

接下来,我解释了属性复制的主题以及如何将其应用到你的项目中。一旦主要概念被定义,你开始在使用它们在 PlayerState 中,以便在游戏过程中跟踪角色的进度。

作为最后一步,你创建了一个 HUD 来向玩家展示进度。在这里,复制功能尤为重要,因为每个客户端都应该获得自己的更新并将其展示给玩家。

在下一章中,你将深入神秘复制的领域,展示你在从客户端到服务器以及返回的微妙艺术中运用技能,仿佛这并非难事。

准备好将事物提升到下一个层次——我们将一步步攀登多人游戏开发的阶梯,每次前进两步!

第七章:使用远程过程调用(RPCs)

现在你已经对属性复制的世界有了牢固的把握,是时候向你介绍函数如何在网络上调用的方式了。在 Unreal 中,这是通过远程过程调用RPCs)实现的——这是引擎网络系统中最强大的功能之一。

在本章中,你将学习如何通过 RPC 执行函数,并理解如何在服务器、客户端或所有具有特定对象实例的客户端上运行它们。

此外,你还将了解正确调用这些类型函数的常见要求——特别是,我将解释可靠函数和不可靠函数之间的区别。

最后,你显然会将这新获得、宝贵的知识应用到迄今为止你开发的项目中。

因此,在接下来的几节中,我将介绍以下主题:

  • 理解 RPC 是什么

  • 在网络上执行 RPCs

  • 实现门系统

技术要求

要跟进本章中介绍的主题,你应该已经完成了前面的章节,并理解了它们的内容。

此外,如果你希望从本书的配套仓库开始,你可以下载本书配套项目仓库中提供的.zip项目文件:

github.com/PacktPublishing/Multiplayer-Game-Development-with-Unreal-Engine-5

你可以通过点击Unreal Shadows – Chapter 06``End链接下载与最后一章结尾一致的文件。

理解 RPC 是什么

RPC是一个可以在本地调用但在不同机器上执行的函数——例如,服务器计算机可以调用客户端计算机上的函数,命令它在关卡中的某个地方生成视觉效果或声音。RPC 的另一个有用应用是能够在服务器和客户端之间通过网络连接双向发送消息。

Unreal Engine 中提供了三种类型的 RPCs:

  • 服务器:该函数将由客户端 PC 上的对象调用,但仅在相同对象的服务器版本上执行。客户端必须拥有调用该方法的对象(如果你需要,请检查第五章在多人环境中管理 Actor,以复习关于拥有 Actor 的内容)。

  • 客户端:该函数将由对象在服务器上调用,但仅在调用该函数的对象所属的客户端版本上执行。

  • NetMulticast:该函数可以由对象在服务器上调用并在服务器和调用该函数的对象的所有客户端版本上执行。它也可以由客户端调用,但在此情况下,它将仅在本地执行(即,在调用它的客户端上)。

为了使一个函数能够作为 RPC 正确执行,它必须由 Actor 调用,并且 Actor 必须是复制的。此外,该函数需要使用 UFUNCTION() 宏进行装饰。

仅在拥有客户端上运行的功能将在 .h 文件中声明,如下代码片段所示:

UFUNCTION(Client)
void DoSomething_Client();

在上一个头文件对应的 .cpp 文件中,你需要使用 _Implementation 后缀来实现这个函数。这个类的自动生成代码位于 .generated.h 文件中,当需要时,它将自动包含对 _Implementation 方法的调用。关于 .generated.h 文件的更多信息,请参阅第四章设置您的第一个 多人游戏环境

看一个例子,假设你的 .h 头文件中的方法声明类似于以下代码片段:

UFUNCTION(Server)
void DoSomething_Server();

你需要在你的 .cpp 文件中实现以下函数:

void DoSomething_Server_Implementation()
{ /* Your code here */ }

由于性能原因,方法并不总是能保证被接收者接收到;然而,这种行为是可以调整的,如下小节所示。

RPC 的可靠性

RPC 默认是不可靠的——这意味着无法保证函数调用能够到达目的地。如果执行代码不是那么重要,例如在客户端生成视觉效果或播放靠近角色的随机噪音,这通常是可接受的;如果消息没有收到,效果将不会生成或声音将不会被听到,但游戏玩法不会受到影响。

然而,有些情况下,你希望强制执行可靠性并确保消息能够安全地到达目的地——例如,在本章中,你将从服务器端执行冲刺动作(你真的不希望失去与玩家的重要交互)。为了确保 RPC 调用在远程机器上执行,你可以使用 Reliable 关键字。

为了说明这一点,一个应该在客户端可靠执行的功能将被以下代码声明:

UFUNCTION(Client, Reliable)
void DoSomethingReliably_Client();

这将保证方法调用将被客户端接收并正确执行,不会因为网络不可靠而存在数据丢失的风险。

注意

避免在 Tick() 事件中使用可靠的 RPC,并且在将它们绑定到玩家输入时要小心。这是因为玩家可以非常快速地重复按按钮,导致可靠 RPC 队列溢出。

除了可靠性之外,你可能还希望有一个方法在执行前进行验证——这正是我现在要向你展示的!

验证 RPCs

Unreal Engine 提供了一个额外的功能,它增加了检查函数是否会在没有坏数据或输入的情况下执行的能力——这就是验证的全部内容。

要声明一个方法应该为 RPC 调用进行验证,你需要在UFUNCTION()声明语句中添加WithValidation指定符,并实现一个额外的函数,该函数将返回bool类型,并以验证函数的名称命名,但带有_Validate后缀。

例如,带有验证的函数在.h文件中的声明将类似于以下代码:

UFUNCTION(Server, WithValidation)
void DoSomethingWithValidation();

然后,在.cpp文件中,你需要实现两个方法。第一个将是常规函数,其代码如下:

void DoSomethingWithValidation_Implementation()
{ /* Your code here */ }

第二个将是实际的验证函数,其代码如下:

bool DoSomethingWithValidation_Validate()
{ /* Your code here */ }

_Validate函数将返回true如果代码已验证,否则返回false。如果验证成功,相应的函数将被执行;否则,它将不会执行。

在本节中,我介绍了 RPC 以及虚幻引擎如何处理它们。请耐心等待——如果你在联网游戏行业工作,掌握 RPC 是保持你的工作和职业发展的关键!

现在你已经对如何实现 RPC 有了坚实的理解,是时候编写一些代码了——我们将从追踪那个讨厌的小虫子开始,它阻止我们的盗贼英雄在地下城中自由(且正确地)冲刺。

在网络上执行 RPC

在本节中,你将通过修复角色正确冲刺的问题来练习使用 RPC。如你所记,当角色在客户端冲刺时,你会得到“跳跃”的行为——角色似乎开始奔跑,但立即被恢复到步行速度。

这是因为冲刺动作是在玩家客户端上执行的,但并没有在服务器上执行,而服务器才是掌握控制权的一方;因此,来自服务器的覆盖操作会在每次更新时将角色移动速度减慢。这意味着你试图以冲刺速度移动你的角色,但一旦服务器在客户端复制了移动动作,它就会将角色速度恢复到正常移动速度。

我们甚至不希望客户端控制这种重要交互——记住,服务器才是掌握控制权的一方——所以,回到项目中,开始编写一些代码来修复这个问题!

在服务器上调用函数

要让我们的角色跑步,我们只需在服务器上执行移动速度的改变,而不是在客户端。这将确保对行为有完全的控制,并在所有客户端上正确复制。

让我们从打开US_Character.h文件并进行一些代码声明开始。在protected部分,添加以下两个方法:

UFUNCTION(Server, Reliable)
void SprintStart_Server();
UFUNCTION(Server, Reliable)
void SprintEnd_Server();

这些函数具有Server属性,正如前一小节所述,它将在服务器上执行它们。我们还添加了Reliable属性,因为我们不希望由于系统的默认不可靠性而丢失这个 RPC。_Server后缀不是强制性的,只是为了清晰(有些人使用前缀,所以这取决于个人喜好!)。

现在打开US_Character.cpp文件,通过添加以下代码来实现这两个函数:

void AUS_Character::SprintStart_Server_Implementation()
{
 if (GetCharacterStats())
 {
  GetCharacterMovement()->MaxWalkSpeed = GetCharacterStats()->SprintSpeed;
 }
}
void AUS_Character::SprintEnd_Server_Implementation()
{
 if (GetCharacterStats())
 {
  GetCharacterMovement()->MaxWalkSpeed = GetCharacterStats()->WalkSpeed;
 }
}

代码相当简单,因为我们只是在两个新函数中执行速度更改,并且很快,我们将从它们之前的位置(即客户端调用)中移除它们。

这里,请注意_Implementation后缀——这是强制性的,因为SprintStart_Server()SprintEnd_Server()函数将由 Unreal 在.generated.h类文件中自动生成,并负责调用实际实现。

我们现在需要更改SprintStart()SprintEnd()函数,以便调用相应的服务器函数(即SprintStart_Server()SprintEnd_Server())。找到这两个函数,删除它们的所有内容(即对MaxWalkSpeed的更改),然后在SprintStart()函数中添加以下简单代码行:

SprintStart_Server();

SprintEnd()函数中添加以下代码行:

SprintEnd_Server();

要使冲刺动作完全可用,我们需要采取最后一步。目前,如果角色正在奔跑并升级,移动速度将恢复到步行速度。这是因为,在UpdateCharacterStats()函数中,我们将MaxWalkSpeed属性设置为新的步行速度,即使角色正在冲刺。

让我们通过找到UpdateCharacterStats()方法并在其开头添加以下代码来解决这个问题:

auto IsSprinting = false;
if(GetCharacterStats())
{
 IsSprinting = GetCharacterMovement()->MaxWalkSpeed == GetCharacterStats()->SprintSpeed;
}

这段代码只是检查角色是否在冲刺,并将结果存储在局部变量中。

然后,找到以下代码行:

GetCharacterMovement()->MaxWalkSpeed = GetCharacterStats()->WalkSpeed;

在它之后添加以下命令:

if(IsSprinting)
{
 SprintStart_Server();
}

虽然很简单,但如果角色正在冲刺,我们只需在服务器上调用相应的方法来正确更新一切。

我们几乎完成了移动管理,但还有一些小事情需要我们处理。不过别担心,我们正在努力确保在第十章,“增强玩家体验”结束前完成一切。所以请耐心等待,保持关注!

在本节中,你已经开始在你的角色类中实现简单的 RPC。具体来说,你从拥有角色的客户端发送了一个命令到服务器,以便正确更新移动速度。

在下一节中,你将为你的游戏添加一些更复杂的 RPC。特别是,你将开发一个巧妙的开门系统。准备好展示你的编程技能吧!

实现门系统

在本节中,你将重复一些之前解释过的关于 RPC 的主题,但会有一个小调整——你将在网络上开发一些 Actor 到 Actor 的通信。更重要的是,它将在一个 C++ 类——你的角色——和一个蓝图类——一个应该被打开的门——之间进行。

为了实现这种行为,你将使用你在 第四章 中创建的功能,即 设置您的第一个多人游戏环境——交互动作。由于你至今为止开发的所有内容,它可能已经从你的脑海中溜走了,但不用担心——现在是时候把它翻出来,再次投入使用。

创建交互式界面

为了在您的角色和门之间建立通信,您将使用一个 接口。正如你可能已经知道的,C++ 中的接口是创建不同类之间抽象的强大工具。它们允许你定义一个所有实现类都必须遵守的合同,从而允许你创建更易于维护、扩展和重用的代码。

在 Unreal Engine 中,接口与传统编程接口的不同之处在于,不是必须实现所有函数。相反,实现它们是可选的。更重要的是,你可以在 C++ 中声明一个接口,并在蓝图(Blueprint)中实现它——这正是你在这里要做的。

让我们从打开您的开发 IDE 并创建一个名为 US_Interactable.h 的文件开始。然后,将以下代码添加到该文件中:

#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "US_Interactable.generated.h"
UINTERFACE(MinimalAPI, Blueprintable)
class UUS_Interactable : public UInterface
{
 GENERATED_BODY()
};
class UNREALSHADOWS_LOTL_API IUS_Interactable
{
 GENERATED_BODY()
public:
 UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Interaction", meta=(DisplayName="Interact"))
 void Interact(class AUS_Character* CharacterInstigator);
 UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Interaction", meta=(DisplayName="Can Interact"))
 bool CanInteract( AUS_Character * CharacterInstigator) const;
};

你可能会注意到你刚刚添加的代码中有些奇怪的地方——有两个类。为了正确地声明 Unreal Engine 接口,你需要声明两个类:

  • 带有 U 前缀的类并扩展 UInterface:这并不是实际的接口,而是一个空类,其唯一目的是使该类在 Unreal Engine 系统中可见

  • 带有 I 前缀的类:这是实际的接口,将包含所有接口方法定义

如你所见,带有 U 前缀的类被 UINTERFACE() 宏和 Blueprintable 属性装饰,这将允许你从蓝图(Blueprint)中实现此接口。这不是很酷吗?

最后,我们声明了两个函数,分别命名为 Interact()CanInteract()。这两个函数可以在蓝图(Blueprint)中调用和实现(多亏了 BlueprintCallableBlueprintNativeEvent 属性)。

尽管我们不会在我们的门蓝图(Blueprint)中实现第二个函数(即 CanInteract()),但拥有这样的功能是很好的——例如,检查角色是否可以使用在地下某个地方找到的钥匙打开门。正如我之前告诉你的,Unreal Engine 中的接口不会强制实现所有方法声明。

因此,你已经创建了一个接口,允许角色与...好吧,与某物交互。现在是时候让盗贼角色执行这个英勇行为——你将在下一个子节中实现的行为。

实现交互动作

现在,你准备好回到US_Character.h头文件类,并为交互动作添加一些代码逻辑。因为我们已经为角色移动做了,我们需要从服务器执行这个交互。

为了做到这一点,打开头文件,并在protected部分查找以下声明:

void Interact(const FInputActionValue& Value);

在它之后添加相应的服务器调用:

UFUNCTION(Server, Reliable)
void Interact_Server();

至于冲刺动作,这个调用必须是Reliable的,因为我们需要确保它将被正确执行,并且不会丢失任何信息。

作为最后一步,将以下代码行添加到private部分:

UPROPERTY()
AActor* InteractableActor;

你将使用这个属性作为与对象交互的引用。

现在头文件已经正确更新,打开US_Character.cpp文件,在文件开头添加以下包含语句:

#include "US_Interactable.h"
#include "Kismet/KismetSystemLibrary.h"

然后,查找到目前为止只是一个空壳的Interact()方法。在方法内部,添加以下代码:

Interact_Server();

此代码执行一个简单的 RPC 调用到相应的服务器交互实现。显然,你需要实现服务器调用,将其添加到你的代码中,紧随Interact()函数之后:

void AUS_Character::Interact_Server_Implementation()
{
 if(InteractableActor)
 {
  IUS_Interactable::Execute_Interact(InteractableActor, this);
 }
}

仅当找到InteractableActor的引用时,才会执行此调用。

如果你来自面向对象编程(OOP)背景,并且不熟悉 Unreal 中接口的工作方式,这个调用可能看起来相当奇怪——我们在没有类型检查的情况下对 Actor 引用执行调用!这是 Unreal Engine 中接口工作的方式;它们只是发送给对象引用的消息。如果对象没有实现该接口,调用将简单地丢失。

显然,我们希望调用执行到可以与之交互的东西(即实现了US_Interactable接口的东西)。为了实现这一点,我们将持续检查角色是否指向实现了该接口的任何东西,如果找到,我们将在InteractableActor属性中引用它。

在你的.cpp类中查找Tick()方法,并开始添加以下代码片段:

if(GetLocalRole() != ROLE_Authority) return;
FHitResult HitResult;
FCollisionQueryParams QueryParams;
QueryParams.bTraceComplex = true;
QueryParams.AddIgnoredActor(this);
auto SphereRadius = 50.f;
auto StartLocation = GetActorLocation() + GetActorForwardVector() * 150.f;
auto EndLocation = StartLocation + GetActorForwardVector() * 500.f;
auto IsHit = UKismetSystemLibrary::SphereTraceSingle(
 GetWorld(),
 StartLocation,
 EndLocation,
 SphereRadius,
 UEngineTypes::ConvertToTraceType(ECC_WorldStatic),
 false,
 TArray<AActor*>(),
 EDrawDebugTrace::ForOneFrame,
 HitResult,
 true
);

我们在这里做的第一件事是检查执行跟踪的实例是否有权这样做——这意味着只有服务器将为所有角色执行跟踪,这显然是为了避免客户端作弊。

然后,我们执行一个常规的球体跟踪来检查角色是否指向某个东西。如果你不熟悉 Unreal Engine 中的跟踪,它是一种用于检测对象之间碰撞和重叠的工具。它用于诸如视线、武器发射甚至 AI 路径查找等事物。跟踪可以通过诸如碰撞通道、对象类型过滤、形状、起点/终点等参数进行配置,这允许你指定应该检测哪种类型的碰撞以及它应该如何与环境交互。

注意

更多关于虚幻引擎中追踪内部工作原理的信息,您可以查看官方文档,链接如下:docs.unrealengine.com/5.1/en-US/traces-with-raycasts-in-unreal-engine/

在追踪后,结果存储在HitResult变量中,我们将使用它来检查我们是否找到了可交互的 Actor。为此,在您刚刚编写的代码之后添加以下代码:

if (IsHit && HitResult.GetActor()->GetClass()->ImplementsInterface(UUS_Interactable::StaticClass()))
{
 DrawDebugSphere(GetWorld(), HitResult.ImpactPoint, SphereRadius, 12, FColor::Magenta, false, 1.f);
 InteractableActor = HitResult.GetActor();
}
else
{
 InteractableActor = nullptr;
}

之前的检查是我们交互控制的核心 - 如果被追踪的对象实现了US_Interactable接口,我们存储引用并绘制一个洋红色调试球体以进行测试。如果没有找到任何东西,我们只需从任何以前的引用中清理InteractableActor属性。

为了检查一切是否按预期工作,您可以打开虚幻引擎编辑器,编译后,您可以玩游戏。现在服务器应该为每个角色绘制一个红色球体轨迹,当击中物体时变为绿色。我们还没有任何可以与之交互的东西,所以您不会看到调试球体。

在下一个子节中,您将实现一个对角色交互做出反应的门蓝图。

创建门蓝图

现在是时候创建可以与之交互的东西了,我们将通过添加一些门到地牢中来实现这一点。所以,让我们打开Blueprints文件夹并完成以下步骤:

  1. 创建一个新的从Actor派生的蓝图类,命名为BP_WoodenDoor,并打开它。

  2. 细节面板中,勾选复制属性以启用此 Actor 的复制。

  3. 添加一个静态网格组件,并将门网格分配给静态****网格属性。

  4. 组件面板中,选择静态网格组件,然后在细节面板中,勾选组件复制以启用复制。

最终结果应该类似于图 7.11*所示:

图 7.1 – 木门蓝图

图 7.1 – 木门蓝图

现在,打开事件图,执行以下操作:

  1. 创建一个类型为DoorOpen的变量。在其细节面板中,将复制属性设置为已复制

  2. 选择类设置选项卡,然后在接口类别中添加US_Interactable接口。这将向我的****蓝图窗口添加接口部分。

  3. 我的蓝图标签页的接口部分,打开交互类别,右键单击交互方法,并选择实现事件。这将向事件图添加一个事件交互节点。

  4. 从事件的输出引脚添加一个分支节点,并在条件引脚中添加DoorOpen变量的获取节点,该变量位于变量部分。

  5. 分支节点的False引脚连接到Door Open变量的Setter节点,并检查此最后一个节点的输入值引脚以将其设置为True

到目前为止创建的事件图在图 7.2中显示:

图 7.2 – 门检查

图 7.2 – 门检查

目前的图表相当简单;它只是检查门是否已经打开,如果它是关闭的,则将其标记为打开。你将通过使门网格在开启动画中旋转来完成蓝图。

  1. Set Door Open节点的输出引脚连接到时间线节点。将此节点命名为DoorOpening,并双击它以打开其相应的图表。

  2. 添加一个浮点轨迹并将其命名为RotationZ。在轨迹上添加两个键,分别具有值(0, 0)(1, -90)

时间线窗口在图 7.3中显示:

图 7.3 – 门时间线窗口

图 7.3 – 门时间线窗口

  1. 返回主事件图,并将静态网格组件的引用从组件面板拖动到图中本身。

  2. 将此引用的输出引脚连接到设置相对旋转节点。

  3. 右键单击新旋转引脚并选择拆分结构引脚以暴露新旋转Z值。

  4. 时间线节点的更新执行引脚连接到设置相对旋转节点的输入执行引脚。将Rotation Z引脚连接到新旋转 Z以完成图表。

图表的最后部分在图 7.3中描述:

图 7.4 – 图表的第二部分

图 7.4 – 图表的第二部分

图表的这一部分将仅在网格的z轴上启动旋转动画,使其在交互时打开。

现在让我们给盗贼英雄一个展示的时刻,让他自由地在地牢中游荡,急切地打开门,寻找要解放的囚犯和要挖掘的宝藏!

测试交互动作

打开你的游戏关卡,并将门蓝图的一个或两个实例拖动到图中开始测试游戏。每当服务器控制的球体追踪击中门时,你应该能看到一个洋红色的球体,表示该对象可以交互。在客户端按下I键将打开门并显示隐藏的宝藏(或危险!)。

交互检查的最终结果,连同调试球体,在图 7.4中显示:

图 7.5 – 交互检查的实际操作

图 7.5 – 交互检查的实际操作

因此,门系统最终已经创建,你现在可以自由地在地牢中放置尽可能多的门。作为额外的练习,你可以从BP_WoodenDoor创建一个蓝图子类,并使用door_gate网格为你的关卡添加一些变化。

在本节的最后部分,你已经实现了一个蓝图,允许角色与游戏中的其他 Actors 进行交互。具体来说,你创建了一个可以通过玩家交互打开的门系统,并且将在网络上进行同步。这意味着每个连接的玩家都将看到正确的更新。

概述

在本章中,你被介绍到了虚幻引擎多人环境中最重要且最有用的功能之一,远程过程调用,或称 RPC。正如你所见,它们允许你从服务器执行函数到客户端,反之亦然。

在本章中,你通过改进角色冲刺系统和在角色与其他游戏中的 Actors(例如,地牢门)之间添加交互逻辑,从客户端向服务器发起请求。请放心,到本书结束时,你也将看到 RPC 的其他用例,因为它们在多人游戏中非常普遍。

本章结束了本书的第二部分——从下一章开始,你将开始在网络中实现一些 AI 逻辑。让我们通过总结那些讨厌的 Lichlord 小兵,并给我们的角色一个挑战来提升自己来增加一些趣味吧!

第三部分:提升你的游戏

在本书的这一部分,你将发现如何增强你的游戏对玩家的吸引力。这个过程从创建吸引人的对手开始。之后,你将增强玩家角色的能力,并添加一些非玩家角色进行交互。此外,你还将学习如何在虚幻引擎中调试和修复网络系统。

本部分包括以下章节:

  • 第八章将 AI 引入多人环境

  • 第九章扩展 AI 行为

  • 第十章增强玩家体验

  • 第十一章调试多人游戏

第八章:将人工智能引入多人游戏环境

人工智能AI)系统通过提供不可预测和吸引人的动态挑战,为玩家提供令人兴奋和独特的游戏体验。这允许开发者创建具有逼真行为的沉浸式世界,来自非玩家****角色NPC)。

在本章中,我将向您介绍虚幻引擎中人工智能的基础知识,但由于这是一本关于多人游戏的书籍,我将不会深入探讨系统的细节——相反,你将迈出创建对手的第一步,这将使你的游戏从网络角度完全可玩。

到本章结束时,你将创建一个敌人演员,它在关卡中四处游荡,一旦检测到玩家角色就会积极追击。这将成为你在游戏中创建更多样化和引人入胜的敌人的起点。

因此,在本章中,我将向您介绍以下主题:

  • 设置人工智能系统

  • 创建人工智能对手

  • 向关卡添加对手

技术要求

要跟随本章中介绍的主题,你应该已经完成了前面的章节,并理解了它们的内容。

此外,如果你希望从本书的配套仓库开始编写代码,你可以下载本书配套项目仓库中提供的.zip项目文件:

github.com/PacktPublishing/Multiplayer-Game-Development-with-Unreal-Engine-5

您可以通过点击Unreal Shadows – Chapter 07``End链接下载与上一章结尾相符的最新文件。

设置人工智能系统

在虚幻引擎中制作人工智能对手可能是一项相当困难的任务。幸运的是,这本书专注于提升你的多人游戏技巧,而不是陷入人工智能的所有细节,所以我将不会深入探讨虚幻引擎的人工智能系统。然而,如果你想使你的游戏有趣,了解如何创建一个值得尊敬的人工智能对手肯定有帮助。

为了让人工智能角色在关卡中移动,你需要定义哪些区域是允许的,哪些是不允许的(例如,你肯定需要给角色一个安全的地方,对手不敢踏入)。一旦我们完成了这个,在接下来的步骤中,我们将创建这些区域,以便人工智能系统可以管理小兵的行走路径。一旦我们完成了这个,我们就会朝着创建令人惊叹的、无意识的、行走的亡灵迈出坚实的步伐。

要使一切正常工作,首先,我们需要给我们的小兵对手一些可以行走的地方。正如你可能已经知道的,虚幻引擎使用导航系统让 AI 演员使用路径查找算法在关卡中导航。

导航系统将您级别中的碰撞几何形状生成一个导航网格,然后将其分割成部分(即多边形几何形状),这些部分用于创建一个图。这个图是代理(如 AI 角色)用来导航到其目的地的东西。每个部分都会被赋予一个成本,然后代理使用这个成本来计算最有效的路径(成本最低的路径)。这就像为您的游戏角色提供了一个智能 GPS!

注意

如果您想了解更多关于虚幻引擎导航系统及其内部工作原理的信息,您可以访问以下链接的官方 Epic Games 文档:docs.unrealengine.com/5.1/en-US/navigation-system-in-unreal-engine/.

要将导航网格添加到级别中,您需要执行以下步骤:

  1. 打开您迄今为止一直在工作的游戏级别,并从快速添加到项目按钮中选择NavMeshBoundsVolume。这将向级别添加NavMeshBoundsVolume组件和一个RecastNavMesh Actor。

  2. 大纲中,选择NavMeshBoundsVolume,并启用缩放工具,将其大小调整为覆盖您想要的级别部分——避免玩家角色的出生区域,因为您希望他们有一个安全的地方休息或必要时逃离。

  3. 按下键盘上的P键以显示新创建的导航网格,它应该看起来类似于图 8**.1中描述的:

图 8.1 – 导航网格区域

图 8.1 – 导航网格区域

绿色区域(即导航网格)表示 AI 角色可以行走的地方。您会注意到墙壁和门会在该网格中形成“洞”,因此 AI 将无法进入。不用担心您地牢外的部分——没有开放的门将它们连接起来,所以小兵无法到达那里。

在本节中,您简要介绍了虚幻引擎导航系统,并为即将创建的 AI 对手设置了一个可导航的区域。鉴于您急切地想要开始编码,让我们启动您的编程 IDE 并一起编写一些代码!是时候召唤一些亡灵小兵,让他们在地牢中四处走动了。

创建 AI 对手

在本节中,您将开始为您的英雄的敌人创建一个类,包括基本的巡逻和攻击能力。当然,他们可能不是最聪明的人,但嘿,他们是巫妖王的亡灵小兵——并不以他们的智慧著称,对吧?

我们将首先扩展 Character 类,如您可能已经知道的,它可以由AIController控制,允许在游戏过程中进行独立操作。

在这一点上,我们希望小兵具有以下功能:

  • 在该层周围进行随机巡逻移动

  • 一个感知系统,将允许它看到和听到玩家的角色

  • 一旦检测到玩家,就有能力去寻找它

在接下来的章节中,我们将通过添加更多功能(如健康和可召唤的物品,当 AI 被击败时)来进一步扩展 Character 类,但到目前为止,我们只会关注移动和感知系统。

添加导航模块

为了让代理能够通过导航网格导航,首先的事情是向你的项目中添加相应的模块。

要做到这一点,回到你的编程 IDE 并打开你的项目构建文件——名为 UnrealShadows_LOTL.Build.cs 的文件(或者如果你选择了不同的项目名称,则类似)。找到以下代码行:

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput" });

通过添加 NavigationSystem 声明来更改它,如下所示:

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "NavigationSystem" });

更新了项目设置后,我们可以开始着手处理 minion AI,通过创建一个专门的类。

创建 minion 类

是时候创建 AI minion 类了,因此创建一个从 Character 派生的新的类,并将其命名为 US_Minion。一旦类创建完成,打开 US_Minion.h 头文件,在 private 部分,添加以下代码:

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Minion Perception", meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UPawnSensingComponent> PawnSense;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Minion Perception", meta = (AllowPrivateAccess = "true"))
TObjectPtr<class USphereComponent> Collision;
UPROPERTY()
FVector PatrolLocation;

Collision 属性将用作 AI 抓取角色的触发器,而 PatrolLocation 将用于告诉 AI 如果没有追捕角色,它应该去哪里。

PawnSense 属性是 PawnSensingComponent 的声明,这是一个 AI 角色可以用来观察和听到关卡周围棋子的组件(即玩家角色)。这个组件使用起来相当简单,并且易于配置,让你在游戏过程中可以调整对手的“愚蠢”程度。你将在初始化它的一两分钟后得到关于它的更多信息。

现在是时候在 public 部分添加一些属性了。只需添加以下代码:

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Minion AI")
float PatrolSpeed = 150.0f;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Minion AI")
float ChaseSpeed = 350.0f;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Minion AI")
float PatrolRadius = 50000.0f;

我们定义了两个移动速度属性:PatrolSpeed 将在 minion 无目的地四处走动时使用,而 ChaseSpeed 将在 minion 寻找角色时使用,以便让它成为 Lichlord 军队的新棋子!PatrolRadius 属性将用于在关卡中为 minion 找到新的检查位置。

在属性之后,你将声明 AI 对手正确行为所需的公共方法。仍然在 public 部分,添加以下代码块来声明它们:

UFUNCTION(BlueprintCallable, Category="Minion AI")
void SetNextPatrolLocation();
UFUNCTION(BlueprintCallable, Category="Minion AI")
void Chase(APawn* Pawn);
virtual void PostInitializeComponents() override;
FORCEINLINE UPawnSensingComponent* GetPawnSense() const { return PawnSense; }
FORCEINLINE USphereComponent* GetCollision() const { return Collision; }

SetNextPatrolLocation()Chase() 方法将被用来让 AI 角色在场景中移动,寻找新的位置或寻找玩家角色。PostInitializeComponent() 覆盖将用于注册角色事件。最后,我们声明了已添加的字符组件的常用获取器。

在头文件声明中的最后一步是添加此角色的事件处理器:

UFUNCTION()
void OnPawnDetected(APawn* Pawn);
UFUNCTION()
void OnBeginOverlap(AActor* OverlappedActor, AActor* OtherActor);

第一个将管理 minion 逻辑,一旦它通过感官检测到棋子,而第二个将用于检查是否有玩家角色被捕获。

标头已经声明完毕——请注意,目前我们并没有考虑小兵的听觉能力;这是我们将在下一章中实现的内容,当我们的盗贼英雄开始制造一些噪音时!

实现小兵的行为

你已经声明了所有你的函数和属性,现在是时候通过实现一些 AI 小兵的行为来充分利用它们了。让我们确保一切运行顺利,并让这个项目启动起来!

打开US_Minion.cpp文件,并在顶部添加以下include语句:

#include "AIController.h"
#include "NavigationSystem.h"
#include "US_Character.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Perception/PawnSensingComponent.h"
#include "Blueprint/AIBlueprintHelperLibrary.h"
#include "Components/SphereComponent.h"

如同往常,这些代码行将声明我们将从现在开始使用的类。在你完成这些之后,现在是时候实现构造函数了,通过添加所需组件并初始化所有属性。

声明构造函数

一旦include语句被正确声明,你可以开始定位AUS_Minion()构造函数,并插入角色初始化代码。在括号内,紧接在PrimaryActorTick.bCanEverTick声明之后,添加以下代码:

bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
AIControllerClass = AAIController::StaticClass();
PawnSense = CreateDefaultSubobject<UPawnSensingComponent>(TEXT("PawnSense"));
PawnSense->SensingInterval = .8f;
PawnSense->SetPeripheralVisionAngle(45.f);
PawnSense->SightRadius = 1500.f;
PawnSense->HearingThreshold = 400.f;
PawnSense->LOSHearingThreshold = 800.f;
Collision = CreateDefaultSubobject<USphereComponent>(TEXT("Collision"));
Collision->SetSphereRadius(100);
Collision->SetupAttachment(RootComponent);
GetCapsuleComponent()->InitCapsuleSize(60.f, 96.0f);
GetCapsuleComponent()->SetGenerateOverlapEvents(true);
GetMesh()->SetRelativeLocation(FVector(0.f, 0.f, -91.f));
static ConstructorHelpers::FObjectFinder<USkeletalMesh> SkeletalMeshAsset(TEXT("/Game/KayKit/Skeletons/skeleton_minion"));
if (SkeletalMeshAsset.Succeeded())
{
 GetMesh()->SetSkeletalMesh(SkeletalMeshAsset.Object);
}
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f);
GetCharacterMovement()->MaxWalkSpeed = 200.f;
GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;

你将熟悉从盗贼角色创建中大部分的代码,但你将看到一些明显的添加。首先,我们正在设置AutoPossessAI属性,这让我们可以定义游戏系统是否会在关卡中占用 AI 角色一次——我们希望它在运行时生成时以及当游戏开始时已经在关卡中时都能完全控制,所以我们选择了PlacedInWorldOrSpawned的值。

然后,我们通过设置AIControllerClass属性来定义将用于 AI 系统的控制器;在这种情况下,我们只是使用基础的AAIController类,但你显然可以添加更多功能来自定义实现。

最后一个值得注意的是PawnSense组件的创建——正如你所见,我们正在初始化将使小兵在一定距离内看到和听到的属性。你应该注意SensingInterval的初始化,这将让我们调整两次感知之间经过的时间。这将决定一个非常反应灵敏的角色(即,较低的值)或一个非常愚蠢的角色(即,较高的值)。

初始化小兵

现在是时候在角色被添加到游戏中时初始化角色了。正如你所知,这通常是通过BeginPlay()方法完成的。所以,在Super::BeginPlay()声明之后,添加以下内容:

SetNextPatrolLocation();

这个调用将简单地启动巡逻行为。然后,通过向文件中添加以下代码来实现PostInitializeComponents()

void AUS_Minion::PostInitializeComponents()
{
 Super::PostInitializeComponents();
if(GetLocalRole() != ROLE_Authority) return;
 OnActorBeginOverlap.AddDynamic(this, &AUS_Minion::OnBeginOverlap);
 GetPawnSense()->OnSeePawn.AddDynamic(this, &AUS_Minion::OnPawnDetected);
}

如你所见,我们正在使用两个委托来响应 Actor 重叠,用于检查我们是否到达了玩家角色,以及处理 Pawn 感知以检查我们是否可以看到玩家角色。注意,它们仅在对象的角色为权威角色(即,该方法是在服务器上执行的)时才初始化。

下一步是实现这两个代理函数,以管理上述事件。

处理代理函数

无论小兵检测到棋子,它都会立即检查它是否是一个角色,如果结果是成功的,它就会开始追逐它。让我们在源文件中添加处理代理的方法:

void AUS_Minion::OnPawnDetected(APawn* Pawn)
{
 if (!Pawn->IsA<AUS_Character>()) return;
 GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("Character detected!"));
 if (GetCharacterMovement()->MaxWalkSpeed != ChaseSpeed)
 {
  Chase(Pawn);
 }
}

这里的代码相当简单——我们只是添加了一条调试信息,表明检测到了一个角色。

我们需要处理的第二个代理是重叠,因此添加以下方法实现:

void AUS_Minion::OnBeginOverlap(AActor* OverlappedActor, AActor* OtherActor)
{
 if (!OtherActor->IsA<AUS_Character>()) return;
 GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Yellow, TEXT("Character captured!"));
}

如您所见,我们再次检查是否找到了一个角色,然后简单地显示一个调试信息——看起来我们的英雄离小兵太近了,现在他们已经被套上了绳索,加入了巫妖王的亡灵军队!稍后,您将实现一个重生系统,让玩家能够用全新的角色重新开始游戏。

下一步将是 AI 在导航网格中的实际移动,包括巡逻和追逐行为。

实现追逐和巡逻行为

现在是时候开始实现你的 AI 角色的运动控制了——具体来说,你将实现SetNextPatrolLocation()函数,该函数将为小兵找到一个新的可到达点,以及Chase()函数,该函数将小兵派往一个“寻找并摧毁”任务,目标是角色。为此,请将以下代码添加到文件中:

void AUS_Minion::SetNextPatrolLocation()
{
 if(GetLocalRole() != ROLE_Authority) return;
 GetCharacterMovement()->MaxWalkSpeed = PatrolSpeed;
 const auto LocationFound = UNavigationSystemV1::K2_GetRandomReachablePointInRadius(
     this, GetActorLocation(), PatrolLocation, PatrolRadius);
 if(LocationFound)
 {
  UAIBlueprintHelperLibrary::SimpleMoveToLocation(GetController(), PatrolLocation);
 }
}
void AUS_Minion::Chase(APawn* Pawn)
{
 if(GetLocalRole() != ROLE_Authority) return;
 GetCharacterMovement()->MaxWalkSpeed = ChaseSpeed;
 UAIBlueprintHelperLibrary::SimpleMoveToActor(GetController(), Pawn);
 DrawDebugSphere(GetWorld(), Pawn->GetActorLocation(), 25.f, 12, FColor::Red, true, 10.f, 0, 2.f);
}

第一个函数将角色速度设置为巡逻值,并使用UNavigationSystemV1::K2_GetRandomReachablePointInRadius()方法在导航网格中找到一个可到达的点。然后,AI 被简单地命令到达那个位置。

第二个函数做类似的事情,但目标点将是角色——毕竟,它肩负着从巫妖王那里获取尽可能多的即将成为亡灵的英雄的任务!

实现 Tick()事件

为了使巡逻系统完全运行,你需要实现最后一件事情,即检查 AI 角色是否已到达目的地;在这种情况下,它只需在导航网格中找到另一个点。由于我们需要持续检查 AI 和目标点之间的距离,编写代码的最佳位置是在Tick()事件中。让我们找到这个方法,并在Super::Tick(DeltaTime)调用之后,添加以下代码:

if(GetLocalRole() != ROLE_Authority) return;
if(GetMovementComponent()->GetMaxSpeed() == ChaseSpeed) return;
if((GetActorLocation() - PatrolLocation).Size() < 500.f)
{
 SetNextPatrolLocation();
}

如您所见,第一行检查角色是否在巡逻(即最大速度不应等于追逐速度)。然后,我们检查我们是否足够接近巡逻位置(大约半米),以便寻找另一个可到达的点。

测试 AI 对手

现在敌人 AI 已经创建,你可以在游戏级别中测试它。为此,打开 Unreal Engine 编辑器,从内容浏览器中拖动US_Minion类的一个实例(位于C++ Classes | UnrealShadows_LOTL文件夹)到级别中。你应该看到类似于图 8**.2的类似内容:

图 8.2 – 级别中的 AI 对手

图 8.2 – 级别中的 AI 对手

围绕角色的工具代表PawnSense组件——它的视线和听觉能力。视线区域由一个绿色圆锥体表示,显示了 AI 可以看到的宽度和距离。听觉感知由两个球体表示——一个黄色的球体表示如果没有障碍物阻挡,AI 可以听到多远的声音,一个青色的球体表示即使声音在障碍物后面生成,AI 也能感知多远的声音,例如墙壁。

进入游戏模式,对手应该开始在级别和导航网格中四处游荡。每当玩家角色进入小兵的视线范围内(即,绿色圆锥体),敌人就会做出反应,并以更高的速度开始追逐玩家。

一旦角色到达,你会注意到小兵会停止移动——它的任务已经完成,它可以休息了!

作为额外的练习,你可能想要添加一个计时器来检查 AI 是否停留得太久;如果是这样,它将通过寻找一个新的可到达位置来重新启动其巡逻系统。

因此,在本节中,你已经创建了你的 AI 对手,准备好在地下城中四处游荡,寻找下一个受害者。你已经创建了一个简单但有效的巡逻系统,并为 AI 添加了感知能力,以便在玩家不够隐秘时拦截他们。

在下一节中,你将创建一个生成系统,以便在游戏进行过程中添加小兵,并为玩家制造更多的挑战。

向级别添加对手

现在你已经为你的盗贼英雄找到了对手,是时候让系统在运行时生成一大群它们了。你将通过实现与第三章中使用的类似生成系统——这次,你将在 C++中创建生成器。

我们在这里想要实现的是一个具有以下功能的 Actor:

  • 在游戏开始时生成几个小兵。

  • 在预定义的间隔生成新的小兵。

  • 在选定的区域内生成小兵。

  • 每次生成时随机选择一个小兵类型。目前我们只有一个小兵类型,但在接下来的章节中,我们将添加更多变体。

让我们开始吧。

创建一个生成器类

首先创建一个从 Actor 扩展的 C++类,命名为US_MinionSpawner。创建后,打开.h文件,在private部分添加以下声明:

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spawn System", meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UBoxComponent> SpawnArea;
UPROPERTY()
FTimerHandle SpawnTimerHandle;

你应该已经熟悉了来自 第三章 的第一个声明,使用项目原型测试多人游戏系统 – 我们正在声明一个将用于随机生成随从位置的区域。第二个声明将用于存储生成器使用的计时器处理程序引用,以在预定间隔生成新的随从。

现在,我们将声明一些属性,使这个类在关卡中可定制。在 public 部分添加以下属性声明:

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Spawn System")
TArray<TSubclassOf<class AUS_Minion>> SpawnableMinions;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Spawn System")
float SpawnDelay = 10.0f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Spawn System")
int32 NumMinionsAtStart = 5;

第一个属性将公开一个包含所有可生成随从类型的数组。如前所述,目前我们只有一个类型,但稍后我们会添加更多。其他两个属性是自解释的,让我们能够定义生成时间和游戏开始时应该在关卡中存在的随从数量。

最后一步是在头文件的保护部分添加一个 Spawn() 方法:

UFUNCTION()
void Spawn();

现在头文件已经完成。现在,让我们切换到 .cpp 文件并实现一些代码逻辑。

实现生成器逻辑

是时候实现生成器的功能了。为了做到这一点,打开 .cpp 文件,找到构造函数,并在文件顶部添加所需的包含项:

#include "US_Minion.h"
#include "Components/BoxComponent.h"

然后,添加以下代码片段:

SpawnArea = CreateDefaultSubobject<UBoxComponent>(TEXT("Spawn Area"));
SpawnArea->SetupAttachment(RootComponent);
SpawnArea->SetBoxExtent(FVector(1000.0f, 1000.0f, 100.0f));

你已经非常熟悉创建组件,所以让我们直接进入 BeginPlay() 方法,并在 Super::BeginPlay() 声明之后添加这段代码:

if(SpawnableMinions.IsEmpty()) return;
if(GetLocalRole() != ROLE_Authority) return;
for (int32 i = 0; i < NumMinionsAtStart; i++)
{
 Spawn();
}
GetWorldTimerManager().SetTimer(SpawnTimerHandle, this, &AUS_MinionSpawner::Spawn, SpawnDelay, true, SpawnDelay);

首先,我们检查至少有一个可生成的随从类型 – 如果数组为空,就没有必要继续执行代码。然后,我们检查角色是否有生成东西的权限;像往常一样,我们希望服务器完全控制正在发生的事情。

之后,我们通过循环调用 Spawn() 函数,以创建一个敌人的起始池。最后一步是创建一个计时器,该计时器将在 SpawnDelay 值定义的间隔内调用 Spawn() 函数。

要使生成器完全功能,最后一步是添加 Spawn() 函数的实现。让我们把它添加到文件的末尾:

void AUS_MinionSpawner::Spawn()
{
 FActorSpawnParameters SpawnParams;
 SpawnParams.SpawnCollisionHandlingOverride =
 ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButDont SpawnIfColliding;
 auto Minion =
  SpawnableMinions[FMath::RandRange(0, SpawnableMinions.Num() - 1)];
 const auto Rotation =
  FRotator(0.0f, FMath::RandRange(0.0f, 360.0f), 0.0f);
 const auto Location =
  SpawnArea->GetComponentLocation() +
   FVector(
    FMath::RandRange(-SpawnArea->GetScaledBoxExtent().X,SpawnArea->GetScaledBoxExtent().X),
    FMath::RandRange(-SpawnArea->GetScaledBoxExtent().Y, SpawnArea->GetScaledBoxExtent().Y),
    0.0f);
 GetWorld()->SpawnActor<AUS_Minion>(Minion, Location, Rotation, SpawnParams);
}

虽然看起来可能很复杂,但这段代码相当简单,你在这本书的开头已经做过类似的事情(还记得掉落的水果吗?)。我们只是从数组中随机选择一个随从类型,检索生成区域中的随机位置,然后在该位置生成随从。唯一值得提的是 SpawnCollisionHandlingOverride,它被设置为生成角色,避免与关卡中的其他对象发生碰撞。

作为额外的练习,你可以在单个生成器对象生成的随从数量上设置一个限制。这将避免你的关卡过于拥挤,让你的玩家无法玩游戏!

生成器角色已经准备好了,所以现在是时候编译你的项目并进行一些适当的测试。

测试生成器

现在是时候探索虚幻引擎编辑器的辉煌,召唤那些顽皮的亡灵小兵,让它们在整个关卡中嬉戏!找到US_MinionSpawner类(位于C++ Classes | UnrealShadows_LOTL文件夹中),并将其拖入您的关卡以创建其实例。

接下来,将演员放置在合适的位置,并调整盒尺寸参数以设置小兵所在的好位置。在我的情况下,我选择将生成器放置在标记为SP3的房间中,盒尺寸属性设置为(900,400,100),正如您在图 8.3中看到的那样:

图 8.3 – 出生区域

图 8.3 – 出生区域

然后,在演员仍然被选中时,执行以下操作:

  1. 详细信息面板中找到出生系统类别。

  2. 可生成小兵数组添加一个元素,将其标记为Index[0]。从相应的下拉菜单中选择US_Minion

  3. 调整出生延迟起始最小兵数量以满足您的需求;在我的情况下,我保留了默认值,正如您在图 8.4中看到的那样:

图 8.4 – 出生设置

图 8.4 – 出生设置

您显然有自由添加尽可能多的生成器演员,以平衡您的游戏级别。

一旦您进入游戏模式,请注意,亡灵小兵将出现在您的眼前,它们在所有客户端上的复制和同步是对 Lichlord 神秘力量的证明!实际上,这是对虚幻引擎复制系统力量的证明,但您不希望让您的玩家知道这个秘密。让他们保持黑暗,让他们惊叹于您游戏性能的无缝魔法。

图 8.5显示了您测试生成器时的外观,但更简单、更不诗意:

图 8.5 – 生成器在行动中

图 8.5 – 生成器在行动中

在本节中,您创建了一个完全可定制的出生系统,该系统可用于并调整您游戏中任何级别的游戏。现在是时候总结一下,继续这个充满冒险的多人游戏的新篇章了。

摘要

在本章中,您了解了多人游戏中 AI 的基础。首先,您获得了一些关于如何创建导航系统以使 AI 能够在关卡中独立移动的信息。之后,您创建了一个基础小兵,它将在关卡中巡逻寻找玩家角色,一旦找到它们,就会将其行为改为更具侵略性的姿态。最后一步,您在地下城周围添加了出生点,以便用有价值的对手填充区域。

从本章中最重要的收获是,凭借你之前获得的知识,所有内容在网络中都是正确同步的。我承诺过,从一开始学习将会在未来带来巨大的优势!而且请相信我,投入努力并真正掌握基础知识,现在正通过你的项目得到回报!

在下一章中,我们将继续探索为你的英雄实现有价值的对手的一些可能性——我们将给它赋予听觉和健康系统,以便使其更具吸引力,至少是可击败的。

第九章:扩展 AI 行为

在多人游戏中增强敌人角色的行为是让游戏更具挑战性和趣味性的好方法。它还可以帮助创造更沉浸式的体验,因为敌人变得更聪明、更快、更强。通过引入新的能力或改变现有的能力,你可以让你的游戏在市场上与其他类似标题脱颖而出。

在本章中,你将学习如何为你的随从的 AI 行为添加改进——这涉及到在你的英雄角色的潜行能力和不死随从感知系统之间建立某种形式的沟通。此外,你还将学习如何让你的对手相互沟通和合作,以便让你的盗贼难以对付。

你还将为 AI 对手实现一个健康系统,让你的角色攻击,并对他们造成伤害。最后,你将为随从创建一些变化,使它们更不可预测、更有趣。

到本章结束时,你将提高你在多人游戏中管理 AI 角色的理解。此外,你将深入了解如何在网络环境中确保有效的沟通。

在本章中,我将引导你通过以下部分:

  • 使 AI 对手更具挑战性

  • 实现警报系统

  • 为 AI 添加健康值

  • 为角色添加武器系统

  • 创建 AI 变化

技术要求

要跟随本章中介绍的主题,你应该已经完成了前面的章节,并理解了它们的内容。

此外,如果你希望从本书的配套仓库开始编写代码,你可以下载本书配套项目仓库提供的.zip项目文件:github.com/PacktPublishing/Multiplayer-Game-Development-with-Unreal-Engine-5

你可以通过点击Unreal Shadows – Chapter 08``End链接下载与上一章结尾相符的最新文件。

使 AI 对手更具挑战性

到目前为止,你的不死随从已经装备了(或多或少)敏锐的视觉,使其能够窥视地牢的深渊,搜寻毫无戒备的猎物。然而,即使是最狡猾的小偷在悄无声息地穿过阴影时也可能意外地撞上障碍。狡猾的巫妖王对此了如指掌,并赐予了他的仆从敏锐的听觉,以至于连针掉落的声音都逃不过他们的耳朵!

在本节中,你将基于玩家角色移动实现一个基于噪音的系统。你将添加的游戏逻辑基于以下要求:

  • 盗贼角色在冲刺时会发出噪音

  • 噪音水平将基于角色统计数据

  • AI 随从在听到噪音时会做出反应

因此,打开你的 IDE,因为现在是时候给你的英雄添加一个新的组件功能了!

制造一些噪音

为了让您的盗贼角色在冲刺时发出噪音,您将添加一个新的组件——一个pawn 噪音发射器。这个组件不会产生实际的声音或噪音,但它会发出一个信号,可以被您附加到小兵角色上的 pawn 感知组件截获。

为了声明这个组件,打开US_Character.h头文件,并在private部分添加以下代码:

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Stealth", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UPawnNoiseEmitterComponent> NoiseEmitter;

现在组件已经声明,是时候初始化它了。打开US_Character.cpp文件,并在文件顶部添加必要的include声明:

#include "Components/PawnNoiseEmitterComponent.h"

然后,找到构造函数,并在FollowCamera初始化之后,添加这两行代码:

NoiseEmitter = CreateDefaultSubobject<UPawnNoiseEmitterComponent>(TEXT("NoiseEmitter"));
NoiseEmitter->NoiseLifetime = 0.01f;

在组件创建后,我们只需将其生命周期初始化为一个非常低的值(即0.01)——这个值表示在新噪音发射覆盖之前的应经过的时间。由于我们使用Tick()事件来发射噪音,而这个事件每帧都会执行,所以我们不需要一个高值。

现在,查找Tick()函数,并在其括号关闭之前,添加以下代码:

if (GetCharacterMovement()->MaxWalkSpeed == GetCharacterStats()->SprintSpeed)
{
 auto Noise = 1.f;
 if(GetCharacterStats() && GetCharacterStats()->StealthMultiplier)
 {
  Noise = Noise / GetCharacterStats()->StealthMultiplier;
 }
 NoiseEmitter->MakeNoise(this, Noise, GetActorLocation());
}

在前面的代码中,我们验证了角色是否在冲刺,并且只有当结果为肯定时才继续进行。然后,我们根据StealthMultiplier角色的统一值来计算噪音。如您在第六章中记得的,在网络中复制属性,这个值是在角色统计数据表中声明的,并且随着角色的等级提升而增长。这意味着乘数越高,角色产生的噪音就越低。噪音评估后,通过NoiseEmitter组件使用MakeNoise()方法发射。

现在我们的人物已经学会了在冲刺时制造噪音的技能,是时候给我们的不死小兵装备一些敏锐的听力才能并将它们投入行动了!

启用听觉感知

小兵角色已经通过 pawn 感知组件具有了听到噪音的能力,但目前这个能力尚未使用。您需要打开US_Minion.h头文件,并在protected部分添加以下声明:

UFUNCTION()
void OnHearNoise(APawn* PawnInstigator, const FVector& Location, float Volume);

如您所见,这是一个简单的回调声明,它将被用来处理任何噪音的监听。

接下来,在public部分添加以下方法声明:

UFUNCTION(BlueprintCallable, Category="Minion AI")
void GoToLocation(const FVector& Location);

这是一个简单的实用函数,我们将用它将小兵发送到噪音的源头。

现在,打开US_Minion.cpp文件,查找PostInitializeComponents()的实现。在括号关闭之前,添加听觉事件的委托绑定代码:

GetPawnSense()->OnHearNoise.AddDynamic(this, &AUS_Minion::OnHearNoise);

现在,通过添加以下代码来实现OnHearNoise()函数:

void AUS_Minion::OnHearNoise(APawn* PawnInstigator, const FVector& Location, float Volume)
{
 GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, TEXT("Noise detected!"));
 GoToLocation(Location);
 UAIBlueprintHelperLibrary::SimpleMoveToLocation(GetController(), PatrolLocation);
}

一旦检测到噪音,我们就将矮人送到噪音产生的位置。正如你所见,我们没有检查噪音制造者是否是我们的盗贼角色 - Lichlord 命令他的矮人细致入微地调查任何和所有的可听到的干扰,不留任何角落未被探索!

最后,添加 GoToLocation() 函数的实现:

void AUS_Minion::GoToLocation(const FVector& Location)
{
 PatrolLocation = Location;
 UAIBlueprintHelperLibrary::SimpleMoveToLocation(GetController(), PatrolLocation);
}

在这里,我们只是设置 PatrolLocation 并将矮人送到那里(这并不复杂,但非常实用,你将在本章后面的部分看到)。

现在矮人已经准备好了,所以编译你的项目并开始测试。

测试听觉感知

为了测试全新的听觉感知功能,开始一个游戏会话,在矮人周围走动,注意不要进入他们的视野锥。除非角色开始冲刺,否则矮人不会注意到这个角色。在那个时刻,你应该会收到一个调试信息,矮人将开始追逐盗贼。图 9**.1 展示了一个场景,其中角色不小心跑到了一对骷髅矮人后面,随后被他们的听觉感知发现。

图 9.1 – 角色已被发现

图 9.1 – 角色已被发现

在本节中,你为矮人角色添加了一个新的感官;这将使游戏对玩家更具战术性 - 像明天没有明天一样在地下城中四处奔跑将不再是可行的解决方案!

在接下来的部分,你将为一个消息系统铺路,这个系统能够让最不起眼的矮人在发现新鲜猎物时发出动员令。哦,你以为英雄主义都是一帆风顺的吗?啊,人的思维是多么容易出错(Lichlord 笑着说)。

实现警报系统

在本节中,你将工作于一个系统,允许 AI 角色在检测到玩家角色时向其同伴矮人发出警报。乍一看,你可能认为警报附近 AI 对手的代码逻辑可以直接在矮人类中实现 - 这只是发送消息的问题,对吧?但事情远不止如此,亲爱的读者。看起来 Lichlord 对通信的雄心比你预想的要大。别担心,他已经命令你使用一个直到此刻才被忽视的游戏框架类 - 游戏模式。

如你从 第四章 中所记得的,设置您的第一个多人游戏环境游戏模式是一个管理游戏规则和设置的类 - 这包括与关卡中的 AI 演员通信的任务。在地下城警报新入侵者肯定是我们想在这个类中拥有的功能。

声明游戏模式函数

如同往常,你需要在类头文件中声明所需的函数 - 在这种情况下,你需要一个名为 AlertMinions() 的函数。打开 US_GameMode.h 头文件,并在 public 部分声明它:

UFUNCTION(BlueprintCallable, Category = "Minions")
void AlertMinions(class AActor* AlertInstigator, const FVector& Location, float Radius);

虽然这个函数可能看起来很简单,但它将提供有价值的信息,例如哪个小兵检测到了某些东西,调查的位置,以及其他小兵应该被警报的距离。

现在,打开US_GameMode.cpp文件,并在代码的顶部添加以下include声明:

#include "US_Minion.h"
#include "Kismet/GameplayStatics.h"

正如你所知道的那样,这些声明是正确实现你在类中编写的代码所必需的。一旦你添加了这些行,你就可以添加以下方法实现:

void AUS_GameMode::AlertMinions(AActor* AlertInstigator, const FVector& Location, const float Radius)
{
 TArray<AActor*> Minions;
 UGameplayStatics::GetAllActorsOfClass(GetWorld(), AUS_Minion::StaticClass(), Minions);
 for (const auto Minion : Minions)
 {
  if(AlertInstigator == Minion) continue;
  if (const auto Distance = FVector::Distance(AlertInstigator ->GetActorLocation(), Minion->GetActorLocation()); Distance < Radius)
  {
   if (const auto MinionCharacter = Cast<AUS_Minion>(Minion))
   {
    MinionCharacter->GoToLocation(Location);
   }
  }
 }
}

代码通过GetActorsOfClass()在关卡中查找所有扩展AUS_Minion的类,并将它们存储在数组中。之后,它遍历这个数组,计算每个小兵与警报小兵之间的距离。如果距离在范围内(即Radius属性),AI 将被命令前往该位置并通过GoToLocation()函数进行调查。

游戏模式的警报行为已经实现;这意味着一旦侦测到入侵者,小兵就可以请求援助。

让 AI 发送警报消息

从 AI 角色发送消息是一个相当直接的任务,因为只要它在服务器上,游戏模式就可以从游戏中的任何演员访问——正如你可能已经知道的,这是由虚幻引擎游戏框架提供的酷炫功能。所以,让我们打开US_Minion.h文件,并在private部分声明即将发送的消息的警报半径:

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Minion AI", meta = (AllowPrivateAccess = "true"))
float AlertRadius = 6000.0f;

使用可配置的半径范围将有助于创建不同类型的小兵——你想要一个超级警觉、刺耳的哨兵吗?将其设置为非常高的值!或者选择一个黏糊糊、自私的 AI,只为 Lichlord 的恩惠和亡灵晋升而存在,将其设置为零——这样,其他小兵都不会被警报,哨兵(希望)在到达玩家角色时会被其主人抚摸一下头。选择权在你!

要实现这个功能,打开US_Minion.cpp文件,并在文件的非常开头添加以下include

#include "US_GameMode.h"

然后,定位到Chase()方法。在其闭合括号之前,添加以下代码:

if(const auto GameMode = Cast<AUS_GameMode>(GetWorld()->GetAuthGameMode()))
{
 GameMode->AlertMinions(this, Pawn->GetActorLocation(), AlertRadius);
}

如你所见,一旦检索到游戏模式,我们只需发送带有适当参数的警报消息。现在是时候编译项目并进行一些测试了。

测试警报实现

开始一个新的游戏会话,一旦小兵的数量足够多,让你的角色被其中之一侦测到。一旦警报,所有附近的小兵将开始调查该区域,对玩家构成严重威胁,因为越来越多的 AI 角色发现了他们,可能导致潜在的连锁反应。

图 9.2显示了这种情况之一——玩家不够隐秘,AI 对手已经开始检测角色并相互警报。

图 9.2 – 警报系统正在运行

图 9.2 – 警报系统正在运行

在本节中,你为你的 AI 对手实现了一个消息系统,并学习了拥有集中管理游戏逻辑的力量的重要性。

在下一节中,你将使用游戏框架伤害系统让玩家击败敌人。你真的相信我会让可怜的盗贼英雄在没有帮助的情况下在巫妖领主的手中腐烂吗?好吧,再想想,亲爱的读者!

为 AI 添加健康值

在这个项目部分,你将为小兵 AI 添加一个健康系统,使其在游戏过程中可以被击败。你还将添加一个生成系统,以便当对手被击败时,玩家将获得应得的奖励。

要实现这些功能,我们需要打开迷你兵类并开始编写代码——打开US_Minion.h头文件,并在private部分添加以下两个声明:

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Health", meta = (AllowPrivateAccess = "true"))
float Health = 5.f;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Pickup", meta = (AllowPrivateAccess = "true"))
TSubclassOf<class AUS_BasePickup> SpawnedPickup;

第一个用于跟踪敌人的健康值,而第二个将包含在击败小兵后将生成的物品拾取物的类。两者都可以在子蓝图类(多亏了EditDefaultsOnly属性指定符)中进行修改,因此你可以构建自己版本的迷你兵。

现在,定位到protected部分,并添加伤害处理声明:

UFUNCTION()
void OnDamage(AActor* DamagedActor, float Damage, const UDamageType* DamageType, AController* InstigatedBy, AActor* DamageCauser);

现在标题已经完成,是时候打开US_Minion.cpp文件并实现健康系统了。像往常一样,首先在文件顶部添加所需的include声明:

#include "US_BasePickup.h"

接下来,声明当角色被击败时将生成的基类拾取物;你将使用在第六章中创建的拾取物硬币,在网络中复制属性 O**ver the Network。定位到构造函数,并在闭合括号之前添加以下代码:

static ConstructorHelpers::FClassFinder<AUS_BasePickup> SpawnedPickupAsset(TEXT("/Game/Blueprints/BP_GoldCoinPickup"));
if (SpawnedPickupAsset.Succeeded())
{
 SpawnedPickup = SpawnedPickupAsset.Class;
}

这段代码逻辑应该很熟悉,因为我们从项目库中获取一个蓝图资产并将其分配给SpawnedPickup引用。

然后,我们需要实现伤害处理逻辑。定位到PostInitializeComponents()方法,并添加以下代码行:

OnTakeAnyDamage.AddDynamic(this, &AUS_Minion::OnDamage);

在这里,我们只是将OnDamage处理程序绑定到OnTakeAnyDamage委托。作为最后一步,我们需要实现OnDamage()方法,因此将以下代码添加到你的类中:

void AUS_Minion::OnDamage(AActor* DamagedActor, float Damage, const UDamageType* DamageType, AController* InstigatedBy,
 AActor* DamageCauser)
{
 Health -= Damage;
 if(Health > 0) return;
 if(SpawnedPickup)
 {
  GetWorld()->SpawnActor<AUS_BasePickup>(SpawnedPickup, GetActorLocation(), GetActorRotation());
 }
 Destroy();
}

这个函数的作用是从Health属性中减去Damage值;如果小兵的生命值达到零,它将立即生成奖励(即拾取物),然后它会自我销毁。

在本节中,你通过添加Health属性并跟踪其值(在受到伤害时)为 AI 对手创建了一个简单的健康系统,当小兵被击败时,它会生成一枚硬币或类似的奖励,供最近的(或最快的)角色拾取!

不幸的是,对于你的玩家来说,这个不幸的盗贼英雄团伙目前装备不足,无法在险恶的地下领域中派遣巫妖领主的仆从!别担心,我们将在下一节中通过为他们增添丰富的武器库来帮助他们。

将武器系统添加到角色中

您心爱的角色自从您开始实现它以来就一直渴望一个武器系统。在本节中,我们将最终满足它的愿望,并赋予它使用(不那么)强大的破坏工具的能力。让我们通过装备一个惊人的武器来使我们的角色变得更强大、更可怕!

由于我们的角色英雄是一个狡猾的小偷,他更喜欢避免与更强壮、装甲更厚的对手进行直接战斗,因此我们将专注于投掷匕首系统。

为了避免在 US_Character 类中添加杂乱的代码,你将实现一个新的组件来处理武器逻辑——这意味着你将专注于以下功能:

  • 将要添加到角色中的一个组件,用于处理玩家输入和匕首生成逻辑

  • 将在运行时投掷并造成敌人对手伤害的匕首武器

作为第一步,我们将创建在游戏过程中攻击时由角色生成的武器投射物。

创建匕首投射物

首先要做的事情是创建一个投射物类,它将作为可投掷的匕首。为此,在 Unreal 编辑器中,创建一个新的 C++类,该类将扩展 Actor,并将其命名为 US_BaseWeaponProjectile。一旦创建,打开 US_BaseWeaponProjectile.h 文件,在 private 部分,添加以下组件声明:

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
TObjectPtr<class USphereComponent> SphereCollision;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UStaticMeshComponent> Mesh;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UProjectileMovementComponent> ProjectileMovement;

如您所见,我们将添加一个碰撞区域以在游戏过程中检查击中,一个用于匕首模型的静态网格,以及投射逻辑,使匕首在被投掷后移动。

仍然在 private 部分,添加 Damage 属性,其基本值为 1

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Weapon", meta = (AllowPrivateAccess = "true"))
float Damage = 1.f;

然后,在 public 部分,添加组件的常规获取方法:

FORCEINLINE USphereComponent* GetSphereCollision() const { return SphereCollision; }
FORCEINLINE UStaticMeshComponent* GetMesh() const { return Mesh; }
FORCEINLINE UProjectileMovementComponent* GetProjectileMovement() const { return ProjectileMovement; }

最后,我们需要添加一个处理武器与目标接触时的处理程序。将以下代码添加到 protected 部分:

UFUNCTION()
void OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse,
 const FHitResult& Hit);

头文件已经准备好了,所以我们需要实现逻辑——打开 US_BaseWeaponProjectile.cpp 文件,并在其顶部添加必要的 include 声明:

#include "US_Character.h"
#include "US_CharacterStats.h"
#include "Components/SphereComponent.h"
#include "Engine/DamageEvents.h"
#include "GameFramework/ProjectileMovementComponent.h"

然后,找到构造函数并添加以下代码:

SphereCollision = CreateDefaultSubobject<USphereComponent>("Collision");
SphereCollision->SetGenerateOverlapEvents(true);
SphereCollision->SetSphereRadius(10.0f);
SphereCollision->BodyInstance.SetCollisionProfileName("BlockAll");
SphereCollision->OnComponentHit.AddDynamic(this, &AUS_BaseWeaponProjectile::OnHit);
RootComponent = SphereCollision;
Mesh = CreateDefaultSubobject<UStaticMeshComponent>("Mesh");
Mesh->SetupAttachment(RootComponent);
Mesh->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
Mesh->SetRelativeLocation(FVector(-40.f, 0.f, 0.f));
Mesh->SetRelativeRotation(FRotator(-90.f, 0.f, 0.f));
static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticMesh(TEXT("/Game/KayKit/DungeonElements/dagger_common"));
if (StaticMesh.Succeeded())
{
 GetMesh()->SetStaticMesh(StaticMesh.Object);
}
ProjectileMovement = CreateDefaultSubobject<UProjectileMovement Component>("ProjectileMovement");
ProjectileMovement->UpdatedComponent = SphereCollision;
ProjectileMovement->ProjectileGravityScale = 0;
ProjectileMovement->InitialSpeed = 3000;
ProjectileMovement->MaxSpeed = 3000;
ProjectileMovement->bRotationFollowsVelocity = true;
ProjectileMovement->bShouldBounce = false;
bReplicates = true;

这段代码逻辑虽然较长,但易于理解——我们只是创建和初始化必要的组件:

SphereCollision 包含一些你应该熟悉的基本值:

  • Mesh 设置为匕首模型,并旋转定位以与整体 Actor 对齐

  • ProjectileMovement 禁用了重力,并具有使 Actor 快速移动并模拟真实匕首的速度

有一点需要说明的是,我们通过 AddDynamic 辅助宏将 OnHit() 方法绑定到 OnComponentHit 代理。此外,注意代码的最后一行激活了武器的复制功能——始终记住,默认情况下 Actors 不会被复制!

现在,添加 OnHit() 实现方法:

void AUS_BaseWeaponProjectile::OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor,
 UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
 auto ComputedDamage = Damage;
 if (const auto Character = Cast<AUS_Character>(GetInstigator()))
 {
  ComputedDamage *= Character->GetCharacterStats()->DamageMultiplier;
 }
 if (OtherActor && OtherActor != this)
 {
  const FDamageEvent Event(UDamageType::StaticClass());
  OtherActor->TakeDamage(ComputedDamage, Event, GetInstigatorController(), this);
 }
 Destroy();
}

代码可以分为三个主要部分:

  • 在第一部分,我们从Damage基本值开始计算伤害。如果肇事者(即生成投射物的角色)是US_Character,我们将从统计数据中获取其伤害乘数并更新引起的伤害。这意味着角色的等级越高,伤害就越高。

注意

想要回顾一下如何管理角色统计数据,请回顾第六章在网络中复制属性 O**ver the Network

  • 代码的第二部分验证发射的投射物是否击中了演员。如果是,它将造成相应的伤害量。

  • 最后的部分只是销毁投射物 – 它的任务已经完成,这意味着它应该从游戏中移除。

当投射物已经设置好并准备就绪时,是时候实现一些生成逻辑,以便你的盗贼英雄能够释放这把闪亮新武器的全部力量。

实现武器组件

让我们先创建一个将添加新功能的类。如您从第四章设置您的第一个多人环境中可能记得,一个组件将允许你实现可重用的功能,并可以附加到任何演员或另一个组件。在这种情况下,我们将实现一个武器系统,具有Transform属性) – 这将允许你将组件放置在角色内部某个位置,并作为投掷投射物的生成点。

让我们先创建这个类。为此,创建一个新的类,从场景组件扩展而来,并将其命名为US_WeaponProjectileComponent。一旦创建过程完成,打开US_WeaponProjectileComponent.h,在private部分,添加以下声明:

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Projectile", meta = (AllowPrivateAccess = "true"))
TSubclassOf<class AUS_BaseWeaponProjectile> ProjectileClass;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Input", meta=(AllowPrivateAccess = "true"))
class UInputMappingContext* WeaponMappingContext;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Input", meta=(AllowPrivateAccess = "true"))
class UInputAction* ThrowAction;

如您所见,我们声明了投射物类(即我们之前创建的投射物,或其子类)。然后,我们声明了必要的元素,这将使我们能够利用增强的输入系统。由于我们不希望向主要角色添加依赖项,我们将使用与第五章在多人环境中管理演员中使用的不同映射上下文 – 这将使我们能够实现灵活的战斗系统,并添加我们想要的任何功能,而不会给主要角色类添加杂乱。想象一下,看着你的狡猾盗贼英雄悄悄穿过阴影,静静地从背后刺杀那个令人恐惧的巫妖领主最令人憎恨的走狗!混乱和恶作剧的可能性将是无穷无尽的!

好吧,让我们停止做梦,回到编码上来。在public部分,添加一个用于投射物类的 setter,这将允许你更改生成的匕首投射物:

UFUNCTION(BlueprintCallable, Category = "Projectile")
void SetProjectileClass(TSubclassOf<class AUS_BaseWeaponProjectile> NewProjectileClass);

这个函数与扔东西无关,但如果你想在你的游戏中添加武器拾取功能,以提高角色的战斗技能,它将非常有用。

最后,在protected部分,声明Throw()动作及其对应的服务器调用:

void Throw();
UFUNCTION(Server, Reliable)
void Throw_Server();

这段代码将使我们能够在游戏过程中从服务器生成投掷匕首 – 总是记住,在生成复制的 Actor 时,服务器应该处于指挥地位。

现在头文件已经完成,打开US_WeaponProjectileComponent.cpp文件以开始实现其功能。像往常一样,找到文件顶部,并添加我们将使用的类的include声明:

#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "US_BaseWeaponProjectile.h"
#include "US_Character.h"

然后,在构造函数中,添加以下单行代码:

ProjectileClass = AUS_BaseWeaponProjectile::StaticClass();

在这里,我们只需声明当抛掷动作被触发时将生成的基礎投射物;显然,如果你需要不同的武器,你可以在派生蓝图类中更改它。

现在,定位BeginPlay()方法,并在Super::BeginPlay()声明之后添加以下代码:

const ACharacter* Character = Cast<ACharacter>(GetOwner());
if(!Character) return;
if (const APlayerController* PlayerController = Cast<APlayerController>(Character->GetController()))
{
 if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
 {
  Subsystem->AddMappingContext(WeaponMappingContext, 1);
 }
 if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerController->InputComponent))
 {
  EnhancedInputComponent->BindAction(ThrowAction, ETriggerEvent::Triggered, this, &UUS_WeaponProjectileComponent::Throw);
 }
}

在前面的代码中,我们检查组件所有者是我们的US_Character类,以便获取其控制器并初始化映射上下文及其动作。请注意,这个初始化是在BeginPlay()函数内完成的,这意味着这些步骤只会执行一次 – 也就是说,当游戏开始时 – 以确保存在一个 Actor 所有者和相应的控制器。

现在,通过添加以下方法实现来实施投掷逻辑:

void UUS_WeaponProjectileComponent::Throw()
{
 Throw_Server();
}
void UUS_WeaponProjectileComponent:: Throw_Server_Implementation()
{
 if (ProjectileClass)
 {
  const auto Character = Cast<AUS_Character>(GetOwner());
  const auto ProjectileSpawnLocation = GetComponentLocation();
  const auto ProjectileSpawnRotation = GetComponentRotation();
  auto ProjectileSpawnParams = FActorSpawnParameters();
  ProjectileSpawnParams.Owner = GetOwner();
  ProjectileSpawnParams.Instigator = Character;
  GetWorld()->SpawnActor<AUS_BaseWeaponProjectile>(ProjectileClass, ProjectileSpawnLocation, ProjectileSpawnRotation, ProjectileSpawnParams);
 }
}

如你所见,Throw()方法只是调用服务器端实现,从组件位置生成投射物。你已经熟悉了生成动作(你还记得小兵生成器吗?),但这次有一个重要的事情要注意 – 我们使用FActorSpawnParameters结构来设置投射物的所有者和,最重要的是,发起者(即生成对象的 Actor)。这个属性被投射物用来检索角色统计数据和处理伤害乘数,这是我们之前章节中实现的代码逻辑。

最后,添加一个设置器方法,让你可以更改角色生成的武器:

void UUS_WeaponProjectileComponent::SetProjectileClass(TSubclassOf<AUS_BaseWeaponProjectile> NewProjectileClass)
{
 ProjectileClass = NewProjectileClass;
}

组件现在已经被正确设置 – 你只需将其实例附加到盗贼角色上,使其完全可用。

将武器投射物组件附加到角色上

现在你已经创建了一个武器组件,是时候将其添加到角色上了。打开US_Character.h头文件,并在private部分添加组件声明:

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Weapon", meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UUS_WeaponProjectileComponent> Weapon;

然后,像往常一样,在public部分添加相应的获取器实用方法:

FORCEINLINE UUS_WeaponProjectileComponent* GetWeapon() const { return Weapon; }

然后,打开US_Character.cpp源文件,并在文件顶部包含组件类声明:

#include "US_WeaponProjectileComponent.h"

现在,定位构造函数,并在噪声发射器创建和初始化之后添加以下代码:

Weapon = CreateDefaultSubobject<UUS_WeaponProjectileComponent>(TEXT("Weapon"));
Weapon->SetupAttachment(RootComponent);
Weapon->SetRelativeLocation(FVector(120.f, 70.f, 0.f));

如您所见,在创建组件后,我们将其附加到角色的根组件上,并将其定位在相对位置,设置为 (120, 70, 0)。如果您想让您的角色为左撇子,只需为 X 坐标使用负值(即,-120.f)即可。

虽然难以相信,但将武器组件附加到角色的代码已经完成;代码逻辑已经在组件本身中处理,所以您可以坐下来放松,让一切像一台运转良好的机器一样各就各位!

现在,您现在可以切换回虚幻编辑器并编译您的项目 – 完成后,您可以打开 WeaponProjectile 组件,将 弹道类 设置为默认值,如图 图 9**.3 所示:

图 9.3 – 附加到角色演员的 WeaponProjectile 组件

图 9.3 – 附加到角色演员的 WeaponProjectile 组件

WeaponProjectile 组件附加到角色后,最后要做的就是为玩家输入创建映射上下文,并为抛出逻辑创建输入动作。

添加武器输入系统

在本节的最后部分,您将定义映射上下文和抛出交互的输入动作。这您已经很熟悉了,因为您之前在 第五章在多人环境中管理演员 中创建了类似的资产。

所以,无需多言,让我们打开内容浏览器并导航到 内容 | 输入 文件夹。我们将按照以下步骤创建抛出动作资产。

设置抛出交互的输入映射上下文

要创建抛出交互的动作资产,请按照以下步骤操作:

  1. 在内容浏览器中右键单击并选择 IA_Throw

  2. 双击资产以打开它,然后从 值类型 下拉菜单中选择 数字(bool)

  3. 确认 消耗输入 复选框已被勾选。

抛出动作资产的最终结果如图 图 9**.4 所示:

图 9.4 – 抛出动作设置

图 9.4 – 抛出动作设置

现在动作已经设置好了,让我们为武器交互创建一个映射上下文。

设置武器交互的输入映射上下文

要创建武器上下文映射,请按照以下步骤操作:

  1. 在内容浏览器中右键单击并选择 IMC_Weapons。双击资产以打开它。

  2. 通过点击 映射 字段旁边的 + 图标添加一个新的映射上下文。

  3. 从将添加的下拉菜单中选择 IA_Throw 以将此动作添加到映射上下文。

  4. 在下拉菜单旁边的 + 图标上单击两次以添加此动作的两个其他控制绑定(其中一个已默认设置)。在每个新字段旁边的下拉菜单中,使用以下属性:

    • 第一个绑定应设置为 左 Ctrl 来自 键盘 类别

    • 第二个绑定应该设置为游戏手柄面键右侧,来自游戏手柄类别

    • 第三个绑定应该设置为鼠标左键,来自鼠标类别

武器映射上下文的最终结果应该像 图 9**.5 中所示的那样:

图 9.5 – 武器映射上下文设置

图 9.5 – 武器映射上下文设置

现在资产已经准备好了,是时候将它们添加到角色上了:

  1. 打开 BP_Character 蓝图,选择 Weapon 组件,并在 Details 面板中找到 Input 类别。

  2. Weapon Mapping Context 字段中,分配 IMC_Weapons 资产。

  3. Throw Action 字段中,分配 IA_Throw 资产。

一旦设置了这些属性,你的 Input 类别应该看起来像 图 9**.6 中所示的那样:

图 9.6 – 更新的输入类别

图 9.6 – 更新的输入类别

现在输入设置已经得到适当的更新,是时候进行一些测试以检查一切是否正常工作。

测试武器系统

是时候向巫妖领的奴仆展示谁是老板,并在他们的地下巢穴中造成一些混乱了!让我们通过开始一个游戏会话来让他们尝尝我们英雄的瞄准技巧。

在游戏过程中,你的角色应该能够在使用投掷动作时随时生成匕首 – 例如,通过点击左鼠标按钮。匕首应该在其击中任何东西时销毁自己,并对 AI 奴隶造成伤害。

当一个奴仆的生命值降到零时,它应该从游戏中移除,并在关卡中生成一个硬币。收集足够的硬币将使你的角色升级,因此,当角色击中任何敌人时,角色本身将造成额外的伤害。

图 9**.7 展示了角色在游戏过程中投掷匕首:

图 9.7 – 匕首攻击动作

图 9.7 – 匕首攻击动作

在本节中,你通过将一个新组件附加到你的角色和一个可以在游戏中生成并通过网络正确复制的投射体 Actor 来实现武器系统。

在接下来的章节中,你将引入一些 AI 对手的多样化变体,目的是增强游戏的多样性和整体可玩性。

创建 AI 变体

现在我们已经将 AI 对手全部设置好并准备好进行一些史诗般的战斗,让我们为 AI 奴隶添加更多变体,使游戏更加有趣和吸引人。如果一个项目已经得到了良好的规划,改变 AI 的行为 – 即使是像我们在本章中创建的基本 AI – 通常只是调整一些设置的问题!

在本节中,你将创建一个新的 AI 对手,从基本的 US_Minion 类开始,然后你将调整其属性以赋予它不同的行为。

创建 AI 哨兵

当看到无脑的小兵在地下城 cluelessly 漫游时,可能会引起一两声笑声,但这绝对不足以满足巫妖王的狡猾计划。他想要确保他的领地每个角落都安全并有良好的守卫。这意味着你需要制作一些具有敏锐感官和更具领地性的不死生物监视者。

首先,我们创建一个蓝图类,继承自基本的小兵。打开内容浏览器,完成以下步骤:

  1. Blueprints 文件夹中,右键单击并选择 蓝图类

  2. 从弹出的窗口中,从 所有 部分选择 US_Minion

  3. 将新创建的蓝图命名为 BP_MinionSentinel,然后双击它以打开。

  4. 6000,0

  5. 设置 60,0

  6. 设置 20,0

  7. 设置 1000,0

  8. 本类别的最终设置如图 9.8 所示:

图 9.8 – 监视者小兵 AI 设置

图 9.8 – 监视者小兵 AI 设置

  1. 然后,在 600,0

  2. 设置 1000,0

  3. 设置 2500,0

  4. 设置 60,0

本类别的最终设置如图 9**.9 所示:

图 9.9 – 监视者 AI 设置

图 9.9 – 监视者 AI 设置

使用这些设置,你将创建一个将在大约小区域内巡逻的小兵,频繁改变方向,移动非常缓慢。它的感官将非常敏锐,其警报半径将大于普通小兵。当发现入侵者时,监视者会减速,呼叫帮助,让更具有侵略性的同类来处理追逐。它不是战斗型,但仍然在寻找任何可疑活动!

  1. 作为最后的点缀,你可以通过将网格 Materials 列表中的 Element 5 改为 M_Base_Emissive 材质资产,为这个注视黑暗的不死生物角色添加一对发光的眼睛,如图 9**.10 所示:

图 9.10 – 材质设置

图 9.10 – 材质设置

监视者的最终结果(添加了一些戏剧性的闪电效果)可以在 图 9**.11 中看到:

图 9.11 – 场景中添加的监视者

图 9.11 – 场景中添加的监视者

如您所见,您只需对 详情 面板进行一些调整,就创建了一个新的 AI。让我们再创建一个,一个更具侵略性的不死生物小兵。

创建 AI 小型 Boss

实际上,你可以使用之前小节中有效的方法来创建一个新的 AI,它将以完全不同的方式处理英雄入侵者。这就像在食谱中发挥创意,制作出既新颖又出乎意料(但仍然危险地美味)的东西!

打开内容浏览器并完成以下步骤:

  1. Blueprints 文件夹中,右键单击并选择 蓝图类

  2. 从弹出的窗口中,从 所有 部分选择 US_Minion

  3. 将新创建的蓝图命名为 BP_MinionMiniboss,然后双击它以打开。

  4. 100,0

  5. 设置 100,0

  6. 设置 400,0

  7. 设置 50000,0

  8. 该类别的最终设置如图 图 9.12 所示:

图 9.12 – 小型 Boss 随从 AI 设置

图 9.12 – 小型 Boss 随从 AI 设置

  1. 然后,在 200,0

  2. 设置 400,0

  3. 设置 200,0

  4. 设置 20,0

该类别的最终设置如图 图 9.13 所示:

图 9.13 – 小型 Boss AI 设置

图 9.13 – 小型 Boss AI 设置

这个 AI 对手在巡逻时被设置为非常单调的行为(感知低,移动速度慢等),但一旦被警告,它就会变得非常快。

此外,小型 Boss 开始看起来有点单调,所以 Lichlord 决定给它一个字面上的装甲改造。为此,请按照以下步骤操作:

  1. 选择 网格 组件,并将 骨骼网格资产 属性更改为 skeleton_warrior 资产。

  2. 网格 缩放比例更改为 1.2 以使其更大。

  3. 健康 属性设置为 20 以使其更具抗伤害性。

这个敌人即将从“一般”变成威胁,英雄入侵者最好小心!与基础随从相比,小型 Boss 的最终结果如图 图 9.14 所示:

图 9.14 – 小型 Boss 随从

图 9.14 – 小型 Boss 随从

它的美丽之处在于你可以对你的敌人对手进行非常具有创造性的设定,并测试出各种不同的行为和战术。如果有什么东西对你不起作用,别担心!只需删除它,然后在短短几分钟内用新的东西重新开始。

作为 AI 的一个额外亮点,为什么不使用我们在本章早期添加的拾取出生系统来为游戏增添趣味?根据被打败的随从的稀有程度或危险程度,你可以让它孵化不同类型的硬币!

一旦你的不死军准备好,你就可以回到你的敌人出生器,并将蓝图添加到系统中——我们将在下一小节中这样做。

更新随从出生器

如你所猜,添加你全新的随从种类只需将它们的蓝图放入出生器中。为此,选择你之前添加到关卡中的出生器,并在 详细信息 面板中找到 可出生随从 数组属性,在 出生系统 类别中——列表中应该已经有一个项目,US_Minion

添加你想要的任何物品,选择你需要用于特定出生区域的随从。图 9.15 显示了我在关卡主出生区域中的设置:

图 9.15 – 出生区域设置

图 9.15 – 出生区域设置

如您所见,我选择了五个元素,每次调用Spawn()方法时,每个元素有 20%的机会被添加到关卡中。由于基本小兵利用了这三个元素,因此它以 60%的概率出现为对手,而哨兵和迷你 Boss 只有 20%的概率生成。

一旦您对设置满意,您就可以测试游戏。图 9.16显示了游戏开始时我的生成器正在运行:

图 9.16 – 游戏开始时的生成器

图 9.16 – 游戏开始时的生成器

在本节的最后,您为基本小兵创建了一些变体;通过改变它们的一些基本属性,您改变了它们在游戏中的行为方式,使您的关卡更具吸引力和多样性。

摘要

在本章中,您积极努力改善关卡内敌人 AI 的行为。重点是开发新的功能,使游戏体验对玩家来说更具吸引力和挑战性。您实际上改善了 Lichlord 小兵的听觉,使它们在发现那些可怜的盗贼英雄时更加警觉和敏锐。

反过来,您还为小兵实现了健康系统,并为玩家的装备库添加了一些相当锋利(字面意思!)的工具,他们可以使用这些工具击败那些讨厌的敌人!最后,您还创建了一些敌人变体,使地牢对玩家来说不再无聊,更具吸引力。

如您在此处所构建的所示,如果您提前规划,提高游戏玩法可以变得轻而易举!通过花时间仔细制定策略并实施正确的功能,您可以使游戏玩法对玩家更具吸引力和沉浸感,同时也能实现您期望的结果。

在下一章中,我们将通过添加动画和需要解救的囚犯来改善游戏的整体外观和感觉。此外,我将为您提供一些如何将您的游戏提升到下一个层次的技巧,但让我们明确一点——我不会为您编写所有代码!我相信您有能力创造出一些惊人的东西,我迫不及待地想看看您能想出什么!

第十章:提升玩家体验

提高视频游戏的最佳方式之一是为它添加良好的外观和感觉。一款外观出色的游戏将创造一个沉浸式的体验,吸引玩家并使他们想要不断回来玩。

因此,对于开发者来说,专注于调整视觉和音频反馈,直到一切看起来都恰到好处非常重要!这可能需要一些时间,但确保这些最后的细节正确无误将确保您的视频游戏拥有令人惊叹的外观和感觉——这是玩家们短时间内不会忘记的!

考虑到这一点,下一章将专注于改进某些方面,例如使用不同的动画组合并在网络上同步它们,或者添加非玩家角色(NPCs)进行交互——这些是主角渴望已久的特性。

此外,您将为玩家提供一个战斗的理由:勇敢地营救一些被囚禁的同志!

最后,我将分享一些额外的想法,以帮助您完成您的多人游戏。这本书可能没有足够的页面来涵盖每一个细节,但这不应该阻止您的创造力和想象力翱翔!

到本章结束时,您将拥有一个光滑且健壮的多人游戏原型,并且将准备好开始下一阶段——学习如何优化它。

在本章中,我将引导您通过以下部分:

  • 为角色动画

  • 添加 NPC 演员

  • 进一步改进游戏

技术要求

要跟随本章介绍的主题,您应该已经完成了前面的章节,并理解了它们的内容。

此外,如果您希望从本书的配套仓库开始编写代码,您可以下载提供的.zip项目文件:

github.com/PacktPublishing/Multiplayer-Game-Development-with-Unreal-Engine-5

您可以通过点击Unreal Shadows – 第九章 结束链接下载与上一章结尾一致的最新文件。

为角色动画

到目前为止,您的英雄一直在探索地牢,寻找隐藏的宝藏,同时避开敌人,但还有一些东西缺失,才能真正让它栩栩如生——一个合适的动画系统。

在本节中,我将引导您创建简单的动画,这些动画将在您项目的网络环境中工作。这涉及到创建专门为动画系统设计的蓝图,并建立它们与您的角色类的连接——您将创建所需的动画资源,然后添加所需的代码以确保一切正常工作。

创建动画资源

在虚幻引擎中动画角色涉及创建动画蓝图来处理角色动作和移动的逻辑。本书并不优先考虑这个主题 – 实际上,这通常不是游戏程序员的主要关注点!然而,对内部工作原理有一些基本了解将有助于丰富你的游戏开发工具箱。

要为玩家角色创建一个简单但功能齐全的动画系统,我们需要三个资源:

  • 用于控制从空闲到行走再到跑步,以及相反方向移动过渡的资源

  • 用于播放投掷动画的资源

  • 一个蓝图来控制上述两个资源

要开始,我们首先需要一个文件夹来存放所有资源。因此,打开虚幻编辑器,在Animations中。一旦创建,你就可以添加第一个资源。

创建运动混合空间

在虚幻引擎中,混合空间是一个特殊资源,允许根据两个输入值来混合动画。它允许通过将多个动画绘制到一维或二维图表上来混合多个动画。动画师和游戏开发者经常使用混合空间来创建游戏中角色不同动画之间的平滑和逼真的过渡。

在我们的案例中,我们需要混合三个动画 – 空闲、行走和冲刺动画 – 这些动画将根据角色的速度进行管理。

要创建这个混合空间,完成以下步骤:

  1. Animations文件夹内,右键点击并选择动画 | 混合空间。然后,从弹出的选择骨架窗口中,选择rogue_Skeleton,如图 10.1所示:

图 10.1 – 创建混合空间

图 10.1 – 创建混合空间

  1. 将新创建的资源命名为BS_WalkRun,双击它以打开混合空间 编辑器窗口。

  2. Speed

  3. 设置500

  4. 保持垂直轴部分不变(即,设置为),因为我们不会使用它。

我们在这里所做的初始化动画混合的主要设置值,暴露了将被我们稍后添加的控制蓝图使用的Speed属性。

现在,你将添加将被混合在一起的动画资源。

  1. 资源浏览器中找到rogue_Idle动画,并将其拖拽到编辑器中心的图中。这将创建一个在图坐标系统中的点。

  2. 选择点并设置其0和其0

你应该得到一个看起来像图 10.2中描述的图表:

图 10.2 – 空闲动画设置

图 10.2 – 空闲动画设置

现在,我们将向图表中添加两个更多资源 – 一个用于行走动画,另一个用于跑步动画。

  1. 拖动45及其0

  2. 再次,拖动100及其0

  3. 拖动500及其0

完整的混合空间资源可以在图 10.3中看到:

图 10.3 – 完整混合空间

图 10.3 – 完整混合空间

要测试角色上的动画混合,您可以简单地按住Ctrl键并将鼠标悬停在您想要检查的图表区域上 – 您将看到角色开始行走和移动循环,动画资产无缝混合。混合空间已完成,因此我们现在可以开始创建处理投掷动画的资产。

创建投掷动画蒙太奇

动画蒙太奇是一种资产类型,它允许从蓝图组合多个动画并选择性地播放。动画蒙太奇通常用于创建复杂的动画序列,如攻击组合、场景和其它交互式游戏元素。在我们的项目中,我们将使用它来播放由控制蓝图播放的单次投掷动画。

要创建动画蒙太奇,请完成以下步骤:

  1. Animations文件夹中,右键单击并选择Animation | Animation Montage。然后,从弹出的选择骨架窗口中,选择rogue_Skeleton

  2. 将新创建的资产命名为AM_Throw并双击它以打开动画蒙太奇****编辑器窗口。

  3. 资产浏览器中,将rogue_Throw资产 – 在DefaultGroup.DefaultSlot行 – 拖动到编辑器中心的时序线上。

动画蒙太奇的最终结果如图图 10**.4所示:

图 10.4 – 投掷动画蒙太奇

图 10.4 – 投掷动画蒙太奇

此蒙太奇和之前的混合空间资产将由一个专门的蓝图控制,我们将在下一步骤中将它添加到项目中。

创建角色动画蓝图

动画蓝图是一种专门类型的蓝图,用于创建和控制游戏中 Actors 的复杂动画行为。它定义了动画应该如何处理和混合,以及动画输入应该如何映射。

在我们的案例中,我们需要控制混合空间速度参数,以便在需要时让角色行走和奔跑,并在角色攻击时启动投掷动画蒙太奇。

要创建动画蓝图,请完成以下步骤:

  1. Animations文件夹中,右键单击并选择Animation | Animation Blueprint。然后,从弹出的创建动画蓝图窗口中,选择rogue_Skeleton,如图图 10**.5所示:

图 10.5 – 动画蓝图创建

图 10.5 – 动画蓝图创建

  1. 将新创建的资产命名为AB_Character并双击它以打开编辑器窗口。

如果您还不熟悉动画蓝图,您会注意到它与常规蓝图类有一些相似之处,例如我的蓝图事件图选项卡。如果尚未选择,请打开事件图以开始一些可视化脚本代码,然后继续以下步骤。

  1. 添加一个事件蓝图初始化 动画节点。

  2. 点击并拖动Try Get Pawn Owner(它将已经存在于图表中)的返回值输出引脚,并添加一个Cast To US_Character节点。

  3. 将事件执行引脚连接到 cast 节点输入执行引脚。

  4. Character

  5. Set Character节点的输出引脚点击并拖动,添加一个Character Movement获取节点。

  6. 从这个获取节点的输出引脚点击并拖动,选择Movement Component,并将自动添加到图表中的Set Movement Component节点连接到Set Character节点的执行引脚。

最终图表如图图 10.6所示:

图 10.6 – 事件蓝图初始化动画图表

图 10.6 – 事件蓝图初始化动画图表

这段视觉脚本在蓝图初始化时执行,并基本上设置你在游戏过程中需要的变量。

现在,定位图表中已经存在的事件蓝图更新动画节点。

  1. 变量部分拖动一个获取节点用于Character属性。右键单击它并选择转换为验证获取选项;这将把节点转换成一个可执行的节点,该节点将检查Character变量是否有效。

  2. 事件蓝图更新动画执行引脚连接到Get Validated Character节点的输入执行引脚。

  3. CharacterSpeed中。将一个Set节点用于此变量拖入图表。

  4. 变量部分拖动一个Get节点用于Movement Component变量。

  5. Movement Component节点的输出引脚点击并拖动,创建一个Get Velocity属性节点。

  6. Get Velocity节点的输出引脚点击并拖动,创建一个Vector Length XY节点。

  7. Vector Length XY节点的输出引脚连接到Set Character Speed节点的输入引脚。

图表的最终结果如图图 10.7所示:

图 10.7 – 事件蓝图更新图表

图 10.7 – 事件蓝图更新图表

这段视觉脚本代码基本上跟踪角色的速度大小并将其存储在Character Speed变量中,该变量将在以下步骤中用于混合移动动画。

接下来,选择编辑器的AnimGraph选项卡,它将显示一个单独的输出姿态节点——这个节点代表角色的最终动画姿态。我们现在需要告诉图表如何动画化角色。

  1. 变量部分将Character Speed属性拖动以创建一个获取节点。

  2. Character Speed输出引脚点击并拖动,创建一个Blendspace Player ‘****BS_WalkRun’节点。

  3. 点击并拖动 Blendspace Player ‘BS_WalkRun’ 节点的输出引脚,创建一个 Slot ‘Default Slot’ 节点——我们将从 C++ 代码中使用此节点来执行投掷动画蒙太奇。

  4. Slot ‘Default Slot’ 的输出引脚连接到 Output Pose 节点的输入引脚。

动画图最终结果如图 图 10.8 所示:

图 10.8 – 动画图

图 10.8 – 动画图

通过这一最终步骤,动画蓝图就完成了;现在,你只需要将其连接到角色蓝图,使其工作。

将动画系统添加到角色中

要将动画系统添加到角色中,你只需在蓝图类中声明动画蓝图。要这样做,打开 BP_Character 蓝图并选择 Mesh 属性。然后,在 Details 面板中,找到 Anim Class 属性。从其旁边的下拉菜单中选择 AB_Character,如图 图 10.9 所示:

图 10.9 – 分配给蓝图类的动画蓝图

图 10.9 – 分配给蓝图类的动画蓝图

如果你现在测试游戏,你应该看到角色开始动画循环,并在行走和跑步时对玩家输入做出反应。然而,跑步动画会奇怪地跳跃和出现错误——这是因为这些动画没有被复制,只是检查角色速度来更新。

从技术角度来看,速度值(即 MaxWalkSpeed)只是存储在角色的服务器实例中,但客户端将有自己的 MaxWalkSpeed 值。如果你只是移动一个 Actor,这可能是可以接受的,因为服务器会不断更新 Actor 位置,但基于速度的骨骼网格组件动画则完全是另一回事。事实上,动画系统正在使用本地值(即客户端的值),系统将不断在服务器和客户端数据之间发生冲突,导致动画损坏。

正因如此,我们需要将我们在 第七章**,使用远程过程调用 (RPCs) 中实现的开始和停止冲刺逻辑从服务器移动到客户端,并将相应的调用作为多播调用,这样所有客户端都会意识到这个变化。

要这样做,打开 US_Character.h 头文件,并添加以下客户端声明:

UFUNCTION(NetMulticast, Reliable)
void SprintStart_Client();
UFUNCTION(NetMulticast, Reliable)
void SprintEnd_Client();

如你所见,我们使用了 NetMulticast 指定符,以便让所有客户端都知道角色已经开始冲刺。此外,这个调用需要是一个 Reliable 的调用,这样你就可以保证将所有数据发送给接收者,而不会有任何数据包丢失。

注意

如需了解 RPCs 和 NetMulticast 指定符的复习,请参阅 第七章**,使用远程过程调用 (RPCs)

现在,打开US_Character.cpp文件并定位到SprintStart_Server_Implementation()SprintEnd_Server_Implementation()。你需要将这两个方法的所有内容移动到相应的客户端调用中。为此,删除所有内容(即括号内的代码),并在SprintStart_Server_Implementation()中添加客户端调用:

SprintStart_Client();

对于SprintEnd_Server_Implementation()方法,添加以下内容:

SprintEnd_Client();

之后,将之前移除的代码移动到客户端实现中:

void AUS_Character::SprintStart_Client_Implementation()
{
 if (GetCharacterStats())
 {
  GetCharacterMovement()->MaxWalkSpeed = GetCharacterStats()->SprintSpeed;
 }
}
void AUS_Character::SprintEnd_Client_Implementation()
{
 if (GetCharacterStats())
 {
  GetCharacterMovement()->MaxWalkSpeed = GetCharacterStats()->WalkSpeed;
 }
}

总体行为将如下:

  • 由玩家控制的客户端接收移动输入并将此数据发送到服务器

  • 服务器处理这个输入并向所有客户端发送更新请求

  • 所有客户端相应地更新MaxWalkSpeed

一旦编译了项目,尝试测试游戏——我们的角色现在可以像专业人士一样移动和冲刺,你将看到他们的动画在所有荣耀中闪耀!

为了额外练习,尝试对随从角色进行工作并实现相同的动画逻辑。这就是召唤一帮骨头成为一个完整的、重新激活的随从的真实含义,谁知道呢?巫妖王可能会因为你的出色工作而给你带来一些惊喜!

添加投掷动画

目前缺少的是投掷动画,在这种情况下,网络同步是我们真正想要的——游戏中所有连接的玩家都需要在角色在地下城投掷匕首时看到角色动画,并且这个动画应该对所有客户端同时播放。

首先要确保武器投射物组件能够正确复制。为此,打开US_Character.cpp文件。然后,在构造函数中找到Weapon组件初始化,并添加以下代码行:

Weapon->SetIsReplicated(true);

接下来,打开US_WeaponProjectileComponent.h文件,并在private部分添加以下动画蒙太奇引用:

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Projectile", meta=(AllowPrivateAccess = "true"))
UAnimMontage* ThrowAnimation;

之后,在protected部分添加以下声明:

UFUNCTION(NetMulticast, Unreliable)
void Throw_Client();

这是将在客户端执行的投掷方法。注意,我们使用具有Unreliable属性指定符的 RPC 多播到所有客户端——尽管我们希望这个动画在网络中同步,但这只是一个美学上的附加功能,因此我们可以承受在网络中丢失数据。其他客户端不会看到动画,但匕首仍然会被生成。

完成头文件声明后,打开US_WeaponProjectileComponent.cpp文件并添加客户端投掷方法:

void UUS_WeaponProjectileComponent::Throw_Client_Implementation()
{
 const auto Character = Cast<AUS_Character>(GetOwner());
 if (ThrowAnimation != nullptr)
 {
  if (const auto AnimInstance = Character->GetMesh()->GetAnimInstance(); AnimInstance != nullptr)
  {
   AnimInstance->Montage_Play(ThrowAnimation, 1.f);
  }
 }
}

如你所见,代码将获取此组件的所有者,如果它是US_Character类型,将播放动画蒙太奇。

此方法将从其服务器端对应方法 Throw_Server_Implementation() 中调用,因此找到该方法。我们本可以直接执行方法调用,但我们需要给生成逻辑添加一点延迟,因为投掷动画需要一些时间才能完成,提前生成匕首会给玩家带来不美观的视觉反馈。为此,删除函数中的所有内容,并用以下代码替换:

if (ProjectileClass)
{
 Throw_Client();
 FTimerHandle TimerHandle;
 GetWorld()->GetTimerManager().SetTimer(TimerHandle, [&]()
 {
  const auto Character = Cast<AUS_Character>(GetOwner());
  const auto ProjectileSpawnLocation = GetComponentLocation();
  const auto ProjectileSpawnRotation = GetComponentRotation();
  auto ProjectileSpawnParams = FActorSpawnParameters();
  ProjectileSpawnParams.Owner = GetOwner();
  ProjectileSpawnParams.Instigator = Character;
  GetWorld()->SpawnActor<AUS_BaseWeaponProjectile>(ProjectileClass, ProjectileSpawnLocation, ProjectileSpawnRotation, ProjectileSpawnParams);
 }, .4f, false);
}

在这里,我们只是将生成逻辑移动到计时器句柄中,以在调用客户端投掷逻辑的同时延迟生成过程,以便立即开始动画。

注意

Unreal Engine 提供了比简单延迟方法调用更高级的动画同步方法,例如 Animation Notifies (docs.unrealengine.com/5.1/en-US/animation-notifies-in-unreal-engine/)。然而,为了本书的目的,延迟方法是满足我们需求的一个快速且实用的解决方案。

作为最后一步,打开 BP_Character Blueprint,选择 Weapon 组件,在 Details 面板中查找 Throw Animation 属性,并分配你已创建的 AM_Throw 蒙太奇。

你现在可以测试游戏,角色应该能够投掷匕首,并且与投掷动画正确同步。

在本节中,你刚刚涉足神秘的动画领域(尽管你还没有准备好与巫妖领主战斗),并创造了一个基本的动画系统,网络玩家会欣赏。在下一节中,你将使一些友好角色复活,并通过为玩家提供可以解救的人来为你的游戏增添更多乐趣。

添加 NPC 角色演员

在地下漫步,躲避或刺杀僵尸可能会很有趣,但我们不要忘记国王为我们支付的丰厚报酬。我们手头有一个救援任务——在骑士们被变成不死生物之前,从巫妖领主的地下城中解放他们。是时候开始认真工作了,我的无畏的开发者!

在本节中,你将创建一个 Actor Blueprint,它将作为你心爱的盗贼需要解救的囚犯(为了获得更多的经验值)。为了实现这样的系统,你将充分利用在 第七章* 使用远程过程调用 (RPCs)* 中实现的 Interactable 接口。

创建 NPC 角色

你将要创建的 NPC 是一个简单的、可复制的演员,当玩家与之交互时,它会欢呼,并授予一些经验值。我们需要做的第一件事是播放空闲和欢呼动画的动画蒙太奇。为此,完成以下步骤:

  1. Animations 文件夹中添加一个新的 AM_KnightIdle

  2. knight_Idle 动画添加到 montage 的 DefaultGroup.DefaultSlot 部分。

  3. 添加另一个 AM_KnightCheer

  4. knight_cheer动画添加到蒙太奇中。

在这两个动画资产准备就绪后,您可以开始创建囚犯蓝图。打开Blueprints文件夹并完成以下步骤。

  1. 基于的BP_KnightPrisoner创建一个新的蓝图。双击它以打开它。

  2. 组件面板中,添加一个骨骼 网格组件。

  3. 详细信息面板中,选中复制属性以在网络中复制演员。

  4. 创建一个EarnedXp。将其设置为20。选中实例可编辑属性以使变量公开。

  5. 创建一个MontageIdle。将其默认值设置为AM_KnightIdle。选中实例 可编辑属性。

  6. 创建另一个MontageCheer。将其默认值设置为AM_KnightCheer。选中实例 可编辑属性。

在这些基本设置可用的情况下,您可以从开始播放事件开始添加一些视觉脚本到事件图,以使演员完全功能化。您将从开始播放事件开始,以启动空闲动画。为此,完成以下步骤:

  1. 在图中添加一个事件开始播放节点。

  2. 组件面板,将骨骼 网格组件的引用拖入图中。

  3. 变量面板,拖动一个用于MontageIdle变量的获取节点。

  4. 事件开始播放执行引脚,创建一个播放 动画节点。

  5. 骨骼网格引脚连接到播放 动画节点的目标引脚。

  6. Montage Idle引脚连接到播放 动画节点的新动画播放引脚。

  7. 选中播放 动画节点的循环属性。

该图部分的最终结果如图图 10**.10所示:

图 10.10 – 事件开始播放图

图 10.10 – 事件开始播放图

然后,创建一个自定义事件,当玩家角色救出囚犯时,将启动欢呼动画。为了在所有客户端上启动动画,此事件需要作为多播事件执行。为此,完成以下步骤:

  1. 右键单击图,创建一个CharacterCheer。选择事件后,在详细信息面板中找到复制属性,并从下拉菜单中选择多播,保留可靠复选框未勾选,如图图 10**.11所示:

图 10.11 – 自定义事件复制

图 10.11 – 自定义事件复制

  1. 组件面板,将骨骼 网格组件的引用拖入图中。

  2. 变量面板中,拖动一个用于MontageCheer变量的获取节点。

  3. 事件开始播放执行引脚,创建一个播放 动画节点。

  4. 骨骼网格引脚连接到播放 动画节点的目标引脚。

  5. Montage Idle引脚连接到播放 动画节点的新动画播放引脚。

  6. 选中播放 动画节点的循环属性。

图表这一部分的最终结果如图 图 10.12 所示:

图 10.12 – CharacterCheer 自定义事件

图 10.12 – CharacterCheer 自定义事件

使演员正常工作的最后一步是通过实现 US_Interactable 接口使其与玩家角色可交互。为此,完成以下步骤:

  1. 打开 类设置 面板并定位到 接口 类别。

  2. 实现接口 中添加 US_Interactable 接口。

  3. 我的蓝图 面板中,定位到 接口 类别,右键单击 Interact 方法,选择 实现事件。将在事件图中添加一个 Event Interact 节点。

  4. 从事件 Character Instigator 引脚点击并拖动以添加一个 PlayerState 节点,并将其 Target 引脚连接到 Event Interact 节点的 Character Instigator 引脚。

  5. PlayerState 节点的输出引脚点击并拖动,创建一个 Cast To US_PlayerState 节点。将其执行输入引脚连接到 Event Interact 节点的输出执行引脚。

  6. 从 cast 节点的 As US PlayerState 点击并拖动,创建一个 Add Xp 节点。将其执行输入引脚连接到 cast 节点的 Success 执行引脚。

  7. 变量 面板拖动一个 EarnedXp 变量的获取节点。将其输出引脚连接到 Add Xp 节点的 Value 引脚。

  8. Add Xp 节点的输出引脚点击并拖动,创建一个 Character Cheer 节点以完成图表,如图 图 10.13 所示:

图 10.13 – 交互图

图 10.13 – 交互图

你可能已经注意到我们之前没有使用任何权限检查;这是因为我们知道这个事件只会在服务器上调用。

蓝图现在已完成,因此是时候进行一些测试了。

测试 NPC 演员行为

要测试蓝图,可以将其实例拖入关卡并开始游戏会话。盗贼角色应该能够到达 NPC,如果我们使用交互按钮,动画应该显示他们在欢呼。解放 NPC 角色的英雄将获得应得的丰富经验点作为奖励。是时候升级并成为更伟大的英雄了!

图 10.14 显示了 NPC 演员被盗贼英雄解放后的最终结果:

图 10.14 – 游戏过程中被解放的演员

图 10.14 – 游戏过程中被解放的演员

好吧,看来我们现在又有一个新囚犯可以玩弄了!但为什么只满足于一个呢?当我们能够有多种变化时,为什么不发挥创意,给我们的囚犯一些新鲜的外观,以保持事情的新鲜感呢?通过创建子蓝图并更改演员的骨骼网格组件和动画蒙太奇,你将能够充分利用项目中可用的野蛮人和法师模型。你甚至可以创建一个游侠囚犯的变化版本——谁说我们不能稍微偏离剧本呢?国王可能付钱给我们来营救他的骑士和战士,但嘿,盗贼公会里一个或两个熟练的英雄从未伤害过任何人!

恭喜你——你已经完成了这个冒险的一部分。现在,是时候让你的想象力自由驰骋,添加你自己的游戏逻辑了!在下一节中,我不会教你任何新的技术,但我会提供一些新想法来增强游戏玩法,使其更加刺激。

进一步改进游戏

现在你已经对虚幻引擎的多玩家系统有了扎实的了解,是时候释放你的创造力,让你的想法变为现实,让你的游戏真正独特和个性化了。在本节中,我将给你一些提示,告诉你如何让你的项目更加生动,但不要犹豫,加入你自己的创意,让它独一无二。

让我们制造一些噪音!

目前,小兵的听觉感知仅用于检测角色是否在奔跑。为什么不调整系统,让游戏中的其他元素也能警告巫妖领主的小兵呢?

不幸的是,PawnNoiseEmitterComponent 只能在,嗯... 基础单位上使用,所以你不能将它附加到其他演员上(它根本不会起作用);然而,在第九章**,扩展 AI 行为中,你构建了一个强大的系统,用于通过游戏模式来警告敌人小兵。由于游戏模式可以被关卡中的任何演员访问,你可以利用AlertMinions()函数并发送在激活时请求帮助的消息。

使用这种方法最好的方式之一是通过陷阱——每当玩家角色踏入这样的装置时,周围的所有小兵都会被警告。这类游戏功能的例子包括以下内容:

  • 吱吱作响的门:每当角色打开一扇门时,它都会发出吱吱或吱吱声,这会警告巫妖领主的仆人有关入侵者。

  • 陷阱:一些地牢区域比其他区域更受保护——设置一些能够召集所有附近敌人的机械装置。毕竟,这只是一个创建碰撞区域并在游戏模式中调用方法的问题!

  • 魔法物品:创建一些玩家可以与之交互的魔法神器。巫妖领主是个狡猾的家伙:他施了一个警报咒语,将不幸的盗贼英雄注定走向不可避免的命运。每当角色试图使用那个诱人的物品时,就会向附近的小兵发送警报。想想看!你甚至可以使用我们在项目开始时创建的漂浮书籍。

我需要一把钥匙!

在地牢中开门可能是一种有趣的游戏,但当你遇到一扇锁着的门时,事情会变得更加有趣。为什么不试试看,看看还有哪些惊喜在等待着你?

第七章《使用远程过程调用(RPCs)》中,你创建了US_Interactable接口,并使用了Interact()方法。然而,该接口还公开了CanInteract()方法,可以用来检查 Actor 是否可以交互。

一扇门可能实现了一个系统,只有当玩家角色有钥匙时,CanInteract()方法才会返回true——这意味着创建一个钥匙拾取物品,并添加US_Character系统来跟踪他们是否有一把或多把钥匙可以使用。这些锁着的门可以用来将 NPC 锁在一些地牢的牢房中,并且只有找到相应的钥匙才能被释放。

当心巫妖领主!他的囚犯被锁得比最深最严密的牢房里的商人钱袋还要紧!

提升你的武器库,我的英雄!

虽然有一把尖锐的匕首可以投掷给你的敌人很酷,但有一把魔法匕首会造成更多伤害,或者甚至一击击败敌人,那就更好了。你可以实现一个拾取蓝图,充分利用你在US_WeaponProjectileComponent类中实现的SetProjectileClass()函数。

拾取物品后,角色将获得一个增强伤害的US_BaseWeaponProjectile类变体。你甚至可以考虑让击败的敌人掉落武器拾取物而不是金币!

作为一项附加功能,你甚至可以考虑创建一些投掷的石头,当它们击中地面时会发送警报信息——只需记住为投射物启用重力。拥有可以投掷并发出噪音以警告小兵并将他们从玩家角色附近引开的物品,将为游戏增加一些新的游戏逻辑,从而提高整体游戏体验。

准备迎接一个狡猾的转折,并运用你的机智来迷惑巫妖领主的盲目仆人!以你的头脑,谁还需要力量?

你不是机枪

目前,玩家在游戏过程中可以投掷无限数量的匕首。虽然这最初可能很有趣,但最终会破坏游戏的平衡,并导致随着时间的推移游戏体验变得单调。

为了使事情更有趣(并且有利于巫妖领主的阴暗计划),限制玩家一次只能使用一把投掷匕首。一旦角色投掷了武器,除非匕首被找回,否则角色将无法再次投掷。

实现这个功能相当简单——一旦角色投掷了投射物,将ProjectileClass武器组件设置为null值,这样角色就无法再生成任何对象。在击中某个物体后,投掷的武器会在自我销毁之前生成一个匕首拾取(见前一小节)。这将迫使角色走到掉落的武器那里并捡起它,以便再次攻击。

作为一项替代功能,你可以给你的角色有限数量的刀子,并在玩家尝试投掷刀子时使用计数变量来检查角色是否有可用的刀子。

有人说地牢生活就像在公园散步一样轻松,显然他们从未在装备简单(而且是单一!)武器的情况下遭遇过一群不死怪物。

没有时间可以浪费

目前,你的角色可以平静地四处走动,并从容不迫地营救囚犯。为什么不添加一个时间计数器来增加一些趣味呢?巫妖领主正在举办一场盛大的庆祝活动,意图将国王的骑士变成他不死之军的忠实成员!你的英雄必须赶紧行动,以免为时已晚!

你可以利用US_GameMode类创建一个时间管理器,一旦第一个玩家进入地牢就会启动——如果玩家不能从地牢中释放所有被俘者,他们将非常不幸,游戏将彻底失败。看来这个任务似乎是一切或一无所有!

遍地都是表格!

随着你的项目进展,跟踪所有敌人和武器的变化将变得越来越困难。为了减轻痛苦,你可以使用在第六章**,跨网络复制属性中引入的结构和数据表系统,为投掷武器和 AI 对手创建专用结构。

让你的创意自由发挥,根据你最喜欢的统计数据提出大量令人惊叹的蓝图选项——准备好让你的英雄冒险家在你的游戏中踏上充满惊喜的惊险旅程吧!

需要一些帮助?

如你所注意到的,一旦你熟悉了你的游戏,潜在的结果将是无限的。你可以添加任何新的游戏逻辑并测试,直到你满意为止。

在我的这一边,我将致力于为游戏开发令人兴奋的新功能,并将它们存储在我的 GitHub 仓库中。随时查看,看看我提出了哪些疯狂的想法!仓库链接是github.com/marcosecchi/unrealshadows-ltol

如果你有一个巧妙的想法,请随时联系我并告诉我——如果时间允许,我会尝试实现它并将其上传到仓库,以便让这个项目不断成长!

摘要

在这一章中,你微调了游戏玩法逻辑并添加了最后的修饰。你从为角色移动和攻击添加一些漂亮的动画开始,提升了游戏的整体吸引力。

此外,你为玩家创造了一个可以营救的角色:一个可以被互动的囚犯 Actor,这将授予盗贼英雄一些应得的经验值。

最后但同样重要的是,我分享了一些新鲜的想法,让你的游戏体验提升到新的层次。通过融入这些想法,你可以使游戏真正成为你自己的独特之作。所以,发挥创意,享受乐趣!

准备进入下一章节,你将深入调试和测试一个网络游戏。这将提升你的开发技能到新的水平,这对于你想要成为一名顶尖的多玩家程序员来说是必要的!

第十一章:调试多玩家游戏

调试应用程序是编程的一般关键方面,尤其是在处理多玩家游戏编程时这一点尤其正确。调试过程帮助开发者识别和解决在运行网络应用程序或游戏时可能出现的任何问题。通过理解网络调试的基本知识,程序员可以确保他们的游戏在所有平台上都能平稳高效地运行。

当涉及到使用虚幻引擎调试网络时,有几种工具可以帮助程序员使这个过程更容易。这个过程的第一步是在您的项目设置中设置日志记录,以便您可以在游戏的生命周期开发或测试阶段跟踪发生的错误。

此外,开发一个模拟的多玩家环境可以是一种非常有效的方法来复制现实生活中的场景,同时评估您系统的操作效率。

此外,像网络分析器这样的工具将提供对关键指标(如连接速度和延迟)的详细洞察,从而能够识别潜在问题和需要改进的区域。

随着您在本章中的进展,您将获得对优化技术的全面理解,这将使您能够微调您项目的性能并确保无缝的多玩家游戏体验。此外,您将学习如何有效地隔离和解决可能破坏整体游戏体验的任何现有问题。

因此,在本章中,我将引导您了解以下部分:

  • 介绍网络调试

  • 模拟网络环境

  • 使用网络分析器

  • 提高性能和带宽利用率

技术要求

要跟进本章介绍的主题,您应该已经完成了前面的章节,并理解了它们的内容。

此外,如果您希望从本书的配套仓库开始编写代码,您可以下载本书配套项目仓库中提供的.zip项目文件:github.com/PacktPublishing/Multiplayer-Game-Development-with-Unreal-Engine-5

您可以通过点击Unreal Shadows – 第十章结束链接下载与上一章结尾同步的文件。

介绍网络调试

网络测试和调试是任何从事多人游戏工作的专业人士必备的技能。这需要深入理解网络协议和技术,以及快速识别和诊断问题的能力。此外,它还涉及在用户的设备上解决客户端问题,以及在游戏服务器上解决服务器端问题。通过掌握这项技能,你可以确保游戏对所有玩家来说运行顺畅,延迟最小。

当你开始开发网络游戏时,考虑以下障碍是至关重要的,这些障碍与为你的观众创造无缝和引人入胜的多人游戏体验相关,而不是单人游戏:

  • 你需要调试多个运行的项目实例

  • 网络通信由于其本质可能不可靠和不稳定,不同的客户端可能会有不同的问题

  • 客户端与服务器的工作方式不同

虚幻引擎配备了专门为调试网络应用程序设计的各种工具和工作流程。通过遵循本章提供的指南,你将获得如何有效利用这些工具的宝贵见解,以及学习解决你可能遇到的任何常见网络问题的专家技巧和最佳实践。

在我深入探讨虚幻引擎的调试工具如何操作之前,了解游戏调试的基本知识是至关重要的。

解释游戏调试

调试的过程涉及测试项目的每个部分,以确保一切按预期工作,并识别任何可以改进的领域——这将确保玩家在玩游戏时获得最佳性能和稳定性。调试还包括在不同平台和设备(例如,移动平台、桌面或 VR 设备)上检查代码功能,在部署前对构建进行自动化测试等。

最后,良好的调试实践将帮助你发现那些可能被忽视的细节,如果这些问题没有及早解决,它们可能会在开发过程中引起严重问题!

在前几章中,你使用了虚幻引擎提供的许多调试工具——可能最常用的是GEngine->AddOnScreenDebugMessage()命令,它增加了在屏幕上显示消息的功能。

一些其他调试工具纯粹是视觉上的——例如,DrawDebugSphere(),你在第七章中使用了它,使用远程过程调用(RPC),以显示玩家可以与之交互的 Actors 的位置。

如果你熟悉像 Microsoft Visual Studio、JetBrains Rider 或其他任何编程 IDE 这样的工具,你可能会知道使用断点的重要性——代码中可以暂时停止执行的点,以便你可以检查程序的数据和状态。

调试多人游戏 – 由于其非常专业的性质 – 需要一些额外的工具来检查幕后发生的事情。在接下来的子章节中,我将向你介绍一些这些工具,以帮助你提高你的多人编程技能。

介绍多人游戏选项

自从 第三章使用项目原型测试多人游戏系统,你已经使用了测试多人环境最常用的工具,通过选择监听服务器网络模式并选择要模拟的玩家数量。这些设置只是可以在项目设置中调整的多人游戏选项类别的一部分。要查看完整选项范围,从主菜单,执行以下操作:

  1. 选择窗口 | 编辑器首选项选项,找到级别编辑器 | 播放设置。

  2. 查找多人游戏选项类别,如图 图 11.1 所示:

图 11.1 – 多人游戏选项类别

图 11.1 – 多人游戏选项类别

这个类别提供了大量自定义和调试游戏选项 – 你已经使用了Play Net ModePlay Number of Clients(位于客户端子类别中),即使你从 Unreal 编辑器的其他部分(即主工具栏)设置了这些值。但还有更多!

例如,你可以找到多人视图大小(以像素为单位)选项,并从常见分辨率下拉菜单中选择客户端显示分辨率。这将让你测试游戏在目标设备上运行时的外观和感觉。图 11.2 显示了游戏在 720x1280 Razer 手机设备上运行后的外观:

图 11.2 – 智能手机显示模拟

图 11.2 – 智能手机显示模拟

嘿,我知道我们的项目最初并不是为了在智能手机上玩而设计的,但你还是明白了,对吧?

在本章的后面部分,我将演示多人游戏选项部分的一些附加功能 – 例如,流量模拟设置 – 这些功能将在你的项目调试阶段提高你的熟练度。

在网络环境中登录

如你所知,在 Unreal Engine – 或任何编程环境 – 日志记录可以用于调试和跟踪运行时代码的流程。日志记录在软件开发中是一种广泛使用的实践,多人游戏开发也不例外。Unreal Engine 提供了广泛的日志类别,其中一些是专门针对网络设计的。

输出日志窗口记录了所有消息,可以通过点击 Unreal Engine 底部的专用按钮打开,如图 图 11.3 所示:

图 11.3 – 输出日志激活按钮

图 11.3 – 输出日志激活按钮

此外,所有日志消息都保存在你的项目文件夹中的 .log 文件中(即 [你的 项目文件夹]/Saved/Logs/)。

每条日志消息都被分类,并且可以被过滤——例如,图 11.4 展示了我在编辑器中将游戏视口大小调整后的日志窗口:

图 11.4 – 日志窗口打开

图 11.4 – 日志窗口打开

在网络调试过程中,你将最常使用的分类是 LogNet,它包括大多数网络日志。

注意

要查看网络环境中所有可用日志分类的详尽列表,请查看官方文档:docs.unrealengine.com/5.1/en-US/logging-for-networked-games-in-unreal-engine/

过滤 LogNet 分类

现在你对网络日志系统有了基本了解,你可以尝试玩游戏并检查 输出日志 窗口,看看底层发生了什么。为此,请按照以下步骤操作:

  1. 在虚幻引擎中打开你的项目,并点击编辑器底部的 输出日志 按钮。可选地,你可以点击 停靠在布局 按钮将窗口停靠在编辑器内部,使其不可折叠。

  2. 一旦日志窗口打开,找到 过滤器 按钮,点击它以打开所有过滤器。你会注意到 LogNet 分类没有显示。要启用它,你需要开始一个游戏会话——一旦进入播放模式,你会注意到该分类是可见的。

  3. 过滤器 列表中,点击 分类 部分,取消选择 显示所有 选项以取消选择所有分类,然后找到 LogNet 分类以启用它,如图 图 11.5 所示:

图 11.5 – 日志分类过滤器

图 11.5 – 日志分类过滤器

一旦你只启用了 LogNet 分类,你将得到一个过滤后的日志列表,如图 图 11.6 所示。6*:

图 11.6 – LogNet 输出

图 11.6 – LogNet 输出

如你所见,这里有很多事情在进行中,这取决于你的游戏会话。

作为额外的练习,花些时间阅读日志消息。一开始你可能不太理解,但随着时间的推移,这种与虚幻引擎的交流将变得熟悉。

在下一小节中,你将学习如何创建日志分类,这样你就可以轻松跟踪应用程序内部发生的事情。

创建自定义日志分类

通常情况下,当你陷入项目的漩涡中时,你会倾向于使用日志消息而不太在意它们的分类。当然,这种错误在长期来看是需要付出代价的。为你的日志创建自定义分类很简单,而且没有不去做的好理由。

要定义一个自定义类别,你需要在头文件中使用DECLARE_LOG_CATEGORY_EXTERN宏,并在相应的源文件中引入DEFINE_LOG_CATEGORY宏。此外,类别名称必须以Log前缀命名 – 例如,LogMyApp。作为一个例子,在接下来的几个步骤中,你将创建一个名为LogUnrealShadows的自定义日志类别,你可以在项目的任何地方使用它。

因此,打开你的编程 IDE 并创建一个名为US_CustomLogs的新空类 – 你不需要 Unreal 类,只需要一个普通的 C++类。

然后,打开US_CustomLogs.h文件,删除类声明,因为你不会使用它。之后,添加以下代码行:

DECLARE_LOG_CATEGORY_EXTERN(LogUnrealShadows, Display, All);

此宏将LogUnrealShadows声明为项目中的一个新日志类别。此类别的详细程度设置为Display;这意味着消息将被打印到控制台和日志文件 – 如果你只需要将消息打印到日志文件而不是控制台,你可以使用Log值代替。

现在,打开US_CustomLogs.cpp文件,通过添加以下代码行来定义日志类别:

DEFINE_LOG_CATEGORY(LogUnrealShadows);

此宏将允许你在项目的任何位置使用LogUnrealShadows类别。一旦进入播放模式,你将能够从输出日志过滤器中选择LogUnrealShadows类别,如图图 11.7所示。7*:

图 11.7 – 类别

图 11.7 – LogUnrealShadows类别

现在类别已经定义,你可以用它来在游戏中添加日志 – 要这样做,你可以使用UE_LOG()宏。例如,打开US_GameMode.cpp文件并添加所需的include声明:

#include "US_CustomLogs.h"

然后,要在AlertMinions()函数类中记录警报消息,你必须添加以下代码行:

UE_LOG(LogUnrealShadows, Display, TEXT("Alerting Minions"));

图 11.8显示了在检测到角色后输出日志面板中显示的上述消息:

图 11.8 – 输出日志面板中的自定义消息

图 11.8 – 输出日志面板中的自定义消息

本节向你介绍了在 Unreal Engine 中可用的关键调试工具,并解释了如何有效地利用它们。在下一节中,你将了解到如何在个人计算机上模拟网络环境,这将为你在执行过程中可能出现的重大问题提供测试项目的能力。

模拟网络环境

创建一个多人网络环境的副本可以是一种有效地模拟真实世界场景并测试系统性能的方法。通过利用 Unreal 的能力,你将能够在单台机器上测试多个连接,并提供一个真实感十足的经验,这将让你在游戏上线后对游戏行为有一个准确的了解。

网络仿真是一个重要的功能,可以帮助您模拟服务器和客户端的延迟和数据包丢失。这在识别和解决网络问题方面尤为重要。Unreal 编辑器、命令行控制台和配置文件都提供了可配置的网络仿真设置,以确保它可以精确地满足您的需求。

启用网络仿真

网络仿真可以从编辑器首选项窗口中的级别编辑器 | 播放部分启用。要启用此工具,找到多人选项类别并勾选启用网络仿真选项,如图图 11.9所示:

图 11.9 – 启用网络仿真

图 11.9 – 启用网络仿真

选择此选项将启用一组选项,您可以在多人游戏中测试不同的场景。首先,您可以通过仿真目标属性选择要仿真的目标 – 此处有三个选项:

  • 仅服务器:此选项将仅模拟网络中的服务器行为

  • 仅客户端:此选项将仅模拟网络中的客户端行为

  • 所有人:此选项将在网络上模拟客户端和服务器的行为

其次,您有权访问网络仿真配置文件,这将允许您为网络游戏选择不同的场景 – 此处有三个选项:

  • 平均:此选项将模拟常规多人游戏

  • :此选项将创建一个最坏的情况,在网络游戏中会有很大的时间延迟和大量数据包丢失

  • 自定义:此选项将允许您使用自己的值自定义仿真体验

如前所述的配置文件将为传入流量传出流量的客户端或服务器(或两者)的值初始化一组值,具体取决于仿真目标的选择。图 11.10显示了选择自定义选项时的扩展配置文件选项:

图 11.10 - 扩展的仿真配置文件

图 11.10 - 扩展的仿真配置文件

在下一小节中,我将向您展示多人选项类别中此部分的大多数设置的含义。

解释传入流量选项

激活传入流量选项将在游戏时间内的数据包接收过程中引入延迟或丢失。您可以修改以下属性:

  • 最小延迟:这表示以毫秒为单位的最小时间延迟

  • 最大延迟:这表示以毫秒为单位的最大时间延迟

  • 数据包丢失百分比:这表示数据包在接收前丢失的概率

例如,典型的坏情况(即,接收流量)将具有大约 100 到 200 毫秒的延迟和大约 5%的接收数据丢失概率。要创建类似的场景,您的设置可能具有以下值:

  • 100 毫秒

  • 200 毫秒

  • 5%

解释出站流量选项

同样,激活出站流量选项将在游戏过程中发送数据包时引入延迟或丢失。您可以修改以下属性:

  • 最小延迟:这表示以毫秒为单位的最低时间延迟

  • 最大延迟:这表示以毫秒为单位的最大时间延迟

  • 丢包百分比:这表示在接收到数据包之前数据包丢失的概率

例如,您可以通过将延迟设置为大约 30 到 60 毫秒,以及数据丢失的概率约为 1%来模拟一个平均情况(即,不是最优但仍可接受的情况)。您的设置可能具有以下值:

  • 30 毫秒

  • 60 毫秒

  • 1%

您现在已经了解了所有可以模拟真实网络游戏环境的方法。现在是时候将所学知识付诸实践,并用您的多人游戏尝试一下!Lichlord 有点不耐烦了,正焦急地等待着您。最好别让他等得太久,所以我们不要浪费时间!

使用网络仿真测试游戏

一旦您理解了上述元素,在网络上仿真测试游戏就相当直接了——选择您首选的设置并运行游戏。我们将模拟不同的网络场景进行游戏仿真,因此请打开您在前几章中工作的项目,并准备好进行一些测试。

在平均条件下测试游戏

在此场景中,您将测试编辑器首选项区域中可用的默认配置文件之一,并检查游戏的行为。为此,请按照以下步骤操作:

  1. 打开编辑器首选项并定位到播放|多人游戏类别。

  2. 打勾选择启用网络仿真复选框,并将仿真目标设置为所有人;使用此选项,我们将测试客户端和服务器网络流量。

  3. 网络仿真配置文件下拉菜单中,选择平均。设置显示在图 11.11。11*:

图 11.11 – 使用平均配置文件的网络仿真

图 11.11 – 使用平均配置文件的网络仿真

设置好此配置文件后,开始一个作为监听服务器游玩的游戏会话并分析您的游戏。您应该看到游戏几乎流畅运行,没有延迟或同步问题。这是因为我们选择了非常低的丢包百分比(即,1%)以及服务器和客户端的延迟将在 30 到 60 毫秒的范围内。

我们这里有一个可接受的游戏场景,玩家的体验将会很顺畅。现在,让我们尝试使用一些恶劣的条件来看看游戏的表现如何。

在最坏条件下测试游戏

在这次第二次测试中,你将测试一个最坏的情况,其中网络将有高比例的数据包丢失,流量延迟将模拟糟糕的网络带宽。为此,请按照以下步骤操作:

  1. 打开编辑器首选项并定位到游戏 | 多人游戏类别。

  2. 打勾启用网络仿真复选框并将仿真目标设置为所有人;使用此选项,我们将测试客户端和服务器网络流量。

  3. 网络仿真配置文件下拉菜单中选择自定义

  4. 对于450

  5. 设置550

  6. 设置10。设置如图图 11.12所示:

图 11.12 – 最坏情况下的网络仿真

图 11.12 – 最坏情况下的网络仿真

使用此配置文件设置后,启动一个作为监听服务器玩游戏的游戏会话并分析你的游戏。你将体验到几乎损坏的游戏体验!同一个角色似乎在不同窗口中完全不同步,你的角色攻击也是如此。

但请注意,我说的是“几乎” – 因为服务器是权威的,我们使用可靠的 RPCs 进行最重要的操作,例如投掷匕首,从执行角度来看,游戏将完美无缺地继续进行。这意味着同一个角色的位置迟早会在所有客户端上同步,无论数据包丢失百分比如何,匕首总是会投掷。

如果你想要测试一个完全损坏的游戏,尝试设置100,这意味着服务器或客户端将不会接收到任何数据包。当进入游戏模式时,客户端甚至不会启动,你将得到的只是一个黑屏。但这意味着巫妖王在网络中施加了一个邪恶强大的魔法,让你的无畏盗贼团伙被困在一个神秘的泥潭中。除非国王法庭上的某个技术娴熟的巫师能够施展一个巧妙的反魔法来解开这个咒语,否则就是如此!

在本节中,你学习了如何直接从你信任的电脑中测试你的游戏在网络仿真环境中。最强大的功能之一是能够模拟数据丢失和网络延迟 – 这意味着检查玩家在任何场景下的体验,包括最坏的情况,在这种情况下,由于糟糕的网络技术,玩家将几乎完全无法体验游戏。

在下一节中,你将了解到提高游戏性能的另一个重要主题 – 如何分析网络应用程序。

使用网络分析器

Unreal Engine 的 网络性能分析器是一个功能强大的独立工具,能够分析和优化多人游戏网络的性能。性能分析会话将为您提供关于连接速度、延迟时间以及其他重要指标的详细信息,这些信息可用于识别潜在问题或改进区域。通过利用这些信息,您将能够获得最佳网络性能并实现更好的用户体验。在本节中,我将指导您了解此工具的主要功能。

如前所述,网络性能分析器是一个独立的应用程序,可以在您的 Unreal Engine 可执行文件文件夹中找到。根据您的引擎安装情况,路径可能有所不同,但通常位于 [您的 PC]/Programs Files/Epic Games/UE_5.1/Engine/Binaries/DotNET/NetworkProfiler.exe

网络性能分析器应用程序如图 11**.13 所示:

图 11.13 – 网络性能分析器应用程序

图 11.13 – 网络性能分析器应用程序

在下一节中,我将向您展示如何使用网络性能分析器记录网络会话并查看其数据。

记录性能分析会话

要使用网络性能分析器,您需要收集一些数据供其分析。为此,您需要与一个已启用统计跟踪的引擎版本一起工作,例如为非调试配置构建的调试器或编辑器 – 在我们的案例中,我们将直接从 Unreal Engine 编辑器使用默认的 平均 仿真配置文件记录数据。

要记录性能分析会话,请按照以下步骤操作:

  1. 编辑器首选项窗口中打开播放 | 多人选项类别,如前文所述。

  2. 选择启用网络仿真并将仿真目标设置为所有人。然后,将网络仿真配置文件设置为平均

  3. 启动您的游戏并定位编辑器底部(或任何打开的 输出日志 窗口的底部)的控制台命令提示符,如图 11**.14 所示:

图 11.14 – 控制台命令提示符

图 11.14 – 控制台命令提示符

  1. 在提示符内部,输入此命令:

    netprofile enable
    

这将启动性能记录会话,输出日志窗口应显示以下消息:

LogNet: Network Profiler: ENABLED
  1. 游戏几分钟,然后输入以下命令。这将关闭性能分析会话:

    netprofile disable
    
  2. 此命令将关闭性能分析会话,并在您的项目文件夹的以下位置保存包含所有记录数据的 .nprof 文件:

    [Your Project folder]/Saved/Profiling
    

作为替代,您也可以直接使用 netprofile 命令,每次使用时都会切换性能分析器。

保存您的性能分析会话后,您可以使用性能分析工具打开它。

分析性能分析会话

在启动 Profiler 应用程序后,您可以点击Profiling文件夹以打开您已记录的会话。一个示例会话如图图 11.15所示:

图 11.15 – 分析会话示例

图 11.15 – 分析会话示例

您将看到很多信息,包括包含所有网络信息的图表。让我们关注右下角的部分,您将看到 IP 地址列表,如图图 11.16所示:

图 11.16 – 服务器和客户端 IP 地址列表

图 11.16 – 服务器和客户端 IP 地址列表

此列表表示会话期间被分析的客户和服务器 - 如您所见,在我的例子中,我有一个服务器(使用端口17777)和两个客户端(使用端口5489754898)。您可以选择需要分析的客户或服务器,然后点击应用过滤器按钮以仅显示其分析数据。此外,您还可以启用一些下拉菜单,这将允许您过滤其他数据,例如游戏中单个演员类型。一个例子是BP_Character蓝图类。

一旦您选择了需要分析的客户端或服务器,您可以通过在图表数据上点击并拖动来选择代表游戏过程中一组帧的图表的一部分,如图图 11.17所示:

图 11.17 – 图表数据的选择部分

图 11.17 – 图表数据的选择部分

如果需要,您甚至可以通过点击图表来选择单个帧。图 11.18显示了图表中的帧选择:

图 11.18 – 图表数据中的单个帧

图 11.18 – 图表数据中的单个帧

现在,选择位于应用程序左下角的演员选项卡;您将看到在所选帧范围内复制的所有演员。我的分析记录如图图 11.19所示:

图 11.19 – 演员选项卡

图 11.19 – 演员选项卡

这里最有趣的列之一是MS,它显示了复制一个演员所需的毫秒数。使用此值,您可以确定演员是否需要过多时间来复制,然后继续调查此问题的根本原因。

另一个需要考虑的非常重要的列是您的演员的NetUpdateFrequency值(即更新网络数据时将经过的时间间隔)到一个更高的值,以减少其值的更新频率。

如果您选择其中一个演员,您将获得在分析时间范围内复制的详细属性信息。例如,图 11.20显示了在约 1 分钟时间范围内的BP_WoodenDoor数据:

图 11.20 – BP_WoodenDoor 蓝图的分析数据

图 11.20 – BP_WoodenDoor 蓝图的性能分析数据

在游戏过程中,其中一个角色与门进行了交互,因此您可以看到 DoorOpen 属性被复制了两次——一次是起始值,一次是当角色打开时——以及 RelativeRotation 属性的 60 次复制——当打开动画被激活时。

这里的问题在于将 NetUpdateFrequency 设置为更高的值。

在积极的一面,看看 BP_MinionMiniboss Actor 的 Waste 值,如图 11.21 所示:

图 11.21 – BP_Miniboss 蓝图的性能分析数据

图 11.21 – BP_Miniboss 蓝图的性能分析数据

我在 1 分钟的游戏中达到了 27.32% 的良好成绩!但请注意...这并不意味着我在这个角色复制上花费了更少的资源。这表明我正在更有效地利用可用资源。

现在,打开位于应用程序顶部的 所有 RPC 选项卡;您将获得在所选帧范围内使用的远程过程调用的列表。图 11.22 显示了我迄今为止使用的示例情况:

图 11.22 – RPC 性能分析窗口

图 11.22 – RPC 性能分析窗口

如您所见,您可以分析远程调用发生的次数以及调用成本 - 这额外的信息可以帮助您优化代码并简化程序性能。

在本节中,我向您介绍了一个在多人游戏开发中最重要的工具——网络分析器。通过深入了解如何使用它,您将能够分析游戏网络性能的各个方面,例如远程调用的频率和成本,以及潜在的瓶颈。这些信息可以帮助您识别可以优化游戏性能的领域,同时为玩家提供更流畅和愉快的体验。

在下一节中,我将与您分享一些希望对提高您的多人项目有用的技巧,并帮助您避免游戏上线后可能出现的常见问题。

提高性能和带宽利用率

尽管虚幻引擎力求最大化复制 Actor 的效率,但通常这是一个耗时过程,可能会对性能产生负面影响。为了简化这项任务,您可以采取一些步骤来优化复制并使其更高效。在本节中,我将为您提供一些关于如何提高性能和避免可能阻止游戏高效运行的瓶颈的建议。

仅在必要时开启复制

在复制 Actor 时,服务器执行各种检查,例如相关性、更新频率和休眠状态等。避免在不需要此功能的 Actor 上开启复制,以避免进行这些检查。

如果你真的需要 Actor 复制,考虑对不太重要(或不太频繁更改)的 Actor 调整NetUpdateFrequency。此属性将设置 Actor 在网络上的最大更新频率。例如,一个背景 Actor,如 NPC,可能以非常慢的速度更新——比如说每 0.5 秒更新一次——而快速移动的敌人可能需要每 0.2 秒更新一次。

在某些情况下,你可能想实现自定义网络相关性规则(或覆盖可用变量),这有助于在运行时减少网络负载。

避免调用不必要的或非必要的 RPC

可以避免的 RPC 被认为是多余的 RPC,应该避免。

例如,如果你能确保非复制函数只会在服务器上运行,那么不需要在服务器 RPC 中包含特定于服务器的逻辑。

另一个例子是在客户端上的方法调用——如果你能保证客户端是本地控制的(即使用APawn::IsLocallyControlled()),你可以避免使用 RPC。

区分可靠和不可靠 RPC

如您在第七章中已知的,使用远程过程调用(RPC),任何复制方法都可以是可靠的或不可靠的,并且默认情况下,RPC 是不可靠的。

在两种选项之间做出正确的选择可能会彻底改变你的游戏行为。为了帮助你,以下是一个关于可靠和不可靠 RPC 的优缺点的列表:

  • 可靠 RPC

    • 优点:函数将以发送时的相同顺序到达目的地

    • 缺点:函数将消耗更多带宽,可能导致更长的延迟

  • 不可靠 RPC

    • 优点:与可靠调用相比,函数将导致更低的带宽使用率;这使得它们成为需要频繁调用的函数的好候选者。

    • 缺点:函数可能无法成功到达目的地,或者 RPC 调用中可能存在缺失,尽管它们将以正确的顺序进行处理。

例如,你应该避免过于频繁地发送可靠事件,比如在Tick()事件上,因为引擎的可靠事件缓冲区可能会过载,从而导致相关玩家断开连接。这种调用使用不可靠函数会更安全——例如,在非关键性的外观事件上,如生成声音和视觉效果。

验证数据

如果你使用 C++,RPC 是唯一从客户端向服务器以及相反方向传递数据的方式,因此,在需要时验证它是好习惯。RPC 验证函数背后的概念是,如果它发现任何无效参数,它可以通知系统断开发起 RPC 调用的客户端或服务器。为了确保响应性,最好直接从客户端检索数据并在服务器端进行验证。

记住 GameMode 只存在于服务器上

这可能看起来是一个非常基础的话题,但您应该始终记住,GameMode 是一个非复制的 Actor,并且它只在服务器上运行。这意味着,无论您何时尝试从客户端获取它,您都会得到一个null值。因此,在它上面调用 RPC 纯粹是胡说八道,因为它只会在服务器上本地运行。

为 RPCs 使用命名约定

随着您的项目规模的增长,跟踪哪些函数是 RPCs 以及哪些不是可能会变得具有挑战性;这意味着使用良好的命名约定可能会节省时间。您可以使用我在前几章中向您展示的_Server_Client后缀,或者您可以选择Server_Client_前缀。如果您愿意,甚至可以区分_Client_Multicast RPCs。

如果您与团队一起工作,这通常是一个要求。然而,即使您单独工作,您也会发现这个约定在长期来看是有用的。

如您可能已经注意到的,提高您的游戏水平是一个永无止境的旅程,它永远不会真正结束。您将始终在调整事物、分析数据和识别任何障碍。通过我提到的建议,我希望能让这个过程对您来说压力小一些!

摘要

在本章中,我谈到了游戏编程中的一个非常重要的话题——确保您的游戏正常运行并修复任何出现的问题。在早期章节中,我们介绍了一些在虚幻引擎中查找和修复问题的工具,但现在,我已经给了你一些更强、更有帮助的工具,您可以使用这些工具使您的游戏开发更加出色。首先,您学习了如何从编辑器首选项中配置多人选项,之后您创建了一个日志类别,以便在调试时正确设置自定义消息。然后,您被介绍如何通过测试网络问题(如数据包丢失或低带宽)在单个 PC 上模拟真实的多人环境。

接下来,我介绍了网络分析器,这是一款独立的软件,可以让您读取和分析多人会话,以帮助您找到代码中的潜在问题和瓶颈。

最后,我给了你一些如何进一步改进项目的建议。

在发布前测试和调试游戏可以帮助确保游戏运行顺畅并为玩家提供积极的体验。此外,拥有调试所需的正确工具和知识可以使过程更快、更简单,在开发过程中节省宝贵的时间和资源。了解如何调试多人游戏至关重要,因为多个玩家的参与可能会产生更复杂的技术挑战,例如同步和延迟问题。

在接下来的章节中,我们将回到我们的项目(以及 lichlord 的邪恶阴谋!)我们的目标这次是掌握管理游戏会话的艺术,以确保我们的未来热情的玩家们拥有难忘的体验。让我们确保他们能继续回来获取更多,好吗?

第四部分:在线部署你的游戏

在本书的最后部分,你将熟悉《虚幻引擎》多玩家系统的更复杂元素。你将从深入游戏会话管理开始,进而构建你游戏的可部署版本。最后,你将了解云服务,这些服务可以增强你的游戏对玩家的吸引力。

本部分包括以下章节:

  • 第十二章管理多玩家会话

  • 第十三章会话期间处理数据

  • 第十四章部署多玩家游戏

  • 第十五章添加史诗在线服务 (EOS)

第十二章:管理多人会话

如您从之前的章节中已经了解到的,游戏会话由一个服务器表示,多个玩家都连接到这个服务器上。

虚幻引擎提供了一个强大的框架来创建、销毁和处理游戏会话。通过掌握如何处理多人会话,程序员可以确保他们的游戏将为所有玩家提供愉快且无瑕疵的体验。

在本章中,您将了解到管理游戏会话所需的主要概念,从基本设置到创建会话。然后,您将学习如何让客户端搜索可用的会话以及如何加入它们。到本章结束时,您将构建一个用户界面,该界面将用于后续处理虚幻引擎的多玩家会话系统。

在接下来的几节中,我将介绍以下主题:

  • 理解游戏会话

  • 准备项目游戏会话

  • 创建会话

  • 加入会话

技术要求

要跟进本章介绍的主题,您应该已经完成了第十一章调试多人游戏,并理解其内容。

此外,如果您希望从本书的配套仓库开始编写代码,您可以下载本书配套项目仓库中提供的.zip项目文件:github.com/PacktPublishing/Multiplayer-Game-Development-with-Unreal-Engine-5

您可以通过点击Unreal Shadows – 第十一章结束链接下载与上一章结尾同步的文件。

理解游戏会话

游戏会话期间,玩家可以通过连接到远程服务器或甚至使用他们的计算机作为专用服务器来参与在线游戏。虚幻引擎的游戏会话系统提供了一系列令人印象深刻的在线功能,如服务器浏览器、玩家限制、网络上的服务器搜索等。它易于使用,只需几个命令即可激活。无论运行在玩家的机器上还是专用服务器上,游戏会话都为玩家提供了连接并沉浸于游戏虚拟世界的方式。在本章中,我们将专注于设置本地网络环境,将专用服务器的设置留到下一章。

然而,在我们开始处理会话之前,我需要向您介绍在线子系统和其独特的特性。

介绍在线子系统

在虚幻引擎中,在线子系统是一个提供访问在线服务(如 Epic 在线服务、Steam、Xbox Live 等)功能的标准方法的系统。这在支持多个平台或在线服务的游戏开发场景中特别有用。在这种情况下,在线子系统通过为每个支持的服务启用配置调整来消除开发者进行代码更改的需求。这确保了编码过程和开发努力在整个支持的平台和服务中都是简化的、高效的和一致的。

在线子系统的核心目的是管理与各种在线平台的异步通信。由于网络速度、服务器延迟和后端服务运行时间通常对本地机器来说是未知的,因此与这些系统的交互在持续时间方面高度不可预测。为了解决这个问题,在线子系统利用委托来处理所有远程操作,确保它们在利用任何支持异步功能时总是执行。委托通过允许系统在请求完成时响应,以及允许开发者查询运行时请求,发挥着双重作用。通过提供一个单一的代码路径来遵循,委托消除了开发者编写自定义代码来处理各种成功或失败条件的需要。

支持的功能被分组到特定于服务的模块化接口中。例如,排行榜接口涵盖了与排行榜访问相关的所有方面——例如注册个人分数或时间,以及检查来自全球玩家或你朋友列表中的排行榜分数——而购买接口涵盖了游戏内购买和处理过去购买历史的过程。每个支持在线服务的功能集都有一个相关的接口,允许开发者编写与所使用的在线服务无关的代码,从而在服务之间促进一致性。

Epic Games 提供了一系列插件,允许开发者在线子系统上工作,包括针对最常见和常用平台的专用插件。图 12.1显示了虚幻引擎的插件 | 在线 平台部分中可用的许多元素的一部分:

图 12.1 – 可用的某些在线子系统插件

图 12.1 – 可用的某些在线子系统插件

在本章中,我们将使用默认的在线子系统,它提供了处理会话的最基本操作,包括其创建、销毁和加入。在第十五章“添加 Epic 在线服务(EOS)”,我们将深入了解更高级的在线子系统功能。让我们先学习如何管理最基本的会话操作。

理解会话命令

那么,如何管理和操作多人会话呢?您可以使用以下四种主要操作来处理会话:

  • 创建会话

  • 销毁会话

  • 查找会话

  • 加入会话

让我们逐一深入了解它们,并详细分析。

创建会话

创建会话命令构成了会话过程的起点。一旦会话创建成功,它就会对游戏的其他实例可见,这些实例随后可以加入。创建会话命令允许您指定关键方面,例如会话中允许的玩家数量和局域网模式,这可以让您管理基于互联网的游戏或局域网游戏。

销毁会话

如果您正在托管会话(即您创建了会话),销毁会话将关闭它,使其不再可用于发现和加入。连接的客户端将立即从会话中断开连接。如果您是连接到会话的客户端,调用此命令是离开会话和游戏的方法。

查找会话

通过使用查找会话命令,您可以检索当前创建和可访问的游戏会话的完整列表。在成功调用此方法后,返回的对象可以查询以获取重要信息,例如服务器的名称、ping 和玩家数量。至于创建会话命令,您可以选择基于互联网的游戏或基于局域网的。

加入会话

在确定所需的会话后,您可以发起一个加入会话调用以加入游戏。在成功连接到服务器后,游戏将自动切换到服务器的地图,并允许您参与正在进行的游戏。

理解连接错误

如您所知,网络操作总是存在遇到错误的风险,对于游戏来说,妥善处理这些错误至关重要。例如,主机离开会话(或崩溃)、临时互联网连接问题或其他不可预见的问题都是常见例子。

与上述会话功能相关的任何失败都将通过适当的事件进行通信,或者在蓝图的情况下,通过专门的执行引脚,以便开发者相应地配置游戏响应。这将确保即使在关键时刻,玩家也能获得良好的体验。

现在您已经掌握了如何创建和监督多人游戏会话,让我们回到虚幻引擎中,将这一知识应用到我们的项目中!在接下来的几节中,您将创建一个用户界面,让您能够托管会话并将其暴露给网络或加入一个会话。

准备项目游戏会话

在本章和下一章中,我们将创建一个新的级别,它将作为你游戏的起点,并允许玩家托管会话或加入会话。如前所述,在本章中,我们将专注于创建局域网托管的游戏——这意味着所有玩家都将连接到同一个本地网络——将托管互联网游戏的更复杂细节留到下一章。

级别将非常简单,并将包含以下元素:

  • 一个用户界面小部件,它将执行以下操作:

    • 允许玩家创建和托管会话

    • 允许玩家查找并加入托管会话

  • 玩家将使用的角色 3D 模型,用于更改皮肤颜色

在本章中,我们将专注于创建用户界面——包括所有必需的 widgets——将级别创建和皮肤处理留给下一章。

此界面将专注于主要会话功能,而不是其视觉外观。然而,你可以根据自己的个人喜好来决定风格和外观——只需让你的创造力自由发挥!

为了创建我们的用户界面,我们再次使用 UMG——你在第六章在网络中复制属性 O**ver the Network中使用了它。使用 UMG 系统的优点之一是你可以创建一个自定义小部件,并使用它来组合完整的界面。这将让你将元素分离成逻辑块,并保持整体系统的整洁和可重用性。

我们将首先创建创建和加入会话所需的所有元素。在本章结束时,我们将把所有内容整合到主菜单界面中。

要开始,我们需要四个主要组件:

  • 一个创建会话小部件,它将帮助我们处理会话创建

  • 一个查找会话小部件,它将允许我们在用户界面中查找并列出可用会话

  • 一个会话项渲染器小部件,它将用于显示每个会话的信息,并允许我们加入会话

  • 一个主菜单小部件,它将允许我们在屏幕上显示之前的 widgets

让我们通过打开虚幻项目并导航到内容 | 蓝图文件夹来开始创建这些小部件。我们将添加的第一个小部件是创建会话

创建会话

在本节中,你将创建一个用户界面小部件,它将允许你管理会话创建。特别是,创建会话小部件将执行以下操作:

  • 允许玩家选择单个会话中允许连接的玩家数量

  • 通过点击按钮创建会话

  • 打开游戏级别并开始游戏

我们必须做的第一件事是创建实际的界面小部件,因此,在内容浏览器的蓝图文件夹中,执行以下操作:

  1. 右键单击并选择用户界面 | 小部件蓝图。在出现的弹出窗口中,选择用户小部件,如图图 12**.2所示:

图 12.2 – 用户小部件创建窗口

图 12.2 – 用户小部件创建窗口

  1. 将新创建的小部件命名为 WB_CreateSession 并双击它以打开它。

现在,我们将添加视觉元素。

添加视觉元素

一旦编辑器打开,选择 设计 视图并执行以下操作:

  1. CreateSessionPanel.

  2. Background.

  3. 设置其 (0, 0, 0, 0.4).

  4. 按住 CtrlShift 键,点击 锚点 下拉菜单并选择右下角的按钮,使背景扩展到 画布 面板的整个区域。选择按钮在 图 12.3 中显示:

图 12.3 – 背景锚点选择

图 12.3 – 背景锚点选择

  1. Container

  2. 按住 CtrlShift 键,点击 锚点 下拉菜单并选择右下角的按钮,使背景扩展到 画布 面板的整个区域

到此为止,您已创建了小部件的容器 – 没有什么花哨的,但它完全功能正常。小部件层次结构可以在 图 12**.4 中看到:

图 12.4 – CreateSession 小部件的部分层次结构

图 12.4 – CreateSession 小部件的部分层次结构

现在我们将添加允许玩家创建会话的工作元素。为此,请按照以下步骤操作:

  1. TitleLabel

  2. 设置其 5.0

  3. 文本属性设置为创建会话

  4. Separator

  5. 设置其 10.0

  6. MaxPlayersLabel

  7. 设置其 5.0

  8. 文本属性设置为最大玩家数

  9. MaxPlayersSpinBox

  10. 设置其 10.0

  11. 启用 1

  12. 启用 5

  13. 设置两个 0

  14. 确认已选中 是变量 复选框

  15. CreateSessionBtn

  16. 设置其 5.0

  17. 确认已选中 是变量 复选框

  18. CreateSessionLabel

  19. 将其文本属性设置为创建

  20. 将其 对齐 属性设置为 文本居中

小部件的最终结构在 图 12**.5 中显示:

图 12.5 – 最终 CreateSession 小部件层次结构

图 12.5 – 最终 CreateSession 小部件层次结构

小部件的 设计 视图在 图 12**.6 中显示:

图 12.6 – CreateSession 小部件的设计视图

图 12.6 – CreateSession 小部件的设计视图

使用此布局,玩家将能够选择会话中托管的最大玩家数并启动会话本身。

实现视觉脚本逻辑

现在视觉部分的小部件已完成,您可以开始添加视觉脚本逻辑。通过点击 图形 按钮打开 图形 面板,在 变量 面板中,您应该已经有两个变量 – CreateSessionBtnMaxPlayersSpinBox。然后,完成以下步骤:

  1. 添加一个名为 MaxPlayers 的新变量。

  2. 选择MaxPlayersSpinBox变量,在Events面板中点击On Value Changed +按钮以创建一个事件。

  3. 选择CreateSessionBtn变量,在Events面板中点击On Clicked +按钮以创建一个事件。

之前的步骤将创建两个事件来处理相应的用户交互,如图12.7所示:

图 12.7 – CreateSession 图事件

图 12.7 – CreateSession 图事件

我们将首先处理旋转框更改事件,这只需要将其值分配给MaxConnections变量。为此,请按照以下步骤操作:

  1. Variables面板中拖动一个Set Max Players节点。

  2. On Value Changed (MaxPlayersSpinBox)事件的输出执行引脚连接到Set Max Players节点的输入执行引脚。

  3. 将事件节点的In Value引脚连接到Set节点的Max Players引脚。这将自动创建一个Truncate节点,将旋转框的浮点值转换为整数。

图的这部分最终结果如图12.8所示:

图 12.8 – 旋转框事件图

图 12.8 – 旋转框事件图

我们现在可以开始处理图中的会话创建部分,它将在CreateSessionBtn按钮被点击时触发。

  1. 在图中添加一个Create Session节点和一个Get Player Controller节点。

  2. Variables面板中拖动一个Get Max Players节点。

  3. On Clicked (CreateSessionBtn)事件的输出执行引脚连接到Create Session节点的输入执行引脚。

  4. Get Player Controller节点的Return Value引脚连接到Create Session节点的Player Controller引脚。

  5. Max Players节点的输出引脚连接到Create Session节点的Public Connections引脚。

  6. 启用Create Session节点的Use LAN复选框。

  7. Level_01添加到listenOptions输入字段,以将级别作为监听服务器打开。

  8. Create Session节点的On Success执行引脚连接到Open Level (by Name)节点的输入执行引脚。

  9. 可选地,将Create Session节点的On Failure执行引脚连接到一个Print String节点;这将显示错误消息。这将跟踪在Output Log窗口中创建会话期间可能出现的任何失败。

最终的图如图12.9所示:

图 12.9 – Create Session 事件图

图 12.9 – Create Session 事件图

你刚刚创建的图简单但功能强大,包括在局域网中创建会话、设置每场会话的最大玩家数以及打开游戏级别以开始实际的多玩家会话。

在下一节中,我们将处理一个界面,该界面将允许玩家搜索并加入现有的会话。

加入会话

在本节中,我们将处理几个小部件,这些小部件将显示网络中可用的会话列表并允许玩家加入它们。我们需要两个小部件:Session Item RendererFind Session。第一个将用于显示单个会话的信息,而第二个将负责将第一个作为可用会话的列表使用。

创建 SessionItemRenderer 小部件

您将创建的小部件将具有以下功能:

  • 显示可用的服务器名称

  • 显示可用的最大连接数,以及已连接玩家的数量

  • 提供一个加入按钮,让玩家能够进入会话

我们必须做的第一件事是创建小部件。因此,在内容浏览器区域,按照以下步骤操作:

  1. 右键单击并选择 用户界面 | 小部件蓝图。在出现的弹出窗口中,选择 用户小部件

  2. 将新创建的小部件命名为 WB_SessionItemRenderer 并双击它以打开它。

再次,我们将从添加用户界面元素开始。

添加视觉元素

一旦编辑器打开,选择 Designer 面板并执行以下操作:

  1. Container

  2. ServerNameLabel 并勾选 Fill5.0

  3. 设置 服务器名称

  4. NumPlayersLabel 并勾选 5.0

  5. 设置 0/0

  6. JoinBtn 并双检查 5.0

  7. JoinLabel

  8. 设置 加入会话

小部件的最终结构显示在 图 12。10*:

图 12.10 – SessionItemRenderer 小部件的层次结构视图

图 12.10 – SessionItemRenderer 小部件的层次结构视图

小部件的 Designer 视图显示在 图 12。11*:

图 12.11 – SessionItemRenderer 小部件的 Designer 视图

图 12.11 – SessionItemRenderer 小部件的 Designer 视图

使用此布局,玩家将能够看到每个可用会话的信息以及加入按钮。

现在部件的视觉部分已完成,您可以从添加 Visual Scripting 逻辑开始。

实现 Visual Scripting 逻辑

通过单击 Graph 按钮打开 Graph 面板并执行以下操作:

  1. 变量 面板中,您应该已经有了三个变量 – JoinBtnNumPlayersLabelServerNameLabel。在 Designer 图中,它们都已被标记为 Is Variable

  2. 添加一个新的 SearchResult 变量

  3. 在其 详细信息 面板中,启用 实例可编辑在生成时暴露 属性,以便从其他蓝图访问此属性

你应该已经熟悉实例可编辑属性,但在生成时暴露可能对你来说是新的。启用它将在生成此蓝图时显示该属性的引脚,这将在我们稍后添加此渲染器到可用会话列表时初始化数据时有所帮助。

现在变量已经设置好了,是时候添加一些视觉脚本了。我们将从实现加入会话逻辑开始。为此,请按照以下步骤操作:

  1. 选择JoinBtn变量后,在事件面板中点击On Clicked +按钮添加一个On Clicked (JoinBtn)事件。

  2. 在图表中添加一个Join Session节点,并将其输入执行引脚连接到On Clicked (JoinBtn)事件的输出执行引脚。

  3. 在图表中添加一个获取玩家控制器节点,并将其返回值引脚连接到Join会话节点的Player Controller引脚。

  4. 变量面板中,将一个获取搜索结果节点拖动到图表中,并将其引脚连接到Join会话节点的Search Result节点。

图表这一部分的最终结果如图图 12所示。12*:

图 12.12 – 加入会话图表

图 12.12 – 加入会话图表

如你所见,一旦你有了会话数据(即搜索结果),加入会话就相当直接;这些数据将在我们稍后添加的查找会话过程中获得。

请注意,加入一个会话后我们不需要像为创建会话小部件那样打开任何级别;一旦我们连接到主机,这将会自动发生。

为了完成这个小部件,我们需要在之前创建的标签中显示搜索结果数据,因为我们之前已经暴露了这些数据,所以在构造时它们已经可用。让我们通过在图表中查找事件构造事件节点来完成小部件的视觉脚本——它应该默认可用。然后,执行以下步骤:

  1. 为了保持整洁,添加一个具有两个输出引脚的Sequence节点(默认设置),并将其输入执行引脚连接到事件节点的输出执行引脚。

  2. 变量面板中,拖动一个获取服务器名称标签节点和一个获取搜索结果节点。

  3. 服务器名称标签输出引脚,点击并拖动以创建一个setText (Text)节点。

  4. setText (Text)节点的输入执行引脚连接到Sequence节点的Then 0执行引脚。

  5. 搜索结果输出引脚,点击并拖动以添加一个获取服务器名称节点。

  6. 获取服务器名称输出的引脚连接到setText (Text)节点的In Text引脚。这将自动创建一个To Text (String)节点,它将文本转换为正确的类型。

到目前为止,图表应该如图图 12所示。13*:

图 12.13 – 事件构造事件节点的第一部分

图 12.13 – 事件构造事件节点的第一部分

之前的图表只是从搜索结果数据中获取可用的服务器名称,并在相应的标签中显示它。我们将对连接的玩家数量做类似处理。为此,继续在同一图表上工作,并继续以下步骤。

  1. 变量面板,拖动一个Get Num Players Label节点和一个Get Search Result节点。

  2. Num Players Label的输出引脚,点击并拖动以创建一个SetText (****Text)节点。

  3. SetText (Text)节点的输入执行引脚连接到Sequence节点的Then 1执行引脚。

  4. Search Result的输出引脚,点击并拖动以添加一个Get Current Players节点。重复此步骤,但这次添加一个Get Max Players节点。

  5. B引脚的输入字段中添加一个/字符。

  6. 将输入引脚C连接到Get Max Players节点的Return Value。这将自动添加一个转换后的节点。

  7. Append节点的输出引脚连接到SetText (Text)节点的In Text引脚。这将自动创建一个To Text (String)节点,该节点会将文本转换为正确的类型。

此部分图表将看起来像图 12**.14

图 12.14 – 事件构造事件节点的第二部分

图 12.14 – 事件构造事件节点的第二部分

此小部件现在已完成,并包含显示会话信息和加入会话的所有逻辑。

我们现在将创建第三个小部件,它将允许我们在网络上搜索可用的会话。

创建FindSessions小部件

FindSessions小部件将具有以下功能:

  • 让玩家通过点击按钮来查找可用的会话

  • 显示可用会话的列表

  • 如有需要,显示信息消息

我们必须做的第一件事是创建实际的小部件。因此,在内容浏览器区域,执行以下操作:

  1. 右键单击并选择用户界面 | Widget Blueprint。在出现的弹出窗口中,选择User Widget

  2. 将新创建的小部件命名为WB_FindSessions,双击它以打开。

如同往常,你将首先向小部件添加视觉元素。

添加视觉元素

一旦编辑器打开,选择Designer面板并按照以下步骤操作:

  1. FindSessionsPanel

  2. Background

  3. 设置其(0, 0, 0, 0.4)

  4. 在按住CtrlShift键的同时,点击Anchors下拉菜单并选择右下角的按钮,使背景扩展到Canvas面板的整个区域。

  5. Container

  6. 在按住CtrlShift键的同时,点击Anchors下拉菜单,并选择右下角的按钮,使背景扩展到Canvas面板的整个区域。

到目前为止,您已经创建了小部件的容器 – 没有什么花哨的,但完全功能。小部件层次结构可以在图 12。15*中看到:

图 12.15 – 部分 FindSessions 小部件层次结构

图 12.15 – 部分 FindSessions 小部件层次结构

现在,我们将添加允许玩家查找网络会话的工作元素。

  1. FindSessionsBtn

  2. 设置其10.0

  3. 确认是变量复选框被选中

  4. FindSessionsLabel

  5. 设置查找会话

  6. Separator

  7. 设置其10.0

  8. SessionsScrollBox

  9. 设置其10.0

  10. 设置其大小属性为填充

  11. 确认是变量复选框被选中

  12. SessionMessage中勾选10.0

  13. 文本属性设置为无****会话可用

小部件的最终结构显示在图 12。16*中:

图 12.16 – 最终 FindSessions 小部件层次结构

图 12.16 – 最终 FindSessions 小部件层次结构

小部件的设计视图显示在图 12。17*中:

图 12.17 – 最终 FindSessions 小部件设计视图

图 12.17 – 最终 FindSessions 小部件设计视图

使用这种布局,玩家将能够点击查找会话按钮来搜索网络中的可用会话,并在可选择的列表中显示它们。您现在需要打开面板并添加一些可视化脚本逻辑。

实现可视化脚本逻辑

这部分将比其他部分复杂一些,因为会有很多事情发生;特别是,代码逻辑必须执行以下操作:

  • 在局域网中搜索可用网络

  • 通过SessionItemRenderer小部件显示可用会话的列表

  • 显示错误消息

  • 根据情况启用和禁用搜索按钮

作为第一步,我需要您检查在变量面板中,有三个引用您将使用的项目:FindSessionBtnSessionMessageSessionScrollBox。然后,按照以下步骤操作:

  1. 添加一个名为SessionResults的新变量,类型为蓝图会话结果,并将其设置为数组 – 这将包含在网络上找到的会话列表。

  2. 选择FindSessionBtn,在事件面板中,通过点击对应元素旁边的+按钮添加一个On Clicked事件。

  3. On Clicked (FindSessionBtn)的输出执行引脚连接到一个具有两个执行引脚的序列节点(即默认的)。

现在,为了保持整洁,您将创建一些函数来执行一些小操作:

  • AddItemRenderer向列表中添加会话项

  • EnableSearchButton/DisableSearchButton根据情况使搜索按钮可交互或不交互

  • GetSessionResultMessage来组成搜索的结果消息

让我们现在创建这些。

创建 AddItemRenderer 函数

我们将从第一个函数开始,通过在 My Blueprint 窗口的 函数 部分的 + 按钮上点击。按照以下步骤操作:

  1. 将函数命名为 AddItemRenderer,在 SearchResult

  2. 在函数节点被选中时,查找 My Blueprint 窗口的 部分并将 访问修饰符 设置为 受保护的

  3. 在图中添加一个 获取玩家控制器 节点。

  4. 在图中添加一个 创建小部件 节点,并执行以下操作:

    • 将其传入执行插针连接到 Add Item Renderer 函数节点的传出执行插针

    • 插针的下拉菜单中选择 WB_SessionItemRenderer

    • 拥有玩家 插针连接到 Get Player Controller 节点的 返回值

    • 搜索结果 插针连接到 Add Item Renderer 函数节点的 搜索结果 插针

  5. 变量 面板将 Get Session Scroll Box 节点拖动到图中。

  6. 在图中添加一个 添加子节点 节点,并执行以下操作:

    • 将其传入执行插针连接到 创建 小部件 节点的传出执行插针

    • 内容 插针连接到 创建 小部件 节点的 返回值 插针

    • 目标 插针连接到 Session Scroll Box 获取器节点

AddItemRenderer 函数的最终图示如 图 12.18* 所示:

图 12.18 – AddItemRenderer 函数

图 12.18 – AddItemRenderer 函数

现在我们可以开始创建一个函数,当请求时启用搜索按钮。

创建 EnableSearchButton 函数

让我们从在 My Blueprint 窗口的 函数 部分的 + 按钮创建函数开始。按照以下步骤操作:

  1. 将函数命名为 EnableSearchButton,在函数节点被选中时,查找 My Blueprint 窗口的 部分并将 访问修饰符 设置为 受保护的

  2. 变量 面板拖动一个 FindSessionsBtn 节点。

  3. 在图中添加一个 设置启用状态 节点,并执行以下操作:

    • 将其传入执行插针连接到 启用搜索按钮 函数节点的传出执行插针

    • 目标 插针连接到 Find Session Btn 获取器

    • 打开 启用 状态 复选框

此函数的最终图示如 图 12.19* 所示:

图 12.19 – EnableSearchButton 函数

图 12.19 – EnableSearchButton 函数

将要禁用按钮的函数几乎与上一个函数相同,所以让我们创建它。

创建 DisableSearchButton 函数

让我们从在 My Blueprint 窗口的 函数 部分的 + 按钮创建函数开始。按照以下步骤操作:

  1. 在函数节点被选中时,查找 My Blueprint 窗口的 部分并将 访问修饰符 设置为 受保护的

  2. 变量面板中,拖动一个FindSessionsBtn节点。

  3. 在图中添加一个设置启用节点,并执行以下操作:

    • 将其输入执行引脚连接到启用搜索按钮函数节点的输出执行引脚

    • 目标引脚连接到查找会话****按钮获取器

    • 保持是否启用复选框未勾选

该函数的最终图示如图 12.20所示:

图 12.20 – 禁用搜索按钮函数

图 12.20 – 禁用搜索按钮函数

我们只需要一个最后的函数——一个将组成会话消息的函数。

创建 GetSessionResultMessage 函数

首先,通过点击我的蓝图窗口中函数部分的+按钮来创建函数。按照以下步骤操作:

  1. 将函数命名为GetSessionsResultMessage并执行以下操作:

    • 在选择函数节点后,查找ReturnValue;这将向图中添加返回节点
  2. 变量面板中,拖动一个获取会话****结果节点。

  3. 会话结果获取器的输出引脚,点击并拖动以添加一个长度节点。

  4. 添加一个Found:

  5. 连接sessions

  6. 追加节点的输出引脚连接到返回节点返回值;这将自动在之间创建一个转换为文本(字符串)转换器节点。

该函数的最终图示如图 12.21所示:

图 12.21 – 获取会话结果消息函数

图 12.21 – 获取会话结果消息函数

最后一个函数已经创建完成,现在是时候回到主事件图并创建搜索和结果逻辑。

实现事件图

主图需要在网络中查找可用的会话并在小部件列表中暴露它们。要开始这个视觉脚本逻辑,请按照以下步骤操作:

  1. 定位到点击(FindSessionBtn)事件,并将一个序列节点添加到输出的执行引脚上。

  2. 变量面板中,拖动一个SessionsScrollBox节点,并从其输出引脚,点击并拖动以添加一个清除****子节点

  3. 清除子节点的输入执行引脚连接到序列节点的然后 0的输出执行引脚。

  4. 函数面板中,拖动一个禁用搜索按钮函数,并将其输入执行引脚连接到清除****子节点的输出执行引脚。

  5. 变量面板中,拖动一个Session Message节点,并从其输出引脚,点击并拖动以添加一个设置文本节点(从内容类别)。

  6. 设置文本节点的输入执行引脚连接到禁用****搜索按钮的输出引脚。

  7. 搜索会话...插入到设置文本节点的文本字段中。

这第一部分的图表基本上从之前的搜索结果中清理会话列表,禁用搜索按钮以避免多次点击,并显示消息。这如图 图 12.22 所示:

图 12.22 – FindSessions 图的第一部分

图 12.22 – FindSessions 图的第一部分

图表的第二部分将负责搜索网络会话并显示实际结果。

  1. 将一个 获取玩家控制器 节点添加到图表中。

  2. 添加一个 10

  3. 打开 使用 LAN 复选框以启用它

  4. 变量 面板中,拖动一个 设置会话结果 节点,并将其输入执行引脚连接到 Find 会话 节点的 On Success 执行引脚。

  5. 函数 面板中,拖动一个 Get Sessions Result 消息 节点。

  6. 变量 面板中,拖动一个 会话消息 获取节点,并执行以下操作:

    • 从其输出引脚,点击并拖动以添加一个 设置文本 节点(来自 内容 类别)

    • 将其 文本 引脚连接到 Get Sessions Result 消息 节点的 返回值 引脚

    • 将其输入执行引脚连接到 设置会话 结果 节点的输出执行引脚

  7. 变量 面板中,拖动一个 获取会话结果 节点。从其输出引脚,点击并拖动以添加一个 For Each Loop 节点。将 For Each Loop 节点的输入 Exec 引脚连接到 Set Text 节点的输出引脚。

  8. 函数 面板中,将一个 添加项目渲染器 节点拖动到图表中,并执行以下操作:

    • 将其输入执行引脚连接到 For Each 循环 节点的 Loop Body 执行引脚

    • 将其 搜索结果 引脚连接到 For Each 循环 节点的 数组元素 引脚

  9. 函数 面板中,将一个 启用搜索按钮 节点拖动到图表中,并将其输入执行引脚连接到 For Each 循环 节点的 Completed 执行引脚。

这第二部分的图表在网络上查找可用会话列表,显示结果消息,并将结果添加到会话列表中。图表在 图 12.23 中展示:

图 12.23 – FindSessions 图的第二部分

图 12.23 – FindSessions 图的第二部分

图表的最后一部分将在搜索结果失败时仅显示错误消息。

  1. 变量 面板中,拖动一个 会话消息 获取节点,并从其输出引脚,点击并拖动以添加一个 设置文本 节点(来自 内容 类别)。

  2. Error searching for available sessions

  3. 将其输入执行引脚连接到 Find 会话 节点的 On Failure 输出执行引脚。

  4. 将其输出执行引脚连接到一个 启用搜索按钮 节点,使按钮可点击。

这一部分的图表是自我解释的,并在 图 12.24 中展示:

图 12.24 – 查找会话图的第三部分

图 12.24 – 查找会话图的第三部分

我们终于完成了这个小部件,现在我们准备在 菜单 小部件中组合这些内容。

创建主菜单小部件

主菜单 小部件 – 第四个也是最后一个 – 简单地充当 创建会话查找会话 的容器。要创建它,请按照以下步骤操作:

  1. 在内容浏览器区域内右键单击,然后选择 用户界面 | 部件蓝图。在出现的弹出窗口中,选择 用户部件

  2. 将新创建的小部件命名为 WB_MainMenu 并双击它以打开。

  3. 调色板 窗口中,将一个 画布面板 项拖入 设计器 视图。

  4. 调色板 菜单中,将一个 WB 创建会话 项拖到 画布 面板上,并将其放置在你认为合适的位置。

  5. 调色板 菜单中,将一个 WB 查找会话 项拖到 画布 面板上,并将其放置在你认为合适的位置。

此小部件的 设计器 视图应类似于 图 12.25 中所示:

图 12.25 – 主菜单小部件

图 12.25 – 主菜单小部件

在本节中,您创建了主菜单级别的用户界面,它由可重复使用的部件组成。每个部件都包含用于创建会话和加入会话的专用逻辑。

摘要

本章向您介绍了 Unreal Engine 的在线子系统中的主要会话命令,为您提供了轻松创建、加入和管理多人游戏会话所需的工具。有了这些知识,您已经创建了一个利用这些功能并将其付诸实际应用的用户界面。

如果你想将你的多人游戏从只是一个基本的原型变成一个功能齐全且完整的游戏体验,那么你必须学习如何使用这些功能。相信我 – 拥有这些知识将大有裨益,并帮助你创建一些真正出色的多人游戏!

在下一章中,我们将使用这些有用的部件来创建游戏的主菜单。我们还将创建所需的 Gameplay Framework 类,以及一个用于自定义角色外观的酷炫系统!

第十三章:会话期间处理数据

要开发多人游戏,你需要一个稳固的系统来管理层级之间的数据流。这意味着跟踪变量——如角色库存或健康——以使玩家能够及时获取所需的信息。简而言之,一个有效的多人游戏需要仔细管理数据,以确保所有玩家都能获得流畅、吸引人的体验。

在本章中,你将通过创建一个系统来完善上一章的会话系统,该系统将作为玩家的入口点。这意味着工作在一个新的层级,让玩家能够创建会话——如果他们是作为服务器开始游戏——或者在网络中寻找可用的会话——如果他们是作为客户端玩游戏。

此外,你还将学习如何通过添加皮肤变体来自定义玩家角色,以及如何将此数据从会话选择层级发送到实际游戏层级。这将使你的角色更加特别,甚至比以前更加酷炫和多彩!

到本章结束时,你将能够以计算机作为监听服务器并让其他 PC 作为客户端连接到它,并为游戏中的每个玩家提供不同的皮肤来托管本地网络游戏会话。

在本章中,我将指导你通过以下部分:

  • 创建主菜单层级

  • 会话期间处理数据

  • 进一步改进

技术要求

要跟随本章介绍的主题,你应该已经完成了第十二章管理多人会话,并理解其内容。

此外,如果你希望从本书的配套仓库开始编写代码,你可以下载本书配套项目仓库中提供的.zip项目文件:github.com/PacktPublishing/Multiplayer-Game-Development-with-Unreal-Engine-5

你可以通过点击Unreal Shadows – 第十二章结束链接来下载与上一章结尾保持一致的文件。

创建主菜单层级

在本节中,你将创建一个新的层级,该层级将作为创建游戏会话或加入会话的起点。你将通过添加所需的游戏框架类,如专用的 GameMode 和玩家 Pawn,利用之前创建的用户界面的强大功能和灵活性。

首先,让我们打开你的编程 IDE 并开始编写一些代码!

创建 Pawn

在本小节中,你将创建一个 Pawn,该 Pawn 将显示角色模型并通过其控制器激活用户界面。这个 Actor 还将被用来在玩家进入主菜单层级时显示角色模型。

因此,从 Unreal Engine 编辑器中创建一个新的从US_MainMenuPawn扩展的 C++类。一旦类被创建,打开US_MainMenuPawn.h头文件,并在GENERATED_BODY()宏之后添加以下代码:

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Arrow", meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UArrowComponent> Arrow;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera", meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UCameraComponent> Camera;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera", meta = (AllowPrivateAccess = "true"))
TObjectPtr<USkeletalMeshComponent> Mesh;

然后,在protected部分添加相应的获取函数:

FORCEINLINE UArrowComponent* GetArrow() const { return Arrow; }
FORCEINLINE UCameraComponent* GetCamera() const { return Camera; }
FORCEINLINE USkeletalMeshComponent* GetMesh() const { return Mesh; }

所有的前述代码都很直接,你应该已经从前面的章节中熟悉了它;我们正在声明所需的组件 – 一个箭头,一个摄像头和一个网格 – 然后我们暴露相应的获取方法。

接下来,打开US_MainMenuPawn.cpp文件并添加所需的include声明:

#include "Camera/CameraComponent.h"
#include "Components/ArrowComponent.h"

然后,找到构造函数并添加以下代码:

Arrow = CreateDefaultSubobject<UArrowComponent>(TEXT("Arrow"));
RootComponent = Arrow;
Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
Camera->SetupAttachment(RootComponent);
Camera->SetRelativeLocation(FVector(450.f, 90.f, 160.f));
Camera->SetRelativeRotation(FRotator(-10.f, 180.f, 0.f));
Mesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Mesh"));
Mesh->SetupAttachment(RootComponent);
Camera->SetRelativeLocation(FVector(0.f, -30.f, 90.f));
static ConstructorHelpers::FObjectFinder<USkeletalMesh> SkeletalMeshAsset(TEXT("/Game/KayKit/Characters/rogue"));
if (SkeletalMeshAsset.Succeeded())
{
 Mesh->SetSkeletalMesh(SkeletalMeshAsset.Object);
}

我们在这里没有添加任何新内容。像往常一样,我们只是为 Actor 添加了一组组件列表,包括一个实用箭头元素、网格和摄像头。

现在基本的Pawn类已经创建,现在是时候从它实现一个蓝图并添加之前创建的用户界面了。因此,返回到 Unreal Engine 编辑器,在BP_MainMenuPawn中。然后,打开蓝图,在事件图表中完成以下步骤:

  1. 添加一个获取玩家****控制器节点。

  2. 在图表中添加一个创建小部件节点,并将其传入的执行引脚连接到事件****开始播放节点的输出执行引脚。

  3. 下拉菜单中选择WB_MainMenu类。

  4. 拥有玩家引脚连接到获取玩家****控制器节点的返回值

  5. 创建小部件节点的输出执行引脚,点击并拖动以添加一个添加到视口节点。然后,将目标引脚连接到创建****小部件节点的返回值

  6. 获取玩家控制器节点的返回值点击并拖动以添加设置显示鼠标光标。然后,勾选显示鼠标光标复选框并将传入的执行引脚连接到添加到****视口节点的输出执行引脚。

最终的图表展示在图 13.1中:

图 13.1 – BP_MainMenuPawn 蓝图中的最终图表

图 13.1 – BP_MainMenuPawn 蓝图中的最终图表

BP_MainMenuPawn蓝图已完成并准备就绪,因此我们现在可以继续工作在 GameMode 类上。

创建 GameMode

现在是时候创建一个将处理主菜单关卡的 GameMode 了。在BP_MainMenuGameMode中。

接下来,打开蓝图。然后在详细信息面板中,定位到类别,并在默认玩家类下拉菜单中选择BP_MainMenuPawn,如图图 13.2所示:

图 13.2 – BP_MainMenuGameMode 设置

图 13.2 – BP_MainMenuGameMode 设置

现在 GameMode 已经准备好了;我们只需要创建一个新的关卡并使用它来显示会话用户界面。

创建关卡

创建主菜单关卡相当直接:

  1. 打开Content | Maps文件夹。

  2. 从主菜单中选择 Level_MainMenu

  3. 打开关卡,在 Worlds Settings 面板中找到 GameMode Override。在相应的下拉菜单中,选择 BP_MainMenuGameMode

  4. Editor 主菜单中,选择 Edit | Project Settings,并在新打开的窗口中找到 Maps & Modes 部分。然后,从 Editor Template Map Overrides 下拉菜单中选择 Level_MainMenu

恭喜你,你已经通过了本节,并构建了游戏的开局关卡!现在,你的玩家可以像老板一样在自己的局域网中创建和托管游戏会话,或者作为客户端加入游戏。让我们通过玩游戏来测试这些功能。

测试会话系统

要开始测试游戏会话,打开 Level_MainMenu 地图,并使用以下设置进行游戏:

  • Net Mode 设置为 Play Standalone

  • 设置 3

你将看到你迄今为止创建的用户界面,如图 图 13**.3 所示:

图 13.3 – 用户界面

图 13.3 – 用户界面

从此界面,你可以创建会话,如下所示:

  1. Max Players 选择器中,将最大玩家数设置为 3 或更多。

  2. 点击 Create 按钮;你将开始游戏并看到游戏关卡。

然后,要加入会话,请按照以下步骤操作:

  1. 选择其他已打开的客户端之一,并点击 Find Session 按钮;此操作将启动服务器搜索,一段时间后,你应该能看到局域网中可用的服务器列表,以及已连接的玩家数量。图 13**.4 展示了一个已有玩家连接的游戏(即监听服务器),最多可容纳 3 名玩家:

图 13.4 – 会话搜索结果

图 13.4 – 会话搜索结果

  1. 点击 Join Session 按钮以加入会话。你的角色将被传送到游戏关卡,你将能够开始游戏。

在本节中,你了解了会话管理,并学习了如何在多人游戏中创建、搜索和加入会话。

准备好,因为在接下来的章节中,你将给你的游戏增添额外的定制化元素。没错——准备好添加皮肤变体,让每个玩家的角色真正独一无二。是时候发挥创意,让你的想象力自由驰骋了!

会话期间处理数据

在本节中,你将学习一个新的主题:在加入会话时,从一个关卡传递数据到另一个关卡。你已经几乎拥有了执行此任务所需的所有知识——你只需要将这些知识整合起来。

我们在这里需要做的是为角色模型创建一个皮肤系统,该系统将执行以下操作:

  • 从可能的变体列表中选择主菜单关卡中的随机皮肤

  • 在加入会话时存储此数据

  • 在加入会话后更新角色皮肤变体

在以下步骤中,您将开始处理一个直到现在都保持不活跃的类,但这个类将证明在未来的工作中非常有用。所以,准备好使用US_GameInstance类并看看它能做什么!

更新 US_GameInstance 类

您可能已经忘记了,但在本项目的开始时,您创建了US_GameInstance类。这个类提供了一些有趣的特性:

  • 它在各个级别之间是持久的

  • 它对每个客户端都是唯一的(也就是说,它不会在网络中复制)

这些特性使它成为在本地保持数据的同时在级别之间传输数据的绝佳候选者。您可以使用它来传递诸如玩家获得的经验点或他们的实际装备等信息。在我们的案例中,我们将使用它来存储一个非常简单的信息:所选皮肤的索引(我们将在本章后面实现皮肤列表)。

打开US_GameInstance.h头文件,在public部分添加以下声明:

UPROPERTY(BlueprintReadWrite)
int32 SkinIndex;

虽然看起来很简单,但这正是我们需要将皮肤选择从一个级别传递到另一个级别的所有内容!

在接下来的几个步骤中,我们将创建一个数据结构来处理角色皮肤变体。

添加 CharacterSkins 数据

在本小节中,您将创建一个类似于您在第六章中创建的数据结构,在网络中复制属性,但这次,您将只存储用于更改角色网格颜色的材质引用。

角色模型有六个材质,如图图 13.5所示:

图 13.5 – 角色模型材质

图 13.5 – 角色模型材质

对于角色定制,我们只需要其中四个——具体来说,是元素 0元素 1元素 2,它们都将改变角色的头发和衣服,以及元素 4,它将改变角色的皮肤。

创建结构

要创建包含皮肤数据的结构,打开您的编程 IDE,创建一个名为US_CharacterSkins.h的文件。然后,在该文件中添加以下代码:

#pragma once
#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "US_CharacterSkins.generated.h"
USTRUCT(BlueprintType)
struct UNREALSHADOWS_LOTL_API FUS_CharacterSkins : public FTableRowBase
{
 GENERATED_BODY()
 UPROPERTY(BlueprintReadWrite, EditAnywhere)
 UMaterialInterface *Material4;
 UPROPERTY(BlueprintReadWrite, EditAnywhere)
 UMaterialInterface *Material0;
 UPROPERTY(BlueprintReadWrite, EditAnywhere)
 UMaterialInterface *Material1;
 UPROPERTY(BlueprintReadWrite, EditAnywhere)
 UMaterialInterface *Material2;
};

如您所见,我们正在从FTableRowBase创建一个数据结构——这个结构将使我们能够创建数据表——然后我们声明了四个材质引用。顺便提一下,请记住,UNREALSHADOWS_LOTL_API API标识符可能会根据您的项目名称而改变。

在接下来的步骤中,您将通过生成数据表来从该结构创建实际的皮肤数据。

创建数据表

现在您已经创建了一个数据结构,您就可以从它创建实际的数据了。要创建您的皮肤数据表,请按照以下步骤操作:

  1. 在内容浏览器中打开您的Blueprints文件夹,右键单击,然后选择Miscellaneous | Data Table

  2. 选择行结构弹出窗口中,从下拉菜单中选择US_CharacterSkins

  3. 点击DT_CharacterSkins

  4. 双击新创建的资产以打开它。你将得到一个空的数据集;使用项目中的任何材质(或创建自定义材质!)创建你的角色皮肤行。

在开发阶段,我喜欢创建调试皮肤,这有助于我识别每个独特的角色。在这种情况下,我使用单一颜色为行集中的所有元素(即所有绿色、所有红色或所有蓝色),如图图 13.6所示:

图 13.6 – 皮肤数据表

图 13.6 – 皮肤数据表

一旦你对皮肤系统感到满意并且已经对其进行了实战测试,你将需要为你的盗贼角色添加一些更逼真的皮肤;这个过程将和创建一个新的包含你选择皮肤颜色的数据表并将此表设置为Pawn变量中的选中表一样简单。

现在你有了皮肤目录资产,你可以开始向主菜单Pawn类添加代码以在运行时设置其皮肤。

更新US_MainMenuPawn

在这个部分,你将通过分配随机皮肤来增强角色的外观。每次玩家连接到主菜单级别时,他们的角色将获得一个独特的皮肤颜色组合,这些颜色来自之前创建的数据表。所以,准备好在游戏中看到更多样性吧!

如我之前提到的,你在第六章中处理了这个问题,在网络中复制属性,但正如老话所说,熟能生巧!

使用你的编程 IDE 打开US_MainMenuPawn.h头文件,并在隐式的private部分添加数据表和皮肤声明:

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Character Data", meta = (AllowPrivateAccess = "true"))
class UDataTable* CharacterSkinDataTable;
struct FUS_CharacterSkins* CharacterSkin;

接下来,在public部分,添加选中角色皮肤的获取方法:

FORCEINLINE FUS_CharacterSkins* GetCharacterSkin() const { return CharacterSkin; }

protected部分,声明一个将处理皮肤随机化过程的函数:

void RandomizeCharacterSkin();

现在,你可以打开US_MainMenuPawn.cpp文件以开始添加皮肤处理实现。首先,声明所需的包含:

#include "US_CharacterSkins.h"
#include "US_GameInstance.h"

然后,添加RandomizeCharacterSkin()的实现:

void AUS_MainMenuPawn::RandomizeCharacterSkin()
{
 if(CharacterSkinDataTable)
 {
  TArray<FUS_CharacterSkins*> CharacterSkinsRows;
  CharacterSkinDataTable->GetAllRows<FUS_CharacterSkins>(TEXT("US_Character"), CharacterSkinsRows);
  if(CharacterSkinsRows.Num() > 0)
  {
   const auto NewIndex = FMath::RandRange(0, CharacterSkinsRows.Num() - 1);
   CharacterSkin = CharacterSkinsRows [NewIndex];
   Mesh->SetMaterial(4, CharacterSkinsRows[NewIndex]->Material4);
   Mesh->SetMaterial(0, CharacterSkinsRows[NewIndex]->Material0);
   Mesh->SetMaterial(1, CharacterSkinsRows[NewIndex]->Material1);
   Mesh->SetMaterial(2, CharacterSkinsRows[NewIndex]->Material2);
   if (const auto GameInstance = Cast<UUS_GameInstance>(GetGameInstance()))
   {
    GameInstance->SkinIndex = NewIndex;
   }
  }
 }

如你所见,我们正在从表引用中检索所有数据行,并在确认表中至少有一个项目后,我们获取一个随机行并将 Pawn 网格材质设置为其中包含的数据。这将更新级别中显示的 Pawn。之后,我们检索游戏实例作为UUS_GameInstance类型并将之前随机化的索引分配给SkinIndex属性。

作为最后一步,我们将在游戏开始时添加随机化调用。因此,在BeginPlay()方法中,添加以下代码:

if(IsLocallyControlled())
{
 RandomizeCharacterSkin();
}

我们现在需要从蓝图设置数据表,所以让我们切换回 Unreal Engine 编辑器。

更新BP_MainMenuPawn蓝图

现在角色Pawn类已经准备好了,你只需将之前创建的数据表分配给 Pawn 蓝图。要做到这一点,打开BP_MainMenuPawn并执行以下操作:

  1. 详细信息面板中,查找角色皮肤数据****表属性。

  2. 从下拉菜单中选择DT_CharacterSkins,如图图 13**.7所示:

图 13.7 – 角色皮肤数据表属性

图 13.7 – 角色皮肤数据表属性

  1. 如果您测试游戏,您将为每个角色随机分配一个皮肤,如图图 13**.8所示:

图 13.8 – 开始时的皮肤随机化

图 13.8 – 开始时的皮肤随机化

角色随机化已完成;为了充分利用它,我们只需从游戏关卡侧检索数据并将其分配给正在玩的角色。

更新 US_Character 类

在本小节中,您将从游戏实例中检索皮肤索引数据并将其设置到玩家角色。一旦您记住游戏实例在关卡之间是持久的并且不会被复制(即每个客户端都有自己的专用实例),这个过程就相当直接了。

首先,从您的编程 IDE 中打开US_Character.h头文件,并在隐式的private部分声明所需的数据表属性:

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Character Data", meta = (AllowPrivateAccess = "true"))
UDataTable* CharacterSkinDataTable;
struct FUS_CharacterSkins* CharacterSkin;

我知道您已经熟悉之前的声明,所以不会浪费您的时间再次解释它们。

接下来,在protected部分,您需要添加以下声明。这些声明将处理皮肤更新:

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, ReplicatedUsing="OnRep_SkinChanged", Category = "Skin")
int32 SkinIndex = 0;
UFUNCTION()
void OnRep_SkinChanged(int32 OldValue);
UFUNCTION(Server, Reliable)
void SetSkinIndex_Server(int32 Value);
UFUNCTION()
void UpdateCharacterSkin();

public部分,添加以下代码:

FORCEINLINE FUS_CharacterSkins* GetCharacterSkins() const { return CharacterSkin; }
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

如您从第六章,“通过网络复制属性”中可能记得的,每当您需要复制一个属性时,您可以使用ReplicatedUsing属性指定符来通知所有客户端属性值已更改。在这种情况下,我们将复制SkinIndex变量,以便所有客户端在加入会话后更新他们的角色皮肤。

此外,始终记住,为了复制一个属性,它应该在GetLifetimeReplicatedProps()方法中通过DOREPLIFETIME宏进行初始化,这就是我们声明该方法的原因。

要实现所有复制逻辑和皮肤更新,打开US_Character.cpp文件,并首先添加所需的include声明:

#include "US_GameInstance.h"
#include "US_CharacterSkins.h"
#include "Net/UnrealNetwork.h"

然后,添加GetLifetimeReplicatedProps()方法实现以实现属性复制:

void AUS_Character::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
 Super::GetLifetimeReplicatedProps(OutLifetimeProps);
 DOREPLIFETIME(AUS_Character, SkinIndex);
}

接下来,添加OnRep_SkinChanged()方法。每当SkinIndex值从服务器更新到客户端时,此方法将被执行:

void AUS_Character::OnRep_SkinChanged(int32 OldValue)
{
 UpdateCharacterSkin();
}

然后,通过添加SetSkinIndex_Server_Implementation()方法从服务器端实现皮肤索引更新:

void AUS_Character::SetSkinIndex_Server_Implementation(const int32 Value)
{
 SkinIndex = Value;
 UpdateCharacterSkin();
}

注意,我们是从服务器端调用UpdateCharacerSkin()事件;如果您使用的是监听服务器,这是强制性的,因为前一个方法只会在客户端调用,在这种情况下,服务器将不会更新皮肤。

第四种方法,UpdateCharacterSkin(),将负责从游戏实例检索数据并更新角色网格材质。为此,请添加以下实现:

void AUS_Character::UpdateCharacterSkin()
{
 if(CharacterSkinDataTable)
 {
  TArray<FUS_CharacterSkins*> CharacterSkinsRows;
  CharacterSkinDataTable->GetAllRows<FUS_CharacterSkins>(TEXT("US_Character"), CharacterSkinsRows);
  if(CharacterSkinsRows.Num() > 0)
  {
   const auto Index = FMath::Clamp(SkinIndex, 0, CharacterSkinsRows.Num() - 1);
   CharacterSkin = CharacterSkinsRows[Index];
   GetMesh()->SetMaterial(4, CharacterSkin->Material4);
   GetMesh()->SetMaterial(0, CharacterSkin->Material0);
   GetMesh()->SetMaterial(1, CharacterSkin->Material1);
   GetMesh()->SetMaterial(2, CharacterSkin->Material2);
  }
 }
}

我们在这里所做的是几乎与主菜单 Pawn 类相同的;我们从一个皮肤数据表中获取一行,并将材质分配给角色网格。唯一的区别是我们没有将皮肤索引设置到游戏实例中,而是获取了它的复制版本。

作为最后一步,你需要在 BeginPlay() 方法实现的末尾添加以下代码:

if(IsLocallyControlled())
{
 if(const auto GameInstanceCast = Cast<UUS_GameInstance>(GetWorld()->GetGameInstance()); GameInstanceCast != nullptr)
 {
  SetSkinIndex_Server(GameInstanceCast->SkinIndex);
 }
}

这段代码检查这个类的实例是否是本地控制的(也就是说,它是一个玩家拥有的角色),并从游戏实例中获取皮肤索引。然后,它调用 SetSkinIndex() 服务器函数来通知所有客户端他们应该更新这个角色的外观。

对于主菜单 Pawn 类,你现在需要更新相应的蓝图来声明皮肤数据表。

更新 BP_Character 蓝图

现在角色类别已经准备好了,你只需将之前创建的数据表分配给相应的蓝图。为此,请按照以下步骤操作:

  1. 打开 BP_Character

  2. 详细信息面板中,查找角色皮肤数据****表属性。

  3. 从下拉菜单中选择DT_CharacterSkins,如图 图 13.9 所示:

图 13.9 – 角色皮肤数据表属性

图 13.9 – 角色皮肤数据表属性

如果你现在测试游戏,你将为每个角色随机分配一个皮肤,并在加入会话后保留该皮肤,如图 图 13.10 所示:

图 13.10 – 使用皮肤角色的游戏玩法

图 13.10 – 使用皮肤角色的游戏玩法

在本节中,你学习了如何在创建或加入会话时保留数据。你是通过将游戏实例作为某种数据桥来创建皮肤功能来做到这一点的;这将使每个角色独一无二,甚至更有吸引力,帮助你给他们一个杀手级的改造,让巫妖王都感到脚底发凉!

在接下来的章节中,我不会介绍任何新主题;相反,我将给你一些额外的想法,帮助你更好地管理你的游戏会话。

进一步改进

在前面的章节中,你出色地理解了如何在会话之间传递数据,使你的多人游戏更具吸引力。现在,是时候通过添加一些附加功能来使你的游戏更加出色了。在本节中,我将提供一些技巧来帮助你给你的项目增添一些兴奋感。一如既往,不要害怕加入你个人的风格,让它真正属于你。

离开和销毁会话

在本章和上一章中,您已经使用了四个会话命令中的三个——创建会话加入会话查找会话——但销毁会话命令尚未使用。使用销毁会话命令节点让玩家离开游戏会话。

这将在游戏关卡(而不是主菜单)中实现,因为玩家在加入会话后需要离开。为了实现这个功能,您可能想要创建一个专门的用户界面,让玩家在决定离开游戏时随时可以离开。

作为旁注,请记住,客户端和服务器在会话方面表现不同,因此您将不得不管理两种会话销毁:客户端的——这几乎是无痛的——以及在客户端/服务器主机的情况下,服务器的——这需要销毁所有客户端的会话(也就是说,所有客户端都应该离开会话,因为服务器不再起作用了)。

处理玩家死亡

目前,当玩家被巫妖领主的小兵捕获时,什么也不会发生——您只会看到一个屏幕信息,玩家将继续游戏。

您可以通过许多方式管理玩家的死亡,但这里有一些想法供您参考:

  • 销毁玩家会话并重新加载主菜单关卡,让玩家加入新的会话。只是记住,如果被击败的玩家是服务器主机,所有其他玩家将立即被从会话中移除。

  • 使用重生方法,将角色放置在可用的出生点,无需重新加入会话。

选择玩家皮肤

在本章中,您开发了一个随机皮肤生成器,但让玩家选择自己的皮肤不是更酷吗?您可以在主菜单级别添加一个用户界面,让玩家执行以下操作:

  • 如果皮肤不符合他们的需求,请再次随机化皮肤

  • 使用专用选择方法从完整集合中选择所需的皮肤

嗯,当谈到创建一个像魔法一样起作用且让竞争对手羡慕的皮肤系统时,这可能是冰山一角。谁知道呢?实现您的游戏内购买甚至可能让像堡垒之夜这样的游戏感到膝盖发软!

摘要

在本章中,您开发了一个在游戏会话期间从一个关卡传递到另一个关卡数据的完整功能系统。在这种情况下,您专注于玩家角色的皮肤系统,但这个特性的用例几乎是无限的。

如您所注意到的,会话处理是一个巨大的主题——它需要两章来正确解释其主要功能——在多人游戏世界中取得成功,掌握它至关重要。如果您想要乘风破浪,那么掌握这项技能是一个不容商量的主题!

在下一章中,我将引导你探索一个全新的主题:如何正确地打包你的游戏。准备好深入探索项目打包的精彩世界,以便你能在服务器和客户端两端像老板一样部署你的游戏!你准备好迎接这次冒险了吗?

第十四章:部署多人游戏

对于任何开发者来说,部署 Unreal 多人游戏可能是一项艰巨的任务。

在本章中,我将向您介绍包装和部署游戏的关键步骤——这是一个需要事先进行适当规划和准备的过程。这将帮助您避免大多数问题,从而在部署时成功发布。

此过程还将涉及一个关键任务——从源代码编译 Unreal Engine,然后将您的游戏打包为独立的服务器可执行文件和客户端可执行文件。

因此,在接下来的几节中,我将向您介绍以下主题:

  • 真正上线

  • 编译 Unreal Engine

  • 设置专用服务器

技术要求

要跟随本章中介绍的主题,您应该已经完成了所有前面的章节,并理解了它们的内容。

虽然不是强制性的,但基本了解 Git 技术 (git-scm.com) 将有助于本章的第二部分。

真正上线

到目前为止,为了处理您的项目,您一直在使用可以通过 Epic Games Launcher 访问的 Unreal Engine 官方发布版本。然而,如果您渴望提高您的多人开发技能,您必须进一步努力,以在多人开发领域熟练地脱颖而出,并成为一名熟练的多人游戏开发者。

首先,您需要知道的是,Unreal Engine 的发布版本并不是软件的“完整”版本;它们提供了几乎您在项目上所需的所有功能,但通常缺少较少见和更高级的功能。其中一个主要原因是保持 Unreal Engine 的常规版本更小、更经济。

很遗憾,这些发布版本缺乏编译多人部署项目所需的功能,这反过来意味着,如果您计划在野外发布您的游戏,那么您将很不幸。

幸运的是,有一个解决方案可用——直接从源代码编译 Unreal Engine 编辑器。完整的 Unreal Engine 源代码存储在 GitHub 上托管的一个仓库中(github.com/),并且只需付出最小的努力,您就能获取它并用于您自己的需求。编译您自己的引擎版本有许多优点,包括在调试游戏时查看实际的引擎实现类(如 Actor 和 Pawn)以及进入引擎代码,以获得对发生情况的洞察。

许多个人通过识别软件漏洞并花费时间修复它们,为该引擎做出了宝贵的贡献。在撰写本章时,该引擎的贡献者人数多达 563 名开发者,如图 14.1 所示:

图 14.1 – Unreal Engine 的 GitHub 页面

图 14.1 – Unreal Engine 的 GitHub 页面

访问引擎源代码并编译自己的可执行版本的好处之一是,您将能够将您的多人游戏编译为专用服务器——一种不渲染任何视觉效果的版本,且不会在客户端机器上运行。这类游戏实例通常被称为无头游戏版本。

使用专用服务器比使用监听服务器具有许多优势:

  • 可执行文件的大小将更小

  • 服务器版本将主要关注游戏逻辑和处理来自客户端的信息

  • 由于托管游戏会话,没有任何客户端会比其他客户端有优势或劣势

  • 您可以构建一个与客户端发布版分开的专用服务器发布版,以及从专用服务器构建的客户端发布版

  • 服务器端代码逻辑可以在服务器上编译,从而隐藏可能被恶意黑客获取的重要代码,如果将其分发到客户端中

在此基础上,让我们开始这段激动人心的旅程,通过使用源代码创建我们自己的虚幻引擎编辑器可执行程序。

首件事是获取 GitHub 仓库中的实际源代码——我们将在下一节中完成这项工作。

编译虚幻引擎

在本节中,您将下载引擎源代码并构建它,以获得个人可执行发布版,这将允许您创建自己的专用服务器进行多人部署。

此过程分为两个主要部分:

  • 从官方 Epic Games GitHub 仓库克隆项目

  • 使用 Visual Studio 设置和构建源代码

如果您不熟悉 GitHub,不要担心——我将用清晰易懂的步骤引导您完成所有操作。

下载虚幻引擎源代码项目

在接下来的步骤中,您将从官方 Epic Games GitHub 仓库下载虚幻引擎源代码,以便将整个项目掌握在手中。您需要满足以下要求:

  • 在您的计算机上安装 Git

  • 拥有 GitHub 账户

  • 将 GitHub 账户连接到您的 Epic Games 账户

注意

如果您已经了解 Git,那么您可能已经满足了一些或所有要求。如果是这样,请随意跳过以下步骤。

您将首先在计算机上安装 Git。

在您的计算机上安装 Git

Git 是一个免费且开源的版本控制系统。它旨在跟踪对计算机文件所做的更改,通过在执行提交时对项目文件进行“快照”来实现。此功能使开发者能够以高效、快速的方式监督和控制其代码的演变。Git 是一个出色的源代码管理工具,适用于从小型到极其大型所有规模的项目。

如前所述,您不需要精通 Git 就能获取 Unreal Engine 仓库——您只需使用其克隆功能下载引擎源代码。

在您的计算机上安装 Git,请访问官方下载页面 (git-scm.com/downloads) 并获取最新版本。下载完成后,就像安装任何常见软件一样简单地安装它。一旦安装阶段完成,您将在您的机器上获得 Git 命令行。

注意

有些人——包括我——更喜欢使用专用客户端软件而不是使用命令行,这样可以减轻使用命令提示符的痛苦。如果您对命令行感到不舒服,您可以使用第三方客户端,其中一些在本页面上列出:git-scm.com/downloads/guis

一旦您安装了 Git,您就可以安全地继续创建 GitHub 账户。

创建 GitHub 账户

GitHub (github.com/) 是一个基于云的服务,允许软件开发者使用 Git 存储管理、跟踪和控制对代码库所做的更改。它使开发者能够从任何地方协作进行项目,并提供项目管理工具、文档、问题跟踪和持续集成和部署等功能。GitHub 在软件开发行业中得到广泛应用,并已成为许多组织和开发人员软件开发工作流程的关键部分。

要创建 GitHub 账户,您只需点击注册按钮,然后添加您的电子邮件后,按照您将收到的指示操作。

一旦您创建了 GitHub 账户,就是时候通过连接 GitHub 和 Epic Games 账户来访问 Epic Games 组织了。

将您的 GitHub 账户连接到 Epic Games 账户

Epic Games 拥有一个 GitHub 组织 (github.com/EpicGames),其中存储了公共仓库,包括 Unreal Engine 项目。访问这个组织是免费的——您只需将 Epic Games 账户连接到 GitHub 账户。以下是操作方法:

  1. 访问您的 Epic Games 账户页面 (www.epicgames.com/account)。

  2. 选择应用程序和账户页面。

  3. GITHUB部分,点击如图 14.2 所示的连接按钮:

图 14.2 – 应用程序和账户部分

图 14.2 – 应用程序和账户部分

一旦您授权了连接操作,您将获得访问 Epic Games GitHub 组织 (github.com/EpicGames) 和 Unreal Engine 仓库 (github.com/EpicGames/UnrealEngine) 的权限。Unreal Engine 仓库包括所有 Unreal Engine 版本,它们分别组织在自己的分支中。

在 Git 中,release分支包含引擎的最新稳定版本。

您现在可以开始在本地机器上克隆仓库:

  1. 在您的 PC 上,导航到一个空文件夹或创建一个新的文件夹。

  2. 右键单击,从下拉菜单中选择Git Bash Here,如图图 14.3所示:

图 14.3 – Git Bash Here 选项

图 14.3 – Git Bash Here 选项

这将打开一个命令提示符,您将准备好克隆虚幻引擎项目。

  1. 输入以下命令:

    git clone https://github.com/EpicGames/UnrealEngine.git
    

您可能需要输入您的凭据;在这种情况下,请使用您的 GitHub 账户的凭据。

下载和克隆过程可能需要很长时间,具体取决于您的互联网连接。

  1. 一旦过程完成,您应该在您的目录中看到一个UnrealEngine文件夹;这是项目源代码。在 Git 终端中,输入以下命令:

    git fetch origin
    

此命令将检索远程仓库中所有可用的分支。

  1. 作为可选步骤,如果您想使用引擎的特定版本,可以输入以下命令:

    git checkout –b [version number] origin/[version number]
    

例如,如果您需要虚幻引擎 5.1,您将输入:

git checkout –b 5.1 origin/5.1

此命令将切换到 5.1 版本分支,并使其源代码可用。

一旦过程完成,您就可以开始编译源文件了。

从源代码编译

一旦您从 GitHub 仓库下载了源代码,您就需要编译它,以便从中生成可执行文件。这将允许您启动编译后的虚幻引擎应用程序并利用其所有功能。

为了拥有一个完全工作的可执行文件,您需要完成以下四个主要步骤:

  • 设置您的编程 IDE

  • 设置项目

  • 生成项目文件

  • 编译源文件

让我们从检查您的编程 IDE 是否更新并准备好开始使用开始。

设置您的编程 IDE

由于您到目前为止一直在使用虚幻和 C++,因此您的编程 IDE 应该已经更新到最新版本,以便编译源文件,但进行双重检查是强制性的,以确保一切设置正确:

  1. 打开您的 PC 上的 Visual Studio 安装程序。

  2. 从您的 Visual Studio 安装中选择修改,如图图 14.4所示:

图 14.4 – Visual Studio 安装程序

图 14.4 – Visual Studio 安装程序

  1. 一旦修改窗口打开,选择单个组件选项卡,如图图 14.5所示:

图 14.5 – 修改窗口

图 14.5 – 修改窗口

  1. 单个组件选项卡安装最新的.NET Framework 开发工具、最新的.NET Framework SDK 以及 – 虽然不是强制性的,但强烈推荐 – 所有的.NET Framework SDK 旧版本。图 14.6展示了我在撰写本书时的设置:

图 14.6 – 单个组件设置

图 14.6 – 单个组件设置

安装完成后,你可以安全地关闭 Visual Studio 安装程序,并准备好设置项目。

设置项目

在这一步,你将下载所需的依赖文件,以便正确设置项目。这个过程相当直接,但可能需要一些时间,具体取决于你的互联网连接。只需打开从 GitHub 克隆的源文件项目文件夹,该文件夹应命名为 Setup.bat 文件,如图 图 14.7 所示:

图 14.7 – Setup.bat 文件

图 14.7 – Setup.bat 文件

然后右键单击此文件并选择以管理员身份运行;这将打开命令提示符并运行所需的命令。

注意

对于更高级的定制级别,你可能想要预先确定目标硬件和平台。例如,你可以使用 -exclude 选项排除不需要的平台来运行 Setup.bat 命令。作为额外的好处,将下载 fewer 文件,并且在构建过程结束时,你将得到一个文件大小更小的引擎。

一旦过程完成,你将添加所需的依赖项,并准备好进行下一步。

生成项目文件

一旦你正确设置了项目,你就可以生成项目文件,以便在 Visual Studio 中打开项目。这个过程几乎与之前相同,但你将不得不运行另一个 .bat 文件。在源项目文件夹(即 UnrealEngine 文件夹)中,找到 GenerateProjectFiles.bat 文件,如图 图 14.8 所示:

图 14.8 – GenerateProjectFiles.bat 文件

图 14.8 – GenerateProjectFiles.bat 文件

再次,右键单击文件并选择将 UE.sln 文件添加到你的源文件文件夹中,如图 图 14.9 所示:

图 14.9 – .sln 文件

图 14.9 – .sln 文件

注意

如果你需要为项目生成命令进行更多定制,我建议参考官方文档,它提供了所有可用命令选项的详尽列表:docs.unrealengine.com/5.1/en-US/how-to-generate-unreal-engine-project-files-for-your-ide/.

你现在可以打开 Visual Studio 并构建 Unreal Engine。

编译源文件

我们现在将通过之前生成的解决方案在 Visual Studio 中编译源代码。要打开它,只需双击 UE.sln 文件,软件应该会打开。

注意

如果你安装了多个 Visual Studio 版本,你可能需要打开正确的版本,以便正确编译项目。这取决于你的 PC 配置和 .NET SDK 安装。

首件事是设置正确的解决方案配置。为此,在工具栏中找到解决方案配置下拉菜单,并将其值设置为开发编辑器,如图图 14.10所示:

图 14.10 – 解决方案配置下拉菜单

图 14.10 – 解决方案配置下拉菜单

接下来,您需要检查您将要编译的解决方案平台。在 Visual Studio 工具栏中,找到解决方案平台下拉菜单,并确认目标平台设置为Win64,如图图 14.11所示:

图 14.11 – 解决方案平台下拉菜单

图 14.11 – 解决方案平台下拉菜单

您终于准备好开始构建过程了。为此,找到Engine文件夹内容。然后右键单击UE5并选择构建,如图图 14.12所示:

图 14.12 – 构建选项

图 14.12 – 构建选项

构建过程将需要大量时间才能完成(这将进一步取决于您计算机的能力),所以您可以放松一下,喝杯咖啡休息一下。

完成过程后,您有值得庆祝的东西!您刚刚创建了自己的全新 Unreal Engine 可执行文件。.exe文件可以在您的源文件项目中找到,在Engine | Binaries | Win 64文件夹中。我的编译二进制文件如图图 14.13所示:

图 14.13 – Unreal Engine 编译的可执行文件

图 14.13 – Unreal Engine 编译的可执行文件

当您双击它时,Unreal Engine 将启动,您会注意到一切看起来几乎与常规 Unreal Engine 编辑器相同。然而,区别在于,在底层,您将拥有一个功能更强大的引擎,拥有更多可供使用的功能。

在本节中,您已经完成了从源代码编译 Unreal Engine 编辑器的挑战性任务 – 这是一件值得骄傲的事情!现在,为下一节中更具挑战性的任务做好准备,在那里您将为多人游戏创建一个专用服务器。

设置专用服务器

在本节中,您将编译一个作为专用服务器的多人项目。为了使事情简单,您将使用从官方模板生成的简单项目,但所有主题和技术都可以轻松地适应任何其他项目,包括您迄今为止一直在工作的 Unreal Shadows 项目。

为了创建一个专用服务器,您的项目必须满足这里列出的特定要求:

  • 您必须使用 Unreal Engine 的源代码构建 – 就是您在前一节编译的那个

  • 您的项目必须创建为 C++ 类型

  • 显然,项目需要支持客户端-服务器游戏玩法

不再拖延,让我们开始创建项目。

创建项目

在本节中,你将从 Unreal Engine 模板开始创建一个新项目。为此,你需要做的第一件事是打开你自己的编译好的 Unreal Engine 编辑器。所以,找到可执行文件,可以使用以下路径在你的 GitHub 下载目录中找到:

[你的项目文件夹] | Engine | Binaries | Win64 | UnrealEngine.exe

双击文件以启动 Unreal Engine 编辑器,一旦启动,就使用以下设置创建一个新项目:

  • 模板游戏 | 第三人称

  • 项目名称TP_Multiplayer

  • 项目类型C++(如果你选择蓝图项目,请记住,稍后当你需要编译专用服务器时,你将不得不将其转换为 C++ 项目;这将是一个非常简单的任务,因为你只需要将一个 C++ 类添加到项目中)

  • 不勾选起始内容

我为这个项目的设置如图 14所示。14

图 14.14 – 项目设置

图 14.14 – 项目设置

一旦创建项目,你就可以设置和构建项目解决方案了。

构建项目解决方案

现在你已经创建了项目,找到项目内的 Source 文件夹。在这里,你可以找到两个名为 TP_Multiplayer.Target.csTP_MultiplayerEditor.Target.cs 的文件。

目标文件是用 C# 语言编写的,它们的目的是定义 Unreal Engine 构建工具将如何编译目标构建。在这种情况下,第一个将用于打包常规的可执行文件,第二个将用于 Unreal Engine 编辑器。

我们需要定义第三个,它将用于打包应用程序的服务器版本。为了做到这一点,在另外两个 .Target.cs 文件相同的文件夹中,创建一个第三个目标文件,并将其命名为 TP_MultiplayerServer.Target.cs

一旦创建文件,使用你选择的文本编辑器打开它,并插入以下代码:

using UnrealBuildTool;
using System.Collections.Generic;
public class TP_MultiplayerServerTarget : TargetRules
{
  public TP_MultiplayerServerTarget(TargetInfo Target) : base(Target)
  {
    Type = TargetType.Server;
    DefaultBuildSettings = BuildSettingsVersion.V2;
    ExtraModuleNames.AddRange( new string[] { "TP_Multiplayer" } );
  }
}

如果你不太熟悉 C# 语法,不要担心!这里没有太多要理解的——我们只是在定义一个名为 TP_MultiplayerServerTarget 的类,并在构造函数中定义了一些构建设置。唯一要注意的是,我们已经将构建目标类型定义为 Server,因为我们需要创建一个专用服务器构建。

返回到你的项目根目录,找到名为 TP_Multiplayer.uproject 的 Unreal Engine 项目文件。右键单击它,从下拉选项中选择生成 Visual Studio 项目文件,如图 14所示。15

图 14.15 – 项目文件生成选项

图 14.15 – 项目文件生成选项

一旦生成过程完成,你的项目将设置为编译服务器构建版本——这是你需要创建专用服务器时所需的。

您现在可以通过双击TP_Multiplayer.sln解决方案文件来打开 Visual Studio,以创建所需的构建。一旦您的编程 IDE 已打开,通过在主工具栏中的解决方案配置下拉菜单中单击并选择开发服务器来创建构建,如图 图 14.16* 所示:

图 14.16 – 设置为开发服务器的解决方案配置

图 14.16 – 设置为开发服务器的解决方案配置

您现在可以通过在解决方案资源管理器窗口中右键单击TP_Multiplayer项并选择构建来构建项目。

一旦过程完成,您将成功构建开发服务器,这将允许 Unreal 构建工具识别服务器构建目标。

在返回 Unreal Engine 编辑器之前,您还必须构建开发编辑器配置,因此请按照以下步骤重复之前的步骤,通过选择以下步骤使用编辑器配置:在主工具栏中,单击解决方案配置下拉菜单并选择开发编辑器。然后在解决方案资源管理器窗口中右键单击TP_Multiplayer项并选择构建

一旦构建过程完成,您可以安全地关闭 Visual Studio 并返回 Unreal Engine 以构建专用服务器。

构建专用服务器

在本小节中,您将从 Unreal Engine 编辑器构建项目的一个专用服务器可执行文件。您首先需要做的是创建一个新的地图,该地图将作为服务器的入口点:

  1. 从主菜单中选择文件 | 新建关卡并创建一个新的基本关卡。

  2. 在内容浏览器中,创建一个Maps文件夹并将级别保存在其中,命名为Map_0

  3. 在级别的世界设置窗口中,将游戏模式覆盖设置为游戏模式基础;这将避免使用默认的第三人称游戏模式打开此关卡。

此地图将在客户端连接到服务器时用作入口点。下一步是在开始服务器打包阶段之前更新一些项目设置。

  1. 让我们从在主菜单中选择编辑 | 项目设置开始。然后选择地图与模式部分。

  2. 默认模式 | 选定的游戏模式类别中,展开高级部分。

  3. 全局默认服务器游戏模式下拉值设置为BP_ThirdPersonGameMode,如图 图 14.17* 所示:

图 14.17 – 全局默认服务器游戏模式

图 14.17 – 全局默认服务器游戏模式

上述设置将定义客户端连接到服务器时使用的游戏模式。

我们现在将定义游戏和服务器地图,以便设置客户端连接时使用的入口点。定位到默认地图类别并执行以下操作:

  1. 游戏默认地图下拉菜单中,选择Map_0

  2. 展开高级部分,并在 服务器默认地图 下拉菜单中选择 ThirdPersonMap

默认地图 类别的最终结果如图 14.18 所示:

图 14.18 – 默认地图类别

图 14.18 – 默认地图类别

我们已经完成了 地图与模式 设置。你现在需要为你的项目定义打包设置:

  1. 项目设置 中,展开 打包 | 高级 部分。

  2. 定位到 包含在打包构建中的地图列表 数组字段。

  3. 使用 + 按钮添加 Map_0.umap 级别。

  4. 再次使用 + 按钮添加 ThirdPersonMap.umap 级别。

你现在应该有一个与图 14.19 中所示相当类似的设置:

图 14.19 – 构建中要打包的地图列表

图 14.19 – 构建中要打包的地图列表

  1. 作为额外步骤,请再次确认在 项目 类别中,构建 配置设置为 开发;这将允许我们在本章后面通过命令行连接到服务器。

  2. 关闭 项目设置 窗口,并在 Unreal Engine 主工具栏中,点击 平台 按钮,选择 Windows | TP_MultiplayerServer 以设置构建目标,如图 14.20 所示:

图 14.20 – 构建服务器

图 14.20 – 构建服务器

  1. 接下来,点击 Windows | Package Content 以开始打包应用程序,一旦构建完成,你将手握一个专用的服务器可执行文件!

  2. 要获取客户端可执行文件,你必须重复上述步骤,使用不同的构建目标。为此,在 Unreal Engine 主工具栏中,点击 平台 按钮,选择 Windows | TP_Multiplayer 以设置构建目标,如图 14.21 所示:

图 14.21 – 构建客户端

图 14.21 – 构建客户端

然后,点击 Windows | Package Content 以开始打包客户端构建。一旦完成,你也将拥有客户端可执行文件,并且你将准备好测试应用程序。

测试项目

现在客户端和专用服务器都已成功构建,是时候测试它们的功能了。

要在本地启动你的服务器,你只需要双击之前步骤中创建的构建可执行文件。

或者,如果你有兴趣检查服务器日志,可以采取以下步骤:

  1. 打开 Windows 命令提示符。

  2. 通过 cd 命令,导航到包含服务器可执行文件的文件夹。

  3. 插入你的服务器可执行文件名称,后跟 –log 参数,例如:

    TP_MultiplayerServer.exe -log
    

一旦服务器启动,它将开始监听来自客户端的连接请求。

由于我们使用了现成的、基础模板,并且我们没有实现任何会话逻辑,我们将从虚幻引擎命令行连接客户端。这显然不是你将在最终版本中使用的功能,但在开发时却非常有用。

命令行在客户可执行文件中可用,因为我们之前将这些步骤编译为开发版本的项目。

要启动客户应用程序,你只需双击可执行文件,其名称应该是TP_Multiplayer.exe。这将使用默认的起始地图(即,Map_0)打开可执行文件。

如前所述,为了连接到服务器,我们将使用控制台命令,默认情况下可以通过单引号字符(即反引号)打开。一旦控制台打开,输入以下命令:

open 127.0.0.1

控制台命令打开的客户应用程序如图 14.22所示。22:

图 14.22 – 控制台命令打开的客户应用程序

图 14.22 – 控制台命令打开的客户应用程序

注意

如果你使用的键盘不支持反引号字符(像我的一样),你可以通过打开编辑器首选项并查找打开控制台命令****框字段来轻松更改键盘快捷键。

客户端现在应该打开常规第三人称地图,你的客户端将连接到专用服务器。为了确认这一点,如果你在启动服务器时启用了–log选项,你应该看到一条类似于以下的消息:

LogNet: Join Succeeded: [Client Identifier]

就这样 – 你终于成功创建了你的专用服务器,并将你的客户端连接到了局域网!

如果你想要一个额外的挑战,你可以尝试使用专用服务器编译 Unreal Shadows 项目。你在这个领域已经有了一些知识,所以这不应该需要太多的努力。

此外,如果你想深入了解这个主题,Epic Games 提供了一个关于如何编译 Lyra Starter Game 的完整教程(docs.unrealengine.com/5.1/en-US/lyra-sample-game-in-unreal-engine/),这是一个包含本书中涵盖的许多主题的实战项目。教程的链接是 docs.unrealengine.com/5.1/en-US/setting-up-dedicated-servers-in-unreal-engine/

一点点的努力就能实现一切!

摘要

在本章中,你面临了从 GitHub 下载虚幻引擎编辑器源代码并将其编译成可执行应用程序的令人畏惧的任务。

如果你计划以多人游戏开发者为生,那么在需要为你的游戏设置专用服务器或需要自定义引擎的网络和多人设置时,构建虚幻引擎源代码是一项必备技能。

正如你所发现的,构建和打包一个专用服务器并不是一项容易的任务,它需要大量的时间和耐心——有时还需要一点运气!

在下一章——也是最后一章中,你将学习如何通过使用在线云服务,特别是 Epic Online Services,在多人游戏开发中更进一步。

第十五章:添加 Epic 在线服务(EOS)

在开发多人游戏时,向整体体验中添加在线服务对于使玩家能够通过互联网与其他人连接和玩游戏至关重要。这对于需要大量玩家或由世界各地不同地点的人玩的游戏尤为重要。添加在线服务可以让玩家享受更社交的游戏体验,并增加游戏的总体乐趣和参与度。此外,它还允许游戏开发者收集玩家的数据和反馈,这对于改进游戏和修复出现的问题非常有用。

在本章中,我将向您介绍Epic 在线服务EOS),这是一个为开发者提供创建、部署和运营高性能游戏体验工具的云平台。由 Epic Games 开发,这个强大的平台将所有现有技术和专业知识整合到一个统一系统中。凭借其可扩展的基础设施和高级功能,如分析和云托管能力,Epic Games EOS 允许开发者构建针对任何设备或操作系统的最大性能优化的游戏。

在接下来的几节中,我将向您介绍以下主题:

  • 介绍 EOS

  • 访问开发者门户

  • 开始使用 EOS SDK

技术要求

要跟随本章介绍的主题,您应该完成所有前面的章节,并理解其内容。

介绍 EOS

EOS(dev.epicgames.com/en-US/services)是一套强大的服务和工具集,旨在帮助开发者创建尽可能沉浸式的在线体验。使用 EOS,开发者可以轻松管理用户身份验证、匹配、排行榜、成就等——所有这些都可以从一个集中的系统中完成。无论您是在开发 MMO 游戏还是具有多人模式或排行榜等在线功能的单人游戏,EOS 都有为每个希望将游戏提升到更高水平的开发者提供的东西。有一点需要提及的是,所有服务都是免费使用的,即使您没有 Epic Games 账户也是如此。

EOS 可以分为三个不同的服务集合:

  • 游戏服务,涵盖多人功能,如会话、大厅或成就

  • 账户服务,涵盖玩家身份,如身份验证和资料处理,以及好友管理

  • 商店服务,涵盖 Epic Games 商店交易,包括目录管理和验证

游戏服务也可以与任何身份提供者一起使用,例如 Discord、Steam、Google,当然还有 Epic Games;这意味着玩家不需要 Epic Game 账户就可以访问这些服务。另一方面,账户服务和商店服务只能与 Epic Games 账户一起使用。

注意

如果您有所疑问,EOS 不包括用于托管专用服务器(就像您在第十四章部署多人游戏)的云机器。要在云中托管您的服务器,您需要使用诸如亚马逊网络服务AWS)或微软 Azure等服务。官方 Epic Games 文档有关于此主题的专用部分,可以在以下网页上找到:docs.unrealengine.com/5.0/en-US/unreal-engine-cloud-deployments/

EOS 可通过Epic Games 开发者门户访问,这是一个基于浏览器的工具,使用户能够通过一系列开发者资源配置和设置他们的游戏。除了 EOS,开发者门户还提供管理您可能在 Epic Games Store 中提供的游戏的功能。

成功注册开发者门户账户后,您将获得管理您产品的能力,配置服务,并为身份提供者和可用平台建立设置。开发者门户内提供的其他功能包括用户更新游戏信息,为玩家提供支持,管理游戏财务,以及访问使用报告和统计数据。

值得注意的是,开发者门户旨在实现跨平台兼容,为开发者提供在多种不同平台(如游戏机、桌面和移动设备)上部署游戏的能力,同时利用单一服务来管理所有这些平台上的游戏玩法。这种功能有助于为这些不同平台上的玩家提供一致且无缝的体验。

注意

由于开发者门户和 EOS 都是基于网络的,它们始终处于开发状态。因此,可能会遇到以下章节中描述的功能和功能的不一致性或变化。尽管存在任何差异,您仍然应该能够有效地导航并参与这些服务,并且对整体体验的干扰最小。

由于 EOS 本身是一个庞大的主题——并且不仅仅是为 Unreal Engine 设计,而是与许多开发者平台一起使用——对其有全面的理解超出了本书的范围;然而,在以下章节中,我将指导您了解开发者门户、EOS 及其特性的主要部分,这样您就有了一个坚实的基础,可以在游戏中使用它们。

首件事是开始创建您自己的组织,以便您能够访问云服务中所有可用的功能。

访问开发者门户

在本节中,我将指导您创建一个开发者门户账户,以便正确设置和管理您自己的项目,无论它们是否是多人游戏。您需要完成的步骤以使您的游戏准备就绪如下:

  • 设置 Epic Games 账户(你应该已经有了)并为开发者门户创建一个组织

  • 创建产品

  • 配置产品

那么,让我们开始吧。

访问 Epic Games 开发者门户

为了访问和使用 Epic Games 开发者门户,你需要做的第一件事是创建一个组织——这是一个负责在 Epic Games 开发者门户中创建和拥有产品的团队。

首先,访问专门的网页,该网页可以通过此链接找到:dev.epicgames.com/portal。使用你的 Epic Games 账户登录后,你将看到一个注册表单,你需要在此插入组织名称和电子邮件,如图图 15.1所示:

图 15.1 – 组织创建表单

图 15.1 – 组织创建表单

注册完成后,你将获得访问开发者门户的权限,在那里你可以管理你的组织,下载 EOS SDK,创建你的项目,最重要的是,设置它们。

作为组织的创建者,你将负责为其设置所有必要的信息,例如如果你计划从你的游戏中获得一些收入,那么你需要提供税务和支付信息。

此外,除非你打算作为一个单打独斗者工作,否则你将能够邀请额外的成员加入组织本身。这个功能在组织 | 成员部分可用。每个成员都可以被分配一个角色,以获取访问组织中的某些或所有功能。你可以创建自己的角色,每个角色都有其自定义的访问级别,但 Epic Games 已经为你创建了一些;其中一些角色在此列出:

  • 管理员:这个角色将授予成员访问门户中所有功能的权限。这是自动分配给组织创建者(即你)的角色。

  • 社区工具:这个角色将使成员能够访问所有与社区相关的功能,例如游戏分析、账户和票务系统。

  • 支付:这个角色将使成员能够访问所有与财务相关的部分,例如支付和报告。

如果你在一个游戏工作室工作——即使是一个小工作室——拥有这种成员访问权限对于使一切完美运行将非常重要。

一旦组织被正确设置并且你有一个组织良好的团队,接下来要做的事情就是创建你的第一个产品,无论是多人游戏还是其他任何东西。

创建产品

一旦你在一个组织中,你需要创建一个产品——这是一个包含一些 EOS 逻辑的游戏或软件项目。一旦创建了一个产品,它将被分配一个默认的沙盒,这是一个包含分发数据(如商店相关或特定部署信息)的开发环境。在撰写本书时,Epic Games 提供了以下默认沙盒:

  • Dev:用于在开发时编辑和配置产品

  • Stage:用于测试产品的就绪状态

  • Live:用于在 Epic Games Store 上分发产品

在沙盒中,你将能够创建一个或多个部署,这是一个特定的分发,将存储所有游戏玩法和玩家数据,例如成就和当前比赛。

例如,让我们想象你想为 Unreal Shadows 游戏实现一些服务,并且不想使事情过于复杂。首先,你需要创建一个专用产品,然后你将与一个沙盒一起工作;在沙盒中,你将根据需要使用上述部署环境。例如,参见以下:

  • Dev环境中,你将进行项目开发和内部测试

  • Stage环境中,你将测试游戏

  • Live环境中,你将作为官方发布发行游戏

让我们现在想象一下,在你的游戏开发过程中,你(或游戏设计团队)决定添加一个新实验性功能,比如为所有玩家提供语音聊天。你将创建一个新的部署,称为Dev-Experimental-VOIP,并在内部测试其功能。一旦这个功能稳定并准备好发布,你只需将其添加到你的Stage部署中,一旦足够稳定,就添加到Live部署中。

作为实际示例,我们将创建一个演示产品。在你的开发者门户仪表板中,点击创建产品按钮,如图图 15.2所示:

图 15.2 – 创建新产品

图 15.2 – 创建新产品

你将得到一个弹出窗口,其中你需要输入产品名称。在我的情况下,我选择了EOS Demo,如图图 15.3所示:

图 15.3 – 命名产品

图 15.3 – 命名产品

注意

创建产品后,你将无法更改其名称,所以请谨慎选择!

如果这是您组织创建的第一个产品,您将需要审查一些与 Epic Games 相关的协议,涉及主题如商店分销和营销订阅者名单;仔细阅读它们,如果您同意条款,请点击接受按钮。一旦接受,您可能需要支付提交费;只需跳过这一步,因为它与 Epic Games Store 有关,与我们目前感兴趣的 Game Services 无关——一旦您决定在 Epic Games Store 上发布游戏,您可能需要它。

现在您已经创建了您自己的产品,您就可以进行下一步,配置您将要使用的服务。

配置产品服务

要访问您的项目页面,您只需点击您的产品链接——在我的情况下,是开发者门户中的EOS 演示产品链接——如图 15**.4所示:

图 15.4 – 选择产品配置

图 15.4 – 选择产品配置

一旦您进入产品页面,您可以通过点击产品设置来访问其设置,如图 15**.5所示:

图 15.5 – 产品设置部分

图 15.5 – 产品设置部分

现在,您可以为您的应用程序创建 EOS 客户端。

创建 EOS 客户端

在 Epic Online Services 的上下文中,术语客户端指的是一个利用 EOS 特定产品功能的应用程序。这可以包括最终用户在其系统上运行的本地安装的游戏构建,由产品所有者维护的专用服务器,或任何需要访问 EOS 提供的后端服务的其他程序。每个客户端都将有自己的 ID 和用于身份验证的秘密密码。

每个客户端还将有自己的客户端策略,这将确定将要实施的功能的访问级别。这意味着,如果您使用 EOS 创建多人游戏,您的专用服务器将需要自己的 EOS 客户端,而玩家的客户端将有自己的专用 EOS 客户端。另一方面,如果您计划分发您游戏的监听服务器版本,您只需要一个 EOS 客户端用于您的游戏。

如果您对这个上下文中的“客户端”一词感到有些困惑,请不要担心——我第一次阅读官方 EOS 文档时也有同样的感觉!为了帮助理解,让我们实际创建一个客户端:

  1. 点击主页面工具栏中的客户端链接,如图 15**.6所示:

图 15.6 – 客户端部分

图 15.6 – 客户端部分

点击添加新客户端按钮;这将打开添加新客户端窗口(稍后将在图 15**.8中展示)。

  1. EOS 演示客户端 中。

  2. 点击EOS 演示策略

  3. 客户端策略类型中,您可以选择预制的配置之一,或者创建一个自定义配置。在本例中,我选择了GameClient选项,这些选项配置为管理不受信任的客户端应用程序,因此将需要一个经过身份验证的用户。

  4. 之前的选项将启用一系列附加选项,例如排行榜匹配大厅,您可以根据自己的需求进行更改。

  5. 一旦您对客户端的配置满意,请点击添加新客户端策略,如图图 15.7所示:

图 15.7 – 客户端策略创建

图 15.7 – 客户端策略创建

  1. 一旦创建客户端策略,您将返回到客户端创建页面。点击添加新客户端按钮,如图图 15.8所示:

图 15.8 – 客户端创建

图 15.8 – 客户端创建

一旦创建客户端,您将返回到产品页面,您应该会看到列出的客户端和客户端策略,如图图 15.9所示:

图 15.9 – 产品设置页面上的客户端和客户端策略

图 15.9 – 产品设置页面上的客户端和客户端策略

客户端部分,您将注意到为该客户端启用的功能及其 ID,这些 ID 将在连接到服务时使用。

访问 Epic 账户服务

现在客户端已经创建,您需要访问产品页面上的Epic 账户服务部分,以便完成 EOS 配置。要访问此部分,请点击Epic 账户服务按钮,如图图 15.10所示:

图 15.10 – Epic 账户服务部分

图 15.10 – Epic 账户服务部分

首件事是为客户端配置权限,因此请点击EOS 演示应用程序部分中的权限按钮,如图图 15.11所示:

图 15.11 – PERMISSIONS 部分

图 15.11 – PERMISSIONS 部分

除非您想添加一些自定义配置,否则只需点击保存更改按钮,以初始化权限配置。这将设置此部分为已配置,如图图 15.12所示:

图 15.12 – 已配置的 PERMISSIONS 部分

图 15.12 – 已配置的 PERMISSIONS 部分

接下来,您需要选择链接客户端部分,以便设置之前创建的客户端为所选客户端。一旦您进入链接客户端部分,您只需从下拉菜单中选择您的客户端 – 在这种情况下,EOS 演示客户端 – 并点击保存更改按钮。这将设置此部分为已配置,如图图 15.13所示:

图 15.13 – 配置好的链接客户端部分

图 15.13 – 配置好的链接客户端部分

品牌设置部分可以在开发阶段保持未配置,因为它只有在您进入发布阶段并且您的应用程序需要由 Epic Games 审查和批准时才需要。

完成此操作后,您的应用程序就准备好了,EOS 可以与您的游戏或应用程序连接。

在本节中,您已经了解了 Epic Games 开发者门户及其在线服务。如您所见,您将需要访问该门户以初始化和配置您将在游戏中实现的所有云功能。在下一节中,我将简要介绍您将在游戏中使用的工具,以及如何将其集成到 EOS 环境中。

开始使用 EOS SDK

EOS SDK是一个独立于任何特定游戏引擎的工具,为开发者提供访问多个跨平台服务的能力,这些服务可以集成到他们的游戏中。根据所使用的游戏引擎,将 EOS SDK 集成到游戏中的集成选项水平可能会有所不同。尽管如此,集成方法由开发团队自行决定,他们甚至可以使用集成选项的组合。

为了开始使用 EOS SDK,您必须:

  • 从开发者门户下载

  • 将其集成到您的游戏中

在本节中,我将简要介绍如何获取 SDK 以及如何在项目中集成它的基本概念。

下载 EOS SDK

要下载 SDK,请转到您的开发者门户仪表板页面,并点击SDK & 发布说明按钮,如图图 15.14所示:

图 15.14 – 下载 SDK 按钮

图 15.14 – 下载 SDK 按钮

如前所述,SDK 旨在针对多个开发者平台,因此您将获得多个下载选项(即 C、C#、iOS 或 Android 的 SDK),如图图 15.15所示:

图 15.15 – 下载选项

图 15.15 – 下载选项

选择您首选的平台后,下载将开始。一旦完成,您就可以开始将 SDK 与您自己的应用程序集成。

将系统集成到您的游戏中

无论您决定使用哪个游戏引擎,您都可以在您的游戏中完全使用 EOS SDK!您需要做的所有事情——在开发者门户中配置产品并下载 SDK 之后——就是将集成代码写入您选择的游戏引擎中。

如果您计划将 SDK 与 Unreal Engine 项目集成,有两种方法可以集成 EOS SDK:

  • 使用 EOS 在线子系统OSS)插件

  • 使用第三方插件或自己编写一个

让我们来看看这两种选项。

使用 EOS 在线子系统

正如您在第十二章**,管理多人会话中已经看到的,Unreal Engine 中的 OSS 是一个工具,通过一系列插件提供了一种统一的方式来访问不同在线服务提供的众多在线功能。这包括 Xbox Live、Steam,最后是 Epic Online Services - 通过专门的 EOS OSS 插件 - 使其在支持多个平台或在线服务的游戏开发工作流程中具有极大的价值。考虑到这一点,您将能够在 Unreal 编辑器中轻松配置您游戏的 EOS 设置,而无需编写代码。

要访问 EOS OSS,您需要在 Unreal Engine 中启用插件。这相当容易实现。一旦您打开了您的 Unreal Engine 项目,您只需做以下事情:

  1. 从主菜单中选择编辑 | 插件

  2. 启用在线子系统 EOSEOS 共享插件。

  3. 重新启动 Unreal Engine 以初始化插件。

一旦插件已启用,您将需要设置您的项目,将其与您之前创建的 EOS 产品连接。这个设置相当长,但相当直接;您可以在官方文档中找到如何操作的说明,文档链接为:docs.unrealengine.com/5.1/en-US/online-subsystem-eos-plugin-in-unreal-engine/。您还可以找到有关 OSS 插件的更多信息,这超出了本书的范围。

值得注意的是,在撰写本书时,EOS SDK 的一些接口尚未在 EOS OSS 插件中开发,因此它们将不会作为默认选项提供。以下是不再可用的接口:

  • 反作弊界面

  • 报告界面

  • 处罚界面

  • 自定义邀请界面

这意味着如果您的游戏需要这些功能,您将不得不编写自己的插件,或者使用第三方插件。

使用第三方插件

如前所述,将您的游戏与 EOS SDK 集成的另一种方法是开发自己的 Unreal Engine 插件。

注意

EOS SDK 需要在插件内部初始化 - 在您的项目中编写自己的实现可能会导致意外的行为或某些界面根本无法工作。

在 Unreal Engine 中开发自己的插件是磨练技能并将其提升到下一个水平的一种美妙方式。要开始,最好的建议是阅读官方文档,该文档详细描述了在 Unreal Engine 中开发和管理工作插件的过程。文档页面可以通过以下链接找到:docs.unrealengine.com/5.1/en-US/plugins-in-unreal-engine/

然而,如果你对插件开发感到不舒服,你可以利用虚幻引擎市场(unrealengine.com/marketplace)并寻找商业解决方案。例如,有几款令人惊叹的集成工具,如由 Redpoint Games 开发的EOS Online Subsystem(unrealengine.com/marketplace/en-US/product/eos-online-subsystem)和由 Betide Studio 开发的EOS Integration Kit(unrealengine.com/marketplace/en-US/product/eos-integration-kit)。两者都提供了与 EOS 的无缝集成,几乎所有的服务接口都暴露出来,以便使你的多人游戏开发更加容易和高效。

值得注意的是,这两个插件都提供某种形式的免费或开源许可,因此你可以尝试它们,以检查它们是否适合你。当然,购买它们将有助于开发团队维护插件并在未来添加新功能。

现在你已经对 EOS 有了基本的知识,是时候开始着手制作你即将成功的游戏,并将其与 Epic Games 提供的某些(或所有!)服务集成。

注意

要了解 EOS 附带的所有功能,你可以从检查这个网页上的官方文档开始:dev.epicgames.com/docs/epic-online-services

摘要

在这本书的最后一章,我向你介绍了开发者门户和 EOS 平台。你访问开发者门户是为了创建自己的产品,并将其与任何类型的应用或游戏连接。你也看到了 EOS 应用程序的基本结构和如何使用一些基本和默认设置进行初始化。

最后,你已经了解到你可以将这些服务与任何开发平台集成,包括虚幻引擎;这可以通过使用官方的——但截至目前还不完整的——插件来实现;你也可以使用第三方集成系统,或者甚至可以自己编写代码,这样你就可以专注于你真正需要的功能。

正如你所猜想的,关于这个话题还有很多内容,而你只是刚刚开始触及这个环境的表面。我的最终建议是尝试使用这些服务,并探索 SDK 提供的所有接口。通过编写自己的代码进行实验,如果你足够勇敢的话——甚至可以创建自己的插件;我向你保证,如果你是一个游戏程序员,这将是非常有趣的事情!(但也许,这个话题最好留给另一本书来讨论!)

这就是《使用虚幻引擎 5 开发多人游戏》的结束。我很高兴与你分享我的知识和经验,并且我确信你现在知道如何为你的即将到来的玩家创建令人惊叹的多人游戏。

感谢您与我一同踏上这段旅程,愿您的项目好运连连!

posted @ 2025-10-27 09:09  绝不原创的飞龙  阅读(21)  评论(0)    收藏  举报