游戏中UI系统具有以下特点:
详细
为了向用户传递更加清晰的信息,UI通常会做得比较仔细。
有了以上的信息,强烈建议我们多花一点时间去创建一个UI系统,其中包括以下内容:
下面我将用一个真实的例子来展示一些非常有用的UI案例。
假如你希望游戏中的物品商店是这样的:
我们将利用下面这些内容来实现这个商店:
在大多数情况,我会提前写出伪代码,列出会使用哪些核心类和包含的内容:
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 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蓝图类,并构建一个布局来接收商品数据。
下面是图表:
让我们使用蓝图创建了widget组件并将其添加到了合适的容器中,所以我们就可以:
现在我们开始创建商品控件,我们将创建两种UMG控件,并继承自UOfferWidgetBase类。命名为OfferTileSmallWidget,OfferTileLargeWidget。上图中GenerateOffer事件驱动下,使用商品数据的商品类型创建了两种widget控件。
在我们将small widget和large widget布局设置为与最上面的实例图片相同后,就可以在图表中添加显示逻辑,最简单的形式如下所示:
到此,我们创建了OfferTileSmallWidget和OfferTileLargeWidget来适应商店屏幕,但是我们的体系结构使这一过程更加容易。想象一下,在我们游戏的另一个屏幕上,我们想展示一个热卖的商品。
这需要2步走:
这种方法处理c++业务逻辑,更加便于调试,修改和扩展。暴露一个基于事件的API在蓝图,让开发人员更加灵活根据需求修改UI。
要注意的问题是蓝图Tick、属性绑定,碎片太多,过多的动画会影响设备的性能。这种方法是利用事件来驱动,这自然会阻碍蓝图 Ticks和属性绑定,因为在事件存在的情况下,蓝图的Tick效率会降低。
动画的性能开销是很大的,当然是有用的,但你应该权衡利弊的做一些取舍,特别是当性能要求高的情况下,比如在HUD中,UI对性能开销预算几乎是为0。