UE Slate урок 2
Это второй урок по Slate, в нем мы начнем создание инвентаря на этом фреймворке.
Инвентарь и предметы
Прежде чем приступать к созданию UI ячейки, нужно написать код, который она будет визуализировать. Систему инвентаря я буду разрабатывать постепенно, добавляя новый функционал в каждом уроке. В этом уроке я напишу только те части, которые необходимы для отображения информации в ячейке.
Создадим файлы InventoryComponent.h и InventoryComponent.cpp:
InventoryComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InventoryComponent.generated.h"
UCLASS(Blueprintable, BlueprintType)
class LEARNSLATE_API AItem : public AActor
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item")
FSlateBrush ItemIcon;
};
USTRUCT(BlueprintType)
struct FInventoryItem
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
TSubclassOf<AItem> ItemClass;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
int32 Count;
FInventoryItem()
{
ItemClass = nullptr;
Count = -1;
}
FInventoryItem(TSubclassOf<AItem> aItemClass, int32 aCount)
{
ItemClass = aItemClass;
Count = aCount;
}
};
UCLASS(Blueprintable, BlueprintType, meta = (BlueprintSpawnableComponent))
class LEARNSLATE_API UInventoryComponent : public UActorComponent
{
GENERATED_BODY()
public:
virtual void BeginPlay() override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
int32 InventorySizeX = 3;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
int32 InventorySizeY = 3;
TArray<FInventoryItem>& GetItems() { return Items; }
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
TArray<FInventoryItem> Items;
};
InventoryComponent.cpp
#include "InventoryComponent.h"
void UInventoryComponent::BeginPlay()
{
Super::BeginPlay();
for(int32 i = Items.Num(); i < InventorySizeX * InventorySizeY; i++)
{
Items.Add(FInventoryItem());
}
}
В этом коде мы добавляем класс AItem, который представляет собой класс предмета и пока хранит в себе только иконку, структуру FInventoryItem, которая содержит информацию о классе предмета и его количестве, а также систему инвентаря UInventoryComponent, которая использует структуру FInventoryItem для хранения предметов. В BeginPlay мы создаем пустые слоты для предметов. Эти слоты являются частью системы инвентаря, и о них мы поговорим в следующих уроках.
Теперь нужно добавить этот компонент в класс нашего Character. Откроем “имя_вашего_проекта_Character.h” и добавим следующий код:
public:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventory")
class UInventoryComponent* InventoryComponent;
В .cpp файле в конструкторе Character создаем компонент и назначаем его в нашу переменную:
InventoryComponent = CreateDefaultSubobject<UInventoryComponent>(TEXT("InventoryComponent"));
Ячейка инвентаря
Понятно, что инвентарь будет состоять из ячеек, по сути, мы создадим только одну ячейку и будем дублировать её нужное нам количество раз.
Ячейка будет нести в себе следующую информацию:
- Иконка
- Количество
Сейчас нам нужно добавить в модули в файле название_вашего_проекта.Build.cs в PublicDependencyModuleNames поле “UMG”, примерно будет выглядеть так:
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "UMG" });
Это необходимо, потому что в этом уроке мы начнем работать с новыми классами пользовательского интерфейса.
Создами два новых файла ItemSlotWidget.cpp и ItemSlotWidget.h:
ItemSlotWidget.h
#pragma once
#include "CoreMinimal.h"
struct FInventoryItem;
class LEARNSLATE_API SItemSlotWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SItemSlotWidget) {}
SLATE_ARGUMENT(FInventoryItem*, Item)
SLATE_END_ARGS()
FInventoryItem* Item;
void Construct(const FArguments& InArgs);
const FSlateBrush* GetItemIcon() const;
FText GetItemCount() const;
};
.h файл ничего нового в себе не несет, кроме аргумента SLATE_ARGUMENT(FInventoryItem*, Item), с помощью которого мы передаем в виджет указатель на Item.
ItemSlotWidget.cpp
void SItemSlotWidget::Construct(const FArguments& InArgs)
{
Item = InArgs._Item;
ChildSlot
[
SNew(SBox)
.WidthOverride(110)
.HeightOverride(110)
[
SNew(SOverlay)
+ SOverlay::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
[
SNew(SImage)
.Image(FCoreStyle::Get().GetBrush("BlackBrush")) // Slot background
]
+ SOverlay::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SNew(SBox)
.WidthOverride(100)
.HeightOverride(100)
[
SNew(SOverlay)
+ SOverlay::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
[
SNew(SImage)
.Image(FCoreStyle::Get().GetBrush("WhiteBrush")) // Item background
]
+ SOverlay::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SNew(SImage)
.Image(this, &SItemSlotWidget::GetItemIcon) // Item icon
]
+ SOverlay::Slot()
.HAlign(HAlign_Right)
.VAlign(VAlign_Bottom)
[
SNew(STextBlock)
.Text(this, &SItemSlotWidget::GetItemCount) // Item count
.ColorAndOpacity(FSlateColor(FLinearColor::Black))
.Font(FSlateFontInfo(FPaths::EngineContentDir() / TEXT("Slate/Fonts/Roboto-Regular.ttf"), 24))
]
]
]
]
];
}
const FSlateBrush* SItemSlotWidget::GetItemIcon() const
{
if(Item && Item->ItemClass)
{
return &Item->ItemClass.GetDefaultObject()->ItemIcon;
}
return FCoreStyle::Get().GetBrush("WhiteBrush");
}
FText SItemSlotWidget::GetItemCount() const
{
if(Item && Item->ItemClass)
{
return FText::FromString(FString::FromInt(Item->Count));
}
return FText::FromString("");
}
.cpp файл заметно увеличился по сравнению с прошлым уроком. Несмотря на его возросший объем, в нем нет ничего сложного. Вот как он устроен:
- Черный задник размером 110 на 110 выполняющий роль рамки ячейки.
/- Белый задник размером 100 на 100 выполняющий роль фона ячейки.
/- Иконка предмета
/- Текст с количеством предметов
Также есть две функции, которые получают информацию из предмета: GetItemIcon() и GetItemCount().
Панель инвентаря
Теперь, когда у нас есть ячейка, нужно сделать виджет, который будет дублироваться столько раз, чтобы отобразить весь инвентарь.
Создадим два файла WInventoryMainBar.h и WInventoryMainBar.cpp
WInventoryMainBar.h
#pragma once
#include "CoreMinimal.h"
#include "Components/Widget.h"
#include "WInventoryMainBar.generated.h"
class LEARNSLATE_API SInventoryMainBar : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SInventoryMainBar) {}
SLATE_ARGUMENT(TWeakObjectPtr<class UInventoryComponent>, Inventory)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
TWeakObjectPtr<class UInventoryComponent> Inventory;
TArray<TSharedPtr<class SItemSlotWidget>> MainInventoryItemsWidgets;
};
UCLASS(Blueprintable, BlueprintType)
class LEARNSLATE_API UWInventoryMainBar : public UWidget
{
GENERATED_BODY()
public:
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (ExposeOnSpawn = true), Category = "InventoryMainBar")
class UInventoryComponent* InventoryComponent;
virtual const FText GetPaletteCategory() override;
protected:
virtual TSharedRef<SWidget> RebuildWidget() override;
TSharedPtr<SInventoryMainBar> InventoryMainBarWidget;
};
В .h появился новый класс UWidget, его можно назвать прослойкой между чистым C++ Slate и Bluepint Widgets. UWidget является наследником UVisual, который в свою очередь наследуется от UObject. Это хорошая новость для нас, так как теперь нам доступен весь его функционал. Кроме того, UWidget можно добавлять напрямую в Blueprint виджете прямо в редакторе, что позволяет заниматься версткой этих элементов мышкой в окне, а не кодом в файле.
Функции:
- virtual void ReleaseSlateResources(bool bReleaseChildren) нужна для очистки нашего Slate виджета из памяти.
- virtual const FText GetPaletteCategory() позволяет нам задать категорию этому виджету, по которой мы потом сможем найти его в Blueprint виджете.
- virtual TSharedRef/
RebuildWidget() создает Slate виджет.
WInventoryMainBar.cpp
#include "WInventoryMainBar.h"
#include "InventoryComponent.h"
#include "ItemSlotWidget.h"
#include "Widgets/Layout/SGridPanel.h"
void SInventoryMainBar::Construct(const FArguments& InArgs)
{
Inventory = InArgs._Inventory;
TSharedPtr<SGridPanel> GridPanel;
ChildSlot
.HAlign(HAlign_Left)
.VAlign(VAlign_Center)
[
SAssignNew(GridPanel, SGridPanel)
];
int32 SlotsCountX = Inventory.IsValid() ? Inventory->InventorySizeX : 3;
int32 SlotsCountY = Inventory.IsValid() ? Inventory->InventorySizeY : 3;
int32 Counter = 0;
for (int32 x = 0; x < SlotsCountX; x++)
{
for (int32 y = 0; y < SlotsCountY; y++)
{
TSharedPtr<SItemSlotWidget> NewWidget;
GridPanel->AddSlot(y, x).Padding(10)
[
SAssignNew(NewWidget, SItemSlotWidget)
.Item(Inventory.IsValid() ? &Inventory->GetItems()[Counter] : nullptr)
];
MainInventoryItemsWidgets.Add(NewWidget);
Counter++;
}
}
}
void UWInventoryMainBar::ReleaseSlateResources(bool bReleaseChildren)
{
InventoryMainBarWidget.Reset();
}
TSharedRef<SWidget> UWInventoryMainBar::RebuildWidget()
{
APlayerController * Controller = GetOwningPlayer();
if(Controller)
{
APawn * Pawn = Controller->GetPawn();
if(Pawn)
{
InventoryComponent = Pawn->FindComponentByClass<UInventoryComponent>();
}
}
InventoryMainBarWidget = SNew(SInventoryMainBar).Inventory(InventoryComponent);
return InventoryMainBarWidget.ToSharedRef();
}
const FText UWInventoryMainBar::GetPaletteCategory()
{
return FText::FromString("Inventory");
}
В .cpp файле обратите внимание на функцию void SInventoryMainBar::Construct(const FArguments& InArgs) :
Так как нам нужно будет циклом создать нужное количество ячеек, GridPanel создается заранее, и его важно назначить в переменную внутри ChildSlot. Далее идет важная проверка на валидность указателя на инвентарь, потому что во время настройки виджета в эдиторе инвентаря не существует!
Затем мы циклом добавляем в GridPanel ячейки инвентаря и назначаем им указатели на структуры элементов инвентаря, которые они будут отображать, снова проверяя валидность инвентаря. В случае невалидности ячейке присваивается nullptr.
Далее наш UWidget класс UWInventoryMainBar, функции ReleaseSlateResources() и GetPaletteCategory() очень просты, поэтому перейдем к RebuildWidget() :
Наша цель тут, создать Slate виджет и дать ему указатель на инвентарь, поэтому пытаемся получить его из Pawn игрока. Если мы не получим инвентарь, что обязательно произойдет во время настройки Blueprint виджета, то Slate виджет создатся с нулевым указателем на инвентарь, что мы предусмотрели, сделав проверки на это.
Теперь у нас есть виджет, который мы можем добавить в Blueprint виджет, но чтобы получить полный контроль над ним в коде, лучше и создать его в коде. Нам нужен класс UUserWidget.
Создадим файл UWMain.h:
UWMain.h
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "UWMain.generated.h"
class UCanvasPanel;
class UWInventoryMainBar;
UCLASS(Blueprintable, BlueprintType)
class LEARNSLATE_API UWMain : public UUserWidget
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadOnly, Category = "UWMain", meta=(BindWidget))
UCanvasPanel* RootWidget;
UPROPERTY(BlueprintReadOnly, Category = "UWMain", meta=(BindWidget))
UWInventoryMainBar* InventoryMainBar;
};
Этот файл небольшой и содержит, помимо самого класса, два указателя: UCanvasPanel RootWidget и UWInventoryMainBar InventoryMainBar. Самое важное здесь - это meta=(BindWidget) в UPROPERTY. Эта мета работает следующим образом: в Blueprint виджете обязательно должен быть дочерний виджет, на который есть указатель, иначе Blueprint не скомпилируется и будет выдавать ошибку. Это гарантирует, что у нас не возникнет ситуации, когда мы обращаемся к виджету через указатель, а там nullptr. В нашем случае мы обязаны добавить в Blueprint виджет UCanvasPanel с именем RootWidget и UWInventoryMainBar с именем InventoryMainBar.
Изменение кода HUD
Теперь нужно добавить этот виджет в HUD, поэтому немного изменим код в MyHUD.h и MyHUD.cpp:
MyHUD.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/HUD.h"
#include "MyHUD.generated.h"
class UWMain;
UCLASS()
class LEARNSLATE_API AMyHUD : public AHUD
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Category = "Widgets")
TSubclassOf<UUserWidget> MainWidgetClass;
protected:
virtual void BeginPlay() override;
UWMain* MainWidget;
};
Мы добавили ссылку на наш класс виджета.
MyHUD.cpp
#include "MyHUD.h"
#include "UWMain.h"
#include "Blueprint/UserWidget.h"
void AMyHUD::BeginPlay()
{
Super::BeginPlay();
MainWidget = CreateWidget<UWMain>(GetWorld(), MainWidgetClass);
if(MainWidget)
{
MainWidget->AddToViewport();
}
}
Здесь в начале игры мы создаем и добавляем виджет на экран.
Последние шаги в блюпринтах
Запускаем движок. Сначала создадим дочерний класс AItem, чтобы у нас был предмет, который можно будет положить в инвентарь.
В этом классе меняем изображение на любое подходящее. Я использую изображение из движка: T_UE4Logo_Mask.
Теперь открываем нашего игрока ThirdPersonCharacter, выбираем InventoryComponent и добавляем в массив предметов наш новый класс, количество можно выбрать произвольное, но элементов в массиве не должно быть больше 9ти, так как это размер нашего инвентаря.
Теперь создадим дочерний класс нашего виджета UWMain и назовем W_Main.
Открыв класс, мы видим те ошибки, о которых я говорил:
A required widget binding "RootWidget" of type Canvas Panel was not found.
A required widget binding "InventoryMainBar" of type WInventory Main Bar was not found.
A required widget binding "RootWidget" of type Canvas Panel was not found.
A required widget binding "InventoryMainBar" of type WInventory Main Bar was not found.
Виджет не будет скомпилирован, пока мы не добавим соответствующие виджеты. Добавляем виджет UCanvasPanel с именем RootWidget и UWInventoryMainBar с именем InventoryMainBar.
Кроме того, я немного изменю настройки InventoryMainBar, чтобы он находился по центру левого края экрана. Настройки указаны на изображении ниже:
Теперь нужно создать дочерний класс HUD.
Назовем его HUD_Base и укажем в нем класс виджета UWMain.
И наконец, последнее, что нам нужно сделать, это создать дочерний GameMode от класса “имя_вашего_проекта_GameMode.h” и назвать его GM_Base.
В нем укажем наш HUD_Base. Все остальные настройки можно оставить по умолчанию.
Осталось указать наш новый GameMode в настройках проекта.
Поздравляю, вы справились! Теперь можно запускать проект, и на экране должен появиться инвентарь с предметами.
Заключение
После этого урока у вас будет хорошее представление о возможностях Slate и понимание того, как его использовать. Самое сложное уже позади, впереди лишь появление новых функций, таких как обработка нажатий на виджет или операции Drag-and-Drop, но основа останется прежней.