斯坦福 UE4 C++ ActionRoguelike游戏实例教程 07.在C++中使用UMG

斯坦福课程 UE4 C++ ActionRoguelike游戏实例教程 0.绪论

概述

本篇文章的目标是创建一个基于C++的UMG类,并以这个类作为子类,为攻击到的敌对小兵添加一个血条UI。

最终效果如下:

目录

  1. 认识UMG
  2. 创建小兵血条

UMG

UE中的UMG(Unreal Motion Graphics)是一种用于创建用户界面(UI)和HUD(头部显示)的工具。UMG提供了一个可视化的编辑器,允许开发人员轻松创建复杂的UI和HUD,而无需编写代码。正如我们常见到的血条UI、操作提示、各种游戏内积分提示都是可以使用UMG实现的。

图里圈出来的UI元素都是UMG的功劳

UMG包括一系列可自定义的预制件,如按钮、文本框、滑块、进度条等等。这些预制件可以通过拖放操作在编辑器中创建,然后进行定位、缩放和旋转等操作以满足特定的UI需求。此外,UMG还提供了脚本编写接口,使得开发人员可以通过编写脚本来实现更加复杂的UI逻辑。

UMG编辑界面就不详细展示了,这里简单提一下UMG的使用。

创建UMG的的通常方法

创建完成后,可以在任意事件蓝图里使用如下方式创建控件,并添加到视口。

使用UMG的的通常方法

今天还要介绍一种用c++创建和编辑UMG的方法,当然,最方便的还是直接使用UMG提供的编辑器来操作,我们用C++通常只是创建一个基类用来定义UMG蓝图的接口。具体怎么使用UMG,让我们边做边说。

创建小兵血条

编辑C++代码

UserWidget可以说是各种用户UI控件的基类,后续使用的蓝图类也是继承于该类。我们将他命名为SurWorldUserWidget,之后这个类还会作为其他控件的基类,大家在设计的时候可以好好考虑这个类里应该实现什么功能。

右键编辑器,创建一个UserWidget子类

创建完成后,UE会自动在uproject文件里自动添加编译"UMG"模块,出于一致性,记得在Build.cs文件里添加"UMG"。

下面我贴出了新创建的类的所有代码。先看看头文件,里面定义了一些成员:

  1. 首先定义了一个SizeBox类型的成员。SizeBox允许使用者自定义其大小,并限定其内容的尺寸。注意这里使用了meta = (BindWidget)修饰符,和名字一样,这是控件专用的修饰符,可以将指针绑定到UMG编辑器里同名同类型的组件。如果编辑器里没有对应组件,编译甚至还会报错。
  2. 作为小兵的血条控件,就需要绑定一个小兵的对象。这里定义了 AActor* AttachedActor,用意很好理解。值得一提的是,由于小兵和UMG控件是独立的两个对象,当小兵对象被销毁时,传统C++的方法是无法在控件类中得知指针是否被释放的,容易造成空悬指针的问题。好在UE引进了垃圾回收机制,只要加上UPROPERTY宏,就自动为成员变量登记垃圾回收。当指针所指的对象销毁掉后,会自动将指针置为nullptr,比C++11的share_ptr还好用(
  3. 最后就是重载了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文件的内容。

具体来说,这段代码的作用如下:

  1. 首先,代码会调用Super::NativeTick(MyGeometry, InDeltaTime)来调用UserWidget类的NativeTick方法,以确保父类的功能正常工作。
  2. 然后,代码会检查目标Actor是否有效。正如上面所说的,垃圾回收机制解决了空悬指针的问题,但是没有解决空指针的问题。因此这里同样需要注意空指针的问题。这里使用了IsValid函数进行判断指针是否有效,该函数除了判断指针是否为空以外,还判断该对象是否被标记为删除,也就是调用了Destroy之类的函数。如果AttachedActor为空,则调用RemoveFromParent()从父级控件中移除自定义控件,并输出日志信息,以告知开发者该控件被移除。
  3. 接下来,代码会通过UGameplayStatics::ProjectWorldToScreen函数将AttachedActor的三维世界坐标投影到二维屏幕坐标系中,得到屏幕坐标。
  4. 接着,代码会使用UWidgetLayoutLibrary::GetViewportScale函数获取视口缩放比例,然后将屏幕坐标除以该比例,以确保在不同分辨率的屏幕上,控件的位置保持一致。
  5. 最后,如果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(尺寸框)

将页面的布局修改成如图:

布局

其中,画布画板允许其中的控件自由排布,非常适合我们根据自己的喜好手动布局。这时我们会发现SizeBox已经可以在界面中自由拖动了。将Image材质设置为前几节课制作的HealthBar,并给Image起一个自己看的顺眼的名。这里将ParentSizeBox设置成大小到内容:

在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_HealthBarProgressAlpha变量,关于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的更多相关文章

  1. 《Genesis-3D开源游戏引擎--横版格斗游戏制作教程07:UI》

    概述: UI即User Interface(用户界面)的简称.UI设计是指对软件的燃机交互.操作逻辑.界面美观的整体设计.好的UI设计不仅可以让游戏变得更有品位,更吸引玩家,还能充分体现开发者对游戏整 ...

  2. Cocos2d-x3.0游戏实例《不要救我》第十篇(结束)——使用Json配置数据类型的怪物

    如今我们有2种类型的怪物,并且创建的时候是写死在代码里的,这是要作死的节奏~ 所以.必须可配置.不然会累死人的. ; i < size; ++i) { int id = root[i][&quo ...

  3. Cocos2d-x3.0游戏实例之《别救我》第八篇——TiledMap实现关卡编辑器

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/musicvs/article/details/25368273 好吧.我真心全然搞不懂.我如今仅仅只 ...

  4. libgdx游戏引擎教程

    第一讲:libgdx游戏引擎教程(一)性能优良的游戏引擎—libgdx http://www.apkbus.com/android-57355-1-1.html 第二讲: libgdx游戏引擎教程(二 ...

  5. 《Genesis-3D开源游戏引擎完整实例教程-2D射击游戏篇:简介及目录》(附上完整工程文件)

    G-3D引擎2D射击类游戏制作教程 游戏类型: 打飞机游戏属于射击类游戏中的一种,可以划分为卷轴射击类游戏. 视觉表现类型为:2D 框架简介: Genesis-3D引擎不仅为开发者提供一个3D游戏制作 ...

  6. Python导出Excel为Lua/Json/Xml实例教程(一):初识Python

    Python导出Excel为Lua/Json/Xml实例教程(一):初识Python 相关链接: Python导出Excel为Lua/Json/Xml实例教程(一):初识Python Python导出 ...

  7. Web 开发中应用 HTML5 技术的10个实例教程

    HTML5 作为下一代网站开发技术,无论你是一个 Web 开发人员或者想探索新的平台的游戏开发者,都值得去研究.借助尖端功能,技术和 API,HTML5 允许你创建响应性.创新性.互动性以及令人惊叹的 ...

  8. 值得 Web 开发人员收藏的20个 HTML5 实例教程

    当开始学习如何创建 Web 应用程序或网站的时候,最流行的建议之一就是阅读教程,并付诸实践.也有大量的 Web 开发的书,但光有理论没有实际行动是无用的.现在由于网络的发展,我们有很多的工具可以用于创 ...

  9. 对《[Unity官方实例教程 秘密行动] Unity官方教程《秘密行动》(十二) 角色移动》的一些笔记和个人补充,解决角色在地形上移动时穿透问题。

    这里素材全是网上找的. 教程看这里: [Unity官方实例教程 秘密行动] Unity官方教程<秘密行动>(九) 角色初始设定 一.模型设置: 1.首先设置模型的动作无限循环. 不设置的话 ...

  10. Cocos2d-x3.0游戏实例《不要救我》第一章——前言

    我们可以学习? 这是一个非常easy游戏.但更多的东西用(对于初学者).至少,对于它的一个例子,有点多. 笨木头花心贡献.啥?花心?不呢.是用心~ 转载请注明,原文地址:http://www.benm ...

随机推荐

  1. Learning Hard C# 学习笔记: 6.C#中的接口

    目的: 由于C#中的类只能单个继承, 为了满足多重继承(一个子类可以继承多个父类)的需求, 所以产生了接口. 多重继承是指一个类可以从多个父类继承属性和方法.在C#中,只允许单继承,即一个类只能有一个 ...

  2. Cython加密python代码防止反编译

    本方法适用于Linux环境下: 1.安装库Cython pip3 install Cython==3.0.0a10 2.编写待加密文件:hello.py import random def ac(): ...

  3. 循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(11) -- 下拉列表的数据绑定以及自定义系统字典列表控件

    在我们开发的前端项目中,往往为了方便,都需对一些控件进行自定义的处理,以便实现快速的数据绑定以及便捷的使用,本篇随笔介绍通过抽取常见字典列表,实现通用的字典类型绑定:以及通过自定义控件的属性处理,实现 ...

  4. Vue之键盘事件

    1.使用keydown触发事件 <!DOCTYPE html> <html lang="en"> <head> <meta charset ...

  5. mysql 实用语句

    -- 查询内存大小 SELECT TABLE_NAME, concat( TRUNCATE (data_length / 1024 / 1024, 2), ' MB' ) AS data_size, ...

  6. AirSim 自动驾驶仿真 (6) 设置采集参数和属性

    https://cloud.tencent.com/developer/article/2011384 1.配置文件在哪 默认情况下,文件位于用户目录下的AirSim文件夹,比如在Windows下,文 ...

  7. Python 潮流周刊#25:性能最快的代码格式化工具 Ruff!

    你好,我是猫哥.这里每周分享优质的 Python.AI 及通用技术内容,大部分为英文.标题取自其中一则分享,不代表全部内容都是该主题,特此声明. 本周刊由 Python猫 出品,精心筛选国内外的 25 ...

  8. GitHub - 如何对开源项目做出贡献

    GitHub - 对项目做出贡献 转载来自git官方教程:https://git-scm.com/book/zh/v2/GitHub-对项目做出贡献 对项目做出贡献 账户已经建立好了,现在我们来了解一 ...

  9. 使用MVVM Toolkit简化WPF开发

    最近. NET 8 的 WPF 推出了 WPF File Dialog改进,这样无需再引用 Win32 命名空间就可以实现文件夹的选择与存储了,算是一个很方便的改进了.顺手写了一个小的 WPF 程序, ...

  10. Boruvka 算法

    Boruvka算法解决某些问题超级好用. 这些问题形如,给你n个点,每个点有点权,任意两个点之间有边权,边权为两个点权用过某种计算方式得出. 求最小生成树. 通常用 \(O(log n)\) 的时间可 ...