UE4 C++ InventorySystem Part.5 Equipment

やあ

UE4 C++でインベントリシステムを作ろう!
Part.5では、装備UIと装備の着脱をやるよ。

前回

pto8913.hatenablog.com

Part.5の結果

f:id:pto8913:20200503152629g:plain
f:id:pto8913:20200503155346g:plain

目次

概要

①プレイヤー : 装備をスロットにドロップ
②装備UIスロット : ドロップされた!イベント発生
③装備UI : ドロップされたスロットから、プレイヤーのどこに装備をアタッチするのかを決める
④プレイヤー : 装備をアタッチ
⑤終わり

装備UIの動作を考える

・スロットの情報に加え、どのスロットがプレイヤーのどのソケットかの情報の保持
これだけ!

装備UIの動作の実装

f:id:pto8913:20200216035919p:plain
こんな感じにするよ。

InventoryWidgetBaseクラスを継承して、EquipmentWidgetクラスを作成
f:id:pto8913:20200502164409p:plain

装備の情報を保持する

MyEnums.hに追加

UENUM(Blueprintable)
enum class EEquipmentType : uint8
{
    None = 0,
    Head = 1,
    Armor = 5,
    Shoulders = 6,
    Gloves = 8,
    Shoes = 9,
    WeaponLeft = 12,
    WeaponRight = 14,
    BothHandsWeapon = 15,
    Accessory = 16,
};
ENUM_CLASS_FLAGS(EEquipmentType)

MyStructs.hに追加

USTRUCT(BlueprintType)
struct FEquipmentInfos
{
    GENERATED_USTRUCT_BODY()
public:
    UPROPERTY(EditAnywhere, Category = "Equip")
        EEquipmentType EquipmentType;
    UPROPERTY()
        int SlotIndex;

    FEquipmentInfos() : EquipmentType(), SlotIndex() {};
    FEquipmentInfos(const EEquipmentType& InType) : EquipmentType(InType) {};
    FEquipmentInfos(const int& Idx, const EEquipmentType& InType) :
        EquipmentType(InType), SlotIndex(Idx) {};
};

USTRUCT(Blueprintable)
struct FItems
{
    UPROPERTY(EditAnywhere, Category = "Item")
        bool bItemIsEquipment;

    FItems() : Name(), Description(), Image(),
        MaxStackSize(0), ItemClass(), bItemIsEquipment(false) {};
}

USTRUCT(Blueprintable)
struct FInventorySlots
{
    UPROPERTY(EditAnywhere, Category = "Inventory")
        FEquipmentInfos EquipmentInfos;
    FInventorySlots(const FItems& InItemInfo, const int& InQ, const FEquipmentInfos& Info) :
        ItemInfo(InItemInfo), Quantity(InQ), EquipmentInfos(Info) 
    {
        if (Info.SlotIndex != 0)
        {
            SlotIndex = Info.SlotIndex;
        }
        ItemInfo.bItemIsEquipment = true;
    };

    void operator=(const FInventorySlots& In)
    {
        ItemInfo = In.ItemInfo;
        Quantity = In.Quantity;
        ItemIsFrom = In.ItemIsFrom;
        EquipmentInfos = In.EquipmentInfos;
    };
};

これで装備の情報を保持する。

装備UIの動作の実装

EquipmentWidget.hに追加

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

#pragma once

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

class AInventoryCharacterBase;

UCLASS()
class INVENTORYSYSTEM_API UEquipmentWidget : public UInventoryWidgetBase
{
    GENERATED_BODY()
public:
    UPROPERTY()
        AInventoryCharacterBase* OwnerChara;

    void OpenUI() override final;
    void CloseUI() override final;
    /* 
   @ Idx : Is Actually Idx -> Inventory[Idx].SlotIndex
   */
    FName GetSocketNameAt(const int& Idx) const;
};

EquipmentWidget.cppに追加

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

#include "UIs/Widgets/EquipmentWidget.h"
#include "UIs/Bases/InventorySlotBase.h"

#include "Characters/Bases/InventoryCharacterBase.h"

#include "Templates/ptoStructs.h"

//#define TP TPair<FName, FEquipmentInfos>
#define TP TPair<FName, FEquipmentInfos>

const TArray<TP> EquipInfos = {
    TP("SocketHead", FEquipmentInfos(1, EEquipmentType::Head)),
    TP("SocketAccesory1",FEquipmentInfos(3, EEquipmentType::Accessory)),
    
    TP("SocketArmor", FEquipmentInfos(5, EEquipmentType::Armor)),
    TP("SocketShoulder",FEquipmentInfos(6, EEquipmentType::Shoulders)),
    TP("SocketAccesory2", FEquipmentInfos(7, EEquipmentType::Accessory)),
    
    TP("SocketGloves", FEquipmentInfos(8, EEquipmentType::Gloves)),
    TP("SocketShoes", FEquipmentInfos(9, EEquipmentType::Shoes)),
    TP("SocketAccesory3", FEquipmentInfos(11, EEquipmentType::Accessory)),
    
    TP("SocketWeaponLeft", FEquipmentInfos(12, EEquipmentType::WeaponLeft)),
    TP("SocketWeaponRight", FEquipmentInfos(14, EEquipmentType::WeaponRight)),
    TP("SocketAccesory4", FEquipmentInfos(15, EEquipmentType::Accessory)),
};

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

void UEquipmentWidget::OpenUI()
{
    for (int Idx = 0; Idx < EquipInfos.Num(); ++Idx)
    {
        /*  SlotIdx : appearance index
           Idx : actually index
       */
        UInventorySlotBase* NewSlot = CreateSlot(Inventory[Idx], Idx);
        if (IsValid(NewSlot) == true)
        {
            NewSlot->OwnerUI = this;
            NewSlot->SlotContents.EquipmentInfos.EquipmentType = EquipInfos[Idx].Value.EquipmentType;
            AddNewItem(NewSlot, EquipInfos[Idx].Value.SlotIndex);
        }
    }
}


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

FName UEquipmentWidget::GetSocketNameAt(const int& Idx) const
{
    return EquipInfos[Idx].Key;
}


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

void UEquipmentWidget::CloseUI()
{
    OwnerChara->EquipmentInventory = MoveTemp(Inventory);
}

InventoryCharacterBase.hに追加

#include "Templates/ptoStructs.h"

class UEquipmentWidget;

UCLASS()
class INVENTORYSYSTEM_API AInventoryCharacterBase : public AInventorySystemCharacter
{
public:
    void ToggleEquip();
    UPROPERTY()
        TArray<FInventorySlots> EquipmentInventory;
    UPROPERTY()
        UEquipmentWidget* EquipmentUI;
private:
    UPROPERTY(EditAnywhere)
        TSubclassOf<UEquipmentWidget> EquipmentUIClass;

InventoryCharacterBase.cppに追加

#include "UIs/Widgets/EquipmentWidget.h"

void AInventoryCharacterBase::ToggleEquip()
{
    if (IsValid(EquipmentUI) == true)
    {
        EquipmentUI->CloseUI();
        EquipmentUI = nullptr;
    }
    else
    {
        if (EquipmentInventory.Num() == 0)
        {
            EquipmentInventory.Init(FInventorySlots(), 11);
        }

        EquipmentUI = CreateWidget<UEquipmentWidget>(GetWorld(), EquipmentUIClass);
        EquipmentUI->OwnerChara = this;
        EquipmentUI->Inventory = EquipmentInventory;
        EquipmentUI->OpenUI();
    }
}

PlayerBelongingsWidget.hに追加

public:
    UPROPERTY(meta = (BindWidget))
        UBorder* EquipBorder;

PlayerBelongingsWidget.cppに追加

#include "UIs/Widgets/EquipmentWidget.h"

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

void UPlayerBelongingsWidget::OpenUI()
{
    UWidgetBase::OpenUI();

    _Owner->InventoryComp->ToggleInventory();
    InventoryBorder->AddChild(_Owner->InventoryComp->InventoryUI);

    _Owner->ToggleEquip();
    EquipBorder->AddChild(_Owner->EquipmentUI);
}


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

void UPlayerBelongingsWidget::CloseUI()
{
    UWidgetBase::CloseUI();

    _Owner->InventoryComp->ToggleInventory();
    _Owner->ToggleEquip();

    InventoryBorder->ClearChildren();
    EquipBorder->ClearChildren();
}

とりあえずうまく表示できるかのテスト。
エディタに戻って、WBP_PlayerBelongingsUIを変更
f:id:pto8913:20200502215337p:plain
EquipmentWidgetクラスを継承して、WBP_EquipmentUIを作成。
f:id:pto8913:20200502215445p:plain
キャラクターにWBP_EquipmentUIを設定して
f:id:pto8913:20200502215357p:plain

見た目上はよくできました。
f:id:pto8913:20200502215634p:plain

装備スロットの動作を考える

・ドロップされたイベントの受信
->装備のアタッチ
・スロットの基本的な動作
->Part.2参照

装備スロットの動作の実装

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

EquipmentSlot.hに追加

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

#pragma once

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

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

EquipmentSlot.cppに追加

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

#include "UIs/Widgets/EquipmentSlot.h"

#include "Operations/ItemDragDropOperation.h"

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

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

    ItemIsFrom = EItemIsFrom::Equip;

    return Res;
}


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

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

        InOperation = Cast<UDragDropOperation>(ItemOperation);
    }

    UInventorySlotBase::NativeOnDrop(InGeometry, InDragDropEvent, InOperation);
    return true;
}

エディタに戻って、EquipmentSlotクラスを継承して、WBP_EquipSlotクラスを作成。
f:id:pto8913:20200502220744p:plain
WBP_EquipmentUIWBP_EquipSlotを設定。 f:id:pto8913:20200502220754p:plain

おーけー。

f:id:pto8913:20200502221017g:plain

アイテムがドロップされたらプレイヤーに装着

これもやるだけですが。

装着の動作を考える

①スロット : ドロップされた
②装備UI : ドロップされたスロットからソケット名を取得
③キャラクター : 装備をスポーンして装着
④終わり。

装備できるアイテムを作る

PickUpableItemBaseクラスを継承して、EquipmnetBaseクラスを作成。
f:id:pto8913:20200503144251p:plain
どうやらメッシュコンポーネントSimulatePhysics = trueになっているとソケットにアタッチできないようです。
EquipmentBase.hに追加

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

#pragma once

#include "CoreMinimal.h"
#include "Items/Bases/PickUpableItemBase.h"
#include "EquipmentBase.generated.h"

UCLASS()
class INVENTORYSYSTEM_API AEquipmentBase : public APickUpableItemBase
{
    GENERATED_BODY()
private:
    AEquipmentBase();
};

EquipmentBase.cppに追加

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

#include "Items/Bases/EquipmentBase.h"

#include "Components/SceneComponent.h"
#include "Components/SkeletalMeshComponent.h"

AEquipmentBase::AEquipmentBase()
{
    MeshComp->SetSimulatePhysics(false);

    ItemInfo.bItemIsEquipment = true;
}

装着の動作の実装

PickUpableItemBase.hに追加

public:
    UPROPERTY()
        bool bIsIgnorePickUp = false;

PickUpableItemBase.cppに追加

bool APickUpableItemBase::PickUp(AInventoryCharacterBase* In)
{
        if (bIsIgnorePickUp == true) return false;
    UActorComponent* _Comp = In->GetComponentByClass(UInventoryComponentBase::StaticClass());
    if (IsValid(_Comp) == true)
    {
        UInventoryComponentBase* _InventoryComp = Cast<UInventoryComponentBase>(_Comp);
        if (IsValid(_InventoryComp) == true)
        {
            Quantity = FMath::Clamp(Quantity, 1, 9999);
            FInventorySlots NewSlotInfo = FInventorySlots(ItemInfo, Quantity);
            if (_InventoryComp->AddToInventory(NewSlotInfo) == true)
            {
                Destroy();
                return true;
            }
        }
    }
    Destroy();
    return false;
}

InventoryCharacterBase.hに追加

class UEquipmentSlot;
class APickUpableItemBase;

class INVENTORYSYSTEM_API AInventoryCharacterBase : public AInventorySystemCharacter
{
private:
        /* 装着された装備を保存する一時変数 */
    UPROPERTY()
        TArray<APickUpableItemBase*> EquipmentActorTemp;
public:
    /* Spawn equipment and attach to socket
       @ Class : Equipment Class
       @ SocketName : SocketName from Widget->Slot
   */
    AActor* AttachEquipment(
        UClass* Class, 
        const FName& SocketName
    );

    /* Called when item was moved from EquipmentSlot to EquipmentSlot
       @ From : for SlotContents and SocketName
       @ To : for SlotContents and SocketName
             装備の持ち替え、例えば左手の武器と右手の武器を持ち変える
   */
    void AttachEquipment(
        UEquipmentSlot* From,
        UEquipmentSlot* To
    );

    /* Called when item was moved from Inventory/HotBar...etc to EquipmentSlot
       @ In : for SlotContents and SocketName
   */
    void AttachEquipment(UEquipmentSlot* In);

    /* Check if the target socket is used
       @ In : SlotContents
       @ Idx : SlotIndex
       @ SocketName : Attach socket name
   */
    void AttachEquipment(
        const FInventorySlots& In,
        const int& Idx,
        const FName& SocketName
    );
    /* Detach idx equipment    */
    void DetachEquipment(const int& Idx);
    // End Action

InventoryCharacterBase.cppに追加

void AInventoryCharacterBase::BeginPlay()
{
        EquipmentActorTemp.Init(nullptr, 16);
}

AActor* AInventoryCharacterBase::AttachEquipment(
    UClass* Class, 
    const FName& SocketName
)
{
    if (Class == nullptr) return nullptr;
    if (SocketName.IsNone() == true) return nullptr;

    APickUpableItemBase* Equipment = Cast<APickUpableItemBase>(GetWorld()->SpawnActor(Class));
    if (Equipment == nullptr) return nullptr;

    // 装備されたアイテムを拾えないようにする。
    Equipment->bIsIgnorePickUp = true;

    Equipment->AttachToComponent(
        GetMesh(),
        FAttachmentTransformRules::KeepRelativeTransform,
        SocketName
    );

    return Equipment;
}

void AInventoryCharacterBase::AttachEquipment(
    UEquipmentSlot* From, UEquipmentSlot* To
)
{
    int FromIdx = From->SlotContents.SlotIndex;
    int ToIdx = To->SlotContents.SlotIndex;
    if (EquipmentActorTemp[ToIdx] == nullptr)
    {
        AttachEquipment(To);
        DetachEquipment(FromIdx);
    }
    else
    {
        AttachEquipment(To);
        AttachEquipment(From);
    }
}

void AInventoryCharacterBase::AttachEquipment(UEquipmentSlot* In)
{
    int Idx = In->SlotContents.SlotIndex;
    DetachEquipment(Idx);
    AttachEquipment(
        In->SlotContents, Idx, 
        Cast<UEquipmentWidget>(In->OwnerUI)->GetSocketNameAt(Idx)
    );
}

void AInventoryCharacterBase::AttachEquipment(
    const FInventorySlots& In, 
    const int& Idx, 
    const FName& SocketName
)
{
    if (!EquipmentActorTemp.IsValidIndex(Idx)) return;

    EquipmentActorTemp[Idx] = Cast<APickUpableItemBase>(
        AttachEquipment(In.GetItemClass(), SocketName)
    );
}

void AInventoryCharacterBase::DetachEquipment(const int& Idx)
{
    if (!EquipmentActorTemp.IsValidIndex(Idx)) return;

    if (EquipmentActorTemp[Idx] != nullptr)
    {
        EquipmentActorTemp[Idx]->Destroy();
        EquipmentActorTemp[Idx] = nullptr;
    }
}

EquipmentSlot.cppに追加

#include "Characters/Bases/InventoryCharacterBase.h"

bool UEquipmentSlot::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::HotBar)
        {
            /*  From : Inventory,  To : Inventory  */
            UInventorySlotBase::ExchangeSlotForUIToUI(ItemOperation->InventorySlotUI);
            Cast<AInventoryCharacterBase>(GetPlayerCharacter())->AttachEquipment(
                Cast<UEquipmentSlot>(ItemOperation->InventorySlotUI), 
                this
            );
        }
        else if (_ItemIsFrom == EItemIsFrom::Inventory || _ItemIsFrom == EItemIsFrom::Chest)
        {
            UInventorySlotBase::ExchangeSlotForCompToUI(ItemOperation->InventorySlotUI);
            Cast<AInventoryCharacterBase>(GetPlayerCharacter())->AttachEquipment(this);
        }
        
        InOperation = Cast<UDragDropOperation>(ItemOperation);
    }

    UInventorySlotBase::NativeOnDrop(InGeometry, InDragDropEvent, InOperation);
    return true;
}

これでおーけー。

エディタに戻って適当にソケットを作る。
f:id:pto8913:20200503150641p:plain

EquipmentBaseクラスを継承して、BP_EquipTestを作成。
f:id:pto8913:20200503150824p:plain

できました。素敵な手袋です。
f:id:pto8913:20200503152629g:plain

装備を外す

これは簡単です。
装備スロット以外のスロットが、装備スロットからのドロップイベントを受け取ったら、装備を外すだけです。
InventorySlotBase.cppに追加

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

        if (ItemOperation->GetItemIsFrom() == EItemIsFrom::Equip)
        {
            Cast<AInventoryCharacterBase>(GetPlayerCharacter())->DetachEquipment(
                ItemOperation->InventorySlotUI->SlotContents.SlotIndex
            );
        }
    }
    UUserWidget::NativeOnDrop(InGeometry, InDragDropEvent, InOperation);
    return true;
}

f:id:pto8913:20200503155346g:plain

今回はここまで。