UE5 C++ 実装:星屑リソースを管理するインベントリシステム

1. データ構造の設計

インベントリは「内部データ」と「表示データ」の二層構造で構成します。

1.1 内部データ(高速検索用)

// 星屑ID → 総保有量
TMap<FName, int32> TotalAmounts;

// 星屑ID → 使用中スロット数
TMap<FName, int32> SlotUsage;

TMapを採用した理由は、UEのガベージコレクションと相性が良く、Find関数を使えば存在チェックも高速に行えるためです。

1.2 表示データ(UI連携用)

USTRUCT(BlueprintType)
struct FInventorySlot
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FName ItemId;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    int32 StackCount = 0;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    int32 MaxStack = 99;

    bool IsEmpty() const { return StackCount <= 0; }
    bool IsFull() const { return StackCount >= MaxStack; }
    int32 RemainingSpace() const { return MaxStack - StackCount; }
};

UCLASS(BlueprintType)
class UInventoryGrid : public UObject
{
    GENERATED_BODY()

public:
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
    TArray<FInventorySlot> Slots;
};

1.3 容量管理

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Inventory")
int32 MaxSlots = 48;

UFUNCTION(BlueprintCallable)
void ResizeInventory(int32 NewSize)
{
    Slots.SetNum(NewSize);
    MaxSlots = NewSize;
}

2. 検索機能

2.1 総保有量の取得

UFUNCTION(BlueprintCallable)
int32 GetTotalAmount(FName ItemId) const
{
    const int32* Found = TotalAmounts.Find(ItemId);
    return Found ? *Found : 0;
}

2.2 追加可能量の計算

UFUNCTION(BlueprintCallable)
int32 GetAddableAmount(FName ItemId, int32 RequestAmount) const
{
    int32 TotalSpace = 0;
    const FStardustData* Data = GetStardustData(ItemId);
    if (!Data) return 0;

    // 既存スロットの空き容量
    for (const FInventorySlot& Slot : Slots)
    {
        if (Slot.ItemId == ItemId && !Slot.IsFull())
        {
            TotalSpace += Slot.RemainingSpace();
        }
    }

    // 空スロットの容量
    int32 EmptySlots = 0;
    for (const FInventorySlot& Slot : Slots)
    {
        if (Slot.IsEmpty()) EmptySlots++;
    }
    TotalSpace += EmptySlots * Data->MaxStack;

    return FMath::Min(TotalSpace, RequestAmount);
}

3. 更新処理

3.1 単一スロットの更新

UFUNCTION(BlueprintCallable)
void UpdateSlot(int32 Index, FName ItemId, int32 Amount)
{
    if (!Slots.IsValidIndex(Index)) return;

    FInventorySlot& Slot = Slots[Index];
    
    // 古いデータを減算
    if (!Slot.IsEmpty())
    {
        TotalAmounts[Slot.ItemId] -= Slot.StackCount;
        SlotUsage[Slot.ItemId]--;
    }

    // 新しいデータを設定
    if (Amount > 0)
    {
        Slot.ItemId = ItemId;
        Slot.StackCount = Amount;
        Slot.MaxStack = GetStardustData(ItemId)->MaxStack;
        
        TotalAmounts.FindOrAdd(ItemId) += Amount;
        SlotUsage.FindOrAdd(ItemId)++;
    }
    else
    {
        Slot = FInventorySlot(); // 空スロットにリセット
    }

    OnInventoryUpdated.Broadcast();
}

3.2 自動整頓

UFUNCTION(BlueprintCallable)
void CompactInventory()
{
    TArray<FInventorySlot> NonEmptySlots;
    for (const FInventorySlot& Slot : Slots)
    {
        if (!Slot.IsEmpty())
        {
            NonEmptySlots.Add(Slot);
        }
    }

    // スタック可能なアイテムを結合
    for (int32 i = 0; i < NonEmptySlots.Num(); ++i)
    {
        for (int32 j = i + 1; j < NonEmptySlots.Num(); ++j)
        {
            if (NonEmptySlots[i].ItemId == NonEmptySlots[j].ItemId)
            {
                int32 TransferAmount = FMath::Min(
                    NonEmptySlots[i].RemainingSpace(),
                    NonEmptySlots[j].StackCount
                );
                
                NonEmptySlots[i].StackCount += TransferAmount;
                NonEmptySlots[j].StackCount -= TransferAmount;
                
                if (NonEmptySlots[j].IsEmpty())
                {
                    NonEmptySlots.RemoveAt(j);
                    j--;
                }
            }
        }
    }

    // 配列を詰める
    Slots.SetNum(MaxSlots);
    for (int32 i = 0; i < MaxSlots; ++i)
    {
        if (i < NonEmptySlots.Num())
        {
            Slots[i] = NonEmptySlots[i];
        }
        else
        {
            Slots[i] = FInventorySlot();
        }
    }

    OnInventoryUpdated.Broadcast();
}

4. アイテム追加・削除

4.1 優先スタック追加

UFUNCTION(BlueprintCallable)
bool TryAddItem(FName ItemId, int32 Amount, int32& Remaining)
{
    Remaining = Amount;
    const FStardustData* Data = GetStardustData(ItemId);
    if (!Data) return false;

    // 既存スロットに追加
    for (FInventorySlot& Slot : Slots)
    {
        if (Slot.ItemId == ItemId && !Slot.IsFull())
        {
            int32 AddAmount = FMath::Min(Slot.RemainingSpace(), Remaining);
            Slot.StackCount += AddAmount;
            Remaining -= AddAmount;
            TotalAmounts.FindOrAdd(ItemId) += AddAmount;

            if (Remaining <= 0)
            {
                OnInventoryUpdated.Broadcast();
                return true;
            }
        }
    }

    // 空スロットに新規追加
    for (FInventorySlot& Slot : Slots)
    {
        if (Slot.IsEmpty())
        {
            int32 AddAmount = FMath::Min(Data->MaxStack, Remaining);
            Slot.ItemId = ItemId;
            Slot.StackCount = AddAmount;
            Slot.MaxStack = Data->MaxStack;
            Remaining -= AddAmount;
            TotalAmounts.FindOrAdd(ItemId) += AddAmount;
            SlotUsage.FindOrAdd(ItemId)++;

            if (Remaining <= 0)
            {
                OnInventoryUpdated.Broadcast();
                return true;
            }
        }
    }

    OnInventoryUpdated.Broadcast();
    return Remaining <= 0;
}

4.2 削除処理

UFUNCTION(BlueprintCallable)
bool TryRemoveItem(FName ItemId, int32 Amount)
{
    int32 CurrentTotal = GetTotalAmount(ItemId);
    if (CurrentTotal < Amount) return false;

    int32 Remaining = Amount;
    
    // 後ろから削除(UIの整合性維持のため)
    for (int32 i = Slots.Num() - 1; i >= 0 && Remaining > 0; --i)
    {
        FInventorySlot& Slot = Slots[i];
        if (Slot.ItemId == ItemId)
        {
            int32 RemoveAmount = FMath::Min(Slot.StackCount, Remaining);
            Slot.StackCount -= RemoveAmount;
            Remaining -= RemoveAmount;
            TotalAmounts[ItemId] -= RemoveAmount;

            if (Slot.IsEmpty())
            {
                Slot = FInventorySlot();
                SlotUsage[ItemId]--;
            }
        }
    }

    OnInventoryUpdated.Broadcast();
    return true;
}

5. UI連携

5.1 動的マルチキャストデリゲート

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FFInventoryUpdateSignature);

UPROPERTY(BlueprintAssignable)
FFInventoryUpdateSignature OnInventoryUpdated;

5.2 UIバインディング例

// インベントリウィジェット
UFUNCTION()
void RefreshGrid()
{
    for (int32 i = 0; i < InventoryComponent->Slots.Num(); ++i)
    {
        if (SlotWidgets.IsValidIndex(i))
        {
            SlotWidgets[i]->UpdateSlot(InventoryComponent->Slots[i]);
        }
    }
}

// 初期化時
InventoryComponent->OnInventoryUpdated.AddDynamic(this, &UInventoryWidget::RefreshGrid);

タグ: UE5 C++ InventorySystem TMap MulticastDelegate

6月27日 18:59 投稿