Это перевод статьи Chris Gagnon, сотрудника Epic Games, который работал над созданием UI движка и игр. Он описал рекомендации и советы, как сделать ваш UI лучше.
Значимость UI(user interface) и его архитектуры.
Игровые UI системы бывают:
Большими и комплексными
Большинство игр имеют относительно обширные системы квестов, достижений, статистики и т.д. UI инженеры, помимо создания самой визуальной части, должны заниматься разработкой базовых систем, предназначенных для отслеживания данных, сохраняемых в учетных записях и облачных хранилищах. Кроме того есть необходимость в системе сбора данных состояний игровых систем.
Изменчивые
Эти системы, как правило, растут и изменяются в процессе разработки, поскольку разработчики пытаются найти идеальное решение, подходящее для игры.
Вариативные
Стиль, компоновка и внешний вид постоянно настраиваются и изменяются в попытке справиться с обратной связью, которая, вероятно включает в себя различные мнения и результаты от пользователей с различным уровнем мастерства и игровым опытом.
Учитывая эту информацию, настоятельно рекомендуется потратить дополнительное время на создание надежной архитектуры которая обеспечит:
Разделение логики и визуального оформления вашего UI;
Обеспечить быструю итерацию layout и визуального оформления;
Эффективную отладку логики;
Высокую производительности;
Ниже будет приведен пример очень эффективной схемы, которую Chris нашел для достижения этих целей, на реальном примере.
Пример
Представим, вы работаете над игрой... Фортнайт подойдет. ;)
Мы хотим чтобы магазин предметов выглядит примерно так:
В этом примере мы подробно рассмотрим:
Архитектуру классов
С++ код
Blueprints
Архитектура классов
Архитектура, которую обычно предлагает Chris, примерно выглядит следующим образом:
UMyData - это С++ класс унаследованный от UObject.
Идея заключается в создании классов данных для инкапсуляции всей информации о предмете, которую ваш UI хочет донести до пользователя.
То, что это UObject, позволяет нам контролировать доступ с помощью public/private, иметь getters/setters и включать полезные нам API.
Такой подход отделяет время жизни данных от UI, что весьма желательно, поскольку предложения магазина, предметы инвентаря, статистика игрока или иные источники данных, могут различаться с временем жизни UI. Более того, вы можете иметь несколько виджетов, получающих данных из одних и тех же источников. Такие классы могут быть созданы другими инженерами в рамках разработки игровых или backend сервисов. Однако даже в этих случаях не редко для UI требуется собственный класс данных, служащий оберткой игрового процесса вместе с другими специфичными для UI данными.
UMyWidget - это C++ класс унаследованный от UUserWidget.
Эти классы С++ предназначены для определения API виджета для использования в Blueprints, а также Blueprintable events для правильного взаимодействия с основной системой.
MyBlueprint - это Blueprint виджет унаследованный от UMyWidget.
В виджетах вы создаете весь необходимый вам видимый UI, оформляя его, а затем используя API из UMyData или UMyWidget, заполняете все UI элементы(Textbox, images, border, etc) необходимыми данными. Также вы можете использовать ивенты UMyWidget, для обработки изменений UI. В случае необходимости обработки inputs, вы можете реагировать на нажатие кнопки и обращаться к какому-либо предоставленному API в UMyData или UMyWidget
Let’s Code
Дата классы fortnite:
Во-первых, соберем данные для предложения в магазине.
UOfferInfo наследуется от UObject, ведь необходимо дать доступ работы с ним из Blueprints, а также есть необходимость иметь UFUNCTIONS для доступа к данным. Для примера, будут использоваться относительно простые данные, но можно представить себе цену, изображение и многое другое:
Код UOfferInfo
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:
// All your data including UProperties!
};
Виджет магазина:
Следующим шагом, рассмотрим виджет магазина, который отвечает за интерфейс и поведение экрана, которые показывает все предложения. Отображение индивидуальных предложений передается виджету UOfferWidgetBase, о котором речь пойдет, позже.
Код UOfferShopWidgetBase
// Abstract because we want to inherit a Blueprint class from this base
// but don't want users to be able to instance the base class directly
UCLASS(Abstract, Blueprintable, BlueprintType, ClassGroup = UI)
class UOfferShopWidgetBase : public UUserWidget
{
GENERATED_UCLASS_BODY()
public:
// This function tells the users that we have started reading offers
// The Blueprint will most likely put up a throbber and text explaining were downloading data
UFUNCTION(BlueprintImplementableEvent, Category = "OfferShopWidget")
void OnStartReadingOffers();
// We tell the Blueprint we want to generate some UI for a specific Offer
// Most likely the Blueprint will create an Offer widget and hand it the UOfferInfo* for it to do it's thing
// I suggest API like this to allow the Blueprint full control over the layout
// Maybe the visual design calls for a vertical box, and next week changes to scroll horizontally,
// or maybe a tile grid, or some combination.... you get the idea ;)
UFUNCTION(BlueprintImplementableEvent, Category = "OfferShopWidget")
void GenerateOffer(UOfferInfo* OfferData);
// This function tells the users that we have finished generating offers
// The Blueprint will most likely hide the throbber and complete any setup of the screen
UFUNCTION(BlueprintImplementableEvent, Category = "OfferShopWidget")
void OnOfferGenerationCompleted();
// Utility functions for the Blueprint to get data it will use
UFUNCTION(BlueprintCallable, Category = "OfferShopWidget")
FDateTime GetStoreRefreshDate() const;
protected:
// Keep all your internal function at least protected
private:
// More internal functions
// All your data including UProperties!
UPROPERTY(transient)
TArray<UOfferInfo*>CurrentOffers;
};
Виджет индивидуальных предложений UOfferWidgetBase
Наконец, рассмотрим базовый класс С++ виджетов для отображения индивидуальных предложений. Для создания layouts будет использоваться два различных типа форм.
код UOfferWidgetBase
// Abstract because we want to inherit a Blueprint class from this base
// but don't want users to be able to instance the base class directly
UCLASS(Abstract, Blueprintable, BlueprintType, ClassGroup = UI)
class UOfferWidgetBase : public UUserWidget
{
GENERATED_UCLASS_BODY()
public:
// This is the function you have to call after Creating a new instance of one of these widgets
// With this pattern you can call SetupOffer again if you want to show something different
// without creating a whole new widget, generally important for performance especially when working with
// inventories and such where you'll most likely want to pool/reuse widgets to keep the UI fast
UFUNCTION(BlueprintCallable, Category = "OfferWidget")
void SetupOffer(UOfferInfo* InOfferData)
{
// Threw in a little implementation here to explain why I don't tend to use BluprintNativeEvents
// The primary reason is that it introduces possible user error where the call the parent function
// in the wrong place or not at all.
// The UX surrounding BlueprintImplementableEvent is also a little simpler.
OfferData = InOfferData;
OnOfferSet();
}
// We tell the Blueprint we got data to show
// The Blueprint will most likely call GetOfferInfo() then the GetName, GetDescription,
// and many more to populate it's text fields, textures and everything else
UFUNCTION(BlueprintImplementableEvent, Category = "OfferWidget")
void OnOfferSet();
// Utility functions for the Blueprint to get data it will use
UFUNCTION(BlueprintCallable, Category = "OfferWidget")
UOfferInfo* GetOfferInfo() const;
private:
UPROPERTY(transient)
UOfferInfo* OfferData;
};
Let’s Blueprint
Пришло время создать UMG-виджет, унаследованный от нашего базового шаблона UOfferShopWidgetBase и создать layout, которые мы будем заполнять во время выполнения.
FeaturedOffers_HorBox
Занимает левую половину экрана и будет местом, где мы разместим наши тематические предложения.
NormalOffers_WrapBox
Занимает правую половину экрана и будет местом, где мы разместим наши обычные предложения.
RefreshTime_TextBlock
Текстовый блок для размещения таймера обновления магазина.
Так как мы используем Blueprint для создания виджетов и добавления их в соответствующие контейнеры, мы получаем возможность:
Весьма просто изменять layout
Менять местами виджеты, используемые в различных ситуациях
Изменять свойства слотов полученных при помощи “Add Child to…”.
Теперь создадим виджеты предложения. Мы создадим два UMG-виджета, наследуемых от UOfferWidgetBase, который мы создали в С++. Назовем их OfferTileSmallWidget и OfferTileLargeWidget. GenerateOffer ивент в Offer Shop Blueprint, изображенном выше, создает эти два виджета на основе типа предложения, хранящегося в наших данных Offer Data.
После того как были настроен дизайн виджета, мы можем создать Event graph, который в самом простом виде будет выглядеть следующим образом
OfferTileSmallWidget и OfferTileLargeWidget были созданы для размещения на экране магазина, и архитектура позволяет легко расширять эту идею. Представьте, что необходимо на каком-то другом экране нашей игры показать горячее предложение!
Нам потребуется всего два шага:
в С++ создаем функцию для горячего предложения, и возможно, она выглядит следующим образом: UOfferInfo* MyLibrary::GetTheHotOffer(). Который просто возвращает экземпляр нашего класса данных OfferInfo.
Поместите один из двух существующих виджетов на экран и вызовите SetupOffer() с функцией которая возвращает GetTheHotOffer(). Или мы можем создать новый ВР "HotOfferWidget" с эффектом огонька и добавить его на вызов SetupOffer().
Заключение
Этот подход позволяет хранить всю логику на С++, что облегчает ее поддержку, отладку и модификацию. Он раскрывает API на основе событийной модели Blueprints, чтобы позволить дизайнера гибко создавать UX и визуальный опыт, который они хотят показать пользователю.
Желательно избегать таких вещей как тик Blueprintа, Bind параметров UI и чрезмерного количества анимаций. В основе вышеописанного подхода лежит предоставление набора событий, которые раскрывают важные стороны системы и ее состояние. Что непосредственно конфликтует с Blueprint Ticks и Bindами, просто потому, что их полезность снижается с учетом событийной модели.
Анимации относительно дорогие, хоть и безусловно, полезны, но вы должны быть благоразумны в их использовании, особенно когда производительность действительно имеет значение, например в HUD, где бюджет на UI всегда кажется близким к нулю. Любой, кто работал над UI, с этим сталкивался.
А на этом статья завершается, и я с вами прощаюсь. Всем спасибо за внимание и да прибудет с вами сила знаний.