В предыдущей статье я рассказывал как создать DataAsset, и почему он такой хороший и удобный. Здесь же мы рассмотрим то, как получить доступ к DataAsset, точнее к назначенным в нем данным, из Blueprint и C++.
Попутно мы ответим на вопрос получения доступа к любому Blueprint из C++.
Со взаимодействием Blueprints все достаточно прозрачно.
Ввиду того, что мы закрыли прямой доступ к нашей базе данных, мы не можем просто так обратится к ней из Blueprint. Обратите внимание на protected: в коде ниже.
protected:
/* This is the main Database for all Items. It contains constant common variables */
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "ItemsDatabase")
TMap<FGameplayTag, FItemsDatabase> ItemsDataBase;
Т.е. наше хранилище будет видимо только в наследуемых классах, и это хорошо, потому что мы прописали функции для безопасного вызова данных.
/* Used in the widget */
UFUNCTION(BlueprintCallable, Category = "ItemDatabase")
FORCEINLINE UTexture2D * GetItemIconTexture(const FGameplayTag & ItemNameTag) const;
BlueprintCallable как раз и означает, что данная функция может быть использована в Blueprint. Если вы читали предыдущую статью, то наверно заметили, что другие функции вызова такого атрибута не имеют. Так сделано только потому, что вызываемые ими данные на данный момент в Blueprint не понадобились. Если кому-то что-то знать не нужно — не спешим об этом сообщать.
Следующий шаг, это создание в любом Blueprint переменной типа созданной нами базы данных (в моем случае это BP_DreampaxItemsDataAsset).
После этого легко непринужденно извлекаем назначенную текстуру.
Теперь рассмотрим как получить доступ к информации в C++.
Мы не можем просто обратиться к классу DreampaxItemsDataAsset, поскольку он не содержит никакой информации. Нам нужно заполучить доступ к BP_DreampaxItemsDataAsset.
Существует два основных метода как достучаться до Blueprint.
Сначала рассмотрим неудобный способ подключения с использованием костыля ConstructorHelpers. В данном случае это доступ к текстуре.
ASHUD::ASHUD(const class FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
/* You can use the FObjectFinder in C++ to reference content directly in code. Although it's advisable to avoid this and instead assign content through Blueprint child classes. */
static ConstructorHelpers::FObjectFinder<UTexture2D> HUDCenterDotObj(TEXT("/Game/UI/HUD/T_CenterDot_M.T_CenterDot_M"));
CenterDotIcon = UCanvas::MakeIcon(HUDCenterDotObj.Object);
}
Пример взят из замечательного проекта EpicSurvivalGameSeries, идеально подходящего для изучения Multiplayer на C++. Автор поставил цель показать как можно больше методов и приемов программирования игры на C++.
Почему данный способ неудобен? Та же беда как и с DataTable — при изменении названия Blueprint или месторасположения, файл не будет найден.
Наиболее предпочтительным можно считать метод в котором мы объявляем переменную в заголовочном файле, для последующего назначения ее уже в наследованном Blueprint. Для примера выше это могло бы выглядеть так:
UPROPERTY(EditDefaultsOnly, Category = "AimPointer")
FCanvasIcon CenterDotIcon;
Намного проще, верно?
Теперь, зная как получить доступ к любому Blueprint, мы можем без проблем подключить нашу базу данных.
UCLASS()
class ADreampaxGameMode : public AGameMode
{
GENERATED_BODY()
public:
ADreampaxGameMode(const FObjectInitializer & ObjectInitializer);
/////////////////////////////////////////////////////////////////////////////
//Data Bases
/////////////////////////////////////////////////////////////////////////////
public:
/* Connect data base in BP for items*/
UPROPERTY(EditDefaultsOnly, Category = "Database")
class UDreampaxItemsDataAsset * DreampaxItemsDataAsset;
FORCEINLINE UDreampaxItemsDataAsset * GetDreampaxItemsDataAsset() const;
Как человек почти незнакомый с C++, я сломал немало копий, пытаясь понять как правильно объявлять кастомные переменные.
Если стоит цель объявить переменную созданного нами класса, как, например
UDreampaxItemsDataAsset * DreampaxItemsDataAsset;
// или
class UDreampaxItemsDataAsset * DreampaxItemsDataAsset;
лично мне какое-то время было непонятно когда нужно применять class, а когда нет.
Все оказалось до боли просто.
- Если не ставить class, то нужно выполнить включение #include "Data/DreampaxItemsDataAsset.h", содержащее объявление этого класса.
- Если ставить class, то #include "Data/DreampaxItemsDataAsset.h" можно сделать уже в .cpp.
- И еще одна опция предыдущего пункта, если нужно объявить сразу много переменных данного класса. Непосредственно после всех #include предварительно объявить наш класс class UDreampaxItemsDataAsset;, а после объявлять переменные уже без приставки class.
Какой из этих способов правильный — не берусь сказать. Если кто-то объяснит, буду благодарен.
Делаем переменную в C++ классе ADreampaxGameMode, так как он виден только серверу, а все что связано со спауном объектов должно идти только через сервер. Данный класс является родителем для BP_DreampaxGameMode, где мы и подключим наш BP_DreampaxItemsDataAsset.
Теперь вся мощь C++ может быть использована для работы с данными нашей базы данных.
В следующей статье (наконец-то!) мы поговорим о создании инвентаря и узнаем почему нам никак не обойтись без уже созданного DataAsset.
Если есть вопросы или пожелания раскрыть какой-либо аспект подробнее, пожалуйста пишите в комментариях.