UE4 | Інвентар для Multiplayer #1 | Сховище даних на DataAsset
Список статей
UE4 | Інвентар для Multiplayer #1 | Сховище даних на DataAsset
UE4 | Інвентар для Multiplayer #2 | Підключення Blueprint до C++
UE4 | Інвентар для Multiplayer #3 | Структура взаємодії
У цій статті я постараюся розкрити зміст і методику створення 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 */
//includes no
#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 вважається більш правильним, оскільки суттєво знижує ризик помилки і має ряд переваг, які нам знадобляться пізніше.
GameplayTag в редакторі
DataTable для GameplayTag експортована з Excel
Хорошим тоном є також прописати в структурі функції для виклику змінних, такі як 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 і заповнювати його.
Приклад заповнення DataAsset
Але перед цим, напишемо ще кілька функцій виклику, щоб мати можливість одержувати дані з бази.
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 */
//includes no
#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.срр
/// Copyright 2018 Dreampax Games, Inc. All Rights Reserved.
#include "DreampaxItemsDataAsset.h"
/* Includes from Engine */
// includes no
/* Includes from Dreampax */
// includes no
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++, і покажу яка з них є найбільш правильною.
Якщо є питання або побажання розкрити будь-який аспект детальніше, будь ласка пишіть в коментарях.