[Из песочницы] UE4 | Инвентарь для Multiplayer #1 | Хранилище данных на DataAsset

habr.png

В этой статье я постараюсь раскрыть смысл и методику создания 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 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 ItemOutfitClass;

    /* The class to spawn the Mesh in the level then it is dropped */
    UPROPERTY(EditDefaultsOnly, Category = "ItemsDatabase")
    TSubclassOf 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 BuildingBlockClass;

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

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

    UPROPERTY(EditDefaultsOnly, Category = "BlocksDatabase")
    TArray BlockMaterials;

    FORCEINLINE TSubclassOf * GetBuildingBlockClass()
    {
        return &BuildingBlockClass;
    }

    FORCEINLINE FVector GetDefaultSize()
    {
        return DefaultSize;
    }

    FORCEINLINE FVector GetSizeLimits()
    {
        return SizeLimits;
    }

    FORCEINLINE TArray GetBlockMaterials()
    {
        return BlockMaterials;
    }
};

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

UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "ItemsDatabase")
TMap 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 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 ItemOutfitClass;

    /* The class to spawn the Mesh in the level then it is dropped */
    UPROPERTY(EditDefaultsOnly, Category = "ItemsDatabase")
    TSubclassOf 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 ItemsDataBase;

public:

    FORCEINLINE TMap * 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 * GetItemOutfitClass(const FGameplayTag & ItemNameTag);

    FORCEINLINE TSubclassOf * 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* 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* UDreampaxItemsDataAsset::GetItemOutfitClass(const FGameplayTag &ItemNameTag)
{
    return & ItemsDataBase.Find(ItemNameTag)->ItemOutfitClass;
}

TSubclassOf* 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++, и покажу какая из них является наиболее правильной.

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

© Habrahabr.ru