虚幻引擎(UE4) UMG实例

举报
Dummy 发表于 2021/11/19 02:03:01 2021/11/19
【摘要】 UI的重要性 游戏中UI系统具有以下特点: 庞大而复杂               一个游戏会包含多个系统,包括任务系统,进度系统,统计系统等等,而每个系统都有自己的可视化界面。善变          &...

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,我想要将其暴露给蓝图类并且可以提供一些函数来让蓝图获取数据。我们建立一个简单的关系如(价格-图片-其他)


  
  1. UENUM(BlueprintType)
  2. enum class EOfferType:uint8
  3. {
  4. Normal,
  5. Featured
  6. };
  7. UCLASS(BlueprintType)
  8. class UOfferInfo : public UObject
  9. {
  10. GENERATED_UCLASS_BODY();
  11. public:
  12. UFUNCTION(BlueprintCallable, Category = "OfferInfo");
  13. FText GetName() const;
  14. UFUNCTION(BlueprintCallable, Category = "OfferInfo");
  15. FText GetDescription() const;
  16. UFUNCTION(BlueprintCallable, Category = "OfferInfo");
  17. EOfferType GetOfferType() const;
  18. private:
  19. //其他属性数据
  20. }

商店类:

负责显示所有商品


  
  1. //抽象类,因为我们希望用蓝图来继承此类,但是又不希望用户直接实例化此类。
  2. class UOfferShopWidgetBase : public UUserWidget
  3. {
  4. GENERATED_UCLASS_BODY()
  5. public:
  6. //此函数告诉用户开始读取商品信息,蓝图中最好采用文字提示的方式告诉用户正在处理数据
  7. UFUNCTION(BlueprintImplementableEvent, Category = "OfferShopWidget")
  8. void OnStartReadingOffers();
  9. //告诉蓝图我们希望生成一些指定的商品。
  10. //在多数情况下,蓝图会创建一个商品控件,并使用其UOfferInfo*指针来处理自己的逻辑。
  11. //建议让蓝图完整控制API界面布局的逻辑,因为可能设计需要一个竖直列表,
  12. //然后下次又要一个横向的滚动列表、或者一个网格、或者有其他组合,让蓝图控制更加灵活。
  13. UFUNCTION(BlueprintImplementableEvent, Category = "OfferShopWidget")
  14. void GenerateOffer(UOfferInfo* OfferData);
  15. //这个函数提示已经完整地生成了所有的商品,蓝图中应该隐藏文字提示,并更新屏幕其他的显示。
  16. UFUNCTION(BlueprintImplementableEvent, Category = "OfferShopWidget")
  17. void OnOfferGenerationCompleted();
  18. //蓝图获取数据的方法
  19. UFUNCTION(BlueprintCallable, Category = "OfferShopWidget")
  20. FDateTime GetStoreRefreshDate() const;
  21. protected:
  22. //内部函数,关键字的含义请查阅c++语法。
  23. private
  24. //隐私级别更高的内部函数
  25. //商品列表
  26. TArray<UOfferInfo*> CurrentOffers;
  27. }

商品类:

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

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


  
  1. //抽象类,因为我们希望用蓝图来继承此类,但是又不希望用户直接实例化此类。
  2. UCLASS(Abstract, Blueprint, BlueType, ClassGroup = UI)
  3. class UOfferWidgetBase : public UUserWidget
  4. {
  5. GENERATED_UCLASS_BODY()
  6. public:
  7. //这是你创建了一个新的商品实例后必须调用的函数。
  8. //在这样的方式下,当你想显示其他不同的商品,直接再次调用SetupOffer,
  9. //而不用再次创建一个全新的商品Widget,这对于性能优化格外重要。
  10. //尤其是这种清单类条目,采用这种方式创建更少的widget控件,实现更高效UI。
  11. UFUNCTIONO(BlueprintCallable, Category = "OfferWidget")
  12. void SetupOffer(UOfferInfo* InOfferData)
  13. {
  14. //这里用一个小小的实现来解释我为什么不倾向于使用BlueprintNativeEvents
  15. //主要的原因是这可能导致用户调用一个父类不存在的函数而报错。
  16. //围绕BlueprintImplementableEvent的UX会更加单一些。
  17. OfferData = InOfferData;
  18. OnOfferSet();
  19. }
  20. //告诉蓝图已经获取了数据
  21. //蓝图通常会调用GetOfferInfo(),然后调用GetName(),然后GetDescription()和更多的数据来填充文本,图片和其他
  22. UFUNCTION(BlueprintImplementableEvent, Category = "OfferWidget")
  23. void OnOfferSet();
  24. //获取商品数据
  25. UFUNCTION(BlueprintCallable, Category = "OfferWidget")
  26. UOfferInfo* GetOfferInfo() const;
  27. private:
  28. UPROPERTY(transient)
  29. UOfferInfo* OfferData;
  30. }

蓝图类:

现在创建一个派生自上面的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。

文章来源: blog.csdn.net,作者:呦呦鹿鸣.,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/zhang1461376499/article/details/113558077

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。