Starbud Station has a lot of things that hold items. Player backpacks. Storage crates on each island. Machine input and output slots. Vendor inventories. The shop floor display. The packaging output buffer. When we started designing the inventory system, the first decision was whether each of these would have its own specialised implementation or share a common one. We went with a unified UInventoryComponent that every container in the game uses, configured per-instance through data rather than through subclassing.

This paid off immediately. A transfer between any two containers — player to storage, machine output to shop floor, vendor trade — uses the same function call regardless of what the containers are. The replication logic is written once. The save logic is written once. Adding a new type of container to the game means placing a UInventoryComponent on the actor and setting a few properties in the editor.

The Core Structs

Everything in the inventory system flows through two structs. FItemDefinition is a DataAsset-based definition of an item — its ID, display name, category, stack size limit, weight, and base value. It never changes at runtime. FInventorySlot is a runtime instance of an item in a container — it holds a reference to the item definition, the current quantity, and any instance-specific data like quality score or processing state.

USTRUCT(BlueprintType)
struct FInventorySlot
{
    GENERATED_BODY()

    UPROPERTY(Replicated) FName  ItemId;
    UPROPERTY(Replicated) int32 Quantity;
    UPROPERTY(Replicated) float QualityScore;  // 0.0 - 1.0
    UPROPERTY(Replicated) FGuid  SlotId;        // Stable ID for UI binding

    bool IsEmpty() const { return ItemId.IsNone() || Quantity <= 0; }
};

The UInventoryComponent holds a TArray<FInventorySlot> with a configurable maximum slot count. A player backpack might have 20 slots. A machine input buffer has 1. A storage crate has 40. The component doesn't behave differently based on slot count — it just enforces the limit set in the editor.

Adding and Removing Items

The AddItem function handles stack merging automatically. It first searches for an existing slot containing the same item ID with available stack space. If found, it fills that stack as far as possible and creates overflow stacks as needed. If no existing slot can accept the item, a new slot is created. If the component is full, the remainder is returned as an overflow quantity that the caller handles.

int32 UInventoryComponent::AddItem(
    FName ItemId, int32 Quantity, float Quality)
{
    int32 Remaining = Quantity;
    const int32 StackLimit = GetStackLimit(ItemId);

    // Fill existing partial stacks first
    for (FInventorySlot& Slot : Slots)
    {
        if (Slot.ItemId != ItemId || Slot.Quantity >= StackLimit) continue;

        int32 Space  = StackLimit - Slot.Quantity;
        int32 ToAdd  = FMath::Min(Remaining, Space);
        Slot.Quantity += ToAdd;
        Remaining     -= ToAdd;

        if (Remaining == 0) return 0;
    }

    // Open new slots for remainder
    while (Remaining > 0 && Slots.Num() < MaxSlots)
    {
        int32 ToAdd = FMath::Min(Remaining, StackLimit);
        Slots.Add({ ItemId, ToAdd, Quality, FGuid::NewGuid() });
        Remaining -= ToAdd;
    }

    return Remaining; // 0 if fully added, > 0 if overflow
}

Transferring Between Containers

Item transfers between any two containers go through a single server-side function: TransferItem. It takes a source component, a destination component, the slot ID to transfer from, and a quantity. It validates that the source has the item, that the destination has capacity, removes from source, adds to destination, and fires delegates on both components to notify any bound UI.

All transfers are server-authoritative. A client requesting a transfer sends an RPC to the server. The server validates and executes. The result propagates back to clients through the replicated slot arrays. This means a player can never move items they don't have or into a container that's full — the server is the only authority on what inventory contains.

One important design decision: quality scores are averaged on merge, not summed. When two stacks of the same item combine, the merged stack's quality is the weighted average of both. High-quality inputs are worth keeping in separate stacks if you want to maintain their quality for premium pricing — bulk merging dilutes the score.

Replication

The slot array is replicated using Unreal's fast array serializer via FFastArraySerializer. This only replicates changes — added slots, removed slots, modified quantities — rather than the entire array on every update. For an inventory that might have 40 slots, this matters. A single item transfer replicates one or two slot deltas rather than the full container state.

Each slot has a stable FGuid slot ID generated on creation. UI elements bind to slot IDs rather than array indices, so when the array changes due to stacking or sorting, the UI updates correctly without losing track of which visual element corresponds to which slot.

Weight and Capacity

Player backpacks enforce a weight limit in addition to slot count. Each FItemDefinition carries a weight value. The UInventoryComponent tracks current total weight and exposes it as a replicated property for the HUD. When a player tries to pick up an item that would exceed their carry weight, the transfer returns an overflow rather than a refusal — the player gets as much as they can carry and the rest stays in the source container.

Storage crates, machine buffers, and shop containers don't enforce weight — only slot count. The weight system is specifically a player-character constraint, not a universal one. This is set per-component instance via a boolean flag in the editor rather than being encoded into different component subclasses.

The Foundation Everything Else Builds On

The inventory system was one of the first systems completed because almost everything else depends on it. The machine system feeds job inputs from inventory and pushes outputs back into it. The shop system reads from inventory to populate listings. Traders exchange items through it. The save system serializes it. Getting this right early — and keeping it genuinely unified rather than having specialised cases sneak in — has kept the rest of the systems clean.