[Из песочницы] UE4 | Инвентарь для Multiplayer #1 | Хранилище данных на DataAsset
В этой статье я постараюсь раскрыть смысл и методику создания DataAsset, как хранилища для различного рода данных, а нашем случае это библиотека для Actors и их параметров.
Принять решение создавать игру около 2-х лет назад, мне помогло то, что я случайно наткнутся на информацию об Unreal Engine 4 и прочитал как это круто и просто. На деле же, человеку не умеющему писать код (язык программирования не имеет значения в данном контексте) очень сложно создать что-то, сложнее небольшой модификации стандартного набора заготовок из движка. Поэтому, изначальное желание сделать супер-мега игру, с ростом знаний о реальности данного проекта, постепенно переросло в хобби. Поднять все пласты разработки игры, от 3D моделирования и анимации, и до написания кода, для одного человека представляется мало осуществимым предприятием. Тем не менее, это хорошая тренировка для мозга.
Почему решил что-то написать?… Наверно из-за того. что представленные мануалы либо дают очень поверхностные знания (и таких большинство), либо уж для совсем профи и содержат лишь общие указания.
Начинать почти всегда лучше с начала. Не могу сказать, что поступаю так всегда, но постараюсь излагать последовательно, насколько это возможно.
Конечно же, лучше всего начать со структуры, но, к сожалению, имея закрытый ящик с инструментами, очень сложно понять, что именно можно с их помощью построить. Так давайте же откроем это ящик и посмотрим что содержится внутри.
Первый вопрос, на который следует ответить. Почему именно DataAsset?
- Очень часто в статьях и «туториалах» можно увидеть применение DataTable. Почему это плохо? Если вы храните адрес к конкретному Blueprint, то при переименовании или перемещении его в другую папку вы будете вынуждены изменить этот адрес вручную. Согласитесь — неудобно? С DataAsset же такого не случится. Все связи обновятся автоматически. Если же вы абсолютно уверены в структуре своего проекта на годы вперед, то, конечно же, можно использовать таблицы.
- Второе неоспоримое преимущество — это возможность хранить сложные типы данных, например, такие как структуры (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 и заполнять его.
Но перед этим, напишем еще несколько функций вызова, чтобы иметь возможность получать данные из базы.
/// 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;
};
/// 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++, и покажу какая из них является наиболее правильной.
Если есть вопросы или пожелания раскрыть какой-либо аспект подробнее, пожалуйста пишите в комментариях.