返回随笔游戏开发
旧稿归档ENGINEERING NOTE

UE4学习笔记

以第一人称射击原型为线索,记录材质、碰撞、移动、HUD、射击系统、胜利条件与敌人 AI 的实现过程。

Unreal EngineBlueprint

从零开始的FPS

一、创建材质 & 为Actor附加材质

创建 材质 蓝图后,查找

VectorParameter //设置颜色
ScalarParameter //设置高光度与粗糙度 (推荐默认值0.1)

二、在发生碰撞时改变Actor的材质

创建Actor的蓝图,打开事件图表。

查找 事件命中 以及设置材质

连接二者,编译运行,此Actor在任何目标碰撞时都会改变颜色。

优化1:仅响应子弹(FirstPersonProjectile)的碰撞

查找 类型转换为FirstPersonProjectile,连接事件命中的 Other 输出引脚和其Object 输入引脚。

优化2:两种颜色来回切换

查找Flip Flop,连接A和B引脚至两个设置材质

优化3:在击中两次后销毁

创建bool类型变量(命名为isHited,默认值false)用于储存 是否击中过 这一信息。

查找分支,False引脚后设置isHited为真(表示击中过),连接设置材质;True引脚连接销毁Actor。

优化3.1:在击中后添加音效和特效

依次搜索 在位置处生成音效和在位置处生成发射器,将Location输入引脚依次和事件命中的Hit Location输出引脚相连(生成位置为碰撞处)。

优化3.2:为摧毁的Actor计数

创建int类型变量hitCount,默认值0,用于储存 击中次数。

在销毁Actor后连接设置Hit Count,将变量拖到蓝图中,选择获取变量,连接整数+整数并将值设置为1,在连接设置Hit Count与整数+整数。

二、为Actor添加固定移动属性

创建flout变量(Speed,默认值200),用于储存速度;

创建vector变量(Location,默认值0,10,0),用于储存移动方向,数值的大小也部分反映了速度。

1.使速度与时间相关

获取Speed和获取场景差量(秒)相乘。

获取场景差量(秒):返回两帧之间间隔秒数

2.规格化方向向量

获取Location与规格化相乘。

3.获取Actor当前位置信息

获取差量变换连接拆分变换,返回三个值,分别为Location,Ratation和Scale。

获取差量变换:貌似就是获取位置信息。

拆分变换:变换类型包含了三个数据,用这个函数将三个数据拆开。

4.组合以上数据

规格化与速度后浮点*浮点相乘,这一步得出了Actor运动的方向和速度。

拆分变换Location引脚和上面向量*浮点相加,得到实际物体的位置信息。

查找创建变换,将Location引脚与刚才的值相连,剩下两个引脚连接拆分变换的对应引脚。这一步创建了一个新的变换,需要将这个新的变化赋值给原Actor。

查找设置场景变换,将创建变换的返回引脚连接New Transform输入引脚。

查找事件Tick,连接至设置场景变换,使得运动事件每Tick执行一次。

优化1:在两个方向上交替运动

查找延迟,连接至设置场景变换后,此项决定了每多少秒改变移动路径。

后将Flip Flop分别连接两个设置Location上,设置移动方向;其本身输入引脚与延迟相连。

三、疾跑,体力值,在疾跑时消耗体力

1.疾跑

转到编辑-项目设置-输入,添加操作映射Sprint,绑定键位左Shift。

打开FirstPersonCharacter蓝图

查找Sprint //即自定义的输入事件名,从组件列表中拖出角色移动,从角色移动输出引脚拖出并查找设置Max Walk Speed,Pressed,Released引脚和两者输入引脚分别相连。

查找设置Max Walk Speed时必须从角色移动输出引脚拖出,否则找不到。

Pressed决定按下时执行的操作,Released决定松开时的操作。

2.体力值,在疾跑时消耗体力

创建flout变量(stamina,默认值为1.0)

默认值为1.0而不是100的原因是UHD的进度条满值即为1.0

之后需要用到自定义函数。

查找自定义事件,输入函数名:Sprint,连接输出引脚至设置stamina,设置stamina - 0.1

在输入操作Sprint后查找以函数名设置定时器,输入Function Name为Sprint,Time设置合理的值,勾选Looping。

Function:执行的函数名

Time:每Time秒执行一次

Looping:如为false,则只执行一次;如为true,则每Time秒循环执行

在输入操作Sprint的Release输出引脚后查找以函数名清除定时器,输入Function Name为Sprint。这一步使得松开Shift后体力值不会一直消耗。

此时已经可以做到在疾跑时消耗体力,但体力消耗完后冲刺没有停止。

优化1:体力消耗完后停止冲刺 & 没有体力时停用冲刺

创建float变量(SprnitCost,默认值为0.1),用来储存冲刺消耗的体力/每Time秒。

使用stamina - SprintCost >= 0的分支语句来判断是否可以进行冲刺,若为True,将stamina - SprintCost和0的最大值赋予stamina(防止体力值为负);若为false,设置行走速度为600(重置),并以函数名清除定时器。

碰撞对象筛选

优化2:在不冲刺时恢复体力

依旧是自定义事件,这次是Recover。

用一个分支语句判断stamina是否大于最大值1.0,只有当不大于1.0时才会执行下面的恢复操作。

对于输入操作Sprint,Pressed引脚后依次以函数名清除Recover 以函数名设置Sprint;Released引脚后依次以函数名清除Sprint 以函数名设置Recover。

碰撞材质切换

四、创建UHD界面

创建UI文件夹,在其中新建用户界面,命名为UHD。

1.向画布面板中添加组件

1.1.生命,体力

一共需要一个水平框和两个垂直框,将它们组合,在拖入文本和进度条。文本和进度条均设置为填充,此项可以使其在垂直方向上均匀分布。

1.2.子弹,摧毁数量

如果这都不会建议重开。

2.绑定组件和实际数值

2.1.体力,生命的绑定

创建绑定后,事件图表中会有两个节点:Get Percent 0和返回节点。

这两个节点的作用分别为:事件开始,事件结束(或许还有其他作用)。

从Get Percent 0引出节点类型转换为FirstPersonCharacter,查找获取玩家角色,将返回值与Object输入引脚相连。

在类型转换为FirstPersonCharacter的As First Person Character的输出引脚查找获取stamina,即之前创建的体力值变量,将返回值与返回节点的Return Value相连。

类型转换为FirstPersonCharacter和获取玩家角色的组合才能真正获取玩家蓝图里变量,函数等信息。

As First Person Character输出引脚后即可获取该蓝图中的信息。

生命值同理。

2.2.子弹,摧毁数量

这两个同理,只是要加上整数转文本。

别忘了在FirstPersonCharacter蓝图里面加这两个变量。

3.将UI界面显示于抬头显示

在第一人称蓝图中找到事件开始运行,查找创建控件,设置class为UHD;查找添加到视口,依次连接输入输出引脚,并注意连接创建UHD控件Return Value引脚和添加到视口目标引脚。

五、优化射击和弹药系统

1.在开火时消耗弹药 & 在弹药为零时不允许射击

找到输入操作Fire,获取Ammo,Ammo - 1,设置Ammo,一气呵成。

在设置Ammo前面用分支判断一下就能完成第二点。

2.换弹

添加输入操作Reload,使用分支判断子弹是否满,设置子弹数为默认值。

可以添加延迟使得换单需要时间,但会引入新的问题:在延迟中可以反复按下R,导致重复换弹。

优化1.拒绝重复换弹

新建isReloading变量,用以储存是否正在换弹。

在输入操作Fire后添加分支,若为false,设置为true,执行换弹,换弹结束再设置为false。

优化2.在弹药耗尽后自动换弹

在输入操作Reload后创建自定义事件Reload,在射击分区最后先使用分支判断弹药是否耗尽,将True引脚连接Reload。

直接查找Reload,选择 调用函数Reload,这个函数就是刚才的自定义事件。

3.瞄准

瞄准实质上是Fov(视野)的改变。

添加操作Zoom,创建时间轴,在时间轴中添加浮点型轨道,shift+鼠标左键添加关键帧。

在事件图表中,连接Pressed与play;Released与Reverse,在Update后查找设置视野,连接轨道名输出引脚与In Field of View输入引脚。

时间轴的一些引脚:

Play:播放

Play form Start:从头播放

Stop:停止

Reverse:反向播放

Reverse:从末尾反向播放

4.增加可拾取的弹药箱

创建蓝图,选择Actor子类,命名为AmmoBox。

在组件菜单中添加组件StaticMesh(静态网格体组件),在细节界面选择合适的静态网格体和材质。

在Static Mesh组件下添加Capsule Collision(碰撞体),调整合适的大小。

打开事件图表,查找事件 Actor开始重叠,并连接类型转换为FirstPersonCharacter。

事件 Actor开始重叠:有物体和此Actor发生重叠。

类型转换为FirstPersonCharacter:只有此类型的物体(即玩家)可以响应操作。

在类型转换为FirstPersonCharacter后连接设置Play Current Ammo,连接As First Person Character和目标引脚。

如果设置的子弹为定值,直接在SET上设置;若要将子弹增加,在As FPC后连接获取Player Current Ammo,加上数值后再连接SET。

这时AS FPC上应该有两个输出(什么NTR)

5.备用弹药机制

看起来挺简单,实则不然。先创建一个用于储存备用弹药的变量吧。

来分析一下可能遇到的情况:

1.备用弹药大于需要增加的现有弹药(e.g. 20/11)

2.备用弹药小于需要增加的现有弹药(e.g. 20/5)

3.备用弹药为空。

先看第三点,这一点只需要在输入操作Reload后面加上判断就好了。

然后是第一、二点:

用30减去现有弹药得到需要增加的弹药,备用弹药减去需要增加的弹药,得到的数(就是换弹完后的后备弹药数)若为正,直接将此数赋给备用弹药,将现有弹药设置为最大值。

若为负,则说明备用弹药不足以填满现有弹匣,将备用弹药和现有弹药相加赋予现有弹药,备用弹药则置零。

射线检测与弹药系统

**注意:**True分支设置ReserveAmmo须在设置CurrentAmmo前,否则会出现备用弹药不消耗;False分支则相反,否则会出现备用弹药减少了但现有弹药没有增加。

做完这些别忘了在UI界面增加一个用于显示备用弹药的文本,弹药箱也别忘了更改为增加备用弹药。

六、设置游戏胜利条件

1.设计胜利菜单

新建用户界面蓝图WinMenu,打开设计器,放上两个按钮(重开和退出),其他随意设计。

在按下按钮时需要响应事件,将细节图标拖到最底下,新建点击时事件,另一个按钮同理。

打开事件图表,在点击时(重开)后搜索打开关卡(按名称),填写关卡名称为FirstPersonExampleMap //如果关卡名未作更改,查找从父项中移除,连接输入输出引脚。

从父项中移除:将UI界面(子项)从玩家的视口(父项)中移除。

在点击时(退出)后查找退出游戏,连接引脚。

2.显示菜单

转到FirstPersonCharacter蓝图。

新建自定义事件,命名为End Game。

在达成胜利条件后,我们希望游戏暂停并显示胜利界面,同时出现鼠标供操作。

1.查找设置游戏已暂停,并勾选Paused复选框;

2.查找获取玩家控制器,从返回值引脚处搜索设置Show Mouse Cursor节点,勾选显示鼠标光标;

3.查找创建控件,Class选择WinMenu,连接到添加到视口。

吐槽一句,这个byd翻译真脑瘫,一会中文一会英文。

3.触发胜利

打开“可爱的圆柱体”蓝图,在这里写检测胜利的蓝图。

找到销毁Actor(在最后),查找分支,True输出引脚后连接上一点创建的End Game自定义事件。这里需要找到类型转换为FirstPersonCharacter,从As FPC输出引脚后面查找。

同样是AS FPC,从此处引出获取TargetKillCount和获取TargetGoal,将两者比较,若前者大于等于后者,则触发胜利。

一个事件中不能出现两个类型转换为FirstPersonCharacter,否则出现死循环。

搞完啦,祝贺一下吧。

七、使用AI制作移动的敌人

1.使用NavMesh制作导航

在放置Actor界面下找到体积,从其中拖出导航网格体边界体积至关卡中,调整三维大小至所有可行走区域被绿色覆盖。

使用P键查看或隐藏绿色的导航网格

2.使用AI资源设置智能敌人

2.1.创建所需蓝图

1.敌人角色EnemyCharacter:选择蓝图类,查找Ue4ASP_Character。

2.敌人行为树EnemyBehavior:人工智能-行为树。

3.敌人控制器EnemyController:蓝图类-搜索AIController。

3.敌人黑板(数据容器)EnemyBlackborad:人工智能-黑板。

2.2.配置所需蓝图

打开EnemyCharatcer蓝图,点击EnemyCharacter(左上),找到细节面板中的Pawn,将AI控制器类改为之前创建的EnemyController。

点击网格体组件,给它换一个颜色吧。

将它拖入关卡中,重命名为Enemy1。

3.创建导航行为

3.1.设置巡逻点

在放置Actor界面中查找目标点,拖进关卡。在其组件面板中查找Sphere Collider。

Sphere Collider:球体碰撞器,用来检测重叠事件。

3.2.启用资产间通讯
3.2.1.在黑板中创建关键帧

打开黑板,点击新键,类型选择类(Object)。命名为PatrolPoint。

3.2.2.当角色创建时为PatrolPoint赋值

创建三个对象型变量,分别为PatrolPoint1,2和CurrentPatrolPoint。它们分别储存第1,2个路径点和敌人当前正前往的路径点。

打开EnemyCharacter蓝图,找到事件开始运行。

查找获取黑板,从Return Value引脚处查找set value to object(必须为英文),在Target引脚处查找self获取自身对象的引用。

从Key Name引脚处查找创建文字命名,设置值为PatrolPoint(在黑板中创建的关键帧名)。

获取CurrentPatrolPoint,连接至Object Value引脚。这一步即为赋值。

敌人受击事件

3.2.3.在碰撞时切换巡逻点

找到事件Actor开始重叠,连接分支。

在这里需要判断Other Actor引脚的值(即Actor当前位置)是否与PatrolPoint1,2的值相等(判断目标是否抵达目标路径点)。若相等,将CurrentPatrolPoint的值切换为另一个。

最后将赋值和上面将值设为对象相连,旨在将更新信息同步给黑板。

敌人伤害蓝图

3.2.4.将控制器与行为树相连

打开EnemyController,找到事件BeginPlay,将其连接到运行行为树节点,BTAsset设置为EnemyBehavior。

在EnemyBlackBoard中将PatrolPoint的BaseClass设置为Actor,返回关卡编辑器,选择敌人角色,在细节界面找到默认,将PatrolPoint1的值设置为路径点1的值。

3.2.5.一些相关问题

1.未检测出碰撞:放大目标点的碰撞体积。

2.碰撞后未等待(还没解决)