Skip to main content

Items in the world

The framework describes what an item looks like in the world. Spawning it, picking it up and dropping it are your game's decisions, and deliberately so — "pick up" means something different in a survival game and a shooter.

The EDInventoryWorld module

FEDWorldModelFragment and UEDWorldModelComponent live in EDInventoryWorld, not in Core. Streaming meshes and spawning actors is presentation, and Core ships none — so a project that never drops anything can disable the module and not carry the code. Add "EDInventoryWorld" to your module's Build.cs dependencies to use them.

Upgrading from a build where they were in Core? A struct's identity on disk includes its module, so item definitions authored before the move come back with the World Model fragment empty, and nothing says so. Add this to your project's Config/DefaultEngine.ini — it cannot ship with the plugin, because redirects are read while the first packages load, before plugin config is merged:

[CoreRedirects]
+StructRedirects=(OldName="/Script/EDInventoryCore.EDWorldModelFragment",NewName="/Script/EDInventoryWorld.EDWorldModelFragment")
+ClassRedirects=(OldName="/Script/EDInventoryCore.EDWorldModelComponent",NewName="/Script/EDInventoryWorld.EDWorldModelComponent")

Then run the EDValidateInventory commandlet: it loads every definition and reports empty fragments, so 0 error(s) is proof the migration took.

Dropping keeps the item, not just its kind​

A world item carries the entry, not a definition and a count. That distinction is the difference between putting something down and destroying it: durability, rolled stats, tag-state and the contents of any container the item carries all live on the entry, so a definition-and-count drop hands back a repaired sword, a re-rolled stat and an empty backpack.

Coming back in, use Try Add Existing Item rather than Try Add Item:

// Wrong: builds a NEW item of that kind, discarding everything the old one had.
Container->TryAddItem(WorldItem->ItemDefinition, WorldItem->StackCount);

// Right: puts THAT item back, resolving where it belongs in the layout.
Container->TryAddExistingItem(WorldItem->ToItemEntry(Subsystem->AllocateHandle()), Instigator);

TryAddExistingItem resolves the placement before adding, which matters beyond tidiness: an item that lands with no slot address is not in the slot that opens its container, so a picked-up backpack would arrive closed and look empty.

The handle is re-minted on the way in, because the one it had belonged to whoever dropped it.

The World Model fragment​

FEDWorldModelFragment says how an item appears outside an inventory:

FieldMeaning
Model TypeStatic mesh, skeletal mesh, or a spawned actor class
Static Mesh / Skeletal Mesh / Actor ClassWhat to show
Override MaterialAn instance override, so one mesh serves several items
Offset / Rotation / ScaleThe resting transform
Simulate PhysicsWhether the dropped item falls

An item with no World Model fragment has no ground representation. That is fine for something that can never be dropped.

A dropped item that simulates must be able to move its actor

Only the authority simulates, so the item settles once on the server and its resting place replicates. What replicates is the actor's transform, and a simulating component that is not the actor's root moves itself and not the actor — so the item would come to rest on the server while every client went on drawing it where it spawned.

UEDWorldModelComponent therefore makes a simulating model the owner's root component (bMakeSimulatedModelRoot, on by default), bringing the previous root along at its world transform so an interaction sphere follows the item as it rolls; ClearModel puts it back. Give the actor bReplicates and SetReplicateMovement(true) — the component says so in the log if you forget.

Building a pickup​

The minimum: an actor holding a definition and a count, that gives them to whoever asks.

bool AMyPickup::GiveTo(AActor* Collector)
{
if (!HasAuthority())
{
return false;
}

UEDInventoryManagerComponent* Inventory = UEDInventoryManagerComponent::Find(Collector);
if (!Inventory || !Inventory->TryAddItem(ItemDefinition, StackCount))
{
return false; // no room; leave the pickup where it is
}

Destroy();
return true;
}

TryAddItem is all-or-nothing, so a failure means nothing was taken and the pickup is still valid. Never destroy the actor before the add succeeds.

Dropping​

Dropping is the same in reverse: take the entry out, then spawn something carrying its definition and count.

void AMyCharacter::DropItem(FEDItemHandle Handle)
{
if (!HasAuthority()) { return; }

const FEDItemEntry* Entry = nullptr;
UEDContainer* Holder = Inventory->FindItem(Handle, Entry);
if (!Holder || !Entry) { return; }

// Read what you need BEFORE mutating: a failed resolve must never destroy the item.
const int32 Count = Entry->StackCount;
UEDItemDefinition* Definition = UEDItemStatics::ResolveDefinition(Entry->DefinitionId);
if (!Definition) { return; }

if (Holder->TryRemoveItem(Handle))
{
SpawnPickup(Definition, Count);
}
}

The order matters. Resolve first, remove second, spawn third — so a definition that fails to resolve costs you nothing rather than an item.

Dropping needs the Asset Manager

Rebuilding a world item from an entry means turning a FPrimaryAssetId back into a definition. If your item folders are not registered, pickup works and dropping produces a warning and nothing else. See Installation.

Chests, corpses, and other world containers​

An actor with a UEDContainerComponent is a container in the world. Point it at a container definition and author that asset's layout and rules in the details panel; that is the whole setup.

Two things make it behave properly for a player standing at it:

Open a session so the screen closes when they walk away:

PlayerInventory->OpenSession(ChestActor->GetContainer());  // SERVER ONLY — see note below

Let requests route. A chest is server-owned, so a client's request on it would be dropped by the engine. UEDContainer::RequestTransaction finds a component the client does own and sends it through that instead — automatic, but worth knowing when you are reading a call stack.

Loot in a chest​

The Loot module rolls a table into a container:

const TArray<FEDLootRollResult> Roll = UEDLootStatics::RollLootTable(this, LootTable, Budget);
UEDLootStatics::GiveLoot(ChestActor->GetContainer(), Roll);

Rolls come from the world subsystem's seeded stream, so a fixed seed reproduces a chest exactly — which is what makes loot testable. See Trading and loot.

Where next​