阅读量:0
文章目录
一、创建血量属性
Unreal Motion Graphics (UMG)
是 UE中用于创建用户界面 (UI
) 的工具。它可以实现如下复杂功能:
(1)动画:UMG 支持为控件添加动画。可以在Widget Blueprint
中创建动画,并在蓝图脚本中控制动画的播放。
(2)绑定数据:可以将控件的属性绑定到变量或函数,实现动态更新 UI。比如本节中,将进度条的值绑定到角色的健康值。
(3)事件处理:UMG 提供了丰富的事件处理功能。可以为按钮点击、鼠标悬停等事件添加处理函数,实现交互逻辑。
(4)自定义控件:可以创建自定义的Widge
类,封装复杂的 UI 逻辑,并在其他地方复用。UMG
和HUD
,在UE4中的HUD
主要是指
(1)UE3那里继承来的Head-Up Display
功能,在UE4中主要是用来Debug
(2)向画布上添加图形图元的功能,表示文字的功能,还有简单的HitBox
处理,用来检测鼠标等
但是由于这些功能UMG都可以干,所以UE4的HUD功能也不是需要一定要掌握了UMG
是对Slate
进行了扩展方便在游戏中使用的框架,Slate
是不依靠某个平台的UI
框架,UE4的Editor
和游戏中的UI
都是使用Slate
构建的。
(1)继承了UObject
使得Blueprint
也可以方便使用
(2)方便在UE4的Editor
上确认渲染结果
(3)简单的制作UI动画- Widget 层次结构:
- UMG 的
Widget
是Blueprint
和Slate
控件的结合。每个UMG Widget
都有一个对应的Slate Widget
。UMG 通过将高层次的蓝图控件映射到Slate
控件来实现 UI。 UUserWidget
:UMG 中所有Widget
的基类。SUserWidget
:与UUserWidget
对应的Slate
控件。
- 事件驱动系统:
- UMG 和 Slate 使用事件驱动系统来处理用户输入事件。常见的事件包括:
①点击事件:如按钮的点击事件。
②悬停事件:鼠标悬停在控件上时触发的事件。
③键盘事件:处理键盘输入的事件。 - 事件的处理依赖于
Delegate
和Binding
系统。UMG 中的事件通常是通过Blueprint
或C++
中的Delegate
绑定来处理的。
- 数据绑定和属性系统:
- UMG 支持将控件的属性绑定到数据源。数据绑定使得 UI 能够自动更新,以反映数据的变化。常见的数据绑定机制包括:
①Property Binding
:通过 UMG 的绑定功能,将控件属性绑定到类的属性或函数。
②Delegate Binding
:通过 Delegate 绑定,处理属性变化时的自定义逻辑。
UMG
的三种控件:
UWidget
: 所有 UMG 控件的公共基类,不提供增加子节点功能UPanelWidget
: 提供了增加子节点功能,可以有多个子节点UContentWidget
: 继承于UPanelWidget
,是UPanelWidget
的一种特例,只能有一个子节点
(1)不能有子节点的控件:这个类别的控件的公共基类都是UWidget
,每个 UMG 控件,都持有一个 Slate
控件的智能指针。
(2)可以增加子节点的控件:
- 有一个子节点的控件继承的父类依次是
UComponentWidget
->UPanelWidget
,父类UComponentWidget
构造函数中设置了不允许多个孩子标记位:(源码如下)
UContentWidget::UContentWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { bCanHaveMultipleChildren = false; }
- 公共基类
UPanelWidget
定义了AddChild
函数,可以增加对应的子节点:(源码如下)
UPanelSlot* UPanelWidget::AddChild(UWidget* Content) { if ( Content == nullptr ) { return nullptr; //表示无法添加空控件 } //检查是否可以有多个子控件 if ( !bCanHaveMultipleChildren && GetChildrenCount() > 0 ) { return nullptr; } //将 Content 从其当前父控件中移除。这一步确保控件没有其他父控件,并准备将其添加到新的 UPanelWidget 中。 Content->RemoveFromParent(); //初始化新对象的标志。 //默认情况下,使用RF_Transactional,这意味着该对象的更改是可撤销的。 //如果UPanelWidget有RF_Transient标志,则新对象也设置这个标志,表示它在运行时创建,并且不会保存到磁盘。 EObjectFlags NewObjectFlags = RF_Transactional; if (HasAnyFlags(RF_Transient)) { NewObjectFlags |= RF_Transient; } //创建新的 PanelSlot 对象 UPanelSlot* PanelSlot = NewObject<UPanelSlot>(this, GetSlotClass(), NAME_None, NewObjectFlags); //GetSlotClass()是UPanelSlot的类类型NAME_None表示没有指定名称,新对象的标志是之前设置的NewObjectFlags。 //关联控件和PanelSlot PanelSlot->Content = Content; PanelSlot->Parent = this; //设置控件的 Slot //将Content的Slot属性设置为新创建的PanelSlot。这一步建立了控件与其布局信息之间的双向链接。 Content->Slot = PanelSlot; //将PanelSlot添加到Slots数组 //Slots数组维护所有子控件的布局信息 Slots.Add(PanelSlot); //触发Slot添加事件。OnSlotAdded方法是一个虚函数,可以在子类中重写,以处理Slot添加后的特定逻辑。 OnSlotAdded(PanelSlot); //使布局和易变性失效 //这将强制重建控件树和重新计算布局 InvalidateLayoutAndVolatility(); return PanelSlot; }
- UMG中的
Slot
:
(1)Slot
是一种用于管理控件布局和对齐的机制,UMG 中的每个Widget
都有一个Slot
,用于确定其在父控件中的布局规则。
(2)主要 Slot 类型及其属性:
① UCanvasPanelSlot
:
- 用于
UCanvasPanel
,提供绝对位置和大小。
属性包括Position, Size, Anchors, Offsets
等。
②UHorizontalBoxSlot
:
- 用于
UHorizontalBox
,提供水平布局。
属性包括Padding, Size, HorizontalAlignment, VerticalAlignment
等。
③UVerticalBoxSlot
:
- 用于
UVerticalBox
,提供垂直布局。
属性包括Padding, Size, HorizontalAlignment, VerticalAlignment
等。
(3)SPanel
、SCompoundWidget
可以作为父节点,控件之间的父子关系是依赖Slot
实现的。父控件引用Slot
,Slot
引用子控件并且保留子控件相对于父控件的布局信息。
Slate
控件类:
*Slate
中除了SWidget
之外有三个基础类,其他控件都是继承者三个基类。
①SPanel
: 有多个子节点
②SLeafWidget
: 没有子节点
③SCompoundWidget
: 可以有一个子节点UCanvasPanel
控件树:
UCanvasPanel
是 UMG 中的高层次控件,底层的实现是SConstraintCanvas
,它处理实际的布局和渲染逻辑,UCanvasPanel
将子控件添加到SConstraintCanvas
中,并通过UCanvasPanelSlot
设置布局属性。UCanvasPanelSlot
是UCanvasPanel
中每个子控件的Slot
类。它存储子控件的布局信息,比如位置、大小、锚点等。每个添加到UCanvasPanel
的子控件都会有一个对应的UCanvasPanelSlot
。SConstraintCanvas
使用SConstraintCanvas::FSlot
来存储子控件的布局信息。SConstraintCanvas::FSlot
继承自FSlot
,并添加了特定于SConstraintCanvas
的布局属性。- 当
UCanvasPanel
增加一个UCanvasPanelSlot
,其SConstraintCanvas
引用也相应的添加一个FSlot(SConstraintCanvas::FSlot)
,且UCanvasPanelSlot
保存FSlot
的引用。 - 当修改
UCanvasPanelSlot
的属性时,通用引用也修改了SConstraintCanvas::FSlot
对应的属性。
void UCanvasPanelSlot::SetOffsets(FMargin InOffset) { LayoutData.Offsets = InOffset; if ( Slot ) { Slot->Offset(InOffset); } }
- 为角色添加属性,如果直接在角色类中定义一个
Health
变量,这种方式的弊端显而易见:随着项目的扩展、游戏不断更新迭代新版本,角色的属性可能达到几十上百,若是加上角色的技能、特性、天赋或其他各种角色相关内容,角色类的内容将膨胀到极难维护。
在UE中解决这个问题的办法很简单,可以使用UE中的
ActorComponent
类,让整个类以组件的形式被SCharacter
拥有。因此,我们从ActorComponent
创建角色的SAttributeComponent
类,用于实现角色的各种属性。SAttributeComponent.h
#pragma once #include "CoreMinimal.h" #include "Components/ActorComponent.h" #include "SurAttributeComponent.generated.h" UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) class SURKEAUE_API USurAttributeComponent : public UActorComponent { GENERATED_BODY() protected: UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Attributes") float Health; public: USurAttributeComponent(); UFUNCTION(BlueprintCallable, Category="Attributes") bool ApplyHealthChange(float Delta); };
- SAttributeComponent.h
#include "SurAttributeComponent.h" USurAttributeComponent::USurAttributeComponent() { Health = 100; } bool USurAttributeComponent::ApplyHealthChange(float Delta) { Health += Delta; // 用于判断操作是否成功完成,如角色死亡、特殊机制等情况可能失败 // 目前先返回true return true; }
- 然后使用如下方法在
SCharacter
中声明组件,并在.cpp中创建相应实例:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components") USurAttributeComponent* AttributeComp;
二、应用血量更改
- 在
SMagicProjectile
中为球体组件绑定一个OnComponentBeginOverlap
事件,旨在当魔法粒子与物体重叠时触发。比起使用阻挡来触发,使用重叠可以通过判定重叠对象来更方便的忽略友军伤害,并且让魔法粒子直接穿过友军继续在场景中计算(记得对应的Projectile
碰撞设置也要进行更改)。 - 一个小技巧:在为创建绑定事件的函数时,我们可以在VS中利用右键“转到定义”去找到该事件默认的输入,找到参数数量最多的一个函数重载,然后复制粘贴除第一个参数(UE函数的签名)外的其他参数,去掉逗号作为绑定函数的参数。
- 在函数中实现减少血量的代码
//SurMagicProjectile.h UFUNCTION() void OnActorOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult); //SurMagicProjectile.cpp SphereComp->OnComponentBeginOverlap.AddDynamic(this, &ASurMagicProjectile::OnActorOverlap); void ASurMagicProjectile::OnActorOverlap(...) { if (OtherActor) { //获得AttributeComp USurAttributeComponent* AttributeComp = Cast<USurAttributeComponent>(OtherActor->GetComponentByClass(USurAttributeComponent::StaticClass())); // 再次判空,可能碰到的是墙壁、箱子等没有血量的物体 if (AttributeComp) { // 魔法粒子造成20血量伤害 AttributeComp->ApplyHealthChange(-20.0f); // 一旦造成伤害就销毁,避免穿过角色继续计算 Destroy(); } } }
- 在
Player
中利用蓝图实现在屏幕上打印血量的字符串,以让我们实时了解角色的血量。这个蓝图实现很简单,唯一注意的是设置显示时长为0
并关闭Print to Log
,这样屏幕上只会出现一个数字(事件Tick会在每一帧都调用),且不会在Log
中输出大量无用信息: - 利用蓝图系统快速实现一个攻击玩家的敌人 (箭头)
(1) 从Actor
派生一个名为ProjectileCanon
的蓝图类,为其添加Utility
下的箭头组件并设置为根,并设置颜色(UE中用红绿蓝表示XYZ轴,箭头默认的红色与X轴相同容易混乱),去掉“渲染”属性中的“游戏中隐藏”。
(2)在事件图表中从“开始运行”节点添加循环计时器,并添加名为OnTimerElapsed
的自定义事件,设置该事件为朝箭头方向发射魔法粒子;同时,在Tick
节点后实现在每一帧将箭头指向玩家,方向向量由两个Actor的坐标相减得到。
三、血量UI
- 首先在
Content
文件夹下创建UI文件夹,用于存放所有UI相关的资产。在UI文件夹中创建“用户界面” -> “控件蓝图”,命名为PlayerHealth_Widget
。双击打开UMG的操作界面 - 在左侧“控制板”中的“通用”分别添加一个进度条和文本,在“层级”中选择任意组件,右键添加水平框,并将另外一个拖入其中,这样水平框会为我们管理两个元素的水平对齐。
- 然后在进度条右侧的“细节”面板中找到“插槽”,选择“尺寸”为填充,拉动水平框的整体长度就可以控制血条的长度。将水平框的“锚点”选择为左上角,设置位置XY值,完成后就做好了简单的血条UI。
- 在
Player
的蓝图中选择“事件开始运行”,并添加Create Widget
节点,选择PlayerHealth_Widget
,然后将其添加到视口,这样,在我们运行关卡时UI就会显示在屏幕上。 - 此时UI的内容还是固定的,UI需要和数据进行绑定后,才能实时根据数据进行显示。选择文本块,点击 “内容” -> 文本-> 绑定 -> 创建绑定,UE会在蓝图中创建相应函数,我们将其重命名为
GetHealthText
。添加“获取拥有玩家Pawn(Get Owning Player Pawn
)” -> 按类获取组件(Get Component By Class
)-> 选择class
为SAttributeComponet
->GetHealth
-> 连接到“返回节点”的return value
引脚;然后在“获取拥有玩家Pawn”后添加Is Valid
判空。编译保存运行,可以看到数字能够实时显示血量了。