Как стать автором
Обновить

Unreal engine — UMG Best Practices

Время на прочтение7 мин
Количество просмотров7.1K
Автор оригинала: Chris Gagnon

Это перевод статьи 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 были созданы для размещения на экране магазина, и архитектура позволяет легко расширять эту идею. Представьте, что необходимо на каком-то другом экране нашей игры показать горячее предложение!

Нам потребуется всего два шага:

  1. в С++ создаем функцию для горячего предложения, и возможно, она выглядит следующим образом: UOfferInfo* MyLibrary::GetTheHotOffer(). Который просто возвращает экземпляр нашего класса данных OfferInfo.

  2. Поместите один из двух существующих виджетов на экран и вызовите SetupOffer() с функцией которая возвращает GetTheHotOffer(). Или мы можем создать новый ВР "HotOfferWidget" с эффектом огонька и добавить его на вызов SetupOffer().

Заключение

Этот подход позволяет хранить всю логику на С++, что облегчает ее поддержку, отладку и модификацию. Он раскрывает API на основе событийной модели Blueprints, чтобы позволить дизайнера гибко создавать UX и визуальный опыт, который они хотят показать пользователю.

Желательно избегать таких вещей как тик Blueprintа, Bind параметров UI и чрезмерного количества анимаций. В основе вышеописанного подхода лежит предоставление набора событий, которые раскрывают важные стороны системы и ее состояние. Что непосредственно конфликтует с Blueprint Ticks и Bindами, просто потому, что их полезность снижается с учетом событийной модели.

Анимации относительно дорогие, хоть и безусловно, полезны, но вы должны быть благоразумны в их использовании, особенно когда производительность действительно имеет значение, например в HUD, где бюджет на UI всегда кажется близким к нулю. Любой, кто работал над UI, с этим сталкивался.

А на этом статья завершается, и я с вами прощаюсь. Всем спасибо за внимание и да прибудет с вами сила знаний.

Теги:
Хабы:
Всего голосов 3: ↑3 и ↓0+3
Комментарии0

Публикации

Истории

Работа

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань