やあ
UE4 C++でインベントリシステムを作ろう!
Part.2では、インベントリ内のアイテムをドラッグアンドドロップで移動できるようにするよ。
前回
Part.2の結果
目次
概要
①プレイヤー : ドラッグアンドドロップしたいアイテムのあるスロットをクリック。
②スロット : クリックされた!イベント発生
③プレイヤー : ドラッグ
④スロット : ドラッグされたアイテムの情報を持ったオペレーションを作り、自身の持つ情報をそのオペレーションに渡す。
(UE4独自にDragDropOperation
クラスというものがあるので、それを使う。)
⑤プレイヤー : ドロップ
⑥スロット(またはドロップイベントを受け取れるUI) : ドロップされた!イベント発生
⑦スロット : ドラッグ元のスロットの情報と、自身の持つ情報を交換。
⑧終わり
ちょっとこんがらがるかもしれないけど、大したことはしてないね~
スロットの動作を考える
・クリックされた?
-> 左クリック?右クリック?ホイール?
・ドラッグされた?
-> スロットの情報をオペレーションに渡す
・ドロップされた?
-> スロットの情報を交換する
これだけ!
アイテムがどのスロットから来たのかを判別するための列挙体
.cpp
ファイルはいらないので消しといてね
MyEnums.h
に追加
// Copyright(C)write by pto8913. 2020. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "ptoEnums.generated.h" UENUM(Blueprintable) enum class EItemIsFrom : uint8 { None = 0, Inventory = 1, Chest = 2, HotBar = 3, Equip = 4, }; ENUM_CLASS_FLAGS(EItemIsFrom)
MyStructs.h
に追加
#include "ptoEnums.h" USTRUCT(Blueprintable) struct FInventorySlots { GENERATED_USTRUCT_BODY() public: UPROPERTY() EItemIsFrom ItemIsFrom; };
アイテムの情報を保持するオペレーションの作成
DragDropOperation
クラスを継承してItemDragDropOperation
クラスを作成。
.cpp
ファイルはいらないので消しといてね
ItemDragDropOperation.h
に追加
// Copyright(C)write by pto8913. 2020. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "Blueprint/DragDropOperation.h" #include "Templates/ptoStructs.h" #include "UIs/Bases/InventorySlotBase.h" #include "ItemDragDropOperation.generated.h" class UInventorySlotBase; UCLASS() class INVENTORYSYSTEM_API UItemDragDropOperation : public UDragDropOperation { GENERATED_BODY() public: UPROPERTY() UInventorySlotBase* InventorySlotUI; EItemIsFrom GetItemIsFrom() const { return InventorySlotUI->ItemIsFrom; } };
スロットの動作の実装
前回作ったInventorySlotBase
クラスに、クリックされた?、ドラッグされた?を実装する。
ほとんどのスロットで、この二つの処理は共通なのでここで実装する。
クリックとドラッグの実装
xx.Build.cs
に追加してね
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
InventorySlotBase.h
に追加
UPROPERTY() EItemIsFrom ItemIsFrom; private: /* Classes */ UPROPERTY(EditAnywhere) TSubclassOf<UUserWidget>DraggedWidgetClass; protected: virtual FReply NativeOnMouseButtonDown( const FGeometry& InGeometry, const FPointerEvent& InMouseEvent ) override; virtual void NativeOnDragDetected( const FGeometry& InGeometry, const FPointerEvent& InMouseEvent, UDragDropOperation*& OutOperation ) override;
InventorySlotBase.cpp
に追加
#include "Operations/ItemDragDropOperation.h" #include "Blueprint/WidgetBlueprintLibrary.h" FReply UInventorySlotBase::NativeOnMouseButtonDown( const FGeometry& InGeometry, const FPointerEvent& InMouseEvent ) { UUserWidget::NativeOnMouseButtonDown(InGeometry, InMouseEvent); if (!IsValid(SlotContents.GetItemImage())) return FReply::Unhandled(); /* クリックされたマウスのボタンが左クリックかを調べる */ FEventReply Res = UWidgetBlueprintLibrary::DetectDragIfPressed( InMouseEvent, this, EKeys::LeftMouseButton ); if (Res.NativeReply.IsEventHandled() == true) { return Res.NativeReply; } return Res.NativeReply; } void UInventorySlotBase::NativeOnDragDetected( const FGeometry& InGeometry, const FPointerEvent& InMouseEvent, UDragDropOperation*& OutOperation ) { if (!IsValid(SlotContents.GetItemImage())) return; /* スロットの情報をドロップしたスロットへ渡すためのオペレーションの作成 */ UItemDragDropOperation* ItemOperation = Cast<UItemDragDropOperation>( UWidgetBlueprintLibrary::CreateDragDropOperation( UItemDragDropOperation::StaticClass() ) ); if (IsValid(ItemOperation) == true) { /* ドラッグされたアイテムの画像を表示するだけのUI */ UUserWidget* DraggedItem = CreateWidget<UUserWidget>(GetWorld(), DraggedWidgetClass); UImage* ImageBox = Cast<UImage>(DraggedItem->GetWidgetFromName("ItemImage")); if (ImageBox != nullptr) { ImageBox->SetBrushFromTexture(SlotContents.GetItemImage()); } ItemOperation->DefaultDragVisual = DraggedItem; ItemOperation->Pivot = EDragPivot::MouseDown; /* スロットの情報をオペレーションに渡す */ ItemOperation->InventorySlotUI = this; OutOperation = Cast<UDragDropOperation>(ItemOperation); } UUserWidget::NativeOnDragDetected(InGeometry, InMouseEvent, OutOperation); }
これでクリックとドラッグは実装できたよ。
エディタに戻ってWBP_DraggedItem
を作って
WBP_DraggedItem
をWBP_InventorySlot
に設定して、プレイ
とりあえずドラッグまでできました。
ドロップの実装
UMGのドロップイベントは、ドロップしたUMGじゃなくて、
ドロップされたUMG側に実装する必要があるよ。
そのため、インベントリのスロット、宝箱のスロット、装備のスロット等々で処理が全部違うんだ。
だから、新しいクラスを作るよ。
InventorySlotBase
クラスを継承して、InventorySlot
クラスを作成。
実装の前にスロットの情報を交換するための処理
これはとても簡単です。とりあえず実装を見てください。
InventorySlotBase.h
に追加
protected: virtual void ExchangeSlot(UInventorySlotBase* From);
InventorySlotBase.cpp
に追加
#include "UIs/Bases/InventoryWidgetBase.h" void UInventorySlotBase::ExchangeSlot(UInventorySlotBase*& From) { int FromIdx = From->SlotContents.SlotIndex; int ToIdx = SlotContents.SlotIndex; FInventorySlots FromTemp = From->InventoryComp->Inventory[FromIdx]; /* Fromスロットのインベントリコンポーネントのインベントリを更新 */ From->InventoryComp->Inventory[FromIdx] = MoveTemp(InventoryComp->Inventory[ToIdx]); /* インベントリUIのインベントリを更新 */ Cast<UInventoryWidgetBase>(From->OwnerUI)->Inventory[FromIdx] = From->InventoryComp->Inventory[FromIdx]; /* このスロットのインベントリコンポーネントのインベントリを更新 */ InventoryComp->Inventory[ToIdx] = MoveTemp(FromTemp); /* インベントリUIのインベントリを更新 */ Cast<UInventoryWidgetBase>(OwnerUI)->Inventory[ToIdx] = InventoryComp->Inventory[ToIdx]; /* スロットの見た目の更新 */ From->UpdateSlot(); UpdateSlot(); }
なんとこれだけなんです。
後々、コンポーネントを持たないインベントリUIとの交換の処理も書くのですがほぼ、これと同じです。
これでコンポーネントを持つスロット同士の入れ替えができるようになりました。
ドロップの実装
InventorySlot.h
に追加
// Copyright(C)write by pto8913. 2020. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "UIs/Bases/InventorySlotBase.h" #include "InventorySlot.generated.h" UCLASS() class INVENTORYSYSTEM_API UInventorySlot : public UInventorySlotBase { GENERATED_BODY() private: virtual bool Initialize() override; bool NativeOnDrop( const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation ) override final; };
InventorySlot.cpp
に追加
// Copyright(C)write by pto8913. 2020. All Rights Reserved. #include "UIs/Widgets/InventorySlot.h" #include "Operations/ItemDragDropOperation.h" ////////////////////////////////// // Init bool UInventorySlot::Initialize() { bool Res = UUserWidget::Initialize(); ItemIsFrom = EItemIsFrom::Inventory; return Res; } ////////////////////////////////// // Action bool UInventorySlot::NativeOnDrop( const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation ) { /* DroppedItem info exists is in ItemOperation */ UItemDragDropOperation* ItemOperation = Cast<UItemDragDropOperation>(InOperation); if (IsValid(ItemOperation) == true) { EItemIsFrom _ItemIsFrom = ItemOperation->GetItemIsFrom(); if (_ItemIsFrom == ItemIsFrom || _ItemIsFrom == EItemIsFrom::Chest) { /* From : Inventory, To : Inventory */ UInventorySlotBase::ExchangeSlot(ItemOperation->InventorySlotUI); } InOperation = Cast<UDragDropOperation>(ItemOperation); } UUserWidget::NativeOnDrop(InGeometry, InDragDropEvent, InOperation); return true; }
できました。
なにも難しいことはやっていないので説明はありません。
プレイヤーさんにわかりやすいように改善する
※ : これらは、C++上で書く必要は全くないです。
BPで書いたほうが速いし楽なので面倒な人はBPで。
いまのままでも十分なのですが、
人というものは物事をすぐに忘れてしまいますので、
ドラッグされたスロットの見た目を少し変えて、どのスロットをドラッグしたのかを分かりやすくします。
また、ドラッグ中どのスロットにドロップされるのかもわかりにくいです。
なので、ドロップされるかもしれないスロットもわかりやすくします。
ドラッグされたスロットをプレイヤーさんにわかりやすいようにする
InventorySlotBase.h
に追加
public: void SetImageRenderOpacity(const float& In); virtual FReply NativeOnMouseButtonUp( const FGeometry& InGeometry, const FPointerEvent& InMouseEvent ) override; virtual bool NativeOnDrop( const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation ) override;
InventorySlotBase.cpp
に追加
void UInventorySlotBase::SetImageRenderOpacity(const float& In) { ItemImage->SetRenderOpacity(In); } void UInventorySlotBase::NativeOnDragDetected( const FGeometry& InGeometry, const FPointerEvent& InMouseEvent, UDragDropOperation*& OutOperation ) { if (!IsValid(SlotContents.GetItemImage())) return; /* スロットの情報をドロップしたスロットへ渡すためのオペレーションの作成 */ UItemDragDropOperation* ItemOperation = Cast<UItemDragDropOperation>( UWidgetBlueprintLibrary::CreateDragDropOperation( UItemDragDropOperation::StaticClass() ) ); if (IsValid(ItemOperation) == true) { /* ドラッグされたアイテムの画像を表示するだけのUI */ UUserWidget* DraggedItem = CreateWidget<UUserWidget>(GetWorld(), DraggedWidgetClass); UImage* ImageBox = Cast<UImage>(DraggedItem->GetWidgetFromName("ItemImage")); if (ImageBox != nullptr) { ImageBox->SetBrushFromTexture(SlotContents.GetItemImage()); } /* スロットの情報をオペレーションに渡す */ ItemOperation->DefaultDragVisual = DraggedItem; ItemOperation->Pivot = EDragPivot::MouseDown; ItemOperation->InventorySlotUI = this; SetImageRenderOpacity(0.5f); OutOperation = Cast<UDragDropOperation>(ItemOperation); } UUserWidget::NativeOnDragDetected(InGeometry, InMouseEvent, OutOperation); } bool UInventorySlotBase::NativeOnDrop( const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation ) { /* DroppedItem info exists is in ItemOperation */ UItemDragDropOperation* ItemOperation = Cast<UItemDragDropOperation>(InOperation); if (IsValid(ItemOperation) == true) { ItemOperation->InventorySlotUI->SetImageRenderOpacity(1.f); } UUserWidget::NativeOnDrop(InGeometry, InDragDropEvent, InOperation); return true; }
InventorySlot.cpp
に追加
bool UInventorySlot::NativeOnDrop( const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation ) { /* DroppedItem info exists is in ItemOperation */ UItemDragDropOperation* ItemOperation = Cast<UItemDragDropOperation>(InOperation); if (IsValid(ItemOperation) == true) { EItemIsFrom _ItemIsFrom = ItemOperation->GetItemIsFrom(); if (_ItemIsFrom == ItemIsFrom || _ItemIsFrom == EItemIsFrom::Chest) { /* From : Inventory, To : Inventory */ UInventorySlotBase::ExchangeSlot(ItemOperation->InventorySlotUI); } else if (_ItemIsFrom == EItemIsFrom::HotBar || _ItemIsFrom == EItemIsFrom::Equip) { UInventorySlotBase::ExchangeSlotForUIToComp(ItemOperation->InventorySlotUI); } //ItemOperation->InventorySlotUI->SetImageRenderOpacity(1.f); InOperation = Cast<UDragDropOperation>(ItemOperation); } UInventorySlotBase::NativeOnDrop(InGeometry, InDragDropEvent, InOperation); return true; }
これで、ドラッグされたスロットがすこし透けて表示されたと思います。
だいぶんわかりやすくなったのではないでしょうか。
ドロップされるかもしれないスロットをプレイヤーさんにわかりやすいようにする
InventorySlotBase.h
に追加
virtual void NativeOnDragEnter( const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation ) override; virtual void NativeOnDragLeave( const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation ) override;
InventorySlotBase.cpp
に追加
void UInventorySlotBase::NativeOnDragEnter( const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation ) { UUserWidget::NativeOnDragEnter(InGeometry, InDragDropEvent, InOperation); FLinearColor c; ItemImage->SetColorAndOpacity(c.Yellow); } void UInventorySlotBase::NativeOnDragLeave( const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation ) { UUserWidget::NativeOnDragLeave(InDragDropEvent, InOperation); FLinearColor c; ItemImage->SetColorAndOpacity(c.White); }
マウスがターゲットにしているスロットをわかりやすくする
さらに、プレイヤーさんはマウスの場所を見失ってしまうかもしれません。
なのでマウスがどのスロットをターゲットにしているかわかりやすくします。
InventorySlotBase.h
に追加
virtual void NativeOnMouseEnter( const FGeometry& InGeometry, const FPointerEvent& InMouseEvent ) override; virtual void NativeOnMouseLeave( const FPointerEvent& InMouseEvent ) override;
InventorySlotBase.cpp
に追加
void UInventorySlotBase::NativeOnMouseEnter( const FGeometry& InGeometry, const FPointerEvent& InMouseEvent ) { UUserWidget::NativeOnMouseEnter(InGeometry, InMouseEvent); FLinearColor c; ItemImage->SetColorAndOpacity(c.Yellow); } void UInventorySlotBase::NativeOnMouseLeave( const FPointerEvent& InMouseEvent ) { UUserWidget::NativeOnMouseLeave(InMouseEvent); FLinearColor c; ItemImage->SetColorAndOpacity(c.White); }
今回はここまで。