UE4 C++ InventorySystem Part.2 Drag&Drop

やあ

UE4 C++でインベントリシステムを作ろう!
Part.2では、インベントリ内のアイテムをドラッグアンドドロップで移動できるようにするよ。

前回

pto8913.hatenablog.com

Part.2の結果

f:id:pto8913:20200502223926g:plain
f:id:pto8913:20200502224047g:plain

目次

概要

①プレイヤー : ドラッグアンドドロップしたいアイテムのあるスロットをクリック。
②スロット : クリックされた!イベント発生
③プレイヤー : ドラッグ
④スロット : ドラッグされたアイテムの情報を持ったオペレーションを作り、自身の持つ情報をそのオペレーションに渡す。
(UE4独自にDragDropOperationクラスというものがあるので、それを使う。)
⑤プレイヤー : ドロップ
⑥スロット(またはドロップイベントを受け取れるUI) : ドロップされた!イベント発生
⑦スロット : ドラッグ元のスロットの情報と、自身の持つ情報を交換。
⑧終わり

ちょっとこんがらがるかもしれないけど、大したことはしてないね~

スロットの動作を考える

・クリックされた?
-> 左クリック?右クリック?ホイール?
・ドラッグされた?
-> スロットの情報をオペレーションに渡す
・ドロップされた? -> スロットの情報を交換する
これだけ!

アイテムがどのスロットから来たのかを判別するための列挙体

f:id:pto8913:20200501135749p:plain
.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クラスを作成。
f:id:pto8913:20200501134651p:plain
.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を作って
f:id:pto8913:20200501153108p:plain
WBP_DraggedItemWBP_InventorySlotに設定して、プレイ
f:id:pto8913:20200501153436p:plain

とりあえずドラッグまでできました。

f:id:pto8913:20200501153344p:plain

ドロップの実装

UMGのドロップイベントは、ドロップしたUMGじゃなくて、
ドロップされたUMG側に実装する必要があるよ。
そのため、インベントリのスロット、宝箱のスロット、装備のスロット等々で処理が全部違うんだ。
だから、新しいクラスを作るよ。

InventorySlotBaseクラスを継承して、InventorySlotクラスを作成。
f:id:pto8913:20200501145756p:plain

実装の前にスロットの情報を交換するための処理

これはとても簡単です。とりあえず実装を見てください。
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;
}

できました。
なにも難しいことはやっていないので説明はありません。

f:id:pto8913:20200502223926g:plain

プレイヤーさんにわかりやすいように改善する

※ : これらは、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);
}

f:id:pto8913:20200502224047g:plain

今回はここまで。

pto8913.hatenablog.com