这里演示在UE4中UMG使用和C++代码操作Widget对象的示例。实现的功能有:
- 如何在widget蓝图中制作UI模版;
- 如何用C++动态加载资源(UTexture2D);
- 当触发widget组件的事件时如何调用C++函数;
例子的完成工程(包括所有代码)在文章末尾下载。
这个例子的部分代码参考自UE4官方项目的源码,有兴趣深入研究可以去看那些UI比较丰富的官方项目。
注:当前使用的是最新版本v4.10。下面的代码涉及的API可能在后续版本中变动,注意下。
具体步骤:
1,先新建一个空的C++工程
2,然后我们新建一个自己的PlayerController:工程编辑器中的点击File -》 new C++ class -》 选择Player Controller -》 下一步 -》 起一个类名:MyPlayerController
自定义PlayerController是为了添加角色控制的逻辑,每个游戏都有特定的角色(可以是3d人身角色,也可以是一个2d面板),所以一个正常的游戏都会需要添加自己的PlayerController,当前例子没有角色,这里添加PlayerController是为了将后面的UMG widget实例加入到PlayerController中。
我们先在PlayerController构造函数中添加一些配置代码,来实现鼠标显示,默认是不显示:
AMyPlayerController::AMyPlayerController() { //显示鼠标 bShowMouseCursor = true; DefaultMouseCursor = EMouseCursor::Crosshairs; //启用鼠标事件 bEnableClickEvents = true; bEnableMouseOverEvents = true; }
3,再GameMode类的构造函数中,设置默认的PlayerController为刚刚我们新建的PlayerController:
UMGDemoGameMode.h
// Fill out your copyright notice in the Description page of Project Settings. #include "UMGDemo.h" #include "UMGDemoGameMode.h" #include "MyPlayerController.h" AUMGDemoGameMode::AUMGDemoGameMode() { //使用自定义PlayerController类 PlayerControllerClass = AMyPlayerController::StaticClass(); }
4,再新建一个自定义的UBlueprintFunctionLibrary,这个类是一个提供各种通用的静态函数的管理类,我们将在这个类中实现一个获取自定义PlayerController的静态函数。工程编辑器中的点击File -》 new C++ class -》 勾选Show All Classes -》 找到BlueprintFunctionLibrary并选中 -》 下一步 -》 类名我们起为:MyBlueprintFunctionLibrary
添加完毕后会自动编译,这里会提示需要在编辑器中重新Compile编译一次,否则编辑器的内容视图中看不到这个文件,这个窗口我们先关掉,后面会统一编译
5,在“工程名.h”头文件加入我们刚刚新建的BlueprintFunctionLibrary:
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "Engine.h" #include "MyBlueprintFunctionLibrary.h"
并实现一个静态的函数用于获取自定义PlayerController:
MyBlueprintFunctionLibrary.h
public: /** 获取所有PlayerController列表,然后再找到本地客户端的第一个PlayerController并返回 */ UFUNCTION(BlueprintCallable, Category = "TD_Test Gameplay") static APlayerController* GetLocalPlayerController(UObject* WorldContextObject);
MyBlueprintFunctionLibrary.cpp
APlayerController* UMyBlueprintFunctionLibrary::GetLocalPlayerController(UObject* WorldContextObject) { if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject)) { for (FConstPlayerControllerIterator Iterator = World->GetPlayerControllerIterator(); Iterator; ++Iterator) { APlayerController* PlayerController = *Iterator; if (PlayerController->IsLocalController()) { // For this project, we will only ever have one local player. return PlayerController; } } } return nullptr; }
6,新建一个自定义UserWidget类,添加的目的是:将widget组件需要调用的C++逻辑都放到这个类中,且只能放到自定义UserWidger类中,如果你想在UMG蓝图中让widget组件调用UserWidget类之外的函数是行不通的。工程编辑器中的点击File -》 new C++ class -》 勾选Show All Classes -》 找到UserWidget并选中 -》 下一步 -》 类名我们起为:MyUserWidget
添加完以后还无法编译,需求在“工程名.Build.cs”中加入配置,加入红色部分即可:
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "UMG", "Slate", "SlateCore" });
7,添加Widget蓝图,并将此蓝图的父类指定为刚刚新建的MyUserWidget class。右击内容视图-》用户界面-》Widget蓝图,名字起为:MyWidgetBlueprint
然后双击打开蓝图,点击Graph(图形)界面 -》 点击 Class settings -》 找到Parent Class -》 选择我们刚刚新建的自定义UserWidget类。(如果找不到自定义UserWidget,在编辑器中重新编译下代码,实在不行重启下编辑器)
8,再回到Designer界面,从左侧的组件列表中,我们拖拽几个组件进来,分别是:Button,Overlay,Image。层级关系是:Button 《 Overlay 《 Image
后面会实现这样的一个功能:给定两张图片(Texture2d),每点击一次,button的图片变一次。这个功能其实也可以用存蓝图脚本实现,这里为了演示蓝图调用C++函数,我们把替换图片的逻辑放在C++代码中。官方文档说,对于调用非常频繁的逻辑,若要提高性能,建议用C++实现而不要在蓝图中实现。
先修改widget组件的一些参数:
设置设置button的大小,因为我们使用的图片大小是205X115,所以button的大小也这样设置
属性Style中Normal、Hovered、Pressed中的Draw As都修改为None,因为按钮的图片我们准备设置在Image组件当中。之所以不直接在button中设置背景图片,是因为仅仅依靠一个button组件,无法只用一张图片就实现按钮下压的效果,这里我们想只用一张图片来实现按钮下压的效果,所以除了button,还用了Overlay和Image两种组件
然后再设置下Image组建的size,设置为图片一样大小:205x115。
控制button按下后图片浮动的上下左右距离,在Style下的Pressed Padding中设置,如果想要浮动的效果明显一点,根据需要设置大一下:
到此我们把UMG widget蓝图模版绘制完成,接下来我们用C++代码操作这些widget组件。
9,首先我们在GameMode类中重载父类的BeginPlay()函数
/** Called when the game starts. */ virtual void BeginPlay() override;
同时在GameMode内定义了几个函数:
//创建widget对象并添加到Viewport
void CreateWidgetInstance();
//获取需要操作数据的widget对象
void FindImageComponents();
//动态加载icon所需Texture2D
void LoadAssetsDynamic();
其中CreateWidgetInstance()函数用来创建Widget实例对象(以之前的MyUserWidgetBlueprint为模版),并添加到游戏的Viewport中;
FindImageComponents()用来获取创建的widget实例中的Image组件的引用,因为我们后面要用代码操作这个Image组件,所以先把这个组件的指针存起来,不用每次操作时再查找;
LoadAssetsDynamic()用来加载Texture资源。我们对Image组件更换图片时,实际是对Texture的更换,所以,进入游戏时我们就先加载到内存中,等后面点击button的时候,直接修改Image组件中对这些Texture内存的引用,来实现图片切换。
具体代码如下:
UMGDemoGameMode.h
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "GameFramework/GameMode.h" #include "Blueprint/UserWidget.h" #include "Runtime/UMG/Public/Components/Image.h" #include "UMGDemoGameMode.generated.h" /** * */ UCLASS() class UMGDEMO_API AUMGDemoGameMode : public AGameMode { GENERATED_BODY() public: AUMGDemoGameMode(); /** Called when the game starts. */ virtual void BeginPlay() override; private: UImage* imageHero; UTexture2D* texHero1; UTexture2D* texHero2; UTexture2D* defaultTexHero; protected: /** The widget class we will use as our game over screen when the player wins. */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Game") TSubclassOf<UUserWidget> MyWidgetClass; /** The widget instance that we are using as our menu. */ UPROPERTY() UUserWidget* MyWidgetInstance; //创建widget对象并添加到Viewport void CreateWidgetInstance(); //获取需要操作数据的widget对象 void FindImageComponents(); //动态加载icon所需Texture2D void LoadAssetsDynamic(); public: //修改icon图片:切换英雄时调用 void ChangeHeroIcon(); };
UMGDemoGameMode.cpp
// Fill out your copyright notice in the Description page of Project Settings. #include "UMGDemo.h" #include "UMGDemoGameMode.h" #include "MyPlayerController.h" AUMGDemoGameMode::AUMGDemoGameMode() { //使用自定义PlayerController类 PlayerControllerClass = AMyPlayerController::StaticClass(); imageHero = NULL; texHero1 = NULL; texHero2 = NULL; defaultTexHero = NULL; } void AUMGDemoGameMode::BeginPlay() { Super::BeginPlay(); UMyBlueprintFunctionLibrary::SetGameModeInstance(this); CreateWidgetInstance(); FindImageComponents(); LoadAssetsDynamic(); ChangeHeroIcon(); } //创建widget对象并添加到Viewport void AUMGDemoGameMode::CreateWidgetInstance() { if (MyWidgetInstance) { MyWidgetInstance->RemoveFromViewport(); MyWidgetInstance = nullptr; } if (MyWidgetClass) { if (AMyPlayerController* PC = Cast<AMyPlayerController>(UMyBlueprintFunctionLibrary::GetLocalPlayerController(this))) { MyWidgetInstance = CreateWidget<UUserWidget>(PC, MyWidgetClass); if (MyWidgetInstance) { MyWidgetInstance->AddToViewport(); } } } } //获取需要操作数据的widget对象 void AUMGDemoGameMode::FindImageComponents() { if (UCanvasPanel* CanvasPanelWidget = Cast<UCanvasPanel>(MyWidgetInstance->GetRootWidget())) { //获取英雄头像的UImage指针 if (UButton* btn = Cast<UButton>(CanvasPanelWidget->GetChildAt(0))) { if (UOverlay* overlay = Cast<UOverlay>(btn->GetChildAt(0))) { if (UImage* image = Cast<UImage>(overlay->GetChildAt(0))) { imageHero = image; } } } } } //动态加载icon所需Texture2D void AUMGDemoGameMode::LoadAssetsDynamic() { //加载英雄头像的Texture if (texHero1) { //如果已经加载过,则先销毁掉 texHero1->ConditionalBeginDestroy(); texHero1 = NULL; GetWorld()->ForceGarbageCollection(true); } texHero1 = Cast<UTexture2D>(StaticLoadObject(UTexture2D::StaticClass(), NULL, TEXT("Texture2D'/Game/Textures/hero1.hero1'"))); if (texHero2) { texHero2->ConditionalBeginDestroy(); texHero2 = NULL; GetWorld()->ForceGarbageCollection(true); } texHero2 = Cast<UTexture2D>(StaticLoadObject(UTexture2D::StaticClass(), NULL, TEXT("Texture2D'/Game/Textures/hero2.hero2'"))); defaultTexHero = texHero2; } void AUMGDemoGameMode::ChangeHeroIcon() { if (!defaultTexHero || !texHero1 || !texHero1 || !imageHero) { return; } defaultTexHero = defaultTexHero == texHero1 ? texHero2 : texHero1; imageHero->SetBrushFromTexture(defaultTexHero); }