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元素的直接子元素才能设置锚点。接下来看看如何使用锚点。

如图,这是一个元素的细节面板,它的锚点信息如下:

可以用三种方式调整锚点:

  1. 在上图中展开Anchors,然后选择锚点的贴靠位置,然后Ctrl+Shift+左键完成对元素和锚点的定位操作。
  2. 在上图中设置锚点X和Y的最小值和最大值。
  3. 在设计器中手动拖拽锚点的四个角,然后按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》