The inventory manager
UEDInventoryManagerComponent is the inventory of an actor: every container it carries, in one
component, answering questions about the actor rather than about any one bag.
Why one component
The alternative — a component per container — means every system that touches the inventory has to know how many bags exist and thread a reference to each one. Adding a stash becomes a component, a variable, and an edit to everything that counts items.
Here, adding a stash is a line in an array. And "how many arrows do I have" spans the quiver, the pack and the bag being worn without the caller knowing those exist.
Setup
Inventory = CreateDefaultSubobject<UEDInventoryManagerComponent>(TEXT("Inventory"));
Inventory->InitialContainers = { HotbarDef, MainDef, EquipmentDef };
Inventory->InitialItems.Add(FEDInitialItem{ StarterSword, 1, EquipmentTag });
| Property | Meaning |
|---|---|
| Initial Containers | Built on the authority at BeginPlay, in this order. |
| Initial Items | Granted once every container exists — so an item that belongs in the quiver is never put in the pack just because the quiver was declared second. |
| Default Container Query | Which container catches an item nothing else claims. |
| Stranded Item Handler Classes | What happens to contents when a container is removed. |
| Watched Definitions | Narrows the total-changed event. Empty reports everything. |
| Max Session Distance | How far the actor may get from an open world container before it closes. |
Finding it
UEDInventoryManagerComponent* Inv = UEDInventoryManagerComponent::Find(SomeActor);
Find asks IEDInventoryOwner first and falls back to the actor's components. Implement the
interface when the answer is not the obvious one — a Pawn forwarding to its PlayerState so the
inventory survives a respawn.
Asking about containers
UEDContainer* Hotbar = Inv->GetContainer(HotbarTag); // first match, hierarchical
Inv->GetContainersByTag(BagTag, OutContainers); // all of a kind
UEDContainer* One = Inv->GetContainerById(SavedGuid); // exactly one
Inv->GetContainersByItem(BackpackHandle, OutContainers); // what an item brought
Inv->GetContainers(OutContainers); // everything
GetContainerDefinition(Tag) gives back the asset a container was built from, which is what a UI
needs for a tab label or icon.
Acting on the whole inventory
These span every container and are authority-only.
| Call | Behaviour |
|---|---|
TryAddItem(Definition, Count, PreferredContainer) | Routed by data when no container is named. All-or-nothing per container — it does not spread one stack across several. |
TryRemoveItems(Definition, Count) | Spends across containers, smallest piles first. All-or-nothing overall. |
TryRemoveItem(Handle) | Removes one specific stack, wherever it is. |
TryMoveToContainer(Handle, TargetTag, Count) | Moves between this actor's own containers. |
GetItemCount / HasItem | Summed across everything. |
FindItem(Handle, OutEntry) | Which container holds a handle. |
FindContainerFor(Definition, Count) | Where an item would go — the honest answer for a "cannot carry that" prompt. |
Naming a container turns a hint into an instruction: if that container refuses, the call fails rather than quietly putting the item elsewhere. That distinction matters for "equip this", which should fail loudly.
Gaining and losing containers at runtime
UEDContainer* Satchel = Inv->AddContainer(SatchelDefinition); // a quest reward
Inv->RemoveContainer(Satchel); // taken away again
Inv->RemoveContainerByTag(Tag); // the first of a kind
A container that belongs to an item is added with AddNestedContainer(Definition, OwningItem) and
cannot be removed on its own — RemoveContainer refuses it and says so. Removing it directly
would leave the item convinced it still had storage, with nothing to reopen it. Move or remove the
item and its container follows. See Nested containers.
What happens to the contents
Removing a container offers each item to stranded-item handlers in order; the first that takes it owns it.
| Handler | What it does |
|---|---|
UEDStrandedItemHandler_Rehome (default) | Puts the item in another container the owner has, using the same placement rules a pickup would. |
| your own | Subclass UEDStrandedItemHandler in C++ or Blueprint — drop it at the player's feet, mail it, charge storage. |
Anything no handler accepts is destroyed and logged as a warning. At that point the game has not said what should happen to a player's things, and neither silently deleting them nor leaking the container is an acceptable default.
Events
| Event | Fires when |
|---|---|
OnInventoryChanged | Anything in any container changed. One binding covers containers gained later. |
OnStackCountChanged | An item's actor-wide total changed. Quest counters, ammo readouts, pickup toasts. |
OnContainersChanged | A container was added or removed — or became identifiable on a client. |
OnInitializationFinished | Setup finished, or gave up. |
OnSessionOpened / OnSessionClosed | A world container was opened or closed. |
WhenReady(Callback) runs a callback once the inventory is usable, immediately if it already is.
Every system that touches an inventory at startup hits the same race — containers are built in
BeginPlay on the server and arrive later still on clients — and polling for it is what everybody
writes instead, always slightly wrong.
Open containers, and what they grant
The manager tracks which containers elsewhere in the world this actor has open — a chest, a corpse, a vendor's stock. A session is not bookkeeping for your UI: it is the permission that lets a client operate on a container it does not own. See Transactions.
// SERVER ONLY. Ignored (and logged) on a client.
Inv->OpenSession(ChestContainer);
Inv->CloseSession(ChestContainer); // safe from a client — giving up access needs no permission
Inv->HasOpenSession(ChestContainer); // what this machine believes
Inv->HasOpenSessionAuthoritative(Container); // what the server knows; false off the authority
Opening is authority-only, and that is the point. If a client could open a session it could authorise itself on anything it can name, and the permission check would be decorative. The server decides, because only the server knows whether opening this container is allowed at all — whatever already runs there when a player interacts with a chest is where this call belongs, and that code has the context to say no.
Nothing closes a session on its own except the container being destroyed. MaxSessionDistance
defaults to 0 (off): a session is a permission, and how far is too far is a question about your
game — a remote stash, a vehicle looted while driving and a bank reachable from anywhere are all
ordinary designs that a number chosen here would quietly break. Set it and you get the server-side
range sweep for free, closing the session through OnSessionClosed when the player walks away.
Nothing ticks until something is open.
Where next
- Placement and routing — how a container is chosen
- Transactions — what the
Try*calls actually run - Replication — what a client is allowed to believe