斯坦福 UE4 C++ ActionRoguelike游戏实例教程 07.在C++中使用UMG
斯坦福 UE4 C++ ActionRoguelike游戏实例教程 07.在C++中使用UMG
斯坦福课程 UE4 C++ ActionRoguelike游戏实例教程 0.绪论
概述
本篇文章的目标是创建一个基于C++的UMG类,并以这个类作为子类,为攻击到的敌对小兵添加一个血条UI。
最终效果如下:
目录
- 认识UMG
- 创建小兵血条
UMG
UE中的UMG(Unreal Motion Graphics)是一种用于创建用户界面(UI)和HUD(头部显示)的工具。UMG提供了一个可视化的编辑器,允许开发人员轻松创建复杂的UI和HUD,而无需编写代码。正如我们常见到的血条UI、操作提示、各种游戏内积分提示都是可以使用UMG实现的。
UMG包括一系列可自定义的预制件,如按钮、文本框、滑块、进度条等等。这些预制件可以通过拖放操作在编辑器中创建,然后进行定位、缩放和旋转等操作以满足特定的UI需求。此外,UMG还提供了脚本编写接口,使得开发人员可以通过编写脚本来实现更加复杂的UI逻辑。
UMG编辑界面就不详细展示了,这里简单提一下UMG的使用。
创建完成后,可以在任意事件蓝图里使用如下方式创建控件,并添加到视口。
今天还要介绍一种用c++创建和编辑UMG的方法,当然,最方便的还是直接使用UMG提供的编辑器来操作,我们用C++通常只是创建一个基类用来定义UMG蓝图的接口。具体怎么使用UMG,让我们边做边说。
创建小兵血条
编辑C++代码
UserWidget可以说是各种用户UI控件的基类,后续使用的蓝图类也是继承于该类。我们将他命名为SurWorldUserWidget,之后这个类还会作为其他控件的基类,大家在设计的时候可以好好考虑这个类里应该实现什么功能。
创建完成后,UE会自动在uproject文件里自动添加编译"UMG"模块,出于一致性,记得在Build.cs文件里添加"UMG"。
下面我贴出了新创建的类的所有代码。先看看头文件,里面定义了一些成员:
- 首先定义了一个SizeBox类型的成员。SizeBox允许使用者自定义其大小,并限定其内容的尺寸。注意这里使用了
meta = (BindWidget)
修饰符,和名字一样,这是控件专用的修饰符,可以将指针绑定到UMG编辑器里同名同类型的组件。如果编辑器里没有对应组件,编译甚至还会报错。 - 作为小兵的血条控件,就需要绑定一个小兵的对象。这里定义了
AActor* AttachedActor
,用意很好理解。值得一提的是,由于小兵和UMG控件是独立的两个对象,当小兵对象被销毁时,传统C++的方法是无法在控件类中得知指针是否被释放的,容易造成空悬指针的问题。好在UE引进了垃圾回收机制,只要加上UPROPERTY宏,就自动为成员变量登记垃圾回收。当指针所指的对象销毁掉后,会自动将指针置为nullptr,比C++11的share_ptr还好用( - 最后就是重载了NativeTick函数,每次Tick都会执行一次。
//SurWorldUserWidget.h
UCLASS()
class FPSPROJECT_API USurWorldUserWidget : public UUserWidget
{
GENERATED_BODY()
protected:
//该宏允许蓝图编辑器的同名同类型的组件 与 C++中的成员指针相关联
//因此要想成功绑定,蓝图里的控件必须是同名同类型的
UPROPERTY(meta = (BindWidget))
USizeBox* ParentSizeBox;
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
public:
//一个很重要的一点就是添加了UPROPERTY宏的对象都会被列入UOBJECT的垃圾回收系统管理。当这个对象在其他地方被释放(destroy)后,所有指向这个对象的指针都会被赋为NULL,有点像shared_ptr
UPROPERTY(BlueprintReadOnly, Category = "UI")
AActor* AttachedActor;
};
接下来是Cpp文件的内容。
具体来说,这段代码的作用如下:
- 首先,代码会调用Super::NativeTick(MyGeometry, InDeltaTime)来调用UserWidget类的NativeTick方法,以确保父类的功能正常工作。
- 然后,代码会检查目标Actor是否有效。正如上面所说的,垃圾回收机制解决了空悬指针的问题,但是没有解决空指针的问题。因此这里同样需要注意空指针的问题。这里使用了IsValid函数进行判断指针是否有效,该函数除了判断指针是否为空以外,还判断该对象是否被标记为删除,也就是调用了Destroy之类的函数。如果AttachedActor为空,则调用RemoveFromParent()从父级控件中移除自定义控件,并输出日志信息,以告知开发者该控件被移除。
- 接下来,代码会通过UGameplayStatics::ProjectWorldToScreen函数将AttachedActor的三维世界坐标投影到二维屏幕坐标系中,得到屏幕坐标。
- 接着,代码会使用UWidgetLayoutLibrary::GetViewportScale函数获取视口缩放比例,然后将屏幕坐标除以该比例,以确保在不同分辨率的屏幕上,控件的位置保持一致。
- 最后,如果ParentSizeBox不为空,代码会将控件的渲染位置设置为屏幕坐标,以实现控件位置的更新。
//SurWorldUserWidget.cpp
void USurWorldUserWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
Super::NativeTick(MyGeometry, InDeltaTime);
//首先判断目标Actor是否有效
if(!IsValid(AttachedActor))
{
RemoveFromParent();
UE_LOG(LogTemp, Warning, TEXT("AttachedActor no longger valid, removeing Health Widget."));
return;
}
FVector2D ScreenPosition;
//该函数返回三维世界投影到二维平面上的坐标
if(UGameplayStatics::ProjectWorldToScreen(GetOwningPlayer(), AttachedActor->GetActorLocation(), ScreenPosition))
{
float Scale = UWidgetLayoutLibrary::GetViewportScale(GetWorld());
ScreenPosition /= Scale;
}
if(ParentSizeBox)
{
//设置其渲染到屏幕上的位置
ParentSizeBox->SetRenderTranslation(ScreenPosition);
}
}
编辑蓝图
虽然文章讲的是在C++中使用UMG,不得不说在代码编辑UI实在是非常低效的做法。通常蓝图和C++代码截图的正确姿势是使用C++定义类的功能和接口(demo),使用蓝图来做具体方法的实现。这里当然也不例外。
这里为刚才创建的类创建一个蓝图子类,将其命名为MinionHealth_Widget。
进入编辑器,如果点击编译会提示你需要绑定ParentSizeBox,这就刚才Meta修饰符的作用所在了。
将页面的布局修改成如图:
其中,画布画板
允许其中的控件自由排布,非常适合我们根据自己的喜好手动布局。这时我们会发现SizeBox已经可以在界面中自由拖动了。将Image材质设置为前几节课制作的HealthBar,并给Image起一个自己看的顺眼的名。这里将ParentSizeBox设置成大小到内容:
该选项会使SizeBox的大小固定为其包含的组件的大小。这里的组件只有一张图片,即这张图片多大,SizeBox就有多大。
修改根据自己的喜好修改Image控件,主要是修改图像大小,由于大小到内容
的作用,SizeBox会随着Image的大小变化而变化。
为小兵添加控件相关逻辑
代码如下。在.h文件中,添加了UMG控件的对象指针,代表着一个小兵对应一个空间。并定义了要生成空间的类型。
class FPSPROJECT_API ASurAiCharacter : public ACharacter
{
GENERATED_BODY()
......
protected:
USurWorldUserWidget* ActiveHealthBar;
//我们希望生成的UI类型
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<UUserWidget> HealthBarWidgetClass;
......
}
在.cpp文件中,主要修改OnHealthChanged
函数,因为我们希望血条UI只在小兵被攻击时才出现。为了防止每次攻击都会创建一个控件,这里判断控件指针是否为空来避免。
void ASurAiCharacter::OnHealthChanged(AActor* InstigatorActor, USurAttributeComponent* OwningComp, float NewHealth,
float Delta)
{
...
//保证只创建一次控件
if(ActiveHealthBar == nullptr)
{
ActiveHealthBar = CreateWidget<USurWorldUserWidget>(GetWorld(), HealthBarWidgetClass);
if(ActiveHealthBar)
{
ActiveHealthBar->AttachedActor = this;
ActiveHealthBar->AddToViewport();
}
}
...
}
效果如下,当我们击中小兵后,小兵身上会出现一个血条。
当前的血条位置不太对,我们可以在编辑器里对血条的位置进行调整,图中设置了血条的对其,将其都设置为0.5,这样血条就会在目标坐标的正中心出现。
为了抬高血条的高度,在SurWorldUserWidget基类里定义了控件生成的偏移,将该偏移量添加到SurWorldUserWidget.cpp的ProjectWorldToScreen函数调用即可。
//SurWorldUserWidget.h
UPROPERTY(EditAnywhere)
FVector WorldOffset;
将WorldOffset设置为(0,0,100),血条的位置如下(后文会讲解掉血效果)
添加掉血效果
正常来说做到这步,我们已经实现了一个悬浮在小兵头上的血条,他会在我们攻击小兵的时候出现。接下来该做掉血效果,该效果表现为血条的红色部分会随着小兵的血量减少而减少。让我们进入MinionHealth_Widget的图表中,从事件构造
拉出一条线,为AttachedActor的属性组件绑定我们期望在生命值发生变化时产生的事件。
蓝图修改如下,我们获取了AttachedActor的属性组件,为其绑定了一个OnHealthChanged事件,这样,每当属性组件的血量变化时,都会通过委托机制调用这个事件函数。
在该事件函数中,我们获取了动态材质也就是M_HealthBar,就像之间编辑人物血条一样设置了M_HealthBar的ProgressAlpha变量,关于M_HealthBar是什么东西,既然都看到这里了,应该都挺熟悉前面的课程。请允许笔者偷个懒,不展开描述了。具体参考之前创建人物血条的课程Lecture 7。
最后的效果如下。我们发现当第一次攻击小兵时,他出现的血条是满血的状态,也就是出现了BUG。
检查代码逻辑发现,这是程序执行顺序导致的BUG。当我们攻击到小兵时,我们还未创建控件,还未将控件的OnhealthChanged事件绑定到属性组件中,因此本次血量变化的委托之中并没有控件的OnhealthChanged事件,所以血条就不会发生变化。
解决这个BUG的办法很简单,既然构造控件和掉血是同时发生的,那我们在创建控件后手动更新一下血条即可。具体做法就是在控件构造时主动调用一次OnhealthChanged事件。当然,你也可以自己定义一段逻辑来更新材质,道理都是一样的。
当小兵血条归零后,我们可以直接删除血条。这里也是添加了一小段逻辑,将其从父项中移除。在没有父控件的情况下,这里的父项指的就是玩家的视口。至于有父控件的情况会在后面的文章提到。
最终效果&总结
本篇文章我们在C++中创建了一个UMG组件的基类,结合蓝图创建了一个在小兵头上显示的血条。在之后的课程中,我们还将学习如何将这些一个一个的统合到一个总控件上。
参考链接
UE4 UMG的简单使用 https://zhuanlan.zhihu.com/p/461626363
斯坦福 UE4 C++ ActionRoguelike游戏实例教程 07.在C++中使用UMG的更多相关文章
- 《Genesis-3D开源游戏引擎--横版格斗游戏制作教程07:UI》
概述: UI即User Interface(用户界面)的简称.UI设计是指对软件的燃机交互.操作逻辑.界面美观的整体设计.好的UI设计不仅可以让游戏变得更有品位,更吸引玩家,还能充分体现开发者对游戏整 ...
- Cocos2d-x3.0游戏实例《不要救我》第十篇(结束)——使用Json配置数据类型的怪物
如今我们有2种类型的怪物,并且创建的时候是写死在代码里的,这是要作死的节奏~ 所以.必须可配置.不然会累死人的. ; i < size; ++i) { int id = root[i][&quo ...
- Cocos2d-x3.0游戏实例之《别救我》第八篇——TiledMap实现关卡编辑器
版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/musicvs/article/details/25368273 好吧.我真心全然搞不懂.我如今仅仅只 ...
- libgdx游戏引擎教程
第一讲:libgdx游戏引擎教程(一)性能优良的游戏引擎—libgdx http://www.apkbus.com/android-57355-1-1.html 第二讲: libgdx游戏引擎教程(二 ...
- 《Genesis-3D开源游戏引擎完整实例教程-2D射击游戏篇:简介及目录》(附上完整工程文件)
G-3D引擎2D射击类游戏制作教程 游戏类型: 打飞机游戏属于射击类游戏中的一种,可以划分为卷轴射击类游戏. 视觉表现类型为:2D 框架简介: Genesis-3D引擎不仅为开发者提供一个3D游戏制作 ...
- Python导出Excel为Lua/Json/Xml实例教程(一):初识Python
Python导出Excel为Lua/Json/Xml实例教程(一):初识Python 相关链接: Python导出Excel为Lua/Json/Xml实例教程(一):初识Python Python导出 ...
- Web 开发中应用 HTML5 技术的10个实例教程
HTML5 作为下一代网站开发技术,无论你是一个 Web 开发人员或者想探索新的平台的游戏开发者,都值得去研究.借助尖端功能,技术和 API,HTML5 允许你创建响应性.创新性.互动性以及令人惊叹的 ...
- 值得 Web 开发人员收藏的20个 HTML5 实例教程
当开始学习如何创建 Web 应用程序或网站的时候,最流行的建议之一就是阅读教程,并付诸实践.也有大量的 Web 开发的书,但光有理论没有实际行动是无用的.现在由于网络的发展,我们有很多的工具可以用于创 ...
- 对《[Unity官方实例教程 秘密行动] Unity官方教程《秘密行动》(十二) 角色移动》的一些笔记和个人补充,解决角色在地形上移动时穿透问题。
这里素材全是网上找的. 教程看这里: [Unity官方实例教程 秘密行动] Unity官方教程<秘密行动>(九) 角色初始设定 一.模型设置: 1.首先设置模型的动作无限循环. 不设置的话 ...
- Cocos2d-x3.0游戏实例《不要救我》第一章——前言
我们可以学习? 这是一个非常easy游戏.但更多的东西用(对于初学者).至少,对于它的一个例子,有点多. 笨木头花心贡献.啥?花心?不呢.是用心~ 转载请注明,原文地址:http://www.benm ...
随机推荐
- Learning Hard C# 学习笔记: 6.C#中的接口
目的: 由于C#中的类只能单个继承, 为了满足多重继承(一个子类可以继承多个父类)的需求, 所以产生了接口. 多重继承是指一个类可以从多个父类继承属性和方法.在C#中,只允许单继承,即一个类只能有一个 ...
- Cython加密python代码防止反编译
本方法适用于Linux环境下: 1.安装库Cython pip3 install Cython==3.0.0a10 2.编写待加密文件:hello.py import random def ac(): ...
- 循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(11) -- 下拉列表的数据绑定以及自定义系统字典列表控件
在我们开发的前端项目中,往往为了方便,都需对一些控件进行自定义的处理,以便实现快速的数据绑定以及便捷的使用,本篇随笔介绍通过抽取常见字典列表,实现通用的字典类型绑定:以及通过自定义控件的属性处理,实现 ...
- Vue之键盘事件
1.使用keydown触发事件 <!DOCTYPE html> <html lang="en"> <head> <meta charset ...
- mysql 实用语句
-- 查询内存大小 SELECT TABLE_NAME, concat( TRUNCATE (data_length / 1024 / 1024, 2), ' MB' ) AS data_size, ...
- AirSim 自动驾驶仿真 (6) 设置采集参数和属性
https://cloud.tencent.com/developer/article/2011384 1.配置文件在哪 默认情况下,文件位于用户目录下的AirSim文件夹,比如在Windows下,文 ...
- Python 潮流周刊#25:性能最快的代码格式化工具 Ruff!
你好,我是猫哥.这里每周分享优质的 Python.AI 及通用技术内容,大部分为英文.标题取自其中一则分享,不代表全部内容都是该主题,特此声明. 本周刊由 Python猫 出品,精心筛选国内外的 25 ...
- GitHub - 如何对开源项目做出贡献
GitHub - 对项目做出贡献 转载来自git官方教程:https://git-scm.com/book/zh/v2/GitHub-对项目做出贡献 对项目做出贡献 账户已经建立好了,现在我们来了解一 ...
- 使用MVVM Toolkit简化WPF开发
最近. NET 8 的 WPF 推出了 WPF File Dialog改进,这样无需再引用 Win32 命名空间就可以实现文件夹的选择与存储了,算是一个很方便的改进了.顺手写了一个小的 WPF 程序, ...
- Boruvka 算法
Boruvka算法解决某些问题超级好用. 这些问题形如,给你n个点,每个点有点权,任意两个点之间有边权,边权为两个点权用过某种计算方式得出. 求最小生成树. 通常用 \(O(log n)\) 的时间可 ...