虚幻引擎(UE4) UMG实例

UI的重要性

游戏中UI系统具有以下特点:

  • 庞大而复杂
                 
     一个游戏会包含多个系统,包括任务系统,进度系统,统计系统等等,而每个系统都有自己的可视化界面。
  • 善变
                 在游戏开发中,经常会因为某些原因修改用户界面。
  • 详细
                 
    为了向用户传递更加清晰的信息,UI通常会做得比较仔细。
     

有了以上的信息,强烈建议我们多花一点时间去创建一个UI系统,其中包括以下内容:

  • 分离逻辑与可视化UI
  • 允许快速迭代布局和视觉效果
  • 逻辑调试可视化反馈
  • 性能优化

 

案例

下面我将用一个真实的例子来展示一些非常有用的UI案例。

假如你希望游戏中的物品商店是这样的:

TechBlog_UMG_Best_Practices_001.jpg

我们将利用下面这些内容来实现这个商店:

  • Class 属性
  • C++代码
  • 蓝图

在大多数情况,我会提前写出伪代码,列出会使用哪些核心类和包含的内容:

MyData:这是一个C++类并继承自UObject,这样做的目的是创建一个类来管理UI要与用户交互的所有信息。继承自UObject的目的是方便使用Public/Private控制访问类属性setter和getter和一些有用的API。这种方法将数据的生命周期从UI中分离出来,这是非常理想的。而且多个Widget使用的数据能同时从同一份数据中获取。在游戏项目开发中,这些类很可能已经被其他开发者创建并存在,你可以继承自这些已经存在的类来开发,在很多时候我们需要创建自己的类来管理自己的任游戏模块。

UMyWidget:这是一个C++类并继承自UUserWidget。这个类用来定义特定给蓝图使用的API和蓝图事件和约定,以便与底层系统进行交互。

MyBlueprint:这是一个Widget类,继承自UMyWidget。在Widdget蓝图中,你需要创建所有可见的UI和布局,并使用UMyData和UMyWidget中提供的数据或图片进行UI更新。你也可以监听UMyWidget派发的事件,来响应一些UI的刷新。在实际交互中,你可能会调用UMyData和UMyWidget提供的API来响应一个按钮点击事件。

详细代码

数据类:

首先,将商品数据放到一起,以便商店里显示。

UOfferInfo派生自UObject,我想要将其暴露给蓝图类并且可以提供一些函数来让蓝图获取数据。我们建立一个简单的关系如(价格-图片-其他)

UENUM(BlueprintType)
enum class EOfferType:uint8
{
    Normal,
    Featured
};

UCLASS(BlueprintType)
class UOfferInfo : public UObject
{
    GENERATED_UCLASS_BODY();

public:
    UFUNCTION(BlueprintCallable, Category = "OfferInfo");
    FText GetName() const;
    
    UFUNCTION(BlueprintCallable, Category = "OfferInfo");
    FText GetDescription() const;

    UFUNCTION(BlueprintCallable, Category = "OfferInfo");
    EOfferType GetOfferType() const;

private:
    //其他属性数据
}

商店类:

负责显示所有商品

//抽象类,因为我们希望用蓝图来继承此类,但是又不希望用户直接实例化此类。
class UOfferShopWidgetBase : public UUserWidget
{
    GENERATED_UCLASS_BODY()
    
public:
    //此函数告诉用户开始读取商品信息,蓝图中最好采用文字提示的方式告诉用户正在处理数据
    UFUNCTION(BlueprintImplementableEvent, Category = "OfferShopWidget")
    void OnStartReadingOffers();

    //告诉蓝图我们希望生成一些指定的商品。
    //在多数情况下,蓝图会创建一个商品控件,并使用其UOfferInfo*指针来处理自己的逻辑。
    //建议让蓝图完整控制API界面布局的逻辑,因为可能设计需要一个竖直列表,
    //然后下次又要一个横向的滚动列表、或者一个网格、或者有其他组合,让蓝图控制更加灵活。
    UFUNCTION(BlueprintImplementableEvent, Category = "OfferShopWidget")
    void GenerateOffer(UOfferInfo* OfferData);

    //这个函数提示已经完整地生成了所有的商品,蓝图中应该隐藏文字提示,并更新屏幕其他的显示。
    UFUNCTION(BlueprintImplementableEvent, Category = "OfferShopWidget")
    void OnOfferGenerationCompleted();

    //蓝图获取数据的方法
    UFUNCTION(BlueprintCallable, Category = "OfferShopWidget")
    FDateTime GetStoreRefreshDate() const;

protected:
    //内部函数,关键字的含义请查阅c++语法。
private:
    //隐私级别更高的内部函数

    //商品列表
    TArray<UOfferInfo*> CurrentOffers;
}

商品类:

再来看用于显示单个商品的UOfferWidgetBase类。

我们将从该基类派生两个不同的布局视图来实现两个不同的商品。

//抽象类,因为我们希望用蓝图来继承此类,但是又不希望用户直接实例化此类。
UCLASS(Abstract, Blueprint, BlueType, ClassGroup = UI)
class UOfferWidgetBase : public UUserWidget
{
    GENERATED_UCLASS_BODY()
public:
    
    //这是你创建了一个新的商品实例后必须调用的函数。
    //在这样的方式下,当你想显示其他不同的商品,直接再次调用SetupOffer,
    //而不用再次创建一个全新的商品Widget,这对于性能优化格外重要。
    //尤其是这种清单类条目,采用这种方式创建更少的widget控件,实现更高效UI。
    UFUNCTIONO(BlueprintCallable, Category = "OfferWidget")
    void SetupOffer(UOfferInfo* InOfferData)
    {
        //这里用一个小小的实现来解释我为什么不倾向于使用BlueprintNativeEvents
        //主要的原因是这可能导致用户调用一个父类不存在的函数而报错。
        //围绕BlueprintImplementableEvent的UX会更加单一些。
        OfferData = InOfferData;
        OnOfferSet();
    }

    //告诉蓝图已经获取了数据
    //蓝图通常会调用GetOfferInfo(),然后调用GetName(),然后GetDescription()和更多的数据来填充文本,图片和其他
    UFUNCTION(BlueprintImplementableEvent, Category = "OfferWidget")
    void OnOfferSet();

    //获取商品数据
    UFUNCTION(BlueprintCallable, Category = "OfferWidget")
    UOfferInfo* GetOfferInfo() const;

private:
    UPROPERTY(transient)
    UOfferInfo* OfferData;
}

蓝图类:

现在创建一个派生自上面的UOfferShopWidgetBase的UMG Widget蓝图类,并构建一个布局来接收商品数据。

  • FeaturedOffers_HorBox
    • 特殊商品放在商店左边
  • NormalOffers_WrapBox
    • 普通商品放在商店右边
  • RefreshTime_TextBlock
    • 文本计时器刷新商品信息

TechBlog_UMG_Best_Practices_002.jpg

下面是图表:

让我们使用蓝图创建了widget组件并将其添加到了合适的容器中,所以我们就可以:

  • 轻松地修改布局
  • 不用情况下交替使用控件
  • 修改从“add child to…”调用返回的插槽的属性

现在我们开始创建商品控件,我们将创建两种UMG控件,并继承自UOfferWidgetBase类。命名为OfferTileSmallWidget,OfferTileLargeWidget。上图中GenerateOffer事件驱动下,使用商品数据的商品类型创建了两种widget控件。

在我们将small widget和large widget布局设置为与最上面的实例图片相同后,就可以在图表中添加显示逻辑,最简单的形式如下所示:

TechBlog_UMG_Best_Practices_004.jpg

到此,我们创建了OfferTileSmallWidget和OfferTileLargeWidget来适应商店屏幕,但是我们的体系结构使这一过程更加容易。想象一下,在我们游戏的另一个屏幕上,我们想展示一个热卖的商品。

这需要2步走:

  1. 在c++中,添加一个函数用于热卖商品:UOfferInfo* MyLibrary::GetTheHotOffer(),返回OfferInfo实例对象。
  2. 在屏幕上显示一个商品控件,并调用SetupOffer(),将GetTheHotOffer返回值作为参数,或者我创建一个带有火热特效“HotOfferWidget”蓝图并调用SetupOffer()。

结束语

这种方法处理c++业务逻辑,更加便于调试,修改和扩展。暴露一个基于事件的API在蓝图,让开发人员更加灵活根据需求修改UI。

要注意的问题是蓝图Tick、属性绑定,碎片太多,过多的动画会影响设备的性能。这种方法是利用事件来驱动,这自然会阻碍蓝图 Ticks和属性绑定,因为在事件存在的情况下,蓝图的Tick效率会降低。

动画的性能开销是很大的,当然是有用的,但你应该权衡利弊的做一些取舍,特别是当性能要求高的情况下,比如在HUD中,UI对性能开销预算几乎是为0。

  • 0
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呦呦鹿鸣.

你的打赏是给予我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值