[UE4]C++操作UMG Widget实例(demo, example,例子)
这里演示在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
下面代码中的实现思路有点不科学,因为把很多UI相关的操作放在GameMode内部。建议参考新文章中的方法:http://aigo.iteye.com/blog/2296218
// 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);
}
UMGDemo.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "Engine.h"
#include "MyBlueprintFunctionLibrary.h"
#include "Runtime/UMG/Public/UMG.h"
#include "Runtime/UMG/Public/UMGStyle.h"
#include "Runtime/UMG/Public/Slate/SObjectWidget.h"
#include "Runtime/UMG/Public/IUMGModule.h"
#include "Runtime/UMG/Public/Blueprint/UserWidget.h"
10,在UMyUserWidget类中添加一个蓝图函数,在这个函数内调用GameMode的ChangeHeroIcon()函数。这里的宏定义关键字使用了“BlueprintCallable”,表示蓝图中可以调用的函数
MyUserWidget.h
public:
//暴露给蓝图调用的函数,处理Button的OnClicked事件
UFUNCTION(BlueprintCallable, Category = "WidgetFunction")
void handleMyButtonClick();
MyUserWidget.cpp
void UMyUserWidget::handleMyButtonClick()
{
AUMGDemoGameMode* GameModePtr = UMyBlueprintFunctionLibrary::GetGameModeInstance();
GameModePtr->ChangeHeroIcon();
}
11,在Widget蓝图中添加鼠标点击事件OnClicked
然后这OnClicked节点中拖出一个连线,输入函数名handleMyButtonClick,即可看到这个函数(如果看不到需要重新编译下,前提时上面第7个步骤执行过),并选择这个函数
12,建立一个GameMode蓝图。
因为我们再GameMode代码中定义了一个MyWidgetClass成员变量,这个变量类型是一个Widget蓝图模版,要对这个变量赋初值,需要再蓝图脚本中设置(可能也有代码方式来执行对蓝图模版类型的变量赋初值的方法,但我还没找到相关资料)。
操作方式是:右击GameMode class,选择创建GameMode蓝图,名字起为:MyUMGDemoGameMode_BP
建好以后打开GameMode蓝图,然后找到C++代码里面定义的那个成员变量MyWidgetClass,然后选择我们之前创建的Widget蓝图MyWidgetBlueprint。(之前遇到一个很诡异的问题,在GameMode蓝图中设置好MyWidgetClass变量的初始值以后没有生效,需要这个初始值清空后重新设置一遍,或者在菜单Settings -》 World Settings下面设置下默认的GameMode)
13,最后,设置默认GameMode为这个GameMode蓝图,菜单Edit -》Project Settings -》 Project -> Maps & Modes -》 Default Modes -》 Default GameMode:
最终效果如下图,每当点击一次按钮,按钮的图片就会切换一次
此实例的完成工程下载为UMGDemo.rar附件
再给一个备用的站外链接: