UE4 C++ InventorySystem Part.1

やあ

UE4 C++でインベントリシステムを作ろう!
Part.1ではアイテムを拾って表示するだけのインベントリを作るよー
正直UE4何もわからないしC++も何もわかってないのでこここうしたほうがいいとか、間違ってるところがあれば教えてくださると非常に助かります。

Part.1の結果

f:id:pto8913:20200502222900g:plain
f:id:pto8913:20200502223053g:plain

目次

環境

UE4.24.3
・MSVC2019

概要

インベントリシステムを作るのはとても簡単でこれだけ!

① プレイヤー : アイテムを拾う。
② プレイヤー : インベントリに空きがあれば、アイテムをスロットに追加する。
f:id:pto8913:20200208003325p:plain
えぇ!?こんなに簡単に!?

ちょっと詳しく書くとこういうこと!

① プレイヤー : アイテムを拾う。
② アイテム : 拾われた! -> イベント発生!!
③ インベントリコンポーネント(プレイヤー) : ②イベント受信
④ インベントリコンポーネント(プレイヤー) : インベントリの空きスロットを探す。
-> 見つかった ⑤ に
-> 見つからなかった ⑧ に

⑤ インベントリコンポーネント(プレイヤー) : 空きを見つけた!
⑥ インベントリUI : 空きスロットに追加
⑦ インベントリスロット : スロットの情報を更新
⑧ 終了 f:id:pto8913:20200208195712p:plain

まずはプロジェクトを作る

f:id:pto8913:20200208004410p:plain
f:id:pto8913:20200208004413p:plain
f:id:pto8913:20200208004415p:plain
できたねー
わかんないことはAnsweHubで解決しよー

作成できたらMSVCに移動して、xx.Build.cs"UMG"を追加してね。
これをやることでコンパイラUMG_APIを読み込めるようにするよ。

// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;

public class InventorySystem : ModuleRules
{
    public InventorySystem(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[] { 
            "Core", "CoreUObject", "Engine", "InputCore", 
            "HeadMountedDisplay", "UMG"
        });
    }
}

追加したら、プロジェクトフォルダのuprojectを右クリックしてGenerate Visual Studio project filesを押してね。
f:id:pto8913:20200430163906p:plain

次にプレイヤーの基底クラスを作るよ

MyProjectCharacterクラスを継承して、InventoryCharacterBaseクラスを作成。
f:id:pto8913:20200430205251p:plain
InventoryCharacterBase.hに追加

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

#pragma once

#include "CoreMinimal.h"
#include "InventorySystem/InventorySystemCharacter.h"
#include "InventoryCharacterBase.generated.h"

class UInventoryComponentBase;

UCLASS()
class INVENTORYSYSTEM_API AInventoryCharacterBase : public AInventorySystemCharacter
{
    GENERATED_BODY()
private:
    AInventoryCharacterBase();
public:
    UPROPERTY(VisibleAnywhere)
        UInventoryComponentBase* InventoryComp;
};

InventoryCharacterBase.cppに追加

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

#include "Characters/Bases/InventoryCharacterBase.h"

#include "Components/Bases/InventoryComponentBase.h"

#include "UIs/Widgets/PlayerBelongingsWidget.h"

AInventoryCharacterBase::AInventoryCharacterBase()
{
    InventoryComp = CreateDefaultSubobject<UInventoryComponentBase>(TEXT("InventoryComp"));
}

次はプレイヤーや宝箱がインベントリを持つためのコンポーネントを作るよ

こんな感じのイメージ f:id:pto8913:20200208163901p:plain
ActorComponentクラスを継承してInventoryComponentBaseクラスを作成
f:id:pto8913:20200430170343p:plain
デフォルトの関数は全部消してね。

コンポーネントの動作を考える

・インベントリの開閉
・インベントリにアイテムを追加
これだけ・・・?

中身を書いていく前に、インベントリUIを作るよ

UserWidgetクラスを継承して、WidgetBaseクラスを作成。
これはこれから作るUI全部の基底クラスだよ。
f:id:pto8913:20200430170359p:plain
WidgetBase.hに追加

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

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "WidgetBase.generated.h"

UCLASS()
class INVENTORYSYSTEM_API UWidgetBase : public UUserWidget
{
    GENERATED_BODY()
public:
    UPROPERTY()
        UWidgetBase* OwnerUI;
protected:
    virtual void AddDelegate() {};
    virtual void RemoveDelegate() {};
    UPROPERTY(EditAnywhere)
        bool bUseAddToViewport = true;
    UPROPERTY(EditAnywhere)
        bool bUseInputMode = true;
public:
    UFUNCTION(BlueprintCallable)
        virtual void OpenUI();

    UFUNCTION(BlueprintCallable)
        virtual void CloseUI();

    /* Set input mode */
    void SetUIOnly();
    void SetGameAndUI();
    void SetGameOnly();

    /* Set Player controller */
    void SetUIController();
    void SetGameController();
protected:
    APlayerController* GetPlayerController(const int& Idx = 0);
    ACharacter* GetPlayerCharacter(const int& Idx = 0);
};

WidgetBase.cppに追加

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

#include "UIs/Bases/WidgetBase.h"

#include "Kismet/GameplayStatics.h"

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

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

    if (bUseInputMode == true)
    {
        SetGameAndUI();
        SetUIController();
    }
    if (bUseAddToViewport == true)
    {
        AddToViewport();
    }
}


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

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

    if (bUseInputMode == true)
    {
        SetGameOnly();
        SetGameController();
    }
    if (bUseAddToViewport == true)
    {
        RemoveFromParent();
    }
}


////////////////////////////////////////////
// Other

APlayerController* UWidgetBase::GetPlayerController(const int& Idx)
{
    return UGameplayStatics::GetPlayerController(GetWorld(), Idx);
}

ACharacter* UWidgetBase::GetPlayerCharacter(const int& Idx)
{
    return UGameplayStatics::GetPlayerCharacter(GetWorld(), Idx);
}

// Input Mode

void UWidgetBase::SetUIOnly()
{
    APlayerController* Controller = GetPlayerController();
    if (IsValid(Controller) == true)
    {
        Controller->SetInputMode(FInputModeUIOnly());
    }
}

void UWidgetBase::SetGameOnly()
{
    APlayerController* Controller = GetPlayerController();
    if (IsValid(Controller) == true)
    {
        Controller->SetInputMode(FInputModeGameOnly());
    }
}

void UWidgetBase::SetGameAndUI()
{
    APlayerController* Controller = GetPlayerController();
    if (IsValid(Controller) == true)
    {
        Controller->SetInputMode(FInputModeGameAndUI());
    }
}

// Player Controller

void UWidgetBase::SetUIController()
{
    APlayerController* Controller = GetPlayerController();
    if (IsValid(Controller) == true)
    {
        Controller->bShowMouseCursor = true;
        Controller->SetIgnoreMoveInput(true);
        Controller->SetIgnoreLookInput(true);
    }
}

void UWidgetBase::SetGameController()
{
    APlayerController* Controller = GetPlayerController();
    if (IsValid(Controller) == true)
    {
        Controller->bShowMouseCursor = false;
        Controller->SetIgnoreMoveInput(false);
        Controller->SetIgnoreLookInput(false);
    }
}

いきなりいっぱい書いて混乱するかもしれないけど、大したことはしてないね。

インベントリスロット

次にWidgetBaseクラスを継承して、InventorySlotBaseクラスを作るよ。
f:id:pto8913:20200430172442p:plain

スロットの動作を考える

・アイテムの情報を保持。
・アイテムの情報をプレイヤーに見えるようにする。
・情報の更新。
これだけ!
書いていく前に。

スロットの情報を保持するための構造体の定義

Objectクラスを継承して、MyStructsクラスを作るよ。
f:id:pto8913:20200430183814p:plain
.cppファイルはいらないから消してね。
ついでにアイテム情報の構造体も定義しておくよ。
MyStructs.hに追加

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

#pragma once

#include "CoreMinimal.h"
#include "ptoStructs.generated.h"

class UTexture2D;

USTRUCT(Blueprintable)
struct FItems
{
    GENERATED_USTRUCT_BODY()
public:
    UPROPERTY(EditAnywhere, Category = "Item")
        FName Name;
    UPROPERTY(EditAnywhere, Category = "Item")
        FText Description;
    UPROPERTY(EditAnywhere, Category = "Item")
        UTexture2D* Image;
    UPROPERTY(EditAnywhere, Category = "Item")
        int MaxStackSize;
    UPROPERTY(EditAnywhere, Category = "Item")
        UClass* ItemClass;

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

USTRUCT(Blueprintable)
struct FInventorySlots
{
    GENERATED_USTRUCT_BODY()
public:
    UPROPERTY(EditAnywhere, Category = "Inventory")
        FItems ItemInfo;
    UPROPERTY()
        int SlotIndex;
    UPROPERTY(EditAnywhere, Category = "Inventory")
        int Quantity;
    UPROPERTY()
        EItemIsFrom ItemIsFrom;

    FInventorySlots() : ItemInfo(), Quantity(0) {};
    FInventorySlots(const FItems& InItemInfo, const int& InQuantity) :
        ItemInfo(InItemInfo), Quantity(InQuantity) {};

        /* スロットのインデックスは、スロットが作られたとき以外で設定しないので、
            コピーコンストラクタで、インデックス以外をコピーするように、
            operatorをオーバーロードする。
         */
    void operator=(const FInventorySlots& In)
    {
        ItemInfo = In.ItemInfo;
        Quantity = In.Quantity;
        ItemIsFrom = In.ItemIsFrom;
    };

    FName GetItemName() const { return ItemInfo.Name; }
    FText GetItemDesciption() const { return ItemInfo.Description; }
    UTexture2D* GetItemImage() const { return ItemInfo.Image; }
    UClass* GetItemClass() const { return ItemInfo.ItemClass; }
    int GetMaxStackSize() const { return ItemInfo.MaxStackSize; }
};
スロットの動作の実装

InventorySlotBase.hに追加

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

#pragma once

#include "CoreMinimal.h"
#include "UIs/Bases/WidgetBase.h"
#include "Templates/ptoStructs.h"
#include "InventorySlotBase.generated.h"

class UImage;
class UTextBlock;
class UTexture2D;
class UInventoryComponentBase;

UCLASS()
class INVENTORYSYSTEM_API UInventorySlotBase : public UWidgetBase
{
    GENERATED_BODY()
public:
    void SetItemImage(UTexture2D* In);
    void SetQuantity(const int& In);
protected:
    UPROPERTY(meta = (BindWidget))
        UImage* ItemImage;

    UPROPERTY(meta = (BindWidget))
        UTextBlock* QuantityText;

    UPROPERTY()
        FInventorySlots SlotContents;

    UPROPERTY()
        UInventoryComponentBase* InventoryComp;

protected:
    void RefreshSlot();
public:
    virtual void UpdateSlot(const FInventorySlots& InSlot, const int& Idx);
    virtual void UpdateSlot();
};

InventorySlotBase.cpp

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

#include "UIs/Bases/InventorySlotBase.h"

#include "Components/Image.h"
#include "Components/TextBlock.h"

#include "Engine/Texture2D.h"

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

void UInventorySlotBase::SetItemImage(UTexture2D* In)
{
    ItemImage->SetBrushFromTexture(In);
}

void UInventorySlotBase::SetQuantity(const int& In)
{
    QuantityText->SetText(FText::AsNumber(In));
}

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

void UInventorySlotBase::RefreshSlot()
{
    SetItemImage(SlotContents.GetItemImage());
    SetQuantity(SlotContents.Quantity);
}

void UInventorySlotBase::UpdateSlot(
    const FInventorySlots& InSlot, const int& Idx
)
{
    if (InSlot.Quantity <= 0)
    {
        SlotContents = FInventorySlots();
    }
    else
    {
        SlotContents = InSlot;
        SlotContents.SlotIndex = Idx;
    }
    RefreshSlot();
}

void UInventorySlotBase::UpdateSlot()
{
    SlotContents = InventoryComp->Inventory[SlotContents.SlotIndex];
    RefreshSlot();
}

インベントリUI

次にWidgetBaseクラスを継承して、InventoryWidgetBaseクラスを作るよ。
f:id:pto8913:20200430170230p:plain

インベントリUIの動作を考える

・スロットの作成。
・スロットの情報の保持。
これだけ!

インベントリUIの動作の実装

InventoryWidgetBase.hに追加

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

#pragma once

#include "CoreMinimal.h"
#include "UIs/Bases/WidgetBase.h"
#include "Templates/ptoStructs.h"
#include "InventoryWidgetBase.generated.h"

class UInventoryComponentBase;
class UInventorySlotBase;
class UGridPanel;
class UTextBlock;

UCLASS()
class INVENTORYSYSTEM_API UInventoryWidgetBase : public UWidgetBase
{
    GENERATED_BODY()
public:
    UPROPERTY(meta = (BindWidget))
        UTextBlock* InventoryName;

    UPROPERTY(meta = (BindWidget))
        UGridPanel* ItemList;

    virtual void OpenUI() override;
    UPROPERTY()
        UInventoryComponentBase* InventoryComp;
    /* インベントリを何列にするかを決める */
    UPROPERTY(EditAnywhere)
        int GridColumnSize;

    /*
        親インベントリコンポーネントを持たない、
        インベントリUIも出てくるのでここで宣言。
    */
    UPROPERTY(EditAnywhere, Category = "Inventory")
        TArray<FInventorySlots> Inventory;
private:
    /* UIs */
    
    /* Classes */
    UPROPERTY(EditAnywhere)
        TSubclassOf<UInventorySlotBase> InventorySlotClass;

protected:
    virtual UInventorySlotBase* CreateSlot(
        const FInventorySlots& InSlot, const int& SlotIdx
    );

    virtual void AddNewItem(UInventorySlotBase*& NewSlot, const int& Idx);

public:
    virtual void CloseUI() override;
};

InventoryWidgetBase.cppに追加

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

#include "UIs/Bases/InventoryWidgetBase.h"
#include "UIs/Bases/InventorySlotBase.h"

#include "Components/Bases/InventoryComponentBase.h"

#include "Components/GridPanel.h"

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

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

    for (int Idx = 0; Idx < InventoryComp->NumberOfSlots; ++Idx)
    {
        UInventorySlotBase* NewSlot = CreateSlot(InventoryComp->Inventory[Idx], Idx);
        NewSlot->OwnerUI = this;
                NewSlot->InventoryComp = InventoryComp;
        AddNewItem(NewSlot, Idx);
    }
}


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

UInventorySlotBase* UInventoryWidgetBase::CreateSlot(
    const FInventorySlots& InSlot, const int& SlotIdx
)
{
    UInventorySlotBase* _Slot = CreateWidget<UInventorySlotBase>(
        this, InventorySlotClass
    );
    if (IsValid(_Slot) == true)
    {
        _Slot->SlotContents = InSlot;
        _Slot->SlotContents.SlotIndex = SlotIdx;
        _Slot->RefreshSlot();
    }
    return _Slot;
}

void UInventoryWidgetBase::AddNewItem(UInventorySlotBase*& NewSlot, const int& Idx)
{
    ItemList->AddChildToGrid(
        NewSlot,
        Idx / GridColumnSize, Idx % GridColumnSize
    );
}


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

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

    InventoryComp->Inventory = MoveTemp(Inventory);
}

コンポーネントの動作の実装

インベントリUIを使いまわしてもいいんだけど、今回は開閉するたびにインベントリUIを作り直すようにするよ。
InventoryComponentBase.hに追加

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

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Templates/ptoStructs.h"
#include "InventoryComponentBase.generated.h"

class UInventoryWidgetBase;

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class INVENTORYSYSTEM_API UInventoryComponentBase : public UActorComponent
{
    GENERATED_BODY()

public:
/* インベントリUIがいくつのスロットを持つか */
    UPROPERTY(EditAnywhere)
        int NumberOfSlots;

    UPROPERTY()
        TArray<FInventorySlots> Inventory;
        UPROPERTY(BlueprintCallable)
            virtual void ToggleInventory();

    /* UIs */
    UPROPERTY()
        UInventoryWidgetBase* InventoryUI;

    /* UI Classes */
    UPROPERTY(EditAnywhere)
        TSubclassOf<UInventoryWidgetBase> InventoryUIClass;

    UFUNCTION(BlueprintCallable, Category = "Inventory")
        virtual void Init();
};

InventoryComponentBase.cppに追加

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

#include "Components/Bases/InventoryComponentBase.h"

#include "UIs/Bases/InventoryWidgetBase.h"

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

void UInventoryComponentBase::Init()
{
    if (Inventory.Num() == 0)
    {
        Inventory.Init(FInventorySlots(), NumberOfSlots);
    }
}

void UInventoryComponentBase::ToggleInventory()
{
    if (IsValid(InventoryUI) == true)
    {
        // インベントリUIが存在するときの処理
        InventoryUI->CloseUI();
        InventoryUI = nullptr;
    }
    else
    {
        // インベントリUIが存在しない時の処理
        InventoryUI = CreateWidget<UInventoryWidgetBase>(GetWorld(), InventoryUIClass);
        if (IsValid(InventoryUI) == true)
        {
            InventoryUI->InventoryComp = this;
            InventoryUI->Inventory = Inventory;
            InventoryUI->OpenUI();
        }
    }
}

InventoryCharacterBase.cppに追加

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

        InventoryComp->Init();
}

とりあえずはこんな感じ。

最低限のものはできたからエディタに戻るよ エディタに戻ったらInventorySlotBaseクラスを継承してWBP_InventorySlotクラスを作成
作ったら、下の画像みたいにしてね。
Overlayのパディングの値を5にしといてね。 f:id:pto8913:20200430195523p:plain

次にInventoryWidgetBaseクラスを継承してWBP_InventoryUIクラスを作ってね
f:id:pto8913:20200502221543p:plain

キャラクターにInventoryコンポーネントをちょっと設定

f:id:pto8913:20200430211548p:plain

はいできました。

f:id:pto8913:20200502221957p:plain

プレイヤーのインベントリを表示するためのUIをつくるよ

このままじゃ不格好だしプレイヤーさん的にもわかりにくいよね。
なのでインベントリや後々追加する装備UIを表示するためのUIを作るよ。

WidgetBaseクラスを継承して、PlayerBelongingsWidgetクラスを作成
f:id:pto8913:20200430202615p:plain

InventoryWidgetBase.cppUWidgetBase::OpenUI()UWidgetBase::CloseUI()を消してね。

PlayerBelongingsWidget.hに追加

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

#pragma once

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

class UBorder;

class AInventoryCharacterBase;

UCLASS()
class INVENTORYSYSTEM_API UPlayerBelongingsWidget : public UWidgetBase
{
    GENERATED_BODY()
private:
    UPROPERTY(meta = (BindWidget))
        UBorder* InventoryBorder;
public:
    UPROPERTY()
        AInventoryCharacterBase* _Owner;

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

PlayerBelongingsWidget.cppに追加

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

#include "UIs/Widgets/PlayerBelongingsWidget.h"
#include "UIs/Bases/InventoryWidgetBase.h"

#include "Characters/Bases/InventoryCharacterBase.h"

#include "Components/Bases/InventoryComponentBase.h"

#include "Components/Border.h"

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

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

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


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

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

    _Owner->InventoryComp->ToggleInventory();


    InventoryBorder->ClearChildren();
}

InventoryCharacterBase.hに追加

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

#pragma once

#include "CoreMinimal.h"
#include "InventorySystem/InventorySystemCharacter.h"
#include "InventoryCharacterBase.generated.h"

class UPlayerBelongingsWidget;
class UInventoryComponentBase;

UCLASS()
class INVENTORYSYSTEM_API AInventoryCharacterBase : public AInventorySystemCharacter
{
    GENERATED_BODY()
private:
    AInventoryCharacterBase();
public:
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
        UInventoryComponentBase* InventoryComp;

    virtual void BeginPlay() override;

    UFUNCTION(BlueprintCallable)
        void TogglePlayerBelongingsUI();

    /* UIs */
    UPROPERTY()
        UPlayerBelongingsWidget* PlayerBelongingsUI;
    /* Classes */
    UPROPERTY(EditAnywhere)
        TSubclassOf<UPlayerBelongingsWidget> PlayerBelongingsUIClass;
};

InventoryCharacterBase.cppに追加

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

#include "Characters/Bases/InventoryCharacterBase.h"

#include "Components/Bases/InventoryComponentBase.h"

#include "UIs/Widgets/PlayerBelongingsWidget.h"

AInventoryCharacterBase::AInventoryCharacterBase()
{
    InventoryComp = CreateDefaultSubobject<UInventoryComponentBase>(TEXT("InventoryComp"));
}

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

    PlayerBelongingsUI = CreateWidget<UPlayerBelongingsWidget>(GetWorld(), PlayerBelongingsUIClass);
    PlayerBelongingsUI->_Owner = this;
}


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

void AInventoryCharacterBase::TogglePlayerBelongingsUI()
{
    if (IsValid(PlayerBelongingsUI) == true)
    {
        if (!PlayerBelongingsUI->IsInViewport())
        {
            PlayerBelongingsUI->OpenUI();
        }
        else
        {
            PlayerBelongingsUI->CloseUI();
        }
    }
}

エディタに戻ってPlayerBelongingsWidgetクラスを継承して、WBP_PlayerBelogngingsUIを作って。
f:id:pto8913:20200430213737p:plain

キャラクターの設定をちょっといじって
f:id:pto8913:20200430214346p:plain

できました。

f:id:pto8913:20200502222131p:plain

拾えるアイテムを作る

Actorクラスを継承して、PickUpableItemBaseクラスを作成
f:id:pto8913:20200430220235p:plain

アイテムの動作を考える

・拾われた際のイベント発生
これだけ!

実装の前にインベントリにアイテムを追加する処理の実装

InventoryComponentBase.hに追加

 /* スタック可能なスロットのインデックスを返す */
    UFUNCTION(BlueprintCallable, Category = "Inventory")
        virtual bool IsStackable(const FInventorySlots& InSlot, int& StackableIdx);
    /* インベントリに追加 */
    UFUNCTION(BlueprintCallable, Category = "Inventory")
        virtual bool AddToInventory(const FInventorySlots& InSlot);
    /* 新しいスタックを作成 */
    UFUNCTION(BlueprintCallable, Category = "Inventory")
        virtual bool CreateStack(const FInventorySlots& InSlot);
    /* スタックに追加 */
    UFUNCTION(BlueprintCallable, Category = "Inventory")
        virtual bool AddToStack(const FInventorySlots& InSlot, const int& Idx);
    /* 
       CreateSlotでインベントリに入りきらなかったアイテムや、
       DropSlotで捨てたアイテムを生成する
   */
    UFUNCTION(BlueprintCallable, Category = "Inventory")
        virtual void SpawnRemainItem(const FInventorySlots& InSlot);

InventoryComponentBase.cppに追加

#include "Items/Bases/PickUpableItemBase.h"

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

bool UInventoryComponentBase::IsStackable(const FInventorySlots& InSlot, int& StackableIdx)
{
    for (int Idx = 0; Idx < Inventory.Num(); ++Idx)
    {
        if (Inventory[Idx].Quantity < InSlot.GetMaxStackSize() &&
            Inventory[Idx].GetItemClass() == InSlot.GetItemClass())
        {
            StackableIdx = Idx;
            return true;
        }
    }
    return false;
}

bool UInventoryComponentBase::AddToInventory(const FInventorySlots& InSlot)
{
    int Idx = 0;
    bool bIsSuccess = false;

    if (IsStackable(InSlot, Idx) == true)
    {
        bIsSuccess = AddToStack(InSlot, Idx);
    }
    else
    {
        bIsSuccess = CreateStack(InSlot);
    }

    return bIsSuccess;
}

bool UInventoryComponentBase::CreateStack(const FInventorySlots& InSlot)
{
    int _Quantity = InSlot.Quantity;
    if (_Quantity <= 0) return false;

    /*
       アイテムを拾ったとき、スタックできる場所がなければ、新しくスロットを作る
   */
    for (int Index = 0; Index < Inventory.Num(); ++Index)
    {
        if (Inventory[Index].Quantity <= 0)
        {
            _Quantity = FMath::Clamp(_Quantity, 1, 9999);

            if (_Quantity > InSlot.GetMaxStackSize())
            {
                Inventory[Index] = FInventorySlots(InSlot.ItemInfo, InSlot.GetMaxStackSize());
                CreateStack(FInventorySlots(InSlot.ItemInfo, _Quantity - InSlot.GetMaxStackSize()));
            }
            else
            {
                Inventory[Index] = InSlot;
            }
            return true;
        }
    }
    if (_Quantity > 0)
    {
        /* Spawn Item */
        FInventorySlots _NewSlotInfo = InSlot;
        _NewSlotInfo.Quantity = _Quantity;
        SpawnRemainItem(_NewSlotInfo);
    }

    return false;
}

bool UInventoryComponentBase::AddToStack(const FInventorySlots& InSlot, const int& Idx)
{
    int _NewQuantity = InSlot.Quantity + Inventory[Idx].Quantity;
    if (_NewQuantity > Inventory[Idx].GetMaxStackSize())
    {
        /*
           アイテムを拾ったときMaxStackSizeより大きかったら
           インベントリスロットに最大量を入れる
           量が0になるまでAddToInventoryを呼ぶよ
       */
        FInventorySlots _FullSlotInfo = FInventorySlots(InSlot.ItemInfo, InSlot.GetMaxStackSize());
        Inventory[Idx] = _FullSlotInfo;

        _NewQuantity -= InSlot.GetMaxStackSize();
        FInventorySlots NewSlot = FInventorySlots(InSlot.ItemInfo, _NewQuantity);
        AddToInventory(NewSlot);
    }
    else
    {
        FInventorySlots _NewSlotInfo = FInventorySlots(Inventory[Idx].ItemInfo, _NewQuantity);
        Inventory[Idx] = _NewSlotInfo;
    }
    return true;
}


void UInventoryComponentBase::SpawnRemainItem(const FInventorySlots& InSlot)
{
    FRotator SpawnRot = { FMath::RandRange(0.f, 360.f), FMath::RandRange(0.f, 360.f), FMath::RandRange(0.f, 360.f) };

    APickUpableItemBase* _Item = GetWorld()->SpawnActor<APickUpableItemBase>(
        InSlot.GetItemClass(),
        GetOwner()->GetActorLocation(), 
        SpawnRot        
    );
    if (IsValid(_Item) == true)
    {
        _Item->Quantity = InSlot.Quantity;
        _Item->ItemInfo = InSlot.ItemInfo;
    }
}

アイテムの動作の実装

今回は、アイテム以外を拾えるように気がありませんので、インターフェースは使いません。
PickUpableItemBase.hに追加

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

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Templates/ptoStructs.h"
#include "PickUpableItemBase.generated.h"

class AInventoryCharacterBase;

class UScnectComponent;
class UStaticMeshCompnent;

UCLASS()
class INVENTORYSYSTEM_API APickUpableItemBase : public AActor
{
    GENERATED_BODY()
    
public:   
    APickUpableItemBase();

    UPROPERTY(EditAnywhere)
        int Quantity;

    UPROPERTY(EditAnywhere)
        FItems ItemInfo;

protected:
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Item")
        USceneComponent* SceneComp;

    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Item")
        UStaticMeshComponent* MeshComp;

    virtual void BeginPlay() override;

public:
    virtual bool PickUp(AInventoryCharacterBase* In);
};

PickUpableItemBase.cppに追加

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

#include "Items/Bases/PickUpableItemBase.h"

#include "Characters/Bases/InventoryCharacterBase.h"

#include "Components/Bases/InventoryComponentBase.h"

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

// Sets default values
APickUpableItemBase::APickUpableItemBase()
{
    SceneComp = CreateDefaultSubobject<USceneComponent>(TEXT("Scene Comp"));
    SetRootComponent(SceneComp);

    MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh Comp"));
    MeshComp->SetupAttachment(SceneComp);

    MeshComp->CanCharacterStepUpOn = ECanBeCharacterBase::ECB_No;
    MeshComp->SetCollisionProfileName("Custom");
    FCollisionResponseContainer _CollisionRes;
    _CollisionRes.SetResponse(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
    _CollisionRes.SetResponse(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);
    MeshComp->SetCollisionResponseToChannels(_CollisionRes);
    MeshComp->SetCollisionObjectType(ECollisionChannel::ECC_WorldStatic);
    MeshComp->SetSimulatePhysics(true);
}

// Called when the game starts or when spawned
void APickUpableItemBase::BeginPlay()
{
    Super::BeginPlay();
    
}

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

bool APickUpableItemBase::PickUp(AInventoryCharacterBase* In)
{
    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に追加

protected:
    virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;
    UFUNCTION(BlueprintCallable)
        virtual void PickUp();

InventoryCharacterBase.cppに追加

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

    PlayerInputComponent->BindAction("PickUp", EInputEvent::IE_Pressed, this, &AInventoryCharacterBase::PickUp);
    const FInputActionKeyMapping map("PickUp", EKeys::E);
    GetMutableDefault<UInputSettings>()->AddActionMapping(map);

    Super::SetupPlayerInputComponent(PlayerInputComponent);
}

void AInventoryCharacterBase::PickUp()
{
    TArray<AActor*> Out;
    GetOverlappingActors(Out, APickUpableItemBase::StaticClass());

    for (AActor* Actor : Out)
    {
        APickUpableItemBase* _Item = Cast<APickUpableItemBase>(Actor);
        if (IsValid(_Item) == true)
        {
            _Item->PickUp(this);
            break;
        }
    }
}

エディタに戻ってPickUpableItemBaseクラスを継承してBP_ItemTestを作成
f:id:pto8913:20200501112542p:plain
※ : 今回は簡単にするため、アイテムの情報を手動で登録していますが、実際に使う際はデータテーブルで管理してください。

適当にゲームに追加して、プレイ!

できました。
f:id:pto8913:20200502222900g:plain
f:id:pto8913:20200502223053g:plain

いろいろ試すとわかるんですけど、インベントリUIを開いた状態でEキーを押すとインベントリUIにアイテムが追加されず、アイテムだけが消えるんですね。
これは、インベントリコンポーネントには追加(更新)処理を実装してるけど、インベントリUIにはなんの追加(更新)処理も実装してないからです。

なのでちょっと修正。
InventoryWidgetBase.hに追加

public:
    /* ItemListからIdxの位置にあるスロットを探す */
    virtual UInventorySlotBase* GetSlotAtIndex(const int& Idx);
    virtual void UpdateSlot(const int& Idx);

InventoryWidgetBase.cppに追加

UInventorySlotBase* UInventoryWidgetBase::GetSlotAtIndex(const int& Idx)
{
    return Cast<UInventorySlotBase>(ItemList->GetChildAt(Idx));
}

void UInventoryWidgetBase::UpdateSlot(const int& Idx)
{
    UInventorySlotBase* _Slot = GetSlotAtIndex(Idx);
    if (IsValid(_Slot) == true)
    {
        _Slot->UpdateSlot();
    }
}

InventoryComponentBase.cppに追加

bool UInventoryComponentBase::CreateStack(const FInventorySlots& InSlot)
{
    int _Quantity = InSlot.Quantity;
    if (_Quantity <= 0) return false;

    /*
       アイテムを拾ったとき、スタックできる場所がなければ、新しくスロットを作る
   */
    for (int Index = 0; Index < Inventory.Num(); ++Index)
    {
        if (Inventory[Index].Quantity <= 0)
        {
            _Quantity = FMath::Clamp(_Quantity, 1, 9999);

            if (_Quantity > InSlot.GetMaxStackSize())
            {
                Inventory[Index] = FInventorySlots(InSlot.ItemInfo, InSlot.GetMaxStackSize());
                if (IsValid(InventoryUI) == true)
                {
                    InventoryUI->UpdateSlot(Index);
                }
                CreateStack(FInventorySlots(InSlot.ItemInfo, _Quantity - InSlot.GetMaxStackSize()));
            }
            else
            {
                Inventory[Index] = InSlot;
                if (IsValid(InventoryUI) == true)
                {
                    InventoryUI->UpdateSlot(Index);
                }
            }
            return true;
        }
    }
    if (_Quantity > 0)
    {
        /* Spawn Item */
        FInventorySlots _NewSlotInfo = InSlot;
        _NewSlotInfo.Quantity = _Quantity;
        SpawnRemainItem(_NewSlotInfo);
    }

    return false;
}

bool UInventoryComponentBase::AddToStack(const FInventorySlots& InSlot, const int& Idx)
{
    int _NewQuantity = InSlot.Quantity + Inventory[Idx].Quantity;
    if (_NewQuantity > Inventory[Idx].GetMaxStackSize())
    {
        /*
           アイテムを拾ったときMaxStackSizeより大きかったら
           インベントリスロットに最大量を入れる
           量が0になるまでAddToInventoryを呼ぶよ
       */
        FInventorySlots _FullSlotInfo = FInventorySlots(InSlot.ItemInfo, InSlot.GetMaxStackSize());
        Inventory[Idx] = _FullSlotInfo;
        if (IsValid(InventoryUI) == true)
        {
            InventoryUI->UpdateSlot(Idx);
        }

        _NewQuantity -= InSlot.GetMaxStackSize();
        FInventorySlots NewSlot = FInventorySlots(InSlot.ItemInfo, _NewQuantity);
        AddToInventory(NewSlot);
    }
    else
    {
        FInventorySlots _NewSlotInfo = FInventorySlots(Inventory[Idx].ItemInfo, _NewQuantity);
        Inventory[Idx] = _NewSlotInfo;
        if (IsValid(InventoryUI) == true)
        {
            InventoryUI->UpdateSlot(Idx);
        }
    }
    return true;
}

これで更新できるようになりました。

とりあえず今回はここまで。

pto8913.hatenablog.com