В этой статье я постараюсь раскрыть смысл и методику создания DataAsset, как хранилища для различного рода данных, а нашем случае это библиотека для Actors и их параметров.


Небольшое вступление, которое можно пропустить

Принять решение создавать игру около 2-х лет назад, мне помогло то, что я случайно наткнутся на информацию об Unreal Engine 4 и прочитал как это круто и просто. На деле же, человеку не умеющему писать код (язык программирования не имеет значения в данном контексте) очень сложно создать что-то, сложнее небольшой модификации стандартного набора заготовок из движка. Поэтому, изначальное желание сделать супер-мега игру, с ростом знаний о реальности данного проекта, постепенно переросло в хобби. Поднять все пласты разработки игры, от 3D моделирования и анимации, и до написания кода, для одного человека представляется мало осуществимым предприятием. Тем не менее, это хорошая тренировка для мозга.


Почему решил что-то написать?.. Наверно из-за того. что представленные мануалы либо дают очень поверхностные знания (и таких большинство), либо уж для совсем профи и содержат лишь общие указания.


Начинать почти всегда лучше с начала. Не могу сказать, что поступаю так всегда, но постараюсь излагать последовательно, насколько это возможно.


Конечно же, лучше всего начать со структуры, но, к сожалению, имея закрытый ящик с инструментами, очень сложно понять, что именно можно с их помощью построить. Так давайте же откроем это ящик и посмотрим что содержится внутри.




Первый вопрос, на который следует ответить. Почему именно DataAsset?


  1. Очень часто в статьях и “туториалах” можно увидеть применение DataTable. Почему это плохо? Если вы храните адрес к конкретному Blueprint, то при переименовании или перемещении его в другую папку вы будете вынуждены изменить этот адрес вручную. Согласитесь — неудобно? С DataAsset же такого не случится. Все связи обновятся автоматически. Если же вы абсолютно уверены в структуре своего проекта на годы вперед, то, конечно же, можно использовать таблицы.
  2. Второе неоспоримое преимущество — это возможность хранить сложные типы данных, например, такие как структуры (Struct).

Теперь немного об относительных недостатках. На самом деле я вижу только один. Это необходимость писать код на C++.


Если вам уже понятно, что без работы с кодом вы не сделаете ничего эпического, то это уже не недостаток, а особенность.


Надо заметить, что есть один обходной трюк — использовать Actor в качестве такого хранилища. Но такое применение выглядит как последнее оправдание нежелания учить С++, и таит в себе потенциальную возможность попасть в окончательный тупик в будущем.
Если же вы убеждены, что все необходимое для вашего проекта можно сделать на Blueprint, используйте таблицы.




Теперь, когда вы уже уверовали, что DataAsset — это хорошо, рассмотрим как можно его создать для своего проекта.


Для тех кто еще совсем 'не в танке'
Есть очень подробное описание по шагам и с картинками на русскоязычном форуме, посвященному UE4. Просто погуглите по запросу ”UE4 создание DataAsset”. Сам осваивал азы именно по этому руководству около года назад.

Первым делом, создаем C++ Class, как Child от UDataAsset.
(Весь код, который содержится ниже, взят из моего, еще не рожденного проекта. Просто переименуйте названия как вам будет удобнее.)


/// Copyright 2018 Dreampax Games, Inc. All Rights Reserved.

#pragma once

/* Includes from Engine */
#include "Engine/DataAsset.h"
#include "Engine/Texture2D.h"
#include "GameplayTagContainer.h"

/* Includes from Dreampax */
//no includes

#include "DreampaxItemsDataAsset.generated.h"

UCLASS(BlueprintType)
class DREAMPAX_API UDreampaxItemsDataAsset : public UDataAsset
{
    GENERATED_BODY()
}

Теперь уже на базе это класса можно смело создавать Blueprint, но делать это пока рановато… пока это просто пустышка. Хотя, обратите внимание, включения для текстур и имен уже сделаны.




Начиная с этого момента, вы начинаете создавать структуру своего хранилища. Она будет переделываться множество раз, поэтому крайне не рекомендую сразу наполнять свое хранилище. Три-пять элементов, в нашем случае предметов инвентаря, вполне достаточно для тестов. Иногда, после компиляции ваш Blueprint может оказаться девственно пуст, что крайне неприятно, если вы заполнили уже десяток-другой позиций.


Создать структуру можно прямо в заголовочном файле, т.к. в данном случае она вряд ли будет применяться где-то еще. Обычно же, я предпочитаю делать ее в виде отдельного заголовочного файла “SrtuctName.h”, и подключать его где нужно по мере необходимости.


В моем случае это выглядит вот так
USTRUCT(BlueprintType)
struct FItemsDatabase
{
    GENERATED_USTRUCT_BODY()

    /* Storage for any float constant data */
    UPROPERTY(EditDefaultsOnly, Category = "ItemsDatabase")
    TMap<FGameplayTag, float> ItemData;

    /* Gameplay tag container to store the properties */
    UPROPERTY(EditDefaultsOnly, Category = "ItemsDatabase")
    FGameplayTagContainer ItemPropertyTags;

    /* Texture for showing in the inventory */
    UPROPERTY(EditDefaultsOnly, Category = "ItemsDatabase")
    UTexture2D* IconTexture;

    /* The class put on the Mesh on the character */
    UPROPERTY(EditDefaultsOnly, Category = "ItemsDatabase")
    TSubclassOf<class ADreampaxOutfitActor> ItemOutfitClass;

    /* The class to spawn the Mesh in the level then it is dropped */
    UPROPERTY(EditDefaultsOnly, Category = "ItemsDatabase")
    TSubclassOf<class ADreampaxPickupActor> ItemPickupClass;

//TODO internal call functions
};

Будьте аккаунты с TMap . Не реплицируется! В данном случае это неважно.


Обратите внимание, что я не использую FName. Согласно современным веяниям использование FGameplayTag считается более правильным, т.к. существенно снижает риск ошибки и имеет ряд преимуществ, которые нам пригодятся позже.


Хорошим тоном также является прописать в структуре функции для вызова переменных, такие как GetSomething(). Видимо, над моим воспитанием нужно еще поработать, так как конкретно в этой базе данных, я такого вызова еще не сделал.


Вот как это может быть сделано на примере другой базы данных
USTRUCT(BlueprintType)
struct FBlocksDatabase
{
    GENERATED_USTRUCT_BODY()

    /* The class put on the Mesh for the building block */
    UPROPERTY(EditDefaultsOnly, Category = "BlocksDatabase")
    TSubclassOf<class ADreampaxBuildingBlock> BuildingBlockClass;

    UPROPERTY(EditDefaultsOnly, Category = "BlocksDatabase")
    FVector DefaultSize;

    UPROPERTY(EditDefaultsOnly, Category = "BlocksDatabase")
    FVector SizeLimits;

    UPROPERTY(EditDefaultsOnly, Category = "BlocksDatabase")
    TArray<class UMaterialInterface *> BlockMaterials;

    FORCEINLINE TSubclassOf<class ADreampaxBuildingBlock> * GetBuildingBlockClass()
    {
        return &BuildingBlockClass;
    }

    FORCEINLINE FVector GetDefaultSize()
    {
        return DefaultSize;
    }

    FORCEINLINE FVector GetSizeLimits()
    {
        return SizeLimits;
    }

    FORCEINLINE TArray<class UMaterialInterface *> GetBlockMaterials()
    {
        return BlockMaterials;
    }
};

И самый важный момент, это объявление базы данных:


UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "ItemsDatabase")
TMap<FGameplayTag, FItemsDatabase> ItemsDataBase;

Вот теперь уже можно создавать наш Blueprint и заполнять его.
Но перед этим, напишем еще несколько функций вызова, чтобы иметь возможность получать данные из базы.


DreampaxItemsDataAsset.h
/// Copyright 2018 Dreampax Games, Inc. All Rights Reserved.

#pragma once

/* Includes from Engine */
#include "Engine/DataAsset.h"
#include "Engine/Texture2D.h"
#include "GameplayTagContainer.h"

/* Includes from Dreampax */
//no includes

#include "DreampaxItemsDataAsset.generated.h"

USTRUCT(BlueprintType)
struct FItemsDatabase
{
    GENERATED_USTRUCT_BODY()

    /* Storage for any float constant data */
    UPROPERTY(EditDefaultsOnly, Category = "ItemsDatabase")
    TMap<FGameplayTag, float> ItemData;

    /* Gameplay tag container to store the properties */
    UPROPERTY(EditDefaultsOnly, Category = "ItemsDatabase")
    FGameplayTagContainer ItemPropertyTags;

    /* Texture for showing in the inventory */
    UPROPERTY(EditDefaultsOnly, Category = "ItemsDatabase")
    UTexture2D* IconTexture;

    /* The class put on the Mesh on the character */
    UPROPERTY(EditDefaultsOnly, Category = "ItemsDatabase")
    TSubclassOf<class ADreampaxOutfitActor> ItemOutfitClass;

    /* The class to spawn the Mesh in the level then it is dropped */
    UPROPERTY(EditDefaultsOnly, Category = "ItemsDatabase")
    TSubclassOf<class ADreampaxPickupActor> ItemPickupClass;

    //TODO internal call functions
};

UCLASS(BlueprintType)
class DREAMPAX_API UDreampaxItemsDataAsset : public UDataAsset
{
    GENERATED_BODY()

protected:

    /* This GameplayTag is used to find a Max size of the stack for the Item. This tag can be missed in the ItemData */
    UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "ItemsDatabase")
    FGameplayTag DefaultGameplayTagForMaxSizeOfStack;

    /* This is the main Database for all Items. It contains constant common variables */
    UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "ItemsDatabase")
    TMap<FGameplayTag, FItemsDatabase> ItemsDataBase;

public:

    FORCEINLINE TMap<FGameplayTag, float> * GetItemData(const FGameplayTag &ItemNameTag);

    FORCEINLINE FGameplayTagContainer * GetItemPropertyTags(const FGameplayTag &ItemNameTag);

    /* Used in the widget */
    UFUNCTION(BlueprintCallable, Category = "ItemDatabase")
    FORCEINLINE UTexture2D * GetItemIconTexture(const FGameplayTag & ItemNameTag) const;

    FORCEINLINE TSubclassOf<class ADreampaxOutfitActor> * GetItemOutfitClass(const FGameplayTag & ItemNameTag);

    FORCEINLINE TSubclassOf<class ADreampaxPickupActor> * GetItemPickupClass(const FGameplayTag & ItemNameTag);

    int GetItemMaxStackSize(const FGameplayTag & ItemNameTag);

    FORCEINLINE bool ItemIsFound(const FGameplayTag & ItemNameTag) const;

};

DreampaxItemsDataAsset.сpp
/// Copyright 2018 Dreampax Games, Inc. All Rights Reserved.

#include "DreampaxItemsDataAsset.h"

/* Includes from Engine */
// no includes

/* Includes from Dreampax */
// no includes

TMap<FGameplayTag, float>* UDreampaxItemsDataAsset::GetItemData(const FGameplayTag & ItemNameTag)
{
    return & ItemsDataBase.Find(ItemNameTag)->ItemData;
}

FGameplayTagContainer * UDreampaxItemsDataAsset::GetItemPropertyTags(const FGameplayTag & ItemNameTag)
{
    return & ItemsDataBase.Find(ItemNameTag)->ItemPropertyTags;
}

UTexture2D* UDreampaxItemsDataAsset::GetItemIconTexture(const FGameplayTag &ItemNameTag) const
{
    if (ItemNameTag.IsValid())
    { 
        return ItemsDataBase.Find(ItemNameTag)->IconTexture;
    }

    return nullptr;
}

TSubclassOf<class ADreampaxOutfitActor>* UDreampaxItemsDataAsset::GetItemOutfitClass(const FGameplayTag &ItemNameTag)
{
    return & ItemsDataBase.Find(ItemNameTag)->ItemOutfitClass;
}

TSubclassOf<class ADreampaxPickupActor>* UDreampaxItemsDataAsset::GetItemPickupClass(const FGameplayTag &ItemNameTag)
{
    return & ItemsDataBase.Find(ItemNameTag)->ItemPickupClass;
}

int UDreampaxItemsDataAsset::GetItemMaxStackSize(const FGameplayTag & ItemNameTag)
{
    // if DefaultGameplayTagForMaxSizeOfStack is missed return 1 for all items
    if (!DefaultGameplayTagForMaxSizeOfStack.IsValid())
    {
        return 1;
    }

    int MaxStackSize = floor(GetItemData(ItemNameTag)->FindRef(DefaultGameplayTagForMaxSizeOfStack));

    if (MaxStackSize > 0)
    {
        return MaxStackSize;
    }

    // if Tag for MaxStackSize is "0" return 1
    return 1;
}

bool UDreampaxItemsDataAsset::ItemIsFound(const FGameplayTag & ItemNameTag) const
{
    if (ItemsDataBase.Find(ItemNameTag))
    {
        return true;
    }

    return false;
}

От мультиплеера тут пока еще ничего нет. Но это первый шаг, который сделан в верном направлении.


В следующей статье я расскажу о методиках подключения DataAsset (да, и любого Blueprint) для считывания данных в C++, и покажу какая из них является наиболее правильной.


Если есть вопросы или пожелания раскрыть какой-либо аспект подробнее, пожалуйста пишите в комментариях.

Комментарии (8)


  1. Flakky
    01.08.2018 19:11

    Почему не UOBject? Не нужно в мире ничего хранить, но может поддерживать любой функционал. А статичные данные хранить ссылкой на датаассет уже, если сильно хочется… Делать инвентарь чисто на DataAsset, достаточно топорный вариант, я считаю, так как в БП его неудобно дополнять логикой, да и не всегда это можно вообще.
    Да и для мультеплеера подходит, если вы знаете, как их реплицировать.

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

    А, и ещё один совет… Распишите архитектуру инвентаря. Какие классы для чего, как все это хранится и прочее. А то вы сразу начали с узкого места, так ничего не понятно… Почему DataAsset, а не UObject? А что их хранить будет? А как они должны в теории реплицироваться по сети? А что если я конкретному объекту хочу добавить логику в БП, ДатаАссеты не подходят для этого…
    В общем кучу вопросов возникает прежде чем начать создавать какие-то классы зачем-то.


  1. Flakky
    01.08.2018 19:27

    Иногда, после компиляции ваш Blueprint может оказаться девственно пуст, что крайне неприятно, если вы заполнили уже десяток-другой позиций.

    Кстати, это обходится компиляцией с закрытым редактором, либо перезапуском редактора ничего не меняя и не сохраняя.

    И ещё… Перечитал ещё раз, что бы разобраться подробнее. Как я понял, у вас инвентарь на стуркурах получается? Признаться честно, такая реализация имеет очень много проблем. Нельзя делать свои структуры данных, нельзя наследоваться… Инвентарь на структурах это по сути обычная таблица, которая даже уникальный функционал не поддерживает внутри объектов.

    Почему именно структурный вариант, а не объектный, который поддерживает в разы больше и его поддержка куда легче?


    1. SvarogZ Автор
      01.08.2018 22:41
      +1

      Уважаемый Flakky,

      Осведомлен о Вашем видении инвентаря на UOBject, но, к сожалению, Ваша статья, если мне не изменяет память, уже больше года на стадии разработки. Как только будет опубликована, я с удовольствием почитаю и, может даже, пересмотрю свою точку зрения.

      Каждый предмет без проблем дополняется уникальной логикой как в C++, так и в Blueprint, поскольку DataAsset хранит ссылку как раз на Blueprint предмета.

      Архитектура будет. Пока я еще ни слова ни сказал об инвентаре. Пока только это база данных для предметов, которая хранит только общую информацию, необходимую для спауна объекта. Никаких уникальных свойств объекта, отличающего его от другого того же типа, в ней нет.

      Кстати, это обходится компиляцией с закрытым редактором, либо перезапуском редактора ничего не меняя и не сохраняя
      Ценное замечание. Тем не менее у меня периодически получалось обнулить. Хорошо помогают ежедневный backup и Git.

      Немного не понял — почему нельзя делать свои структуры данных?

      Повторюсь, DataAsset хранит только базовую информацию для спауна. Это не инвентарь.


      1. Flakky
        02.08.2018 12:03

        Да, я теперь понял. Прошу прощения. Просто из-за того, что вы не расписали ничего, в общем-то, ничего не понятно. Поэтому создалось ощущение, что вы собираетесь хранить предметы не как объекты, а как структуры.

        Хотя я до сих пор не очень понял, зачем делать таблицу (или базу в вашем случае), если можно просто создать отдельные датаассеты под каждый предмет и в них писать данные. Это так же позволит дополнять информацией отдельные типы предметов. Например для всех предметов у вас есть клас хранения и класс спауна, а для определенного вида объектов у вас будет ещё классы, скажем, сериализации объекта или класс уже одетой экипировки. В список для всех предметов добавлять их смысла мало, мне кажется)

        Конечно таблица в любом случае нужно будет, но в ней будет лишь список датаассетов и айдишники предметов, что бы получать их из базы данных. А вся остальная инфа уже в самих датаассетах, как я сказал выше.

        Впрочем, этот холивар может продолжаться долго, если хотите, можете написать в личку, расскажу, как у нас реализован инвентарь в сингловом проекте и как в другом мультиплеерном, который в общем-то существует только на мета сервере. Может быть пару идей для себя возьмете, а я у вас :)


        1. ROARd
          02.08.2018 22:31

          Давайте просто наберемся терпения, и подождем, когда автор сам скажет хотя бы пару слов об архитектуре. Вместо того, чтобы многословить в пустоту, называя это «холиваром».


          1. SvarogZ Автор
            02.08.2018 22:42

            Постараюсь в выходные продолжить…


            1. ROARd
              03.08.2018 12:02

              Спасибо, начало интригующее =)


        1. SvarogZ Автор
          02.08.2018 22:39

          Конечно таблица в любом случае нужно будет

          Именно этого я и пытался избежать, потому что
          Если вы храните адрес к конкретному Blueprint, то при переименовании или перемещении его в другую папку вы будете вынуждены изменить этот адрес вручную.

          Хранить ссылки на DataAssets… интересно… но мне пока такая структура не понадобилась. Хватает и одного DataAsset.

          С удовольсвием обменяюсь опытом.