5 - UMG入门
本文将初步介绍UE5中有关游戏UI的内容,包括游戏UI是什么,UMG基础知识等内容。
游戏UI
通常,用户界面UI在游戏渲染画面层之上,这意味者可以层次化管理UI,且UI是渲染在最上层的。但嵌入式用户界面(Diegetic UI)除外,这种UI存在于游戏渲染画面层中,例如游戏《死亡空间》中的血条,渲染在玩家的背后。
游戏UI通常有两种:
- 菜单:允许玩家通过按下按钮或输入设备上的按键进行交互,例如:
- 主菜单:玩家可以选择是否继续游戏、创建新游戏、退出游戏等;
- 关卡选择菜单:玩家可以选择玩哪个关卡;
- 其他形式。
- HUD:在游戏过程中呈现的UI面板,会提供给玩家一些应该即时了解的信息,例如人物血条,子弹量。
在UE5中,创建UI的主要方法是使用 Unreal Motion Graphics(UMG),该工具允许我们制作游戏UI控件,包括上面说的菜单和HUD,并将它们添加到视口上。
UMG入门
UMG工具允许我们以控件的形式进行创作和编辑UI。通过它的”设计器“选项卡,可以用可视化方式轻松编辑UI;同时允许我们通过”图表“选项卡向游戏UI中添加新功能。
控件是UE5呈现游戏UI的方式。它可以是基本的UI元素,例如按钮、文本、图像等;也可以是这些元素的组合,如菜单、HUD等。
创建控件蓝图
要想创建一个控件,得先创建一个父类为UserWidget
的蓝图类。这里创建一个用于人物死亡时的菜单控件,起名为BP_RestartWidget
:
在左下角可以看到这些元素的树状结构,这些组件必须是一个Canvas Panel
的子节点,否则将无法显示。
锚点简介
玩家会在不同尺寸和分辨率的屏幕上运行游戏,需要确保创建的UI能够适应不同分辨率的屏幕。而锚点(Anchor)就是实现该目标的主要手段。
锚点通过指定UI元素在视口中所占的比例,来定义其大小如何随屏幕分辨率的变化而变化。需要注意的是,只有Panel元素的直接子元素才能设置锚点。接下来看看如何使用锚点。
如图,这是一个元素的细节面板,它的锚点信息如下:
可以用三种方式调整锚点:
- 在上图中展开Anchors,然后选择锚点的贴靠位置,然后Ctrl+Shift+左键完成对元素和锚点的定位操作。
- 在上图中设置锚点X和Y的最小值和最大值。
- 在设计器中手动拖拽锚点的四个角,然后按Ctrl更新控件位置。
分辨率测试
我们还可在”设计器“面板中以不同的分辨率来可视化控件,这需要拖动画布轮廓右下角的控制点:
创建控件C++类
接下来我们将创建一个控件C++类,然后让上面的蓝图重新继承该类。不过在创建类前,得先让Build.cs
包含UMG相关控件:
PublicDependencyModuleNames.AddRange(new string[]
{
"Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "HeadMountedDisplay", "UMG", "Slate", "SlateCore"
});
然后创建一个继承于UserWidget
类的C++类RestartWidget
,用于编写上边GAME OVER界面的逻辑:
// RestartWidget.h
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "RestartWidget.generated.h"
UCLASS()
class DODGEBALL_API URestartWidget : public UUserWidget
{
GENERATED_BODY()
public:
// 和蓝图子类绑定的按钮, 在蓝图中编辑属性, 在C++中访问它
UPROPERTY(meta = (BindWidget))
class UButton* RestartButton;
UPROPERTY(meta = (BindWidget))
class UButton* ExitButton;
public:
// 初始化后的回调, 类似于Actor的BeginPlay()
virtual void NativeOnInitialized() override;
protected:
// 重启按钮被按下时的回调, 用宏标记是为了充当AddDynamic()的正确参数
UFUNCTION()
void OnRestartClicked();
UFUNCTION()
void OnExitClicked();
};
其中:
- 声明了
UPROPERTY(meta = (BindWidget))
的两个属性,将该变量和要继承的同名蓝图类控件绑定。如果希望不是必须要绑定同名控件,则可以声明UPROPERTY(meta = (BindWidget, OptionalWidget = true))
。 - 声明了
NativeOnInitialized()
方法,这个方法类似于Actor类的BeginPlay()
,用于初始化。 - 声明了两个按钮按下去的回调,只有标记为
UFUNCTION()
才能正确添加回调。
代码实现如下:
// RestartWidget.cpp
void URestartWidget::NativeOnInitialized()
{
Super::NativeOnInitialized();
// 绑定按下后的回调
if (RestartButton != nullptr)
{
RestartButton->OnClicked.AddDynamic(this, &URestartWidget::OnRestartClicked);
}
if (ExitButton != nullptr)
{
ExitButton->OnClicked.AddDynamic(this, &URestartWidget::OnExitClicked);
}
}
void URestartWidget::OnRestartClicked()
{
// TODO
}
void URestartWidget::OnExitClicked()
{
UKismetSystemLibrary::QuitGame(GetWorld(), nullptr, EQuitPreference::Quit, true);
}
其中,我们绑定了两个按钮按下时的回调函数,实现了退出的回调,重启游戏的回调得等我们实现PlayerController类后再说,它可以管理玩家对UI的输入和交互。接下来看看按钮的事件,我们刚用到了OnClicked
事件:
OnClicked
事件:玩家单击和释放按钮时触发;OnPressed
事件:玩家按下按钮时触发;OnReleased
事件:玩家释放按钮时触发;OnHover
事件:玩家悬停在按钮时触发;OnUnhover
事件:玩家将光标从按钮上移走时触发;
编译代码后,就能给刚刚的蓝图类重设父类为这个C++类了。记得编译一下,如果出现错误通常是因为成员变量名和UI组件名不一样。
创建PlayerController和UI交互
我们希望当角色死亡时才跳出这个页面,这需要PlayerController类帮忙。新建一个DodgeballPlayerController
:
// DodgeballPlayerController.h
UCLASS()
class DODGEBALL_API ADodgeballPlayerController : public APlayerController
{
GENERATED_BODY()
public:
// 在蓝图中绑定的UMG蓝图类
UPROPERTY(EditDefaultsOnly)
TSubclassOf<class URestartWidget> BP_RestartWidget;
private:
// PlayerController使用的UMG实例
// 该属性不应该在蓝图类中被编辑, 加上宏只是为了防止GC
UPROPERTY()
class URestartWidget* RestartWidget;
public:
void ShowRestartWidget();
void HideRestartWidget();
};
首先声明用于绑定的UI蓝图子类属性,用于待会创建PlayerController蓝图子类时设置。然后声明PlayerController类自身使用的UMG实例,需要加上UPROPERTY()
防止被GC提前删除。最后声明让这个UMG组件显示和隐藏的方法。
接下来实现相关方法:
// .cpp
void ADodgeballPlayerController::ShowRestartWidget()
{
if (BP_RestartWidget != nullptr)
{
// 暂停游戏
SetPause(true);
// 更新输入模式为UIOnly, 只让屏幕控件接收玩家输入
SetInputMode(FInputModeUIOnly());
// 显示光标
bShowMouseCursor = true;
// 实例化RestartWidget, 然后将其添加到屏幕上
RestartWidget = CreateWidget<URestartWidget>(this, BP_RestartWidget);
RestartWidget->AddToViewport();
}
}
void ADodgeballPlayerController::HideRestartWidget()
{
if (RestartWidget != nullptr)
{
// 移除并删除控件
RestartWidget->RemoveFromParent();
RestartWidget->Destruct();
// 取消ShowRestartWidget()中的设置
bShowMouseCursor = false;
SetInputMode(FInputModeGameOnly());
SetPause(false);
}
}
在ShowRestartWidget()
中,首先判断BP_RestartWidget
是否被配置好,然后暂停游戏,设置玩家输入模式,实例化并显示RestartWidget
。而HideRestartWidget()
则是前者的逆过程。
在UE5中,有3种输入模式:
GameOnly
:玩家角色和玩家控制器将通过Input Action接收输入;UIOnly
:屏幕上显示的控件将会接收玩家输入;GameAndUI
:接收包含上面二者的输入。
接着需要在玩家角色类中使用它:
// character.cpp
void ADodgeballCharacter::OnDeath_Implementation()
{
// 调用DodgeballPlayerController, 让它生成重启界面
ADodgeballPlayerController* PlayerController = Cast<ADodgeballPlayerController>(GetController());
if (PlayerController != nullptr)
{
PlayerController->ShowRestartWidget();
}
}
别忘了回去实现控件中的回调:
void URestartWidget::OnRestartClicked()
{
// 按钮被点击后就能隐藏此窗口了
ADodgeballPlayerController* PlayerController = Cast<ADodgeballPlayerController>(GetOwningPlayer());
if (PlayerController != nullptr)
{
PlayerController->HideRestartWidget();
}
UGameplayStatics::OpenLevel(this, FName(*UGameplayStatics::GetCurrentLevelName(this)));
}
最后编译代码,新建基于此PlayerController的蓝图子类,配置好它。在更改游戏模式类或等效设置后,就能看到玩家死亡后会跳出重启界面供玩家选择了。
参考资料
中文翻译:《UE5 C++ 游戏开发完全学习教程》
英文原版:《Elevating Game Experiences with UE5》