UE4 C++ InventorySystem Part.4 HotBar

やあ

UE4 C++でインベントリシステムを作ろう!
Part.4では、ホットバーを作るよ。

前回

pto8913.hatenablog.com

Part.4の結果

f:id:pto8913:20200502230652g:plain
f:id:pto8913:20200502162414g:plain

目次

ホットバーの動作を考える

・スロットの情報の保持
->Part.1参照
・特定のキーが押されたら特定のスロットのアイテムを使用する
->ゲームによってはスキルを登録したり装備を登録したり

ホットバーの動作の実装

今回は簡単にするため、キー1~5が押されたらスロット0~4のアイテムの数を一つ減らすだけにするよ。
細かい処理は作る物によって変わるだろうしね。
キーをプレイヤーが任意のものに変えられるようにしたい、って人はUE4 key configで検索するといいかもしれない。

Part.1で作ったInventoryWidgetBaseクラスを継承して、HotBarクラスを作成。

f:id:pto8913:20200502111518p:plain

ホットバーを表示するためのUIを作成

これには、Part.3で作ったExchangeInventoryもいれるよ。
f:id:pto8913:20200502120152p:plain
イメージ図

WidgetBaseクラスを継承して、InentoryHUDクラスを作成。
f:id:pto8913:20200502154256p:plain

InventoryHUD.hに追加

// Copyright(C)write by pto8913. 2020. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "UIs/Bases/WidgetBase.h"
#include "InventoryHUD.generated.h"

class UExchangeInventoryBase;
class UBorder;

UCLASS()
class INVENTORYSYSTEM_API UInventoryHUD : public UWidgetBase
{
    GENERATED_BODY()
public:
    UPROPERTY(meta = (BindWidget))
        UBorder* ExchangeBorder;
    UPROPERTY(meta = (BindWidget))
        UBorder* HotBarBorder;

    void OpenUI() override;
    void CloseUI() override;
};

InventoryHUD.cppに追加

// Copyright(C)write by pto8913. 2020. All Rights Reserved.

#include "UIs/InventoryHUD.h"

#include "Components/Border.h"

//////////////////////////////////
// Init

void UInventoryHUD::OpenUI()
{
    AddToViewport();

    ExchangeBorder->SetRenderOpacity(0.f);
}


//////////////////////////////////
// End

void UInventoryHUD::CloseUI()
{
    RemoveFromParent();
}

これに伴って、WBP_ExchangeInventoryを少し修正
f:id:pto8913:20200502121035p:plain

これはプレイヤーのBeginPlayで作って表示するようにするよ。
InventoryCharacterBase.h

class UInventoryHUD;

class INVENTORYSYSTEM_API AInventoryCharacterBase : public AInventorySystemCharacter
{
public:
    /* UIs */
    UPROPERTY()
        UInventoryHUD* InventoryHUD;
private:
    /* Classes */
    UPROPERTY(EditAnywhere)
        TSubclassOf<UInventoryHUD> InventoryHUDClass;
};

InventoryCharacterBase.cpp

#include "UIs/InventoryHUD.h"

void AInventoryCharacterBase::BeginPlay()
{
    Super::BeginPlay();

    InventoryHUD = CreateWidget<UInventoryHUD>(GetWorld(), InventoryHUDClass);
    InventoryHUD->AddToViewport();
}

これでホットバーを表示するUIができたね。

ホットバーの動作の実装

HotBar.hに追加

// Copyright(C)write by pto8913. 2020. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "UIs/Bases/InventoryWidgetBase.h"
#include "HotBar.generated.h"

UCLASS()
class INVENTORYSYSTEM_API UHotBar : public UInventoryWidgetBase
{
    GENERATED_BODY()
public:
    void OpenUI() override;
    void CloseUI() override {};
};

HotBar.cppに追加

// Copyright(C)write by pto8913. 2020. All Rights Reserved.

#include "UIs/Widgets/HotBar.h"

#include "UIs/Bases/InventorySlotBase.h"

/////////////////////////////////////////
// Init

void UHotBar::OpenUI()
{
    if (Inventory.Num() == 0)
    {
        Inventory.Init(FInventorySlots(), GridColumnSize);
    }

    for (int Idx = 0; Idx < GridColumnSize; ++Idx)
    {
        UInventorySlotBase* _Slot = UInventoryWidgetBase::CreateSlot(Inventory[Idx], Idx);
        _Slot->OwnerUI = this;
        AddNewItem(_Slot, Idx);
    }
}

この状態でいったんテストしてみます。

エディタに戻って。
InventoryHUDクラスを継承して、WBP_InventoryHUDを作成。

f:id:pto8913:20200502154143p:plain

HotBarクラスを継承して、WBP_HotBarを作成。
f:id:pto8913:20200502123345p:plain
f:id:pto8913:20200502230029p:plain

CharacterにインベントリHUDのクラスとホットバーのクラスを設定。
f:id:pto8913:20200502123443p:plain

この状態でプレイします。
※プレイしても、アイテムをホットバーにドロップしないでください。クラッシュします
f:id:pto8913:20200502124357p:plain

見た目上はうまい具合にできてるね。

ホットバーにアイテムをドロップするとクラッシュするのはホットバーにインベントリコンポーネントを渡していないからだよ。
インベントリコンポーネントを渡せばいいじゃないかと思うよね。
次の動画を見てほしい。
?????????????

f:id:pto8913:20200502125144g:plain

なんでこんなことになるかというと、インベントリUI(インベントリコンポーネント)のインベントリとホットバーのインベントリが競合してしまっているからだよ。

※ホットバー用のコンポーネントを増やすというのも考えられますが、できるだけコンポーネントを増やしたくないのでやってません。

なので

ホットバー用に新しくスロットを作る

InventorySlotBaseクラスを継承して、HotSlotクラスを作成
f:id:pto8913:20200502132209p:plain

コンポーネントとUI間でのスロットの情報交換処理の作成

InventorySlotBase.hに追加

protected:
    /* コンポーネントからコンポーネントへドロップされたときの処理 */
    virtual void ExchangeSlot(UInventorySlotBase*& From);
    /* コンポーネントからUIへドロップされたときの処理 */
    virtual void ExchangeSlotForCompToUI(UInventorySlotBase*& From);
    /* UIからコンポーネントへドロップされたときの処理 */
    virtual void ExchangeSlotForUIToComp(UInventorySlotBase*& From);
    /* UIからUIへドロップされたときの処理 */
    virtual void ExchangeSlotForUIToUI(UInventorySlotBase*& From);

InventorySlotBase.cppに追加

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();
}

void UInventorySlotBase::ExchangeSlotForCompToUI(UInventorySlotBase*& From)
{
    int FromIdx = From->SlotContents.SlotIndex;
    int ToIdx = SlotContents.SlotIndex;

    FInventorySlots FromTemp = From->InventoryComp->Inventory[FromIdx];

    UInventoryWidgetBase* _ToOwner = Cast<UInventoryWidgetBase>(OwnerUI);

    /* Fromスロットのインベントリコンポーネントのインベントリを更新 */
    From->InventoryComp->Inventory[FromIdx] = MoveTemp(_ToOwner->Inventory[ToIdx]);
    /* Fromスロットのオーナーのインベントリを更新 */
    Cast<UInventoryWidgetBase>(From->OwnerUI)->Inventory[FromIdx] = From->InventoryComp->Inventory[FromIdx];

    /* このスロットのオーナーのインベントリを更新 */
    _ToOwner->Inventory[ToIdx] = MoveTemp(FromTemp);
    /* このスロットの情報を更新 */
    SlotContents = _ToOwner->Inventory[ToIdx];

    From->UpdateSlot();
    RefreshSlot();
}

void UInventorySlotBase::ExchangeSlotForUIToComp(UInventorySlotBase*& From)
{
    int FromIdx = From->SlotContents.SlotIndex;
    int ToIdx = SlotContents.SlotIndex;

    UInventoryWidgetBase* _FromOwner = Cast<UInventoryWidgetBase>(From->OwnerUI);
    FInventorySlots FromTemp = _FromOwner->Inventory[FromIdx];
    
    /* Fromスロットのオーナーのインベントリを更新 */
    _FromOwner->Inventory[FromIdx] = MoveTemp(InventoryComp->Inventory[ToIdx]);
    /* Fromスロットの情報の更新 */
    From->SlotContents = _FromOwner->Inventory[FromIdx];

    /* このスロットのインベントリコンポーネントのインベントリを更新 */
    InventoryComp->Inventory[ToIdx] = MoveTemp(FromTemp);
    Cast<UInventoryWidgetBase>(OwnerUI)->Inventory[ToIdx] = InventoryComp->Inventory[ToIdx];

    From->RefreshSlot();
    UpdateSlot();
}

void UInventorySlotBase::ExchangeSlotForUIToUI(UInventorySlotBase*& From)
{
    int FromIdx = From->SlotContents.SlotIndex;
    int ToIdx = SlotContents.SlotIndex;

    UInventoryWidgetBase* _FromOwner = Cast<UInventoryWidgetBase>(From->OwnerUI);
    UInventoryWidgetBase* _ToOwner = Cast<UInventoryWidgetBase>(OwnerUI);
    FInventorySlots FromTemp = _FromOwner->Inventory[FromIdx];

    /* Fromスロットのオーナーのインベントリを更新 */
    _FromOwner->Inventory[FromIdx] = MoveTemp(_ToOwner->Inventory[ToIdx]);
    /* Fromスロットの情報の更新 */
    From->SlotContents = _FromOwner->Inventory[FromIdx];

    /* このスロットのオーナーのインベントリを更新 */
    _ToOwner->Inventory[ToIdx] = MoveTemp(FromTemp);
    /* このスロットの情報を更新 */
    SlotContents = _ToOwner->Inventory[ToIdx];

    From->RefreshSlot();
    RefreshSlot();
}

これで、コンポーネントからコンポーネントコンポーネントからUI、UIからコンポーネント、UIからUIへの情報の交換ができるようになりました。

他のインベントリからホットバーへの交換処理

Part.2参照
HotSlot.hに追加

// Copyright(C)write by pto8913. 2020. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "UIs/Bases/InventorySlotBase.h"
#include "HotSlot.generated.h"

UCLASS()
class INVENTORYSYSTEM_API UHotSlot : public UInventorySlotBase
{
    GENERATED_BODY()
private:
    virtual bool Initialize() override;
    bool NativeOnDrop(
        const FGeometry& InGeometry,
        const FDragDropEvent& InDragDropEvent,
        UDragDropOperation* InOperation
    ) override final;
};

UHotSlot.cppに追加

// Copyright(C)write by pto8913. 2020. All Rights Reserved.

#include "UIs/Widgets/HotSlot.h"

#include "Operations/ItemDragDropOperation.h"

/////////////////////////////////////
// Init

bool UHotSlot::Initialize()
{
    bool Res = UUserWidget::Initialize();

    ItemIsFrom = EItemIsFrom::HotBar;

    return Res;
}


////////////////////////////////////
// Action

bool UHotSlot::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::Equip)
        {
            /*  From : HotBar,  To : HotBar  */
            UInventorySlotBase::ExchangeSlotForUIToUI(ItemOperation->InventorySlotUI);
        }
        else if (_ItemIsFrom == EItemIsFrom::Inventory || _ItemIsFrom == EItemIsFrom::Chest)
        {
            /* From : Inventory/Chest, To HotBar */
            UInventorySlotBase::ExchangeSlotForCompToUI(ItemOperation->InventorySlotUI);
        }

        InOperation = Cast<UDragDropOperation>(ItemOperation);
    }

    UInventorySlotBase::NativeOnDrop(InGeometry, InDragDropEvent, InOperation);

    return true;
}

これでおーけー

f:id:pto8913:20200502231535g:plain

ホットバーから他のインベントリへの交換処理

これで他のインベントリからホットバーへの移動はできるようになったね。
だけどホットバーから他のインベントリへの移動はできないよ。
理由はわかるよね。
他のインベントリへのドロップ処理を書き換えてないからね。

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);
        }

        InOperation = Cast<UDragDropOperation>(ItemOperation);
    }

    UInventorySlotBase::NativeOnDrop(InGeometry, InDragDropEvent, InOperation);

    return true;
}

できました。

f:id:pto8913:20200502231346g:plain

ExchangeInventoryの表示を直す

ExchangeInventoryBase.cppに追加

void UExchangeInventoryBase::OpenUI()
{
    bUseAddToViewport = false;
    
    UWidgetBase::OpenUI();

    // 省略
}

ChestItemBase.cppに追加

bool AChestItemBase::PickUp(AInventoryCharacterBase* In)
{
    if (InventoryComp->Inventory.Num() == 0)
    {
        InventoryComp->Init();
    }

    if (IsValid(ExchangeInventoryUI) == true)
    {
        /* Close */
        ExchangeInventoryUI->CloseUI();
        In->InventoryHUD->ExchangeBorder->SetRenderOpacity(0.f);
        In->InventoryHUD->ExchangeBorder->ClearChildren();
        ExchangeInventoryUI = nullptr;
    }
    else
    {
        ExchangeInventoryUI = CreateWidget<UExchangeInventoryBase>(
            GetWorld(), ExchangeInventoryUIClass
        );

        if (IsValid(ExchangeInventoryUI) == true)
        {
            ExchangeInventoryUI->_TalkTarget = In;
            ExchangeInventoryUI->_OwnerComp = InventoryComp;
            ExchangeInventoryUI->OpenUI();
            In->InventoryHUD->ExchangeBorder->SetRenderOpacity(1.f);
            In->InventoryHUD->ExchangeBorder->AddChild(ExchangeInventoryUI);
        }
    }

    return true;
}

おーけー
f:id:pto8913:20200502230652g:plain

プレイヤーがキーを押した時の処理

今回は説明を簡単にするためにこう書いてるけど、実際に使うときはデータテーブルでアクションマッピングなどの管理をしてね。
キーをプレイヤーが任意のものに変えられるようにしたい、って人はUE4 key configで検索するといいかもしれない。

InventoryCharacterBase.hに追加

private:
    void AddActionMapping(const FName& InName, const FKey& InKey);
    void UseHotBarItem1();
    void UseHotBarItem2();
    void UseHotBarItem3();
    void UseHotBarItem4();
    void UseHotBarItem5();

InventoryCharacterBase.cppに追加

void AInventoryCharacterBase::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
    // Set up gameplay key bindings
    check(PlayerInputComponent);

    PlayerInputComponent->BindAction("PickUp", EInputEvent::IE_Pressed, this, &AInventoryCharacterBase::PickUp);
    AddActionMapping("PickUp", EKeys::E);

    PlayerInputComponent->BindAction("UseHotBarItem1", EInputEvent::IE_Pressed, this, &AInventoryCharacterBase::UseHotBarItem1);
    AddActionMapping("UseHotBarItem1", EKeys::One);

    PlayerInputComponent->BindAction("UseHotBarItem2", EInputEvent::IE_Pressed, this, &AInventoryCharacterBase::UseHotBarItem2);
    AddActionMapping("UseHotBarItem2", EKeys::Two);

    PlayerInputComponent->BindAction("UseHotBarItem3", EInputEvent::IE_Pressed, this, &AInventoryCharacterBase::UseHotBarItem3);
    AddActionMapping("UseHotBarItem3", EKeys::Three);

    PlayerInputComponent->BindAction("UseHotBarItem4", EInputEvent::IE_Pressed, this, &AInventoryCharacterBase::UseHotBarItem4);
    AddActionMapping("UseHotBarItem4", EKeys::Four);

    PlayerInputComponent->BindAction("UseHotBarItem5", EInputEvent::IE_Pressed, this, &AInventoryCharacterBase::UseHotBarItem5);
    AddActionMapping("UseHotBarItem5", EKeys::Five);

    Super::SetupPlayerInputComponent(PlayerInputComponent);
}

void AInventoryCharacterBase::AddActionMapping(const FName& InName, const FKey& InKey)
{
    const FInputActionKeyMapping map(InName, InKey);
    GetMutableDefault<UInputSettings>()->AddActionMapping(map);
}

/////////////////////////////////////
// Action

void AInventoryCharacterBase::UseHotBarItem1()
{
    HotBar->UseItemAt(0);
}

void AInventoryCharacterBase::UseHotBarItem2()
{
    HotBar->UseItemAt(1);
}

void AInventoryCharacterBase::UseHotBarItem3()
{
    HotBar->UseItemAt(2);
}

void AInventoryCharacterBase::UseHotBarItem4()
{
    HotBar->UseItemAt(3);
}

void AInventoryCharacterBase::UseHotBarItem5()
{
    HotBar->UseItemAt(4);
}

f:id:pto8913:20200502162414g:plain

pto8913.hatenablog.com