前言:
笔者目前在校本科大三,目标方向是人工智能、计算机视觉。上一个OpenCV学习笔记专栏已完结,在学习完OpenCV后,我继续学习C++,并用纯C++做UE4项目的方式继续提升自己的水平。
梁迪老师的水平非常高,他的课程本来也无需笔记:课程本身即为最好的笔记。但由于我天赋有限,还是边看边记,以防遗忘——知识点太多,步骤太繁杂了。在学习过程中,我也偶有思考,思索为什么某个方法老师要这样做。所以,一是为了记录,二是为了分享,才有了这个专栏。
内容方面,由于我在开启这个专栏时,此项目已经做完很多了。所以,前期的一些大篇幅叙述的知识,可能在后期应用中一带而过。以及,前期的一些知识,后期会重新剖析,并加上我的个人理解。
另外,若有学术交流/学业交流意愿,可以邮件联系1246210283@qq.com,希望一齐进步。
本篇学习内容:
28.UE4断言
29.角色Montage动画播放
30.快捷栏UI
28.UE4断言
参考文档:https://docs.unrealengine.com/4.27/zh-CN/ProgrammingAndScripting/ProgrammingWithCPP/Assertions/
在C和C++编程中,assert 可在开发期间帮助检测和诊断不正常或无效的运行时条件。这些条件通常检查是否指针为非空、除数为非零、函数并非递归运行,或代码要求的其他重要假设。但每次检查会使得效率十分低下。某些情况下,assert 会在延迟崩溃发生之前发现导致该崩溃的bug,例如删除未来tick所需的对象,协助开发人员发现引起崩溃的根本原因。assert 的关键特性之一是不存在于发布代码中,这意味着不但不会影响发布产品的性能,也没有任何副作用。对 assert 最简单的理解就是:"断言"必须一律为true,否则程序会停止运行。
虚幻引擎4(UE4)提供 assert 等同项的三个不同族系:check、verify 和 ensure。若要检查这些功能背后的代码,可在 Engine/Source/Runtime/Core/Public/Misc/AssertionMacros.h 中找到相关的宏。各个功能的行为略有不同,但它们都是开发期间使用的诊断工具,目标大致相同。
Check族系最接近基础 assert,因为当第一个参数得出的值为false时,此族系的成员会停止执行,且默认不会在发布版本中运行。
在大部分版本中,Verify族系的行为与Check族系相同。但即便在禁用Check宏的版本中,Verify宏也会计算其表达式的值。这意味着仅当该表达式需要独立于诊断检查之外运行时,才应使用Verify宏。举例而言,若某个函数执行操作,然后返回 bool 来说明该操作是否成功,则应使用Verify而非Check来确保该操作成功。因为在发布版本中Verify将忽略返回值,但仍将执行操作。而Check在发布版本中根本不调用该函数,所以行为才会有所不同。
Ensure族系类似于Verify族系,但可在出现非致命错误时使用。这意味着,若Ensure宏的表达式计算得出的值为false,引擎将通知崩溃报告器,但仍会继续运行。为避免崩溃报告器收到太多通知,Ensure宏在每次引擎或编辑器会话中仅报告一次。若实际情况需要Ensure宏在每次表达式计算得值为false时都报告一次,则使用"Always"版本的宏。
29.角色Montage动画播放
在完成了人物移动后,需要添加上半身动作,这可以通过如下方法实现:
将状态机保存到一个缓存块中,创建2个一样“使用缓存姿势”,在其中一个连接蒙太奇Slot并设置SlotName为UpperBody,随后创建一个骨骼混合,添加一个元素,此元素的骨骼名就是蒙太奇中控制上半身的根骨骼。
在蓝图完成动作蓝图的设置后,还需要完成代码工作。
(1)首先,创建一个Montage动画的枚举类型:
//上半身动画状态
namespace EUpperBody
{
enum Type
{
None,
Punch,
Hit,
Fight,
PickUp,
Eat
};
}
(2)在父类PlayerAnim中定义上半身的5个Montage指针,和一个保存当前播放的Montage的指针:
protected:
//上半身的Montage
UAnimMontage *PlayerHitMontage;
UAnimMontage *PlayerEatMontage;
UAnimMontage *PlayerFightMontage;
UAnimMontage *PlayerPunchMontage;
UAnimMontage *PlayerPickUpMontage;
//保存当前播放的Montage
UAnimMontage *CurrentMontage;
(3)分别在FirstPlayerAnim和ThirdPlayerAnim中给这些指针赋值,即读取对应的第一人称、第三人称Montage资源。
(4)在Character中创建保存上半身动画状态的枚举类型
pubic:
//上半身动画状态
EUpperBody::Type UpperType;
这样就可以在PlayerAnim中获取到Character的动画状态。
(5)准备好之后,就可以在PlayerAnim中定义动画更新函数了。
protected:
//更新动作
virtual void UpdateMontage();
void USlAiPlayerAnim::UpdateMontage()
{
//如果不存在直接返回,避免空指针产生中断
if (!SPCharacter) return;
//如果当前的动作没有停止,不更新动作
if (!Montage_GetIsStopped(CurrentMontage)) return;
switch (SPCharacter->UpperType)
{
case EUpperBody::None:
//如果有哪个动作在播放
if (CurrentMontage != nullptr) {
Montage_Stop(0);//输入0表示停止全部的动作
CurrentMontage = nullptr;
}
break;
case EUpperBody::Punch:
if (!Montage_IsPlaying(PlayerPunchMontage)) {
Montage_Play(PlayerPunchMontage);
CurrentMontage = PlayerPunchMontage;
}
break;
...
}
}
这里整个的逻辑是这样的:在触发按键事件后,将在Controller中更新Character的UpperType为相对应的枚举类型,随后PlayerAnim的帧更新函数每帧读取到Character的动画状态,并播放相应的动画。鼠标抬起后,也触发相应的按键事件,将UpperType设置为None。同时为了点击鼠标后要播放一次完整的动画,所以在当前的动作没有停止时,不会更新动作。
(6)然而,为了避免在第一人称、第三人称同时触发某个事件,我们需要设置一次只播放第一或第三人称的动画,并且在播放动画时不能切换视角。
一次只播放第一或第三人称的动画:
在PlayerAnim中:
protected:
//指定自己的运行人称
EGameViewMode::Type GameView;
在2个子类中分别初始化。
在UpdateMontage()中添加代码:
//如果当前的人称状态和这个动作的不一致,直接返回
if (SPCharacter->GameView != GameView) return;
在播放动画时不能切换视角:
在Character中:
pubic:
//是否允许切换视角
bool IsAllowSwitch;
在PlayerAnim中添加函数:
protected:
//修改是否允许切换视角
void AllowViewChange(bool IsAllow);
实现AllowViewChange函数:
void USlAiPlayerAnim::AllowViewChange(bool IsAllow)
{
if (!SPCharacter) return;
SPCharacter->IsAllowSwitch = IsAllow;
}
修改UpdateMontage函数:在适当地方调用AllowViewChange函数即可
写到这里有一个问题:我们是在哪里实例化FirstPlayerAnim和ThirdPlayerAnim的?是在动作蓝图创建时指定的。我们在创建动作蓝图时,指定了蓝图的父类为FirstPlayerAnim和ThirdPlayerAnim。
30.快捷栏UI
(1)创建一个Slate Wiegdt Style:GameWidgetStyle和两个Slate Widget:GameHUDWidget、ShortcutWidget
(2)在GameHUD中添加widget到viewport:
.h:
public:
ASlAiGameHUD();
private:
TSharedPtr<class SSlAiGameHUDWidget> GameHUDWidget;
.cpp:
ASlAiGameHUD::ASlAiGameHUD()
{
//添加widget到viewport
if (GEngine && GEngine->GameViewport) {
SAssignNew(GameHUDWidget, SSlAiGameHUDWidget);
GEngine->GameViewPort->AddViewportWidgetContent(SNew(SWeakWidget).PossiblyNullContent(GameHUDWidget.ToSharedRef()));
}
}
(3)在GameHUDWidget中添加DPI缩放规则、以SAssignNew形式创建一个ShortcutWidget
(4)在ShortcutWidget中实现结构:
快捷栏UI
分为两行:第一行是文字,用于显示当前物体;第二行是9个Border,用于动态存放物体。
整个UI的底层是SBox,随后添加2个SOverlay。
由于文字和物体都是动态变化的,我们需要创建2个指针来维护。
而每一个Border由最外的边框、中间的物品图标、右下角的数量三部分组成,我们循环创建9个这样的Border,然后添加到网格组件SUniformGridPanel中。
(5)写InitializeContainer()函数,初始化快捷栏内容
(6)继续写JsonHandle类,用于读取物品属性表。和读取游戏设置时的写法相同,只是这次会把数据读取到一个TMap中。
以及,由于DataHandle的构造函数会在MenuMap调用,所以不能在构造函数里初始化这个TMap。要新建一个方法,让游戏跳转到GameMap场景时,调用DataHandle下的一个方法,来对这些数据实例化。
(7)在SlAiTypes中准备好一个快捷栏容器的结构体,结构体内有物品数量、编号,对应容器的三个指针,对应的笔刷,以及构造函数等函数。
(8)继续写InitializeContainer()函数,循环创建容器后实例化一个结构体,并添加到ContainerList