UE5-Aura笔记-UI制作

RPG GAME UI

img

在UE5中,UI是由一个个的Widgets 构成的,Widgets 属于类UUSerWidget,而我们游戏的属性等数据在GAS的AttributeSet 中,Widgets该如何去获取这些属性呢?

img

我们真的应该去在Widgets 中深入每一个类,然后去获取那些对象中的属性吗?

img

在UI中,这些可视化的部件叫做View视图,而数据的总和叫做Model,我们的任务就是从Model中获取所需要的数据然后传递给View

img

我们想要一个中间层Controller来管理视图与数据间的关系,Controller可以从Model中检索数据,并将这些数据广播到View中,这个中间层不仅能够负责获取数据,还要能够有处理数据的逻辑(比如在地图上计算点到点的距离、设计最短路线等等),我们叫它WidgetController; 这使得View层可以专注于呈现UI,Model层可以只限于提供数据

img

以及,在我们摁下按钮时,可以对数据进行一定的改动,比如技能加点,天赋加点等等

在前端中,有一个架构叫做MVVM MVVM模式(Model-View-ViewModel)架构模式,是将View和ViewModel关联起来,通过双向数据绑定实现View和ViewModel的同步更新。View负责展示数据和用户交互,ViewModel负责处理数据和业务逻辑,Model负责存储数据。MVVM的优点是能够降低View和ViewModel之间的耦合,使得代码更加可维护和可测试。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

初步构建MVVM系统

新建C++类AuraUserWidget , AuraWidgetController

AuraUserWidget父类为UserWidget

img

img

AuraWidgetController父类为Object

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在我们先构建AuraUserWidget与WidgetController间的联系

//AuraUserWidget.h
UCLASS()
class AURA_API UAuraUserWidget : public UUserWidget
{
    GENERATED_BODY()
public:
    //蓝图调用方法,可以设置WidgetController
    UFUNCTION(BlueprintCallable)
    void SetWidgetController(UObject* InWidgetController);
    //WidgetController
    UPROPERTY(BlueprintReadOnly)
    TObjectPtr<UObject> WidgetController;
protected:
    UFUNCTION(BlueprintImplementableEvent)
    void WidgetControllerSet();
};
//AuraUserWidget.cpp
void UAuraUserWidget::SetWidgetController(UObject* InWidgetController)
{
    WidgetController = InWidgetController;
    WidgetControllerSet();
}

然后我们想想在WidgetController中,有哪些数据需要获取?

PlayerController` ,`PlayerState`,`AbilitySystemComponent`,`AttributeSet
//WidgetController.h
class UAttributeSet;
class UAbilitySystemComponent;
/**
 * 
 */
UCLASS()
class AURA_API UAuraWidgetController : public UObject
{
    GENERATED_BODY()
protected:
    UPROPERTY(BlueprintReadOnly,Category="WidgetController")
    TObjectPtr<APlayerController> PlayerController;
    
    UPROPERTY(BlueprintReadOnly,Category="WidgetController")
    TObjectPtr<APlayerState> PlayerState;
    
    UPROPERTY(BlueprintReadOnly,Category="WidgetController")
    TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent;
    
    UPROPERTY(BlueprintReadOnly,Category="WidgetController")
    TObjectPtr<UAttributeSet> AttributeSet;
};

制作模板球体UI

我们有血量,蓝量等球体UI,如下图,我们想要创建一个模板的球体UI,可以让血量蓝量等球体UI可以复用

img

  1. 创建UI的层级

img

将SizeBox,Image_Background,ProgressBar,Image_Class全部设置为是变量

  1. 设置模板的默认值

img

将SizeBox的结构设置为所需,并设置为变量,打开宽度重载和高度重载 我们在复用时,可能会在子类中修改高度和宽度

Overlay设置水平对齐和垂直对其为填充

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

设置ImageBackGround水平垂直填充

img

设置ProgressBar_Globe水平垂直填充

img

ImageGlass 设置水平垂直填充

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 设置可被子类覆盖的变量
  • BoxSize

新建变量BoxHeight,BoxWidth,设置为浮点,新建类别GlobeProperties

img

然后设置默认值都为250.0

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • ImageBackGround

img

设置默认值

img

  • ProgressBar

img

新增变量ProgressBarFillImage为Slate笔刷,设置默认值(这个可以不设置),必须设置Tint的颜色为纯透明

Tint为填充背景图

img

  • GlobePadding(用于进度条的padding)

img

  • ImageGlass

img

变量为Slate笔刷,默认设置背景图为MI_Empty

  • GlassPadding(ImageGlass的padding)

img

变量为float,默认值为10.0

然后模板球体UI就做好了,接下来可以制作继承自模板球体UI的生命值和法力值UI了

制作生命值法力值UI

新建控件蓝图,继承自刚才的模板UI

img

img

然后在蓝图中,设置显示继承的变量

img

设置ProgressBarFillImage的值为MI_HealthGlobe就可以了

img

法力值同理,将ProgressBarFillImage 设置为MI_ManaGlobe

制作一个UI汇总的Overlay

我们会将所有UI组件都放置到同一个Overlay中,我们可以拖拽UI组件进去然后布局,Overlay就是我们用户界面的总布局

我们创建一个Canvas,设置大小为填充屏幕

img

然后我们就可以把UI组件拖进Canvas中,设置他们的锚点都为BottomCenter

img

img

然后我们进入关卡蓝图

img

创建新控件

img

然后选择WBP_Overlay,添加进视口

img

进入关卡就可以看到效果了

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

创建HUD

但是,在关卡中设置UI展现是不太好的,因为这会使得在每个关卡都要放置UI,此时,有一个叫做HUD的类可以帮助我们在GameMode上面放置UI并展示在视口

img

此时我们先创建一个AuraHUD的C++类,继承自HUD类

img

//AuraHUD.h
class AURA_API AAuraHUD : public AHUD
{
    GENERATED_BODY()
public:
    UPROPERTY()
    TObjectPtr<UAuraUserWidget> OverlayWidget;
protected:
    virtual void BeginPlay() override;
private:
    //可以在蓝图中放置所需的Widget类
    UPROPERTY(EditAnywhere)
    TSubclassOf<UAuraUserWidget> OverlayWidgetClass;
};
//AuraHUD.cpp
void AAuraHUD::BeginPlay()
{
    Super::BeginPlay();
    //创建Widget,其类为蓝图上放置的Overlay类
    UUserWidget* Widget = CreateWidget<UUserWidget>(GetWorld(),OverlayWidgetClass);
    //添加进视口
    Widget->AddToViewport();
}

然后在Blueprints/UI/HUD中创建BP_AuraHUD,设置OverlayWidgetClass

img

最后在AuraGameMode上设置HUD类为BP_AuraHUD

img

删除关卡蓝图上的Widget,启动游戏,UI依然存在

img

将数据与视图绑定

初始化WidgetController

创建一个C++类OverlayWidgetController

img

我们前面经创建了控制器层基类。接下来会基于控制器层基类创建一个UI专用的控制层类供UI使用。

在基类中,添加一个结构体FWidgetControllerParams,供我们初始化WidgetConstoller所需的数据

//AuraWidgetController.h
struct FWidgetControllerParams
{
    GENERATED_BODY()
    FWidgetControllerParams(){};
    FWidgetControllerParams(APlayerController* PC,APlayerState* PS,
       UAbilitySystemComponent* ASC,UAttributeSet* AS):
    PlayerController(PC),PlayerState(PS),AbilitySystemComponent(ASC),AttributeSet(AS){};
    
    UPROPERTY(EditAnywhere,BlueprintReadWrite)
    TObjectPtr<APlayerController> PlayerController = nullptr;
    
    UPROPERTY(EditAnywhere,BlueprintReadWrite)
    TObjectPtr<APlayerState> PlayerState = nullptr;
    
    UPROPERTY(EditAnywhere,BlueprintReadWrite)
    TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent = nullptr;
    
    UPROPERTY(EditAnywhere,BlueprintReadWrite)
    TObjectPtr<UAttributeSet> AttributeSet = nullptr;
};

class AURA_API UAuraWidgetController : public UObject
{
    GENERATED_BODY()
public:
    //成员函数,绑定所需的数据至WidgetController
    UFUNCTION(BlueprintCallable)
    void SetWidgetControllerParams(const FWidgetControllerParams& WCParams);
    //...
};
//AuraWidgetController.cpp
void UAuraWidgetController::SetWidgetControllerParams
    (const FWidgetControllerParams& WCParams)
{
    PlayerController = WCParams.PlayerController;
    PlayerState = WCParams.PlayerState;
    AbilitySystemComponent = WCParams.AbilitySystemComponent;
    AttributeSet = WCParams.AttributeSet;
}

然后在AuraHUD中添加初始化UI视图和WidgetController

//AuraHUD.h
class AURA_API AAuraHUD : public AHUD
{
    GENERATED_BODY()
public:
    UPROPERTY()
    TObjectPtr<UAuraUserWidget> OverlayWidget;
    //获取WidgetController
    UOverlayWidgetController* GetOverlayWidgetController
        (const FWidgetControllerParams& WCParams);
    //初始化WidgetController和Widget
    void InitOverlay(APlayerController* PC,APlayerState* PS,
       UAbilitySystemComponent* ASC,UAttributeSet* AS);
//在此时,我们不能在BeginPlay中初始化WidgetController,
//因为在BeginPlay时,我们不知道ASC,AS等数据是否已经初始化完毕
//我们必须等数据都初始化完毕后,再去初始化WidgetController,我们需要一个时机去调用InitOverlay
private:
    //WidgetController
    UPROPERTY()
    TObjectPtr<UOverlayWidgetController> OverlayWidgetController;
    //WidgetController所需的WidgetController类,可以在蓝图中设置
    //(因为子类可能所需的WidgetController不同)
    UPROPERTY(EditAnywhere)
    TSubclassOf<UOverlayWidgetController> OverlayWidgetControllerClass;
  
    
    //可以在蓝图中设置所需的Widget类
    UPROPERTY(EditAnywhere)
    TSubclassOf<UAuraUserWidget> OverlayWidgetClass;
};
//AuraHUD.cpp
UOverlayWidgetController* AAuraHUD::GetOverlayWidgetController(const FWidgetControllerParams& WCParams)
{
    if(OverlayWidgetController == nullptr)
    {
       OverlayWidgetController = NewObject<UOverlayWidgetController>(this,OverlayWidgetClass);
       OverlayWidgetController->SetWidgetControllerParams(WCParams);
    }
    return OverlayWidgetController;
}

void AAuraHUD::InitOverlay(APlayerController* PC,APlayerState* PS,
       UAbilitySystemComponent* ASC,UAttributeSet* AS)
{
    checkf(OverlayWidgetClass,TEXT("OverlayWidgetClass没有在蓝图中初始化"))
    checkf(OverlayWidgetControllerClass,TEXT("OverlayWidgetControllerClass没有在蓝图中初始化"))

    
    //创建Widget,其类为蓝图上放置的Overlay类
    UUserWidget* Widget = CreateWidget<UUserWidget>(GetWorld(),OverlayWidgetClass);

    
    OverlayWidget = Cast<UAuraUserWidget>(Widget);
    //初始化WidgetController
    const FWidgetControllerParams WidgetControllerParams(PC,PS,ASC,AS);
    UOverlayWidgetController* WidgetController = GetOverlayWidgetController(WidgetControllerParams);
    
    OverlayWidget->SetWidgetController(WidgetController);
    //添加进视口
    Widget->AddToViewport();
    
}

NewObject 既需要提供模板类型(如 NewObject<UMyObject>())又可以接受一个 UClass 参数,主要是出于灵活性和类型安全性的考虑。我们来详细解释一下这个设计背后的原因。

1. 模板类型的用途:编译时类型安全

模板参数提供了编译时类型安全的机制。通过模板参数 T,编译器可以确保你创建的对象类型是确定的,这样在编译期就能检查类型是否匹配,从而避免不必要的运行时错误。

UMyObject* MyObjectInstance = NewObject<UMyObject>();

上面的代码中,模板 UMyObject 确定了创建的对象类型,并且函数返回值类型也是 UMyObject*。这种方式的好处是:

  • 类型安全:编译器在编译时能够确保你正在创建的对象类型是正确的。如果你尝试将返回的对象赋值给一个不兼容的类型,编译器会发出错误。
  • 简洁:通过模板参数,你可以避免显式地提供 UClass 类型,使代码简洁且易于阅读。

2. UClass 参数的用途:运行时动态性

UClass* 参数则提供了运行时的灵活性。虽然模板可以在编译时提供类型安全性,但在某些情况下,你可能无法在编译期确定要创建的类型,需要在运行时动态决定。这时就需要通过 UClass 来决定对象的类型。

UClass* DynamicClass = GetSomeClass();  // 从运行时动态获取类` `UObject* DynamicObject = NewObject<UObject>(Outer, DynamicClass);

在上面的代码中,DynamicClass 可以在运行时根据游戏逻辑动态变化。这种情况下,模板类型 UObject 作为基类,允许你动态地指定派生类类型。这样你就可以在运行时决定创建哪个类的实例,而不局限于编译期的模板类型。

然后我们要思考,在哪里能够调用InitOverlay这个函数,一定是在PlayerController,PlayerState,ASC和AS都初始化完成的那个时候,也就是AuraCharacter中的InitAbilityActorInfo里

//AuraCharacter.cpp
void AAuraCharacter::InitAbilityActorInfo()
{
    
    AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
    //初始化PlayerState中ASC的信息
    AuraPlayerState->GetAbilitySystemComponent()->InitAbilityActorInfo(AuraPlayerState,this);
    //将PlayerState中的ASC和AS赋给Character,因为在Character构造函数中没有为ASC和AS赋值
    AbilitySystemComponent = AuraPlayerState->GetAbilitySystemComponent();
    AttributeSet = AuraPlayerState->GetAttributeSet();


    //InitOverlay,此时PlayerController,PlayerState,ASC,AS都初始化好了,可以将这些数据与WidgetController绑定
    //在多人游戏的客户端中,这个PlayerController可能为空,因为每个玩家只能拥有自己的PlayerController
    //只有对于自己控制的角色,PlayerController不为空,当然,也不需要在自己的屏幕上添加其他角色的HUD
    if(AAuraPlayerController* PlayerController = Cast<AAuraPlayerController>(GetController()))
    {
       //如果HUD有效,那么我们可以将数据通过HUD与WidgetController绑定
       if(AAuraHUD* AuraHUD = Cast<AAuraHUD>(PlayerController->GetHUD()))
       {
          AuraHUD->InitOverlay(PlayerController,AuraPlayerState,AbilitySystemComponent,AttributeSet);
       }
    }
}

我们在这时可以想到,在多人游戏中,很多时候我们都要判断是否为空指针,比如在AAuraPlayerController::BeginPlay 里,

//...
//check(Subsystem)不能使用,否则在多人游戏中有其他玩家,游戏会崩溃
//在多人游戏中,如果有其他的玩家,此时在本机初始化其他玩家的时候,Subsystem为空
UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer());
if(Subsystem){
    Subsystem->AddMappingContext(AuraContext,0);
}
//...

随后我们在BP_AuraHUD中,设置OverlayWidgetControllerClass与OverlayWidgetClass

img

向View广播数据值

在AuraHUD中,OverlayWidget->SetWidgetController(WidgetController); 我们给Widget设置了WidgetController,此时,WidgetController不知道有谁绑定了它,只有Widget知道它绑定了WidgetController,我们想要在Widget中获取WidgetController中的数据,我们想要一种单向数据流来做这件事情,

我们可以使用GALEGATE 委托来广播数据;委托和JS当中的Event事件相似,是UE中一种自定义的消息机制

  • UE****中的事件委托
    • 使用过程

    • 申明一个代理(消息)类型

    • DECLARE_DELEGATE(FStandardDelegateSignature)

    • FStandardDelegateSignature就是我们自己定义代理类型,可以将FStandardDelegateSignature看成使用UE宏DECLARE_DELEGATE声明的一个类(Class)

    • 定义一个代理(消息)

    • FStandardDelegateSignature MyStandardDelegate;

    • MyStandardDelegate就是我们自己定义的一个代理,可以看成FStandardDelegateSignature的一个实例Instance

    • 绑定响应函数

    • MyStandardDelegate.BindUObject(this, &ADelegateListener::EnableLight)

    • 绑定响应函数就是定义代理(消息)被触发时的响应函数,跟Javascript中的callback函数原理是一样的。

    • 触发代理(消息)

    • MyStandardDelegate.ExeIfBound();

    • 前面三步已经完成消息的注册,这一步就可以触发代理,然后执行代理定义好的响应函数,完成整个消息处理流程。

    • 解除绑定响应函数

    • MyStandardDelegate.Unbind();

    • 如果后续不再需要响应函数,可以及时取消代理与响应函数的绑定。

    • 代理分类

    • 第一种分类:

    • 代理按绑定响应函数的个数分成:Standard delegate(标准代理,或者叫做单播代理)和MultiCast delegate(多播代理)。他们之间区别非常简单,MultiCast支持绑定多个响应函数,Standard delegate只能有一个响应函数,也就是说MultiCast delegate跟广播的概念是一样的,有群发的效果。

    • Multicast delegate的使用

    • DECLARE_MULTICAST_DELEGATE(FMulticastDelegateSignature)

    • FMultiDelegateSignature MyMulticastDelegate

    • MyMulticastDelegate.AddUObject(this, &ResponseFunction)

    • MyMulticastDelegate.Broadcast()

    • MyMulticastDelegate.RemoveAll()

    • 第二种分类:

    • 按是否可以在蓝图中调用来分类:标准(静态)代理与动态代理(Dynamic)

    • 动态代理与标准代理之间区别:动态代理支持序列化,因此可以在蓝图中使用,而标准的却不能。

然后,我们创建一个函数BroadcastInitialValues来向视图广播一次初始值

//AuraWidgetController.h
public:
//...
//向绑定了该WidgetController的Widget广播初始值
virtual void BroadcastInitialValues();
//...
//OverlayWidgetController.h

//声明广播健康值的委托
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChangedSignature,float,NewHealth);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMaxHealthChangedSignature,float,NewMaxHealth);
/**
 * 
 */
UCLASS()
class AURA_API UOverlayWidgetController : public UAuraWidgetController
{
    GENERATED_BODY()
public:
    virtual void BroadcastInitialValues() override;
    
    UPROPERTY(BlueprintAssignable,Category="GAS|Attributes")
    FOnHealthChangedSignature OnHealthChanged;
    UPROPERTY(BlueprintAssignable,Category="GAS|Attributes")
    FOnMaxHealthChangedSignature OnMaxHealthChanged;
};
//OverlayWidgetController.cpp
void UOverlayWidgetController::BroadcastInitialValues()
{
    //不需要调用父类的Broadcast,我们想要有自己的广播
    //转化至UAuraAttributeSet,这样就可以调用GetHealth和GetMaxHealth等获得数据了
    const UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet);
    //如果转化不成功游戏将会崩溃
    //广播初始化值
    OnHealthChanged.Broadcast(AuraAttributeSet->GetHealth());
    OnMaxHealthChanged.Broadcast(AuraAttributeSet->GetMaxHealth());
    
}

然后我们选择一个广播初始化值的时机,在AuraHUD的InitOverlay函数中,在Widget设置完WidgetController后,就可以广播了

//AuraHUD.cpp
//...
    UOverlayWidgetController* WidgetController = GetOverlayWidgetController(WidgetControllerParams);
    OverlayWidget->SetWidgetController(WidgetController);
    //让WidgetController广播初始值,因为此时Widget已经绑定好了WidgetController
    WidgetController->BroadcastInitialValues();
    //添加进视口
    Widget->AddToViewport();
//...

然后我们在WBP_Overlay中,将生命值UI组件(WBP_HealthGlobe)和法力值UI组件(WBP_ManaGlobe)绑定WidgetController

img

这个事件WidgetControllerSet是在AuraUserWidget类中定义的

img

在SetWidgetController中调用的,它是一个将WidgetController在Widget设置好中的一个回调,可以在蓝图中实现

img

而且,生命值UI组件和法力值UI组件的父类都是AuraUserWidget,所以,为他们设置WidgetController后,又可以在他们的蓝图中去调用回调函数,此时我们进入生命值UI的蓝图然后在初始化时将WidgetController广播的初始化生命值和最大生命值赋值

但是,OverlayWidgetController不是蓝图类,所以无法在蓝图中将WidgetController类转换为OverlayWidgetController

img

所以在其UCLASS上设置为蓝图类型

UCLASS(BlueprintType,Blueprintable)
class AURA_API UOverlayWidgetController : public UAuraWidgetController

创建一个蓝图类型的OverlayWidgetController,名为BP_OverlayWidgetController

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在BP_AuraHUD中将OverlayWidgetControllerClass设为BP_OverlayWidgetController

img

然后在生命值蓝图中,分配事件(记住是分配Assign

img

然后,我们获得了Health和MaxHealth,现在可以为生命UI设置生命值进度条所占百分比了

但是,我们无法在子类上设置进度条所占百分比,这个参数是保存在了父类,所以我们要在父类上创建一个函数让子类能够调用

img

OnHealth处理

img

OnMaxHealth、Mana同理

我们初始化角色的血量为100,满血为200

我们打开游戏可以看到角色的血量为一半

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

监听数据的变化

AbilitySystem提供了一个监听数据变化的函数AbilitySystem.GetGameplayAttributeValueChangeDelegate(想要监听的数据).AddUObject(要绑定的 UObject 实例,&成员函数)

我们可以调用这个方法来监听数据的变化,我们把绑定监听数据的回调放到基类的虚函数中

//AuraWidgetController.h
//...
public:
    //...
    //设置监听数据变化的依赖绑定
    virtual void BindCallbacksToDependencies();
 //...

然后我们在OverlayWidgetController设置

//OverlayWidgetController.h
//...
public:
    //设置监听数据变化的依赖绑定
    virtual void BindCallbacksToDependencies() override;
    //...
protected:
    //生命值变化的回调
    void HealthChanged(const FOnAttributeChangeData& Data) const;
    //最大生命值变化的回调
    void MaxHealthChanged(const FOnAttributeChangeData& Data) const;
//...
//OverlayWidgetController.cpp
void UOverlayWidgetController::BroadcastInitialValues()
{
    //...
    //在BroadcastInitialValues的最后绑定依赖
    BindCallbacksToDependencies();
}

void UOverlayWidgetController::BindCallbacksToDependencies()
{
    const UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet);
    //绑定当生命值改变时的回调
    AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetHealthAttribute())
       .AddUObject(this,&UOverlayWidgetController::HealthChanged);
    //绑定最大生命值改变的回调
    AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetMaxHealthAttribute())
       .AddUObject(this,&UOverlayWidgetController::MaxHealthChanged);
}

void UOverlayWidgetController::HealthChanged(const FOnAttributeChangeData& Data) const
{
    OnHealthChanged.Broadcast(Data.NewValue);
}

void UOverlayWidgetController::MaxHealthChanged(const FOnAttributeChangeData& Data) const
{
    OnMaxHealthChanged.Broadcast(Data.NewValue);
}

此时,我们就可以在游戏中看到,当我们捡起血瓶时,生命值增加

img

任务:设置Mana法力值

img

  1. 在AuraAttributeSet的构造函数中InitMana和InitMaxMana
  2. 在OverlayWidgetController中声明法力值改变的委托,并添加回调
  3. 在OverlayWidgetController的BindCallbacksToDependencies添加法力值改变后的回调
  4. 在蓝图中绑定委托广播事件后的操作,改变视图的Mana&MaxMana

img

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值