Skip to content

Instantly share code, notes, and snippets.

@quonic
Last active September 3, 2025 20:19
Show Gist options
  • Select an option

  • Save quonic/645d03f5b0c624407123d67c6035002f to your computer and use it in GitHub Desktop.

Select an option

Save quonic/645d03f5b0c624407123d67c6035002f to your computer and use it in GitHub Desktop.
Unreal Engine Inventory system example
// InventoryComponent.cpp
#include "InventoryComponent.h"
UInventoryComponent::UInventoryComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
void UInventoryComponent::BeginPlay()
{
Super::BeginPlay();
}
FInventoryItem UInventoryComponent::GetItemAt(int32 Index) const
{
if (!IsValidIndex(Index)) return FInventoryItem{};
if (const FInventoryItem* Found = Slots.Find(Index))
{
return *Found;
}
return FInventoryItem{};
}
void UInventoryComponent::BroadcastChange(int32 Index) const
{
const FInventoryItem Current = const_cast<UInventoryComponent*>(this)->GetItemAt(Index);
if (OnSlotChanged.IsBound())
{
OnSlotChanged.Broadcast(Index, Current);
}
}
void UInventoryComponent::ClearSlot(int32 Index)
{
Slots.Remove(Index);
BroadcastChange(Index);
}
bool UInventoryComponent::SetItemAt(int32 Index, const FInventoryItem& Item)
{
if (!IsValidIndex(Index)) return false;
if (Item.IsEmpty())
{
if (Slots.Contains(Index))
{
ClearSlot(Index);
return true;
}
return false;
}
Slots.Add(Index, Item);
BroadcastChange(Index);
return true;
}
FInventoryItem UInventoryComponent::RemoveAt(int32 Index)
{
if (!IsValidIndex(Index)) return FInventoryItem{};
if (FInventoryItem* Found = Slots.Find(Index))
{
FInventoryItem Out = *Found;
Slots.Remove(Index);
BroadcastChange(Index);
return Out;
}
return FInventoryItem{};
}
int32 UInventoryComponent::FindFirstEmptySlot() const
{
for (int32 i = 0; i < Capacity; ++i)
{
if (!Slots.Contains(i)) return i;
}
return -1;
}
int32 UInventoryComponent::MergeSlots(int32 From, int32 To)
{
if (!IsValidIndex(From) || !IsValidIndex(To) || From == To) return 0;
FInventoryItem* A = Slots.Find(From);
FInventoryItem* B = Slots.Find(To);
if (!A || !B) return 0;
if (A->IsEmpty() || B->IsEmpty()) return 0;
if (A->Id != B->Id) return 0;
const int32 Space = FMath::Max(0, B->MaxStack - B->Quantity);
if (Space <= 0) return 0;
const int32 Move = FMath::Min(Space, A->Quantity);
B->Quantity += Move;
A->Quantity -= Move;
// Clean up the source if emptied
if (A->Quantity <= 0)
{
ClearSlot(From);
}
else
{
BroadcastChange(From);
}
BroadcastChange(To);
return Move;
}
bool UInventoryComponent::MoveItem(int32 From, int32 To, bool bMergeIfSame)
{
if (!IsValidIndex(From) || !IsValidIndex(To) || From == To) return false;
FInventoryItem FromItem = GetItemAt(From);
if (FromItem.IsEmpty()) return false;
FInventoryItem ToItem = GetItemAt(To);
// Merge path
if (bMergeIfSame && !ToItem.IsEmpty() && ToItem.Id == FromItem.Id)
{
const int32 Space = FMath::Max(0, ToItem.MaxStack - ToItem.Quantity);
if (Space > 0)
{
const int32 Move = FMath::Min(Space, FromItem.Quantity);
ToItem.Quantity += Move;
FromItem.Quantity -= Move;
// Apply
if (FromItem.Quantity <= 0) ClearSlot(From);
else SetItemAt(From, FromItem);
SetItemAt(To, ToItem);
return Move > 0;
}
// No space to merge; fall through to swap if allowed by caller via SwapItems.
}
// Simple move: overwrite destination and clear source
SetItemAt(To, FromItem);
ClearSlot(From);
return true;
}
bool UInventoryComponent::SwapItems(int32 A, int32 B)
{
if (!IsValidIndex(A) || !IsValidIndex(B) || A == B) return false;
const bool bHasA = Slots.Contains(A);
const bool bHasB = Slots.Contains(B);
// Nothing to swap
if (!bHasA && !bHasB) return false;
if (bHasA && bHasB)
{
// Swap the values in place
FInventoryItem* ItemA = Slots.Find(A);
FInventoryItem* ItemB = Slots.Find(B);
check(ItemA && ItemB);
Swap(*ItemA, *ItemB);
BroadcastChange(A);
BroadcastChange(B);
return true;
}
// Exactly one has an item: move it to the other and clear source
if (bHasA) // move A -> B
{
const FInventoryItem Temp = *Slots.Find(A);
SetItemAt(B, Temp);
ClearSlot(A);
return true;
}
else // bHasB: move B -> A
{
const FInventoryItem Temp = *Slots.Find(B);
SetItemAt(A, Temp);
ClearSlot(B);
return true;
}
}
FInventoryItem UInventoryComponent::AddItem(FInventoryItem Item)
{
if (Item.IsEmpty()) return FInventoryItem{};
// 1) Merge into existing stacks (same Id)
for (int32 i = 0; i < Capacity && Item.Quantity > 0; ++i)
{
if (FInventoryItem* Slot = Slots.Find(i))
{
if (!Slot->IsEmpty() && Slot->Id == Item.Id && Slot->Quantity < Slot->MaxStack)
{
const int32 Space = Slot->MaxStack - Slot->Quantity;
const int32 Move = FMath::Min(Space, Item.Quantity);
Slot->Quantity += Move;
Item.Quantity -= Move;
BroadcastChange(i);
}
}
}
// 2) Place in empty slots
while (Item.Quantity > 0)
{
const int32 Empty = FindFirstEmptySlot();
if (Empty == -1) break;
const int32 ToPlace = FMath::Min(Item.MaxStack, Item.Quantity);
FInventoryItem Chunk = Item;
Chunk.Quantity = ToPlace;
SetItemAt(Empty, Chunk);
Item.Quantity -= ToPlace;
}
// If any quantity remains, it didn't fit (return remainder)
return Item;
}
// InventoryComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InventoryComponent.generated.h"
USTRUCT(BlueprintType)
struct FInventoryItem
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FName Id = NAME_None;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 Quantity = 0;
/** Optional per-item cap (useful for stackables). */
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 MaxStack = 99;
bool IsEmpty() const { return Id.IsNone() || Quantity <= 0; }
static FInventoryItem Make(FName InId, int32 InQuantity, int32 InMaxStack = 99)
{
FInventoryItem R;
R.Id = InId;
R.Quantity = InQuantity;
R.MaxStack = InMaxStack;
return R;
}
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnInventorySlotChanged, int32, SlotIndex, const FInventoryItem&, NewValue);
UCLASS(ClassGroup=(Inventory), meta=(BlueprintSpawnableComponent))
class YOURGAME_API UInventoryComponent : public UActorComponent
{
GENERATED_BODY()
public:
UInventoryComponent();
/** Maximum logical slots (0..Capacity-1). */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Inventory")
int32 Capacity = 20;
/** Slots map keyed by position. Empty slots simply don't exist in the map. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, SaveGame, Category="Inventory")
TMap<int32, FInventoryItem> Slots;
/** Fired whenever a slot changes (added/updated/cleared). */
UPROPERTY(BlueprintAssignable, Category="Inventory")
FOnInventorySlotChanged OnSlotChanged;
/** True if index is within [0, Capacity). */
UFUNCTION(BlueprintPure, Category="Inventory")
bool IsValidIndex(int32 Index) const { return Index >= 0 && Index < Capacity; }
/** Get item at position (empty if not present). */
UFUNCTION(BlueprintPure, Category="Inventory")
FInventoryItem GetItemAt(int32 Index) const;
/** Set a slot to an item (empty item clears it). */
UFUNCTION(BlueprintCallable, Category="Inventory")
bool SetItemAt(int32 Index, const FInventoryItem& Item);
/** Remove and return the item at Index (empty if none). */
UFUNCTION(BlueprintCallable, Category="Inventory")
FInventoryItem RemoveAt(int32 Index);
/** Add item: first merge into existing stacks (same Id), then fill empty slots. Returns remainder (if any). */
UFUNCTION(BlueprintCallable, Category="Inventory")
FInventoryItem AddItem(FInventoryItem Item);
/** Move item between slots. If bMergeIfSame and same Id, merges into To. Returns true if anything changed. */
UFUNCTION(BlueprintCallable, Category="Inventory")
bool MoveItem(int32 From, int32 To, bool bMergeIfSame = true);
/** Swap items in slots; handles empty cases gracefully. */
UFUNCTION(BlueprintCallable, Category="Inventory")
bool SwapItems(int32 A, int32 B);
/** Merge two slots if same Id; returns amount moved. */
UFUNCTION(BlueprintCallable, Category="Inventory")
int32 MergeSlots(int32 From, int32 To);
/** Find first empty slot index; returns -1 if none. */
UFUNCTION(BlueprintPure, Category="Inventory")
int32 FindFirstEmptySlot() const;
protected:
virtual void BeginPlay() override;
private:
void BroadcastChange(int32 Index) const;
void ClearSlot(int32 Index);
};
       DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
               Version 2, December 2025

Copyright (C) 2025 quonic/spyingwind

Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed.

       DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE

TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

  1. You just DO WHAT THE FUCK YOU WANT TO.

Notes & usage:

  • Key design: TMap<int32, FInventoryItem> Slots where missing key = empty slot—simple and efficient.
  • Swap: SwapItems(A, B) covers swap and “move into empty” cases.
  • Merge semantics: automatic in AddItem and optional in MoveItem(..., bMergeIfSame=true).
  • Events: Bind OnSlotChanged in Blueprint or C++ to refresh UI per slot.
  • Capacity: Adjust in editor or at runtime before populating.

If you want replication, I can extend this with GetLifetimeReplicatedProps, ReplicatedUsing notifies per slot, and server-authority guards.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment