UE5 联机游戏 Tutorial

一、Project setup + Character movement

Game Mode 用来定义游戏的规则,得分等等,Game Mode 定义了用于游戏功能的其他类,如 Player Control,Player State,Game State等等, 这都是主要的类,Game Mode 是基于这些类的基础上工作的

首先我们创建一个第三人称游戏并将内容保存至这个样子

在 Blueprint 文件夹中的 GameModes 文件夹的中的 Core 文件夹中,我们创建一些基本结构作为父类用于继承

首先创建一个 BP_GameMode 并命名为 BP_GM_Core,其与BP_GameModeBase的区别在于,前者适合多人游戏而后者适合单人游戏

然后创建一个 BP_PlayerControl 命名为 BP_PC_Core,创建一个 BP_Character 命名为 BP_Core_Character

GameMode 是用来设置关卡中 World Setting 的

给 BP_Core_Character 添加运动,这里其实可以直接将 BP_ThirdPerson 中的蓝图直接搬过来,不过要修改一下这个 Player Control 为 BP_PC_Core

同时,在添加 Camera 的时候,我们要注意将弹簧添加在 Mesh 上而不是 Capsule

为了实现摄像头转向,首先我们需要在 SpringArm 中设置 Use Pawn Control Rotation

我们可以根据需求设置 Character Movement (Rotation Setting)

Use Controller Desired Rotation 可以将人物的方向转向为摄像头的方向,而Orient Rotation to Movement 可以将人物的方向转向为加速度的方向,这样在人物不动的时候,可以看到正面对人物

同时,这里还有一个设置 Use Controller Rotation Yaw

开启后的效果类似于 Use Controller Desired Rotation 取消了转向过程

一般来说:我们需要取消掉 Use Controller Rotation Yaw,Use Controller Desired Rotation,启用 Orient Rotation to Movement

接下来给 BP_Core_Character 添加动画,这里直接照着之前的 UE5 动画蓝图-CSDN博客 设置,得到结果如下

这里为了实现加速这一步,我们在 Blend Space 中 将 walk 的速度设置为300,run 的速度设置为 800,最大速度设置为300,使用 shift 最大速度为 800,从而实现一个跑步的效果;首先我们在 Input 中创建一个 IA_Run,并添加到 IMC_Default 中;

然后在 CharacterMovement 中设置 max walk speed 并 BP_Core_Character 中添加设置函数

以上是第一节的内容;

二、Bootscreen + Basic Main Menu setup

首先创建一些关卡,在左上角中的 File 中创建

当游戏文件启动的时候,我们希望 BootScreen 在启动屏幕中启动,BootScreen 通常用来显示游戏的 UI logo,游戏制作者信息等相关页面,当游戏启动完毕后,我们希望再进入 MainMenu 页面,在这里我们可以创建房间或者加入房间,准确的来说指的是会话,然后进入关卡页面;通常来说我们使用的是专用服务器,这意味这我们可以在主菜单中获得分组系统然后将玩家分组,通常分组系统不会在UE5中 replication 上运行,因为 UE5 replication 需要在会话中,在会话中我们需要一个子系统,如 steam epic online 服务等等,他们的作用是将我们的离线游戏带到互联网上;如果主机作为服务端,如果主机断开链接,那么整个会话也就断开了链接;其次是 Travel_evel,当从一个关卡到另一个关卡的时候,如果不想断开玩家的连接,将不得不使用 Travel_evel,这样可以确保玩家保持链接;关卡过度意味着关卡内的所有的 content 都会被 destory 然后重建,在重建的过程也就是再次加载的过程,为了等待再次加载完毕,我们需要使用 Travel_evel 过度

配置完毕后,我们可以进入到每一个地图执行 Build All Levels

这样会生成所有的照明数据,每一个地图执行完毕后都会生成一个数据,最后得到的结果如下

关卡创建完毕后,我们来创建 Widgets 用于关卡的显示,首先我们创建一下 BootScreen

这里创建一个新的文件夹,Widgets,然后创建一个文件夹 Boot,在这里创建 BootScreen_Level 中的 user interface 的 widgets_blueprint,命名为 WB_BootScreen

这里图片必须在 Hierarchy 中放置在文本的前面,这样会让图片显示在文本的下面,然后我们创建一个动画

然后在 Graph 中设置

在关卡中引用

得到效果如下

接着我们来创建 MainMenu_Level,由于 MainMenu 里面包含一些更多的功能,我们需要一些 blueprint 类而不是仅凭 Level blueprint 来对所有内容进行编码,所以我们先在 content 下的 blueprint 下 创建一个 MainMenu 文件夹并在该文件夹下面创建一个 Game Mode Blueprint 和 Player Controller Blueprint,然后在 MainMenu_Level 的 World Setting 中设置

现在我们需要一些 MainMenu 的 UI,我们可以在 Player Controller 创建 UI,虽然 World Setting 中我们有 HUD Class ,但是我们实际上不需要这么多复杂的东西,我们可以简单的在主菜单中为这个场景在 Player Controller 上创建我们的用户界面管理

回到 Widgets 中我们创建一个 MainMenu 文件夹,在 MainMenu 文件夹中完成 MainMenu UI 创建,由于 MainMenu 有许多的 Button,而 UE5 中的 Button 需要搭配 Text 使用,这里我们创建一个基本类来包含 Button 和 Text 这两个功能,这里我们创建一个简单的 Widget blueprint,命名为 WB_Base_Button,创建界面如下

页面创建完毕后,我们需要进入 Graph 中,对这个 UI blueprint 进行 pre construct 构建

这样就设置完毕了,最后我们在 WB_MainMenu 中设置

然后我们在 Graph 中设置事件,该事件要从对应的 Button 中来引

最后我们把事件 添加到 Player Controller 中,由于我们处于离线关卡中属于离线 单人 状态,所以我们不需要检查 is local controller

然后显式鼠标有两种方式,一种是利用 set mouse cursor,另一种是在 player controller 中设置 show mouse cursor

现在我们只是获得了鼠标,我们还需要做最后一步操作,那就是聚焦到我们的UI组件上,否则我们还需要点击进行聚焦一次,最终得到的 Player Controller 如下所示

得到最后的结果如下

三、Host + Server Browser + Join Widgets

在这里首先进行资源的一些整理,在 Widgets 文件夹中的 MainMenu 文件夹中,我们有一个 WB_Base_Button widget blueprint 组件,我们最好创建一个 Base 文件夹用来存放这些基本类从而让文件有条理性

将任何资产在 UE5 中移动的时候,有时我们会获得 redirector,redirector 在使用的过程中可能会变得混乱,因为有可能有错误的引用,因此为了确保始终有正确的引用 reference,我们可以右键 content 选择 Update Redirector References

接下来创建 host game screen,回到 MainMenu 中,创建一个文件夹命名为 Sessions,然后在该文件夹下面创建一个 WB_CreateGame 的 widget blueprint ;

然后创建一个 WB_ServerBrowser_Item

接着创建一个 WB_ServerBrowser

同时在每一个 Back Button 绑定一个 dispatcher ,命名为 OnClicked_Back_Button

然后在 WB_MainMenu 中使用 WidgetSwitcher 包裹住 WB_CreateGame,WB_ServerBrowser

最后在 WB_MainMenu 的 Graph 中实现事件触发

实现效果如下

四、Hosting, Finding and Joining Sessions

新内容 | 虚幻引擎 5.4 文档 | Epic Developer Community (epicgames.com)

UE5 文档中包含了所有的问题和解决方法,所以尽可能多的查阅文档

当谈到多人游戏的时候,首先要提及的是 Dedicated Server,该服务器能够随意的连接和断开多人游戏;其次是 Client-Server ,这种模式是一个玩家是服务端,其他玩家是客户端,这种模式下当服务端玩家断开连接的时候,所有其他的玩家也断开了连接,游戏就会停止;最后一个模式是 Peer - to - Peer 模型,该模型让所有玩家都作为服务端,所以只要有一个人没有断开连接,游戏就不会结束;

Dedicated Server:实际上是一个专门用于托管的服务器,该服务器不是免费的,同时很难设置,需要优化,该服务器较为稳定用于提供给更多的玩家,一般是60-120人,同时不能使用blueprint构建,而要使用 C++

Client-Server:作为服务端创建 Session,其他玩家可以加入作为客户端,服务端作为 host 负责为客户端玩家提供数据以保持同步,让玩家在游戏引擎中保持同步称作为 Replication。使用只需要在 UE5 中那样缓存游戏,一个玩家成为 host 其他玩家连接,该模式通常适用于12个人以下连接;由于人数较少,可以在 blueprint 中完成,不需要高度优化,不需要安全性;

Peer - to - Peer :免费的,在 UE5 中不可使用,在 Unity 中不是很安全,每个人都是主机,所以每个人都可以操纵文件然后作弊;

接下来是 Session 和 Replication,Session 是存储在本地网络上的,因此不能直接的进行远程连接,如果是在同一个局域网,Find Session 就能够找到这些会话并加入到这些会话中,要实现远程连接,这意味着必须得公开 IP 地址,路由器信息和端口,以便他人加入,但是很显然这不安全,我们可以使用 Steam 或者 Epic 的 EOS 作为 Online Subsystem ,使用 Online Subsystem,就需要我们使用插件替换掉内置的 Create,Find ,Join,Destroy Session 去使用插件中的函数

Replication 是用于客户端与服务端保持同步的一个东西,其主要通过两种方式进行同步通讯,一:remote procedure calls ;二:replicated variables

现在我们开始设置游戏项目,进入 WB_CreateGame blueprint 项目中,我们给 PlayerAmount,LAN,Create 都创建一个事件

创建事件执行如下

进入游戏中,我们可以发现人物不存在,这是应为 BattleRoyale 并没有绑定 Game Mode,同时此时我们在 BP_PC_MainMenu 中将 Input Mode 设置成为了 UI Only;

我们给 Battle Royale 绑定 BP_GM_Core,然后测试得到效果如下

接下来我们实现 Join Session,先打开 WB_ServerBrowser,在这里首先进行一次修改,添加一个 CheckBox LAN

接着我们打开 WB_ServerBrowser_Item,在 Designer 中创建两个事件,一个是 Event Construct;另一个是 On Clicked(Join_Button),再进入到 Graph 中首先创建一个 Blueprint Session Result 变量命名为 Session,在 Detail 中 打开 Instance Editable 和 Expose on Spawn,然后在 Event Construct 中根据 Session 进行初始化定义,在 On Clicked(Join_Button) 中根据 Session 加入会话,这里我们不需要使用 Open Level ,因为会自动加入到服务端所在的 Level 上

然后我们回到 WB_ServerBrowser 中,也是在 Designer 中继续创建两个事件,一个是 Refresh_Button ,一个是 EnableLAN_CheckBox;EnableLAN_CheckBox 事件只是简单的设置 LAN,而 Refresh_Button 需要先将 Server List 清空,然后循环遍历每一个 Blueprint Session Result 构建 WB_ServerBrowser_Item ,然后将 WB_ServerBrowser_Item 添加到 Server List 中

如果我们设置 Number of Players 为2,同时设置 Net Mode 为 Play As Listen Server,我们还需要设置一个东西

那就是在 BP_PC_MainMenu 中需要设置 Is Local Controller

否则会出现错误

只有 Local Player Controller 可以设置 Widget;

五、Network Errors + Destroying Sessions

当玩家自主退出时,玩家的网络出现故障或者游戏突然退出等其他情况发生时,我们需要处理玩家从游戏中离开这一现象

回到 Content 文件夹中的 Blueprint 文件夹,新创建一个 GameModes 文件夹将 Core 文件夹和 MainMenu 文件夹放入其中,接着创建一个 GameInstance 文件夹,GameInstance 存在于 blueprint 之中,可以认为是the king of all blueprint,我们将在这个文件夹制作的 blueprint 与其他 blueprint 不同的是,在游戏启动后,会一直存在,直到游戏结束;GameMode 类在一个关卡到另一个关卡移动时,GameMode 类会被 destroy 的,GameInstance 是为了解决这一问题而存在的

创建完毕后,我们需要在 Porject Setting 中进行绑定

然后我们进入 BP_GI,这里我们可以在 FUNCTIONS 中添加四个初始事件

Event Init :首先开始玩游戏触发的第一个事件,可以用于初始化游戏时初始化一些保存游戏的数据

Event Shutdown:退出游戏后执行最后一个事件,我们可以对游戏数据进行一定的保存,可以让我下次进入游戏的时候可以直接使用

Event NetworkError:顾名思义指的是网络错误,其中 Failure Type 可以使用 switch on ENetworkFailure 表达

Event TravelError:指的是关卡过度的错误,比如可能断开连接,无法加载地图等等,其中 Failure Type 可以使用 switch on ETravelFailure 表达

在这里只是打印一下错误类型,并不做过多的处理,一般来说这里会将玩家传送到 Bootscreen 达到重新开始游戏的效果

为了实现菜单功能,第一步我们需要添加一个输入,回到 Input 中添加一个 输入,这里命名为 IA_GameMenu,在 IMC_Default 中绑定一个键 T

接下来我们回到 Content 文件夹中的 Widgets 文件夹,在这里创建一个 GameWidgets 文件夹,进入游戏文件夹中,我们创建一个 Widget blueprint 命名为 WB_GameMenu,在 Designer 中设计如下

进入到 BP_PC_Core 中,我们首先创建一个 WB_GameMenu 对象命名为 GameMenu,在这里我们首先需要 Enhanced Input Local Player Subsystem

然后设置 EnhancedInputAction IA_GameMenu,定义事件如下

在 BP_PC_Core 定义好关闭和打开后,我们可以将事件进行封装,以便在 WB_GameMenu 中调用,在这里我们进入到 WB_GameMenu ,,在 Graph 中设置如下

在这里介绍两种使用 函数 的方法,这两种方法结合在一起介绍,首先我们可以在 GameInstance BP_GI 中创建一个自定义事件

然后我们可以在 WB_GameMenu 中获取 GameInstance 然后 cast 到 BP_GI 上

这是一种,但是这样还是很麻烦,需要 cast, 并不美观,为了解决这一问题,我们可以在 Content 文件夹下的 Blueprint 文件夹中创建一个 FunctionLibraray 文件夹,在文件夹中创建一个 Blueprint Function Library 并命名为 BP_FL

进入 BP_FL ,创建 Function,定义函数如下

回到 WB_GameMenu,这样就可以直接使用 LeaveSession

最后我们需要在 BP_GI 这个 GameInstance 中为两个 Error 连接 Leave Session 事件

得到效果如下

六、Creating the Battle Royale Map

这里暂时略过

七、Spawning the players + Match Countdown

前期水材质和出生岛构造略过,这里主要完成 Game State 和子类的完整性

Game State 负责跟踪游戏的状态,它是一个复制类,既存在服务器上又存在于所有客户端上,所以如果 Game State 中有 Replication 变量,那么所有的客户端都可以获取,或者如果我们需要在所有的客户端运行 Replication 事件,我们可以用 Game State 轻松实现这些操作,比如说,某个玩家死亡我们想要一个杀戮提示在所有的客户端显示,这在 Game State 中很容易办到

在 Content 文件夹下的 Blueprint 文件夹下的 Core 文件夹创建一个 Game State (而不是 Game State Base,Game State 适用于多人游戏而 Game State Base 适用于单人游戏),命名为 BP_GS_Core;

由于游戏中会存在多种游戏模式,而我们截止在目前完成的都是基础的 GameModes 类型,因此我们可以将这些类作为父类,通过父类创建子类来适应多种游戏模式,这里我们回到 GameModes 文件夹中,创建一个 GamePlay 文件夹,利用 Core 中的 BP_Core_Character,BP_GM_Core,BP_GS_Core,BP_PC_Core 创建子类放入到 GamePlay 文件夹中;

GameMode 是只存在于服务器上的类,其定义了游戏模式,所以他包含了很多类,这也是它与其他类分开的原因,由于 GameMode 只存在于服务端上,因此不可能从游戏模式复制到客户端,所以只能在 GameMode 中使用服务器端的逻辑;而 GameState 不仅存在于服务器端上,也存在于客户端上,所以这个可以用来编码复制逻辑;

为什么要使用 GameMode,是因为 GameMode 中默认提供了很多封装了的事件,如 Find Player Start ,找到出生点,Choose Player Start ,选择出生点, OnPostLogin,在登入后执行的事件;OnLogout,退出登入后的事件等等的功能;

这里我们进入到 BP_GS_GamePlay 中,定义一个事件 GetTheNumPlayers,该事件很简单,只实现了一个打印目前游戏人数的功能;

然后回到 BP_GM_Gameplay 中,在 OnPostLogin 事件函数中执行该功能

我们可以使用 Interface 接口来优化这一过程,Interface 容易使用,更为友好,其清理掉了很多不必要的蓝图逻辑,如 cast to;

首先,我们在 Content 文件夹中的 Blueprint 文件夹中创建一个 Interfaces 文件夹,在其中我们创建一个

Interface 实际上仅用于与其他类进行交互,因此我们可以简单的利用 Interface 发送消息和数据,或者我们可以使用 Interface 来检索数据

创建完毕后,回到 BP_GS_GamePlay 中实施接口,左侧多了一个 Category:INTERFACES,其中包含了 BPI_GamePlay 中定义的函数

接下来我们打开 Interface BPI_GamePlay,可以发现其函数内部是 READ-ONLY ,是无法修改的,对应上文中“Interface 实际上仅用于与其他类进行交互,因此我们可以简单的利用 Interface 发送消息和数据,或者我们可以使用 Interface 来检索数据” 这句话,这里我们把函数命名为 Notify_PlayerConnection

进入到 BP_GS_GamePlay 中,不要理会 INTERFACES 中的接口函数,直接创建一个 Event Notify Player Connection,然后执行 Get the Num Players 事件

接着在 BP_GM_GamePlay 中,可以直接调用 Notify Player Connection 发送信息,这里的 target 指定调用函数的类,所以这里省去了 cast to 节点,更为方便。

在关卡中设置 GameMode 等相关类,设置完毕后运行可以发现,Print 函数都是在 Server 端调用的

接着我们创建一个 Widget 来显示当前游戏人数和倒计时 UI,进入到 Content 文件夹下的 Widgets 文件夹下的 GameWidgets 文件夹,创建多个 User Interface 分别命名为 WB_GamePlay_HUD,WB_MatchStartCountDown,WB_PlayerCount

首先进入到 BP_GS_GamePlay 中,创建两个变量MiniNumRequiredPlayers,CurrentNumPlayer并设置为 Replication 模式,删除掉 Get the Num Players 函数

然后进入到 WB_PlayerCount 文件中,Designer 设计如下

进入 Graph 中,创建 promote 一个变量 As BP GS Game Play,然后给 Text 进行绑定,这里绑定是按tick更新的,这样性能消耗较大,后期可以使用 DISPATCHERS 进行优化;

在 Text 中创建绑定函数,定义函数如下

这里 WB_PlayerCount 就定义完毕了,进入到 WB_MatchStartCountDown 中,在这里我们不使用 Bind,使用自定义事件加上 RepNotify 实现效果,Designer 设计如下

进入到 Graph 中,自定义事件 Update_Widget

接下来我们回到 Game State BP_GS_GamePlay中,创建一个新的变量 MatchStart_CountDown,并设置其 Replication 类型为 RepNotify

RepNotify 代表通知和复制变量即 Replication 和 Notify ;这个变量基本上每次有更新的时候都会将数据发送到客户端 client,对于一般的 Replication 来说,在 Class Default 中可以看到 Replication 的基础设置,每秒更新10次,而对于 RepNotify 来说,其不仅拥有 Replication 的性质,而且在每一次值发生变化的同时会执行一个函数,函数可以通过双击变量进入,不仅可以运行在服务端也可以运行在客户端

在这里创建一个函数 UpdateMatchStartCountDownWidget 用于更新 Widget

自定义事件 Match_CountDown 定义如下

打开 RepNotify 变量对应的函数,设置如下

这里如果没有 Switch Has Authority,就会出现服务端和客户端倒计时不一致的现象,这是因为服务端和客户端都在执行这个逻辑,就会导致服务端使用服务端的倒计时,客户端使用客户端的倒计时,因此出现不同步的现象,Switch Has Authority 相当于设置了只有服务端可以执行这个逻辑,然后发送数据到客户端,这样就可以实现同步这一现象

目前两个 Widget 都设置完毕,WB_PlayerCount 和 WB_MatchStartCountDown,现在将这两个 WB 都添加到 WB_GamePlay_HUD 中

然后将这个 Widget 在 BP_PC_GamePlay 中添加到窗口

实现效果如下

八、Implementing Steam Advanced Sessions

在这我们将用 steam Online Subsystem 来替换默认 Online Subsystem 中的会话节点,首先找到项目文件夹打开然后关闭我们的项目

这里有两个文件夹可以删掉,DerivedDataCache 和 Intermediate 文件夹,这两个文件夹在每次开始游戏的时候都会创建,在 Saved 文件夹下的 Config 文件夹基本包含了所有编辑器设置,如果需要保留设置就不需要删除,其他文件夹中的数据在开始游戏的时候会自动创建,可以删除;所以最后只剩下如图所示的文件夹目录

首先,市场上有很多的插件可以将整个 Steamworks API 暴露给 Blueprint,也有一个非常著名的UE社区,Advanced Sessions Plugin:Advanced Sessions Plugin - Development / Programming & Scripting - Epic Developer Community Forums (unrealengine.com)

同时还有一个在 UE5 中提供的 Online Subsystem Steam 接口的文档
虚幻引擎Online Subsystem Steam接口 | 虚幻引擎 5.4 文档 | Epic Developer Community (epicgames.com)

这里并没有按照文档利用 Steamworks 进行设置,而是按照视频方法利用 Advanced Sessions Plugin 进行设置,首先打开项目根目录 Config 文件夹下的 DefaultEngine.ini 文件,在该文件后面添加下面字符串

[/Script/Engine.GameEngine]
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")

[OnlineSubsystem]
DefaultPlatformService=Steam

[OnlineSubsystemSteam]
bEnabled=true
SteamDevAppId=480

; If using Sessions
; bInitServerOnClient=true

[/Script/OnlineSubsystemSteam.SteamNetDriver]
NetConnectionClassName="OnlineSubsystemSteam.SteamNetConnection"

如图所示,这里的 SteamDevAppID 表示的是发行后的 App 代码

这里有一些关于 SteamDevAppID 的介绍

当使用 480 ID 的时候,我们必须将游戏打包为 Development mode, 在测试游戏时,玩家们必须来自同一个国家,接下来我们下载插件并在 UE5 中安装;

开发插件发布页面:Advanced Sessions Binaries – VR Expansion Plugin (vreue4.com),直接下载符合 UE 版本,下载完毕后解压可以发现有两个文件夹,这里可以删除 ExampleBlueprint 文件夹,进入 AdvancedSessions 文件夹,将 AdvancedSessions 文件夹中的文件拖入到项目目录中的 Plugins 文件夹中(先创建一个空的 Plugins 文件夹)

然后我们可以打开项目,进入到 Plugins 之中,然后勾选 Advanced Sessions Plugin 中的两个插件

然后我们需要更新 Session ,由于所有的 Session 都是在 Widget 中实现的,所以我们只需要修改 Widget 就够了,打开 WB_CreateGame,更新 On Clicked(Create_Button) 事件

在这里的 Extra Settings 中,我们可以给这个 Session 创建一些属性,方便后面的查询和查找,接下来打开 WB_ServerBrowser ,更新 On Clicked(Refresh_Button) 属性

这里的过滤可以过滤掉 Extra Settings 中设置的游戏属性,同时还可以发现 Find Friend Session 函数

这里的 Destroy Session 不用进行修改,因为 Advanced Sessions Plugins 中没有 Destroy Session 这一函数

接下来我们需要将项目转化为混合项目,即包括 Blueprint 和 C++ 的项目,因此我们需要向项目添加一个 C++ 类,以便使插件能够正常工作

这里创建一个 C++ Class,

这里选择 None 然后 Next

点击 Create Class

点击 OK,这意味这我们可以打开 Visual Studio 并在那里重新构建项目

这里点击拒绝,因为编辑该代码在 Visual Studio 中构建项目的时候,我们需要确保项目已关闭,所以我们需要先保存然后打开项目文件 BeanRoyale.sln 进入 Visual Studio

进入 Visual Code 中,我们可以发现我们需要安装一些组件才行

等待组件安装完毕

安装完毕后重新打开,设置 Editor 为 Development Editor,Win64

然后右键 BeanRoyale build 这个项目,接着等待1分钟左右生成完毕

打开 Steam,回到项目目录中再次打开项目,由于 Steam 只能运行在 Standlone,设置如下

可以发现出现了 Steam 社区

进入游戏后,使用 shift + tab 键,可以看到成功呼出游戏概览页面

到这游戏接入 Steam 就构建完毕了,可以看到 测试的 APPID 表示的游戏是 SpaceWar,在测试游戏时,玩家们必须来自同一个国家

九、Creating the Battle Royale Airplane

在这里由于我的资产没有飞机,只有汽车,这里我使用汽车来代替飞机

首先在 Content 文件夹下的 Blueprints 文件夹下创建一个 Actors 文件夹,在 Actors 文件夹里面创建一个 Blueprint Actor,命名为 BP_Car,要使汽车向前移动有很多办法,比如 Time Line 或者 Event Tick 等,在这里我们使用 Projectile Movement 来使汽车运动,在这里 Velocity 表示的是 Actor 的轴方向速度和方向

在 BP_Car 的时间中,我们可以来检查每一个 Tick 移动的距离,在这里 Switch Has Authority 的作用在于只让服务端进行操作

得到效果如下

就像在现实空间中一样,100 个 UE unit 就是 1 米,一 UE 单位 就是 1 cm,接下来要做的是,在飞行一定距离后,移除掉飞机,避免飞机一直飞行消耗资源,这里只需要添加一个判断就好,假设大于 7000 米,就移除飞机

这里查看一下 Landscape,可以看到其 Scale 放大了 100倍,这样就是 5700 × 5700 5700 \times 5700 5700×5700

接下来要实现的是 飞机从多个角度飞过 Landscape,这同样有很多种实现的方法,但是为了保持良好和模块化的灵活程度,我们希望有一个圆圈确定起始点,圆心就是 Landscape 的中心点;我们可以创建一个 Blueprint Actor BP_CarSpawner 来进行控制,这里先生成一个 SpringArm,然后调整 Target Arm Length 为 3500(因为长和宽为5700)

在 BP_CarSpawner EventGraph 中,定义事件如下,由于只需要在 Server 端进行存储,因此使用 Switch Has Authority 实现这一功能

将 BP_CarSpawner 放置到地图中,使用 simulate 可以直接看 Actor 运动

得到效果如下

接下来我们需要将人物生成在 BP_Car 上,在放置之前,我们可以设置两个 PointLight 点亮小车,然后使用 Box 来划定位置和范围;

实现 Event Graph 如下

实现效果如下

最后我们还需要进行一下修改,这里我们只是对 index 为 0 的 Player 进行生成,并没有对所有的 Player进行生成,同时,为了能够在多人游戏中正常运行,我们同时需要打开 Replication ,在 Class Default 中,所以这里我们可以修改一下 BP_Car 的 Blueprint ,修改如下

修改一下 BP_CarSpawner 的 Blueprint ,修改如下,将 BeginPlay 修改为自定义事件命名为 SpawnCar

设置完毕后,我们进入 BP_GS_GamePlay 这个 GameState 中,续写倒计时结束后的逻辑,在 BP_GS_GamePlay 创建一个函数 SpawnPlayerInCar

将这个函数链接到 RepNotify 变量 MatchStart_CountDown 绑定的函数 OnRep_MatchStart_CountDown 上

这样就设置完毕了

接下来给 BP_Car 设置一个声音

设置完毕后,这里可能还会出现一个问题,那就是 服务端里面的 Car 还未消失而客户端里面的 Car 就已经消失了,这是因为 Replication 中有两个参数,一个是 Always Relevant,开启后会一直 Replication;另一个是 Net Cull Distance Squared,这个值很大的原因是其是平方过后的值,当 Actor 与 Character 的距离超过这个值后就不会 Replication 了

最后还有一个 Static Mesh 开启 Collision 后无效的原因,这是应为没有给 Mesh 开启 Collision 预设,在资产源文件中的 Collision 中设置

十、Creating the Replicated Parachute

这里实现降落伞功能,自动检测角色在天空中是否足够高,如果足够高,我们将打开降落伞

在这里我们对 BP_Core_Character 实现该功能,教程在开始之前相机位置摆放至居中偏右以至可以观察到 Character 人物前方一定角度,具体调整过程如下所示,但是这里不需要。

接下来我们来添加 Static Mesh,命名为 SM_Parachute,添加好 Static Mesh,设置好 Sockets,调整好位置,最后在 Rendering 中设置 Visible 为 False,在 Collision 设置为 NoCollision

这样 Viewport 设置完毕,接下来进入事件图中,,首先在 VARIABLES 中创建一个 变量 WantsToParachute,这个变量用于判断是否打开 Parachute

接着我们创建一个自定义事件,命名为 SR_Set_WantsToParachute,其中 SR 表示的是 Server,将事件的 Replicates 设置为 Run on Server,同时开启 Reliable,这能够确保即使有大量的网络数据通过,这个事件仍然会发生,即使有一点滞后也会被执行

设置为 Replication 否则 客户端的 WantsToParachute 一直为 Fasle,会影响后面的步骤

下一步需要检测地面距离,通过地面距离来判断是否可以开伞,这里自定义一个函数 CanUseParachute

我们可以在 Event Tick 中连接 CanUseParachute,观察得到

这里我们可以设置 1000 为开启降落伞的阈值

由于这只是一个简单的判断,我们不需要执行引脚,所以这里我们可以将这个函数 CanUseParachute 修改为 Pure FUNCTIONS

接下来我们创建一个 RepNotify 变量 IsParachute

设置 IsParachute 是由 CanUseParachute 这个 Pure function 和 Replication 变量 WantsToParachute 决定的,在 IsParachute 内绑定的事件如下

设置从 Current Camera Location 生成 Character

观察得到现象如下,可以看到效果很好

这里其实是可以不使用 RepNotify 来完成的

这里需要保证的是,Wants to Parachute 这个变量是可复制的 Replication,因为服务器只存在于服务器上,客户端不仅存在服务器上,也存在客户端上,这里如果不是可复制的 Replication,那么服务器上的 Wants to Parachute 无法发送给客户端,就会是默认值,因此客户端玩家是看不到服务端玩家开启降落伞的。

最后给开启降落伞时一个浮动速度,问题就解决完毕了

得到效果如下

十一、Setting up Player Health + Healthbar

在之前的设置中,可能会出现一种现象,那就是客户端在超过一定距离后就无法观察到服务端的 Actor

这一现象和 net cull distance 的设置有关,removes irrelevant actors from clients view if client is too far from the actor

因此我们需要对 Actor 的 class default 中的 Replication 进行设置,方法1是一劳永逸开启 Always Relevant,方法2是调大 Net Cull Distance Squared

这里先设置一些 UI,当倒计时结束后,我们需要转化一下 2/2 players,变为 2 survivors;进入到 WB_PlayerCount 的 Graph 中,重新定义函数 Get_PlayerAmount_Text_Text,得到结果如下

接下来我们开始设置 HealthBar,打开 Content 文件夹下的 Widgets 文件夹创建一个 WB_HealthBar 的 Widgets Blueprint

然后我们可以将 WB_HealthBar 放置到 WB_GamePlay_HUD 上

然后我们需要进入 Character 设置生命系统,这里我们进入最初始的 Character BP_Core_Character,这里可以给之前的变量设置一个 Category 方便于查找

然后创建新的变量 Health,MaxHealth;放入名为 Health 的 Category 中,MaxHealth 设置为 Replicated,Health 设置为 RepNotify,并将默认值都设置为100

现在需要将 WB_Health 与 Health 进行绑定, 回到 WB_Health 中,如果使用 Event Construct,其只会在开始的时候运行一次,如果 Widget 在 Character 之后出现那么便无法初始化成功,这里就不使用 Event Construct 的方法实现,直接使用绑定 Bind Character 的 Health;

首先绑定 HealthBar,绑定函数如下

接着绑定 HealthText,绑定函数如下

接下来需要给 WB_HealthBar 赋值,首先进入 BP_PC_GamePlay 将 WB_GamePlayHUD 提升为变量

然后回到 BP_Core_Character 中 Event Graph 中

客户端和服务端都有多个 Character,使用 Is Locally Controlled 可以判断是否是对应 Player Control 控制的那个 Character,Get Controller 相当于 Get Player Controller index 0

最后得到效果如下

十二、Dealing Damage, Death, GameState Score

由于测试伤害具有交互性质,所以不在 BattleRoyale_Level 中测试,而是进入到 Testing_Level 中进行测试;设置了关卡,同时我们需要设置 GameMode,这里的 GameMode类 我们使用的是 Core 文件夹中的 BP_Core_Character,BP_GM_Core,BP_GS_Core,GamePlay 文件夹中的 BP_PC_GamePlay;创建子类并放置到 Testing_Level 中的 Game Mode 中;

首先在 Content 文件夹下的 Blueprint 文件夹下创建一个 Utility 文件夹,这个文件夹用于创建一些 Actor 用于测试一些东西;在 Utility 文件夹中创建一个 BP_DamageActor;

创建一个 On Component Begin Overlap 事件如下

然后回到 BP_Core_Character 中,定义 Event AnyDamage 事件如下

这里由于 Health 是一个 RepNotify 变量,我们可以在其 RepNotify 的 Response 中绑定受伤动画和音效,得到效果

现在死亡效果只是单纯的 Destroy,回到 WB_PlayerCount 这个 Widget Blueprint 中,观察发现,人物数量是使用 BP_GS_GamePlay 中 CurrentNumPlayer 实现的

进入到 BP_GS_GamePlay 中,可以观察得到只有在 Player Connection 时,才会刷新一次玩家数量;

因此最好的方式是,在 Character Destroy 的时候,触发 CurrentNumPlayer 减一

所以在 BP_GS_GamePlay 中,我们自定义一个事件 PlayerDied

回到 BP_Core_Character 中,定义 Event AnyDamage 事件

这里可以通过使用 INTERFACE 让页面变得简洁一些,进入BPI_GamePlay,创建一个新的 FUNCTION Notify_PlayerDeath

BP_GS_GamePlay 之前就绑定过 进入BPI_GamePlay,因此双击 Notify Player Death

回到 BP_Core_Character 中,定义 Event AnyDamage 事件

目前只是 Destroy Character,并没有消除掉 HUD,因此我们可以回到 BP_PC_GamePlay 中,创建一个 Event On UnPossess 事件,该事件当 Player Control 未占有 Pawn 的情况下执行

因为服务端和客户端有许多的 Player Control,但是服务端的 Player Control 只有 Local 具有 HUD,以及 Character,因此需要判断是否为 None 再 Remove from Parent

十三、Creating a Replicated Killfeed Widget

首先进入 Content 文件夹下的 Widgets 文件夹下的 GameWidgets 文件夹,创建一个 KillFeed 文件夹,在该文件夹下创建一个 WB_KillFeedItem;WB_KillFeedItem 的 Designer 如图所示

WB_KillFeedItem 的 Graph 如图所示

接下来创建 WB_KillFeedPanel

为了让 WB_KillFeedPanel 中 WB_KillFeedItem 从上往下显示,这里做了一个简单处理,回到 WB_KillFeedItem 中,将 Transformer 中的 Scale 中的 Y 设置为 -1 实现翻转效果;

回到 WB_KillFeedPanel,做同样的处理,将 Transformer 中的 Scale 中的 Y 设置为 -1 实现翻转效果;

在使用了两个 Scale 之后,可以发现实现了添加元素从上往下的效果,这里我们删去 WB_KillFeedPanel 中的 WB_KillFeedItem,直接将 WB_KillFeedPanel 放置到 WB_GamePlay_HUD 之中

在 WB_KillFeedPanel 的 Graph 中,我们创建一个自定义事件,Add_KillFeed,将 Feed Back Text 作为事件的输入

在使用性能中,使用 RepNotify 要比 多个 Cast to 的性能高得多 ,进入BP_GS_GamePlay 在这里我们使用一个独立的变量 KillFeed ,在 KillFeed 的 Repsonse 函数中调用 Add_KillFeed;

回到 BP_DamageActor 中,Event Instigator 表示的是使用 Actor 攻击的 Player Controller,得到的结果应该是玩家A击败玩家B,而不是刀击败玩家B,这里表达的是这个意思

回到 BP_Core_Character 中,对 Event AnyDamage 实现如下,这里如果连接到了 Steam,Player State ,Get Player Name 显示的便是 Steam 名称 ,如果没有连接那么只会得到 PC 的名称

接下来我们实现字符串添加到 Widgets 这一功能,进入 FunctionLibrary 文件夹,打开BP_FL,创建一个 FUNCTIONS 命名为 Add_KillFeed

接下来只需要进入 BP_GS_GamePlay 中的 RepNotify 变量 KillFeed 的 Response

到这里就设置完毕了,接下来我们需要调整一下 Destroy Character 时去除掉所有的 HUD 改为去除掉 HealthBar

得到效果如下

十四、Setting up Widgets for Packaged Build

上述过程在当我们把游戏打包为独立游戏的时候,客户端上的 HealthBar 初始化不成功这一问题;这里将修复这一问题,并且解释为什么会发生这种情况

在编辑器中测试游戏与在独立中测试游戏时是有区别的,最大的区别在于,当我们查看编辑器在项目中使用的所有的这些核心类时,如 Game Mode,Player Controller,Default Pawn Class等等,这些类在编辑器中的初始化与在包中的初始化不同;

在编辑器中,当使用 Play As Listen Server 时,可以立刻连接起来,一切正常,但是在真正的 Standalone 独立会话中,基本上会有一个 host 一个 Session,然后其他人加入这个 Sesssion;在类生成的时候,Character 会立刻生成,Player Controller 可能还未正确设置好 UI,在编辑器中测试的时候,这种情况不会发生,因为一切在编辑器中都是同时发生的,所以服务端在客户端准备好的时候同时也准备好了;

要解决这个问题一种很简单的方法,那就是使用一个 Delay 搭配多个 Cast To

Delay 不能在 Function 中使用,可以在 MACRO 中使用

十五、Randomly Spawning the Lootcrates

这里的资产中正好有一个盒子和一个盖子,我们需要使用盒子和盖子做成一个 Lootcrate 战利品随机的分布在地图各个地方

像 PUBG 中,在房子里面的物资,是在房子的蓝图中产生了一些 spots 去 spawn 物资,同时可以简单的将战利品从天上掉下来,然后给 Lootcrate 一个 Parachute 降落伞

在这里我们是直接在地面上生成,通过一个天空中的 BOX 生成随机点,然后随机点投射到地面,根据地面的位置和倾斜角生成 Lootcrate

我们首先创建一个 箱子的 Blueprint,命名为 BP_LootCrate,并且打开 Replicates 让客户端也可以发现

然后创建一个 BP_LootBox_Spawner,这里需要的东西很简单,就是一个 取消掉了 Collision 的 Collision Box

然后回到 Event Graph 中,创建一个函数 SpawnLoot,定义如下,这里使用 Random Point in Bounding Box 是不需要使用 Divide 2 的;

该函数实现一个生成 BP_Loocrate 的功能

接着我们回到 Event Graph 中,定义 Event BeginPlay 如下,这里的 Switch Has Authority 可以保证只在服务端生成然后复制到客户端;

在这里如果有水,LootCrate 生成在水面上,那我们需要修改水面的 Collision,因为 Line Track 会忽略掉 NoCollision 物体,或者我们可以根据 Hit Result 中的 Phys Mat 来获得;

最后得到效果如下

十六、Creating the Interaction System

要实现与 Actor 交互,首先是要判定选择的 Actor 对象,在这里有许多种方法,如在 Actor 设置一个 Collision,在 Collision 范围内就相当于选择了 Actor,但是在这里并不行,前面我们使用随机生成 LootCrate,如果 Collision 出现了重叠,那么效果就很差

这里我们可以在屏幕中央使用一个交互的十字线

很多人对 Tick 都是十分害怕和小心的,不需要过度使用它来进行非常复杂的逻辑或者计算,但在这里是一个对底层引擎非常简单的操作。要记住整个游戏都是在 Tick 之中的,Kekdot Center 这个游戏,可以在大厅轻松容纳多达16名玩家,还有很多复制的 Actor,里面有许多的 Tick,帧率能轻松保持到120fps;

这里把 Event Tick 上面的有关 Parachute 的 Blueprint 封装成 FUNCTION Parachute

将 Event BeginPlay 上面的有关 HealthBar 的 Blueprint 封装成 MACROS InitialHealthBar

这里我们创建一个新的函数 FUNCTION InteractionTrace,在这里我们将仅在所属客户端上执行此路线追踪 Line Trace;

在定义该函数之前,我们先在 Blueprint 文件夹下的 Interface 文件夹创建一个 Blueprint Interaction 并命名为 BPI_Interaction ;

在 BPI_Interaction 中定义两个 FUNCTION,一个是 Interact,另一个是 SetFocused,并设置一个 Interaction Category;

创建完毕后,我们需要进入到 BP_Lootcrate 中安装 Blueprint Interface

在 Event Interact 事件中,我们需要检查是否打开,如果没打开那么应该打开;在 Event Set Focused 中,我们需要检测 Character 的 Line Trace ,如果 Line Trace 接触到了 Actor,那么应该显示某种交互 Widget;

那么首先进入到 Widget 文件夹下的 GameWidgets 文件夹创建一个交互的 Widget,命名为 WB_InteractionFeedback

然后创建一个自定义事件 UpdateWidget,该事件简单实现一个修改 ActorName 的功能

回到 BP_LootCrate 中,将 WB_InteractionFeedback 加入到 BP_LootCrate,初始在这里可以在 Render 中将 Visible 设置为 False;

回到 Event Graph 中,我们定义一个 Event Set Focused 事件,如果0.1秒 Line Trace 没有发生变化,那么 Retriggerabel Delay Duration 之后就会取消掉 Widget 的可视性

然后创建一个函数 SetUp_InteractionWidget 执行初始化 Widget 的功能

然后连接函数 SetUp_InteractionWidget 到 Event BeginPlay

接下来回到 BP_Core_Character 之中,首先是 Line Trace By Channel 函数,

这里的 Trace Channel 设置为 Visibility,这意味着任何可见的东西都会被命中并且返回,这不是这里需要的,我们需要特定碰撞通道的项目才会被我们的 Interaction Trace 击中;

这里我们需要回到 Project Setting 中,创建一个 Trace Channels 中 Add Interaction,命名为 Interaction;

这里需要重新启动一下 Project,才会在 Line Trace By Channel 中出现 Interaction 这一选项;

进入到 BP_LootCrate 中,观察所有的 Static Mesh 在 Interaction 中是否都设置为 Block

将拥有 BPI_Interaction 的 Blueprint 提升为变量来保存对其的引用以便于控制按钮能对其进行实际的操作,这里要注意对于没有 BPI_Interaction,我们同样也需要对其进行设置为 None,没有 Hit 目标也需要设置为 None

然后连接到 Event Tick 上

这里我觉得最好是使用 Character 进行射线检测来开启箱子,这样更加符合一般的感觉需求,但是这里可能会出现枪支在地面上的情况,其视角并不是平的,因此还是只能使用 Camera 进行 Line Trace;

截止目前我们选择判定了 Actor ,接下来实现交互,首先配置一个按键,这里使用 E 来实现交互功能

回到 BP_Character 中,本质上我们需要在 服务器 上进行交互,所以我们不能直接调用这个角色上存在的服务器事件,这里通过自定义一个事件并设置 Run On Server 来将客户端逻辑转化为服务端逻辑执行;

这里设置完毕后,进入 BP_LootCrate,Interaction 的定义有两种方法,第一种方法是定义 Custom Event OpenLid 为 Not Replicated 然后搭配 IsOpened 这个 RepNotify 变量,在 OnRep 执行 OpenLid 事件,使用该方法有一个弊端那就是 OpenLid 这个事件中不能放声音,因为玩家如果中途加入,已经打开过的 Lid 会被重新打开一遍,这个时候声音也会重新发出,这种方式的修正方法是新定义一个播放声音的事件,该事件使用 Multicast;Multicast 的执行宗旨是错过了就错过了,即客户端复制服务端的 Blueprint 的时候,不去比较复制的值与初始值的状态变化,相当于将复制的值作为初始值,而用 RepNotify,就会去比较,即如果复制值和初始值的状态不同,那么任然会执行 OnRep 中的事件;

这里如果全部使用 Multicast,IsOpend 可以设置为 Replicate 变量,其只执行一个复制的功能,OpenLid 这个自定义事件定义为 Multicast,开盖结合播放声音在一起,然后要在 BeginPlay 中初始化这个 Lootcrate

得到效果如下,可以看到效果很好,但是如果在开盖时加入,默认进入是开盖状态

这里要注意的是 Multicast 和 RepNotify 的使用,以及服务端是没有 Net Cull Distance 的,而客户端在不开启 Always Relevant 的情况下,其复制更新是只在 Net Cull Distance 的范围内

十七、Spawning the Loot / Pickup Items

在这里我们使用 data 和 data table 来产生我们的 Loot;首先,我们在 Blueprint 文件夹下创建一个 Data 文件夹,进入 Data 文件夹,创建一个 Structure 命名为 S_PickUpItem, Structure 是建立数据表的基础,所以当我们开始定义结构的时候,我们可以定义参数例如对象的名称,对象的 Static Mesh 或者分配给的 ID;

进入 S_PickUpItem 中,添加三个变量,ID,SM_Item,Name 并配置属性;

同时进入 Default Values,配置默认值;

接下来创建一个 Data Table,命名为 DT_PickUpItems;

进入 DT_PickUpItems,配置 DT_PickUpItems;这里的 Row Name 是唯一的 ID,不能重复

这样 DT_PickUpItems 就配置完毕了;现在进入到 Blueprints 文件夹中的 Actors 文件夹中,创建一个 Blueprint Actor 命名为 BP_PickUpItem;

进入 BP_PickUpItem 中,首先添加一个 Static Mesh 和 一个 Capsule Collision,然后进入 Class Default 中打开 Replicates

接着修改 Static Mesh 中 Collision 为 NoCollison;

然后添加一个 Widget,配置为 WB_InteractionFeedback,同时 Space 设置为 Screen, Draw at Desired Size 设置为 True;同时 Visibility 设置为 False

接下来需要设置 Capsule Collision 的 Collision,这里的 Collision 设置为 Custom,这里将 Visibility,Camera,Pawn,PhysicsBody,Vehicle 设置为 Ignore,其他的设置为 Block;

在这里设置就完毕了,接下来我们需要给 Static Mesh 添加一些东西,Event BeginPlay 与 Construction Script 不同的是,Construction Script 编译完成后会直接在编辑器中进行显示,其基本性质类似于在构建的时候就开始执行;

在这里我们读取 DT_PickUpItems,并将 ItemID 设置为 Instance Editable ,方便我们在 Level 中可以直接修改其 Item ID,设置为 Expose On Spawn,可以让在生成这个 BP Actor 的时候,设置 Item ID 的属性;同时 Promote Name to be a Variable

这里如果我们需要其具有物理特性,我们需要开启 Physics 中的 Simulate Physics,以及 Collision 中的 Collision Enabled(Query and Physics);开启 Simulate Physics 之后,如果我们需要对限制旋转,那么我们可以在 Constraints 中 Lock Rotation X 和 Y ;

这里为了保持服务端和客户端中 BP_Actor 的运动状态一致,这里我们需要在 Class Default 中勾选 Replicate Movement 以及 Replicates;

在开启 Replicate Movement 的时候,仅适用于 Root Component,如果在 非 Root Component 上开启 Simulate Physics,而不是 Root Component 上时,非 Root Component 上的运动不会自动复制;

最后再添加一个 RotatingMovement 将 BP_Actor 进行旋转,这里是绕 z 轴选择,因此速度调至 180;

接着我们可以复制 BP_LootCrate 中的函数 SetUP_InteractionWidget,同时可以利用 Blueprint Interface 来创建两个 Interaction 事件 Interact 和 Set Focused;

回到 Event Graph 中连接 SetUP_InteractionWidget 给 Event BeginPlay 来实现这一操作,同时设置 Set Focused 如下

得到效果如下

接下来实现打开 BP_LootCrate Spawn BP_PickUpItem 这一功能,首先添加一个 Box Collision,设置好 Box Extent,并将 Collision 设置为 NoCollision;

定义一个 SpawnItems 函数如下

在这里由于只需要在服务器上生成然后复制到客户端,因此不需在 Multicast 事件中使用,由于 Event Interact 是直接在服务端上运行的,所以可以直接连接在 Event Interact 后面;

这里修改完毕后,我们还需要修改一下 BP_PickUpItem,在这里 ItemID 并没有参与生成事件,同时 Replication 设置为 None,要是服务端和客户端生成的东西相同且一致,我们需要将 ItemID 设置为 Replicated;定义一个事件 SetUp_Item;

修改 Construction Script 如下

回到 Event Graph 中,Name 和 Static Mesh 无需设置为 Replication,因为其是由 ItemID 唯一定义的;

最后得到效果如下

十八、Equipping the Weapons + AO Aim Offset

前期我们完成了生成 PickUp 物品的设置,但没有产生交互,在这里我们完成装备武器这一操作,首先在 Blueprint 文件夹下的 Actor 文件夹创建一个 BP_Core_Weapon;和 BP_PickUpItem 一样,在 BP_Core_Weapon 中首先定义一个 SetUp_Item

接着设置 Static Mesh 的 Collision 为 NoCollision;

将 ItemID 设置为 Replicated 之后分别连接到 Event BeginPlay 和 ConstructionScript 上;然后我们回到 BP_Core_Character 之中,创建一个 Static Mesh 和一个 ChildActor,分别命名为 SM_BackSlot 和 ActiveWeapon;

这里添加完毕后,我们可以为 BP_Core_Character 添加 Aim Offset 动画, Aim Offset 的资产需要设置 Addiitive Settings ,设置 Additive Anim Type 为 Mesh Space,Base Pose Type 设置为 Selected animation frame,Base Pose Animation设置为 居中的 Anim Offset;

这里以基础的 MM_Idle 动画为例子,首先创建一个 AimOffset 文件夹存放 AO 动画资产,进入 MM_Idle 中,提取当前帧动画为 Base Pose,并保存在 AimOffset 文件夹命名为 MM_AO_Base;

然后将 MM_AO_Base 复制一份,命名为 MM_AO_CC,进入 MM_AO_CC 中,配置Addiitive Settings ,设置 Additive Anim Type 为 Mesh Space,Base Pose Type 设置为 Selected animation frame,Base Pose Animation设置为 MM_AO_Base;

回到 MM_AO_Base 中,创建旋转头部 Yaw 从 -60 到 60,Pitch 从 -40 到 40;分别获得 LU,LC,LD,CU,CC,CD,RU,RC,RD;然后依次配置Addiitive Settings ,设置 Additive Anim Type 为 Mesh Space,Base Pose Type 设置为 Selected animation frame,Base Pose Animation设置为 MM_AO_Base;

利用这些 AO 动画资产,创建一个 AimOffset,命名为 AO_MM,将所有的 Pose 移入

回到 ABP_Polygon 中,教程中使用的方法是 The Character 获取 Get Base Aim Rotation 和 Get Actor Rotation 的 Delta 来设置 Pitch 和 Yaw;

然后进入 Locomotion 中的 Idle,设置 Pitch 和 Yaw 给 AO_MM;

这样有一个缺陷,那就是服务端可以正确获取所有客户端的 Pitch 和 Yaw;而客户端只能收到其他端的 Pitch;根据检查这里是因为 Get Base Aim Rotation 的问题;这是因为 Player Controller 不仅存在于本地客户端中,同时存在于服务端中,因此客户端在服务端显示是正确的,而服务端 Get Base Aim Rotation 的 Yaw 的值在客户端中是和 Get Actor Rotation 的 Yaw 值是一致的,因此服务端在客户端中是不会发生 Yaw 的变化,只会发生 Pitch 的变化;

上述效果如下

为了解决这一情况,我们可以将 Yaw 和 Pitch 的值都存储在 BP_Core_Character 中,回到 BP_Core_Character 中,创建一个函数 CalculatePitchYaw ,并设置两个 Replicated 变量 Yaw 和 Pitch;

然后利用 Event Tick 事件去更新 Pitch 和 Yaw,发送到服务端

再回到 ABP_Polygon 中,在这里我们不需要再设置 Yaw 和 Pitch 变量,直接从 The Character 中可以取得;

得到效果如下

可以发现正常了,该方法使用的 BP_Core_Character 事件 Event Tick ,和 ABP_Polygon 事件 Event Blueprint Update Animation 性能一致

接下来我们回到 BP_Core_Character 中,清空 SM_BackSlot 的 Static Mesh,回到 BP_Core_Weapon 中,清除掉 Construction Script,即构建函数;同时设置 Replicates 和 Always Relevant;然后创建一些变量用于存储武器信息,如 Ammo,MaxAmmo,ReloadDuration,ShootingSoundEffect,ReloadSoundEffect;

在一款优秀的游戏中,通常具有很多的参数,如 Bullet Spread, fire rate,firing mode,impact effect等等,可以根据自己的喜好进行设置;

在 Blueprint 文件夹下的 Actors 文件夹创建一个 Weapons 文件夹用来存放武器,将 BP_Core_Weapon 移入 Weapons 文件夹中,然后创建一个 BP_Core_Weapon 的子类,命名为 BP_AssaultRifle;

我们需要添加两个按键来实现攻击和装弹的功能,这里设置 鼠标左键是攻击,R 键是装弹;

回到 BP_Core_Character 中,使用 EVENT DISPATCHERS 来定义两个输入按键;

进入到 BP_Core_Weapon 中,创建一个自定义事件 SetUp,使用 Bind 绑定 BP_Core_Character 的EVENT DISPATCHERS 事件;

进入到 BP_Core_Weapon 的子类 BP_AssaultRifle 中,重写事件 Attack 和 Reload

这里我们需要给 BP_Core_Weapon 设置 BP_Core_Character 的 REF,在执行之前,我们先将 Input Mapping 的 Blueprint 转化为函数 Initial Input Mapping;

然后由于 BP_Core_Character BeginPlay 要早于的 Active Weapon 的 Child Actor 的初始化事件,因此需要设置一个 Delay,创建一个 Event 命名为 Initial Weapon 对 BP_Core_Weapon 进行 Character REF 初始化以及 SetUp 事件的调用;

得到效果如下

这里要注意:DEBUG 首先判断是否 Valid ,然后判断是否 DELAY

这里我们要实现捡枪装备这一功能,首先我们进入到 BPI_Interaction 之中,将 Interact 创建一个输入,由于这里只有 BP_Core_Character 才可以交互,这里直接添加一个 BP_Core_Character Object 命名为 Charactrer;

变量的 Replicate 只能处理变量自身的 Blueprint 中的东西,如在 BP_B 中使用 replicate 去复制 BP_A 中的 actor 是不可行的;我们要实现捡起武器的操作,这必然会 Replicate Character 中的 ActiveWeapon,因此捡起武器的操作应该在 BP_Core_Character 中运行,为此,这里创建一个新的 Blueprint Interface,命名为 BPI_Tools;

进入到 BP_PickUpItem 中,定义 BPI_Interaction 事件 Event Interact;

进入到 BP_Core_Character,定义 BPI_Tools 事件 Handle Pick Up,创建一个 RepNotify 变量以便复制这部分的 Blueprint,命名为 CurrentItemID;

在处理之前,我们需要在 Data Structure S_PickUpItem 中创建一个 Blueprint 绑定武器蓝图类: BP_Core_Weapon,注意这里是类,ChildActor 中只能 Set Child Class,比如与上文中的 BP_Core_Weapon 的子类 BP_AssaultRifle

设置完毕后进入 DT_PickUpItems 中绑定对应的武器蓝图,为什么要弄武器蓝图呢,因为不同武器可以有不同的特性,如特效,属性等等,如果只是简单使用一个蓝图来操作,不太现实;

处理 Data Table 完毕后,我们使用 Get Data Table Row 可以轻松获取到武器蓝图,我们进入到 BP_Core_Character,在 CurrentItemID 的 OnRep_CurrentItemID 中,定义如下

大功告成,得到效果如下

十九、Switching Between Weapon Slots

在上一步中,我们只能捡一把武器,在这里我们需要完成三个任务,一个是捡武器,一个是交换武器,一个是丢武器;

首先添加三个 INPUT 按键,分别是 1,2,G;命名为 Slot 1,Slot 2,DropItem;在 Input Mapping Content 中如下:

进入到 BP_Character 中,添加 Action Input 事件,同时删除掉原来的 Event Handle Pick Up 事件和 CurrentItemID 变量,这里逻辑需要重新定义,创建三个变量 ItemList:RepNotify,ActiveSlot:RepNotify,CurrentItemID:None(实际上这里只有两种,CurrentItemID 纯粹是为了避免线乱连创建的一个变量)

两个 Action Input Slot 事件用于切换 ActiveSlot

Action Input DropItem 事件用于 丢弃 Item

创建一个函数 Update_Weapons,定义如下,用于根据 ItemList 和 ActiveSlot 来更新武器 Weapons;

在两个 RepNotify 变量中,引用 Update_Weapons;

切换武器完成,效果如下

这里最好优化一下,每次调用都会新产生一个 BP_Core_Weapon ,对性能有影响

二十、Setting up the Shooting Logic

在这里设置开枪的逻辑,但是由于并没有握枪的动画,简单更新了一些 BP_Wweapon 和 Data 文件夹中的数据,然后把枪放在外面,和教程中的 Bean 一样,虽然不美观但是也是无奈之举;

由于不同枪的性质,有点发和连发,要实现连发就必须要监听键盘 按下 和 抬起 的动作,为此首先进入到 BP_Core_Character 中,在这里新定义一个 EVENT DISPATHERS 命名为 OnIA_Attack_Release,并将之前的 OnIA_Attack 修改为 OnIA_Attack_Press

DISPATCHERS 定义完毕后,我们需要进入到 BP_Core_Weapon 中进行绑定 BIND;

接下来创建一个 Run on Server 的事件,在这里设置了一个事件计时器,在该计时器中,我们需要注意的是如果不设置 Initial Start Delay,即默认为 0 的情况下,是需要等待 Time 参数上设置的时间才开始执行 Event 中的事件,因此我们需要补偿 Time 参数上的时间,所以直接乘一个相反数就好了;

接下来创建一个 MACROS 命名为 CanShoot?在这里我们只考虑两个条件,一个是是否有子弹,另一个是是否在换弹,在实际情况中,这里要考虑的东西有很多,如是否在受伤是否在游泳等情况;

在 Event Timer 的 Shooting 事件中,我们使用 Can Shoot?来判断是否可以开枪,接着创建一个 Multicast 事件,该事件用于播放声音和动画,Shooting 事件定义和 MC_Shoot 声音添加定义如下

在 MC_Shoot 中可以给枪支添加一些动画,如后坐力和弹道上偏, 在这里我们首先创建一个 Time Line 命名为 TL_AnimateWeapon,然后设置两个 Track,分别命名为 Recol:控制枪支旋转,Location:控制枪支后移;

定义 MC_Shoot 如下

在 Polygon 人物中基本上是不会使用 Time Line 做动画的,一般人物的手臂基本上有相应的动画处理后坐力以及枪支旋转;

接下来我们需要 Shooting line traces,在这里为什么要使用 Line Trace 而不是 Projectile,首先 Line Trace 要不 Projectile 在游戏中的优化要多得多;Line Trace 只是一条线,可以在服务器上运行然后检测击中的内容,然后我们可以对其击中的任何内容施加伤害,而 Projectile 内部具有一个物理对象,物理对象具有速度然后使用碰撞来判别是否接触;

大多数枪支,比如突击枪手枪霰弹枪等等所有这些枪基本上都是沿着线轨迹运行的,除了 RPG 或者狙击枪之类的东西;如果全部都使用 Projectile ,那么 Projectile 的数量就会急剧增加,可能会导致客户端更新不及时出现滞后进而影响游戏性能;特别是对于多人游戏而言;

在这里我们使用 Line Trace 来完成射击,首先定义一个函数 命名为 LineTrace_Shoot,定义如下,在这里我们需要获取到 Spring Arm 的距离用于补偿 ShootRange

同时,我们希望给撞击的位置添加一个粒子特效;在这里我们进入到 Blueprints 文件夹中创建一个 Nagera 文件夹,进入 Niagara 文件夹创建一个 Niagara System 命名为 NS_Impact;

由于粒子特效不仅服务端要看见同时客户端也要看见,因此需要单独创建一个 Multicast 事件来执行;这里创建一个 Multicast 事件命名为 MC_SpawnImpact,定义如下

接下来在 LineTrace_Shoot 中调用,同时调用 Apply Damage ,这里要注意的是,这里可以根据 Hit Result 中的 Phys Mat 来控制 NS_Impact 的生成,例如击中人可以变成红色,击中石头变成白色,等等;

接下来实现 Reload 功能,首先创建一个 Run on Server 事件命名为 SR_Reload;创建一个 Multicast 事件命名为 MC_Reload;在开始定义事件之前,和 Attack 部分一样,我们需要定义一个 MACROS 来判断是否可以 Reload;这里我们定义一个 MACROS 命名为 CanReload?

接下来定义 MC_Reload,在这里我们需要设置一个新的 Time Line 命名为 TL_Reload,TL_Reload 设置如下;

TL_Reload 可以在 Variable 中调用,通过调用 Time Line 的 Set Play Rate 函数设置其播放速率;

回到 SR_Reload 的定义之中,在这里我们创建一个 Delay,然后由于 TL_Reload 中时间为 1秒,因此延迟时间为 1 除以 Reload Rate,定义如下

最后得到效果如下

二十一、Simplifying setups + Fixing some bugs

在这里我们简化一下 BP_Core_Character 和 BP_Core_Weapon 的设置,并修复 BP_Core_Weapon 开枪的 bug

函数 Update_Weapons 的 BUG,如果 Item 相同,没办法拾取(只是简单的把地图上的 Item 销毁了又重新生成);

在这里我们直接把红框中的函数删除是解决不了问题的,会出现一个 BUG,那就是客户端的 Character 在服务端中的显示正常,但是在其本身的客户端显示不正常,这是因为相同的武器会导致 ItemList 相同,由于拾取这一事件是在服务端进行计算的,这让服务端进行更新执行 RepNotify 事件,更新完毕后检查客户端中的 ItemList 是否与跟新后的相同再决定是否执行 RepNotify 事件,很明显是相同的,因此客户端不进行更新;这也就导致了这个 BUG 的出现,解决方式我在这里是通过重新赋值 Active Slot 来进行的;

得到最终结果如下

这个 BUG 搞了我一天的时间,巨痛苦;

接下来解决的是切换武器问题,我们需要限制玩家武器不能换过去又立马换回来,实际处理过程中我们可能会增加一些换枪的动画,这样就要注意增加一个状态变量,在换枪的时候不能开枪;在这里只是简单设置一个 Delay;

接着是 EVENT DISPATCHERS 的优化,我们可以使用 Blueprint Interface 来进行优化使其更加的简单;这里进入到我们的接口文件夹,在 Blueprints 文件夹下的 Interfaces 文件夹中创建一个新的 Blueprint Interface 命名为 BPI_Inputs;

然后进入到 BP_Core_Weapon 中,我们需要添加 BPI_Inputs 进入 BP_Core_Weapon,然后定义 Interface 来替换 BIND;

进入到 BP_Core_Character 中,我们可以删除掉所有的 EVENT DISPATCHERS,使用 BPI_Inputs 来调用事件;

进一步,在 BP_Core_Weapon 中的接触绑定后剩下的 SetUp 事件中,其绑定的函数 SetUp_Item 完成的任务就是设置了一个 Static Mesh;

而由于这是初始设置,最好放入到 ConstructionScript 中;所以我们可以删除掉 SetUp 事件 和 SetUp_Item 函数,利用创建的 Static Mesh 变量来定义 ConstructionScript;

然后分别定义 武器蓝图

在这里似乎在 DataTable 中定义然后将所有的变量设置到 BP_Core_Weapon 中处理好像更好一些,特效啊什么的可以留给蓝图来处理,最后得到的 BP_Core_Weapon 很简洁;

二十二、Creating the Health Packs

首先回复包和武器本质上是不一样的,最明显的是回复包没有换弹按键,所以其逻辑和武器是不一样的;因此在这里我们首先修改 BP_Core_Character 的事件和函数名称,将 Initial_Weapon 修改为 Initial_Tool,将 Update_Weapons 修改为 Update_Tools;

然后回到 Initial_Tool 之中,可以观察到 Cast To BP_Core_Weapon 是肯定需要修改的,因为回复包不可能是 BP_Core_Weapon 及其子类;

在这里我们使用接口的方式进行操作,进入到之前创建的 BPI_Tools 中,添加一个新的 FUNCTION 命名为 SetUp_Actor,Category 使用 | 可以创建子目录;

回到 BP_Core_Character 中,我们可以修改 Initial_Tool 事件,将所有的定义丢给 ChildActor;

回到 BP_Core_Character 中,我们可以 Add Interface BPI_Tools,然后定义事件 Set Up Actor

测试得到功能一切正常;接下来可以创建回复包之类的东西;和 BP_Core_Weapon 一样,我们这里创建一个 BP_Core_HealthPack,添加静态网格体,设置为 NoCollision;

进入 BP_Core_Character 中,由于生命包使用一次要丢掉,这里我们重新写一遍 Character 丢掉武器的逻辑,添加一个 Branch 来确认是否摧毁丢掉的武器;

回到 BP_Core_HealthPack 中,定义 Attack 按键然后开启 Replicates 和 Always Relevant;

处理完毕后,创建 BP_Core_HealthPack 的子类命名为 BP_HealthPACK,这里 static mesh 就用斧头代替;

在 Data Table 中添加 BP_HealthPACK,在这里我们需要首先回到 S_PickUpItem 中将 Blueprint 定义为 Actor Class;

处理完毕后,我们进入 DT_PickUpItems 中定义 Sword 为 HealthPack,这里就不修改 Static Mesh 了;

在这里可能会出现 Slot 位置出现不一致的现象,这里我们有两种方式处理,第一种是视频中一样的,我们在 Data Table 中定义一个 Transformer,然后利用该 Transformer 在设置 Static Mesh 的时候去调整 Slot 的位置;第二种是修改 Pivot ;前者难于设置,后者难于调整,而且第二种实用性不好,因为武器放后背上的位置和拿在武器上的位置是不一致的;

处理完毕后打开游戏,测试效果如下

二十三、Determining the Winner (Match Result)

在击败敌人就剩一个人的时候,我们需要决定胜利者,接下来实现这一过程;

首先我们创建一个 Widget Blueprint 来显示胜利者名称;在 Widgets 文件夹下的 GameWidgets 文件夹下创建一个 Winner 文件夹;在 Winner 文件夹中创建一个 Widget Blueprint 命名为 WB_MatchResult,WB_MatchResult 的设计如下,动画设置 Canvas 的 Render 从 0 到 1;

在 Graph 中设置 Event Construct 播放动画,然后绑定 Click 事件为 Leave Session 函数,这个函数是在 BP_FL 中定义的函数;

进入到 BP_GS_GamePlay 中,在这里我们可以发现 CurrentNumPlayer 绑定的是 Player Controllers, 在游戏中击败了对方,摧毁的不是 Player Controllers,而是 Character;这就导致我们需要重新定义一个东西去获取当前的 Character 数量,同时由于当前 GamePlay Mode 绑定的是 BP_GamePlay_Character,所以这里我们需要获取 BP_GamePlay_Character 存在的数量;

而 BP_GamePlay_Character 是数量最好在什么时候检查才好呢,当然是人物死亡的时候,因此我们回到 BP_Core_Character 的 Damage 系统中,可以发现是先通知然后在摧毁,但是这里并不能改为先摧毁后通知,因为摧毁后 Player State 就没了,这就导致 Notify Player Death 中 Player B 为 None;这里有两种处理方式,第一种是不管他,在后续检查数量的时候添加一个延迟;第二种是修改 Notify Player Death,把自身添加进去然后在 GameState 中进行删除,这里我们采取第二种方式;

进入到 BPI_GamePlay 中,修改函数 Notify_PlayerDeath ,在 Inputs 中添加一个 Dying Actor;

然后创建一个新的函数命名为 SetUp_WinnerWidget,其 Inputs 中只有一个 Name 类型的变量命名为 WinnerName;

完毕后,进入到 BP_Core_Character 之中,删除掉 Destroy Actor,然后定义 Dying Actor 为其本身;

进入到 BP_GS_GamePlay 中,创建一个 MACROS 命名为 CheckIsGameOver? 定义如下,其就是实现一个判断 Character 的数量功能;

然后重新定义事件 Notify_PlayerDeath 如下

进入到 RepNotify 变量 Winner 的 Rep 函数之中,定义如下

接着我们需要定义 Winner Name 在 WB_MatchResult 之中,在这里我们可以通过勾选 Instance Editable 和 Expose on Spawn 的方式来在生成 WB_MatchResult 的时候定义 Winner Name;

然后进入到 BP_PC_GamePlay ,在这里定义 Widget,如下

得到效果如下:

二十四、Making the Battle Royale Zone

在这里我们来创建一个毒圈,随着时间圈变小,在圈外要持续收到伤害效果;

首先我们创建一个 Widget 来显示还有多久刷新毒圈,我们在 Content 文件夹下的 Widgets 文件夹下的 GameWidgets 文件夹下创建一个 Zone 文件夹,在该文件夹下创建一个 Widget Blueprint 命名为 WB_ZoneCountdown;其 Graph 定义如下:

随后将其绑定到 WB_GamePlay_HUD 中;

接着我们创建一个 Zone, 由于没有 Static Mesh,这里直接使用 Collision 表示,进入 Blueprints 文件夹中的 Actors 文件夹中创建一个 Zones 文件夹,在该文件夹中创建一个 Blueprint Actor 命名为 BP_Zone,在其中我们创建一个 Box Coliision,并设置 Collision Presets 为 Ignore 除了 Pawn 设置为 Overlap;

在这里根据 SqrtShrinkPercent 简单计算一下刷新点的位置和大小,定义函数 SetTargetLocationAndScale,在这里 CurrentLocation, CurrentScale 都是普通变量;

在这里定义两个自定义事件,命名为 Shrink 和 SR_Shrink,其中 Shrink 是普通事件,而 SR_Shrink 是 Run on Server 事件;其实这里 Switch Has Authority 可以删除,将其移动到 SR_Shrink 中是一样的效果;

在这里 Shrink 和 SR_Shrink 定义如下,其中定义了一个刷新时间 Delta Time,表示间隔刷新,这里 FInterp To 中的 Interp Speed 为 0 表示接着跳转到 Target,为 1 也是这样,这里设置为 0.1 表示每秒取 1/10;而 Delta Time 表示间隔时间,所以每次更新为 ( T a r g e t − C u r r e n t ) × D e l t a T i m e × I n t e r p S p e e d (Target - Current) \times Delta Time \times Interp Speed (TargetCurrent)×DeltaTime×InterpSpeed;在这里设置 Delta Time 为 0.01;

接下来创建一个函数来处理 Overlap 逻辑,在这里没有使用 SR 而是使用 Switch Has Authority 在服务端设置 BP_GamePlay_Character 的变量 OutsideZone? ,由于该变量是一个 Replicate 变量,因此,客户端会复制服务端,建立函数 Handle Zone Logit 如下:

定义 Overlap 逻辑,这里要切记 Box Coliision 的 Collision Presets 所有的 Object 为 Ignore 除了 Pawn 设置为 Overlap;

进一步,在 ConstructionScript 中定义 Box 的 Location 和 Scale;

接下来我们需要进入到 BP_Core_Character 中设置,首先为 BP_Core_Character 添加一个 PostProces,

这个是个全局处理的东西,只要有客户端中有一个,那么整个被处理,这里我们调整其颜色为蓝色,接着设置 OutsideZone? 为 RepNotify 变量;然后定义两个事件,一个 Handle Outside,一个 OutsideDamage;很明显 PostProcess 不可能具有复制属性,所以 Sequence 0 添加一个 Is Locally Controlled 判断,检测触发 OutsideZone 是否是本地客户端控制的 Character,如果不是那么就不添加;Sequence 1 为添加伤害,Apply Damage 只能在服务端上运行,所以这里使用了 Switch Has Authority 提升到服务端;

接下来我们需要进入到 GameState 中去控制 ZoneCountdown ;首先修复一个 BUG,这里我们断开 Player A 到 Set Winner 的链接,因为 Player A 可能是 BP_Zone,而 Winner 必须是一个 Character,这里修改一下 CheckIsGameOver? ;

然后重新定义一下事件 Event Notify Player Death,在这里直接把 CheckIsGameOver? 的 Winner 设置为变量 Winner;

接下来要处理的就是 ZoneCountDown 问题,和 CountDown_Match 一致,首先创建一个 RepNotify 变量,命名为 Zone_Countdown,默认设置为 -1,然后创建一个事件命名为 Countdown_Zone,定义如下:

定义完毕后,我们进入到 RepNotify 变量 MatchStart_CountDown 中的 OnRep_MatchStart_CountDown 函数,在这里创建一个 TimeBetweenZones 的普通变量,用于设置 Zone 缩小的时间间隔;

然后定义 Zone_Countdown 的 Rep 事件如下

这样就处理完毕了,接下来为 WB_ZoneCountdown 连接 GameState 的 Zone_Countdown,在这里创建一个 Bind 函数;

Bind函数定义如下:

这样就处理完毕了,接下来看效果

二十五、Creating the Character Customization

在这里我们创建一个角色 Character 自定义系统,并保存本地下次开启游戏时也可以直接运行保存的 Character;

首先我们进入到 Data 文件夹中,创建两个文件夹,一个命名为 CharacterCustomization,一个命名为 PickUpItems,然后把之前处理 PickUpItem 的文件移入到 PickUpItems 文件夹中;进入 CharacterCustomization 文件夹,创建一个 Structure 的 Blueprint,命名为 S_AppearanceItem;

和 S_PickUpItem 一样,定义 S_AppearanceItem 如下

接着创建 Data Table,命名为 DT_AppearanceItems;

进入 DT_AppearanceItems 中,定义如下:

这里可以将这些 Mesh 的静态图像加入到 Widget 中进行选择,但是这里并没有图像,因此这里只使用 Static Mesh 进行处理;

接下来我们进入到 Widgets 文件夹下的 MainMenu 文件夹,在 MainMenu 文件夹下创建一个新的文件夹命名为 Appearances;在该文件夹中创建两个 Widget Blueprint 文件,命名为:WB_Appearance 和 WB_AppearanceItem;首先设置 WB_AppearanceItem 的 Designer 为

设置 WB_AppearanceItem 的 Graph ,然后将 ItemID 设置为 Instance Editable 和 Expose an Spawn 用于自定义生成 Widget;

然后设置 WB_Appearance 的 Graph 为

在 Designer 中创建一个 DISPATCHERS,配置到 Back 的按钮上用于 MainMenu 中使用;

这里设置完毕了,接下来回到 WB_MainMenu 中添加一个 Appearance 按钮用于设置 Appearance;然后将 WB_Appearance 添加到 WidgetSwitcher 中;

进入到 Graph 中,为 Appearacne 按钮添加 index 切换 WidgetSwitcher,同时给 WB_Appearance 中的 Back 绑定离开事件;

这里 Widget 设置完毕了;现在需要保存游戏数据,在 Blueprint 文件夹下创建一个新的文件夹命名为 SaveGame,进入文件夹中创建一个 SaveGame Blueprint 并命名为 BSG_Appearance;

AppearanceItem 都是 structure 中的数据,为了使用 structure 中的数据我们需要利用到 ItemID,所以我们只需要保存 ItemID;因此我们只需要创建一个变量用来保存就好;

在这里我们就设置完毕了,接下来我们进入到 Blueprint 文件夹下的 FunctionLibraray 文件夹下,进入 BP_FL,用于保存和加载我们的外观

首先创建一个保存外观的函数,命名为 Save_Appearancce 定义如下

然后创建一个加载外观的函数,命名为 Load_Appearance 定义如下,同时设置为 Pure 无需要链接主线段;

回到 WB_AppearanceItem 之中,在 Event Construct 之中,判断 ItemID 和 Load Appearane 的 ID 是否一致,如果一致就使用 Set_Selected;在 On Clicked 事件中,使用 Save Appearance 绑定 ItemID;

Widget 部分到此设置完毕,接下来我们可以进一步处理 Character ,进入 BP_Core_Character 之中,首先创建一个 Hat 的 Static Mesh 然后使用 socket 去绑定位置,在调整好位置后,记得 clear;

接下来定义一个 MACROS 命名为 InitialAppearance 用于初始化 Appearance,在初始化 Appearance 之前,创建一个 Run on Server 事件命名为 SR_UpdateAppearance,在事件中创建一个 RepNotify 变量命名为 Row Name,定义如下

Row Name 的绑定事件如下

InitialAppearance 定义如下

然后加入到 BeginPlay 中

最后得到效果如下:

附录

Switch Has Authority 和 Run on Server 是不一样的,Switch Has Authority 是处理服务端上的???

有两种方式实现服务端和客户端同步效果,第一个先设置好客户端,然后 run on server 事件或者 Switch Has Authority 发送给服务端;第二个,创建 repnotify 变量,使用run on server 和 replicate 进行复制;

Multicast 主要用于声音以及影响的同步效果,执行宗旨是不管未来和过去,只看现在是否执行;

联机注意事项

  1. no 360
  2. no ladder
  3. same lan
  • 11
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值