Pickup Actor & Bag UI

CrimsonInventory ships an abstract ACrimsonPickupActorBase that handles mesh + payload + per-slot takes but stays UI-free. CrimsonCore provides the concrete drop-in subclass ACrimsonCorePickupActor (wires ICrimsonInteractableTarget from CrimsonCommon and a USphereComponent trigger) plus the UCrimsonCorePickupBagScreen activatable widget for multi-item inspect-and-take.

Plugin dependencies pulled in by this page

PluginWhat it provides
CrimsonInventoryACrimsonPickupActorBase, UCrimsonInventoryRoutingComponent::TakePickupSlot/All, FCrimsonInventoryPickup
CrimsonCommonICrimsonInteractableTarget, FCrimsonInteractionOption, UCrimsonPrimaryGameLayout
CrimsonUIUCrimsonActivatableWidget base, UCrimsonTileView, MVVM helpers

ACrimsonCorePickupActor

Concrete pickup actor: implements ICrimsonInteractableTarget, owns a USphereComponent (150 cm default) for interaction-range detection, and dispatches between two flows based on IsMultiItem().

cpp
UCLASS(Blueprintable, ClassGroup = "Crimson|Pickup")
class CRIMSONCORE_API ACrimsonCorePickupActor : public ACrimsonPickupActorBase, public ICrimsonInteractableTarget
{
// BP-tunable visuals
UPROPERTY(VisibleAnywhere)
TObjectPtr<USphereComponent> InteractionSphere;
// BP-tunable bag UI
UPROPERTY(EditDefaultsOnly, Category = "Crimson|Pickup")
TSoftClassPtr<UCommonActivatableWidget> BagScreenClass;
UPROPERTY(EditDefaultsOnly, Category = "Crimson|Pickup", meta = (Categories = "UI.Layer"))
FGameplayTag BagScreenLayerTag = FGameplayTag::RequestGameplayTag("UI.Layer.Menu");
// Localizable option text — {0} = item name, {1} = stack count / total slots
UPROPERTY(EditDefaultsOnly, Category = "Crimson|Pickup|Display")
FText SingleItemOptionText = LOCTEXT("PickUp", "Pick Up {0} x {1}");
UPROPERTY(EditDefaultsOnly, Category = "Crimson|Pickup|Display")
FText BagOptionText = LOCTEXT("OpenBag", "Open Bag ({0} items)");
virtual void GatherInteractionOptions(const FCrimsonInteractionQuery&,
FCrimsonInteractionOptionBuilder&) override;
virtual void NotifyInteractionSuccess_Implementation(AActor* InInstigator) override;
};

Interaction dispatch

ConditionOption textOn success
Single item / template / currencyPick Up <Name> x NRouting->TakePickupAll(this, Destination) — server takes everything, actor self-destroys
IsMultiItem() (>1 slot total)Open Bag (N items)Bag screen pushed onto BagScreenLayerTag on the local client; player chooses what to take

NotifyInteractionSuccess_Implementation resolves the instigator's UCrimsonInventoryManagerComponent (walks PC → PlayerState → Pawn → component) and UCrimsonInventoryRoutingComponent (PC component). Routing forwarders handle the Server RPC indirection so this method can run on client or server without ownership headaches.

Bag UI is client-local
Pushing the bag screen requires the instigator's PC to be locally controlled (we check IsLocalController()). The interaction is normally driven client-side by the player's input, which lines up. If your project runs the interaction ability server-side for remote players, the bag UI won't push on the server — wire a Client_PushBagScreen RPC on the routing component if you need that path.

UCrimsonCorePickupBagScreen

UCrimsonActivatableWidget subclass. The pickup actor pushes it via UCrimsonPrimaryGameLayout::PushWidgetToLayerStackAsync, then calls BindToPickup(SourceActor, Destination) via ProcessEvent on the constructed instance (so this module doesn't need to include the bag screen header — the call is by-name UFUNCTION lookup).

cpp
UCLASS(Abstract, Blueprintable)
class CRIMSONCORE_API UCrimsonCorePickupBagScreen : public UCrimsonActivatableWidget
{
// Fixed signature — must match ProcessEvent struct used by the pickup actor.
UFUNCTION(BlueprintCallable, Category = "Crimson|Pickup|Bag")
void BindToPickup(AActor* SourceActor, UCrimsonInventoryManagerComponent* Destination);
// BP: bind a UCrimsonTileView named EntryTileView; rows auto-populate from the VM.
UPROPERTY(Transient, BlueprintReadOnly, meta = (BindWidgetOptional))
TObjectPtr<UCrimsonTileView> EntryTileView;
UPROPERTY(Transient, BlueprintReadOnly)
TObjectPtr<UCrimsonCorePickupBagScreenViewModel> BagViewModel;
// BP hooks fired after VM ready + on every entries-changed broadcast.
UFUNCTION(BlueprintImplementableEvent) void OnViewModelReady();
UFUNCTION(BlueprintImplementableEvent) void OnEntriesVisualRefresh(
const TArray<UCrimsonCorePickupBagEntryViewModel*>& InEntries);
};

Lifecycle follows the standard Crimson activatable-widget pattern: bind VM in NativeOnInitialized (the first push) AND re-bind on NativeOnActivated (handles pooled widget reuse — re-resolves source, refreshes from current state). Subscribes to ACrimsonPickupActorBase::OnPickupContentsChanged; auto-deactivates when the bag empties or the source actor is destroyed.

ViewModels

ClassPurpose
UCrimsonCorePickupBagScreenViewModelOwns the Entries list; rebuilds from ICrimsonPickupable::GetCrimsonPickupInventory() on every contents change. RequestTakeAll() mirror.
UCrimsonCorePickupBagEntryViewModelOne row VM: DisplayName, Icon, StackCount, SlotType, SlotIndex. RequestTake(Quantity) routes through Routing->TakePickupSlot.

Entry VMs are recycled across rebuilds (size matches Instances+Templates+Currencies). Per-row populate is one of three: PopulateFromInstance / PopulateFromTemplate / PopulateFromCurrency — each pulls the icon and display name from Fragment_Visuals::ItemIcon and UCrimsonInventoryItemDefinition::DisplayName when an ItemDef is available.

BP author checklist

StepAction
1Subclass ACrimsonCorePickupActor as e.g. BP_LootBag.
2Set BagMeshOverride (inherited from the base) to a sack / chest mesh — used when the pickup is multi-item.
3Subclass UCrimsonCorePickupBagScreen as WBP_LootBagScreen. Lay out a UCrimsonTileView named EntryTileView and any close / take-all buttons. Wire row widgets to call RequestTake(0) for full-stack and any quantity for partial.
4Set BagScreenClass on BP_LootBag to WBP_LootBagScreen.
5Optional: set BagScreenLayerTag if your project uses a layer other than UI.Layer.Menu.

End-to-end flow (multi-item bag)

text
[Server] Inventory::DropEntriesAsBag(ids, loc, BP_LootBag)
-> spawns BP_LootBag, SetCrimsonPickupInventory(payload)
-> ApplyResolvedMesh() (BagMeshOverride applies, IsMultiItem true)
-> OnPickupContentsChanged broadcast
[Server -> Client] PickupInventory replicates -> OnRep_PickupInventory
-> ApplyResolvedMesh() (client mesh now correct)
-> OnPickupContentsChanged broadcast
[Client] Player interacts (interaction ability finishes)
-> ICrimsonInteractableTarget::NotifyInteractionSuccess(this, Pawn)
-> bag screen pushed onto UI.Layer.Menu
-> BindToPickup(this, PlayerInventory) via ProcessEvent
-> screen subscribes to OnPickupContentsChanged
[Client -> Server] EntryVM::RequestTake(qty)
-> Routing::TakePickupSlot -> Server_TakePickupSlot (RPC)
-> Pickup::TakeSlot (server-only)
-> CalculateAddableQuantity + AddItemInstance
-> decrement slot, mark dirty
-> OnPickupContentsChanged broadcast
-> Destroy() if empty
[Server -> Client] PickupInventory replicates -> bag screen Refresh
-> bag screen DeactivateWidget() if empty
Single-item flow
For single-item pickups, NotifyInteractionSuccess_Implementation skips the bag screen entirely and calls Routing->TakePickupAll(this, Destination) directly. The actor self-destroys when the take resolves. BagScreenClass is irrelevant in this path.

Notes & pitfalls

BindToPickup is a fixed UFUNCTION signature
The pickup actor invokes the bag screen via ProcessEvent on a (AActor*, UCrimsonInventoryManagerComponent*) UFUNCTION named BindToPickup. Don't rename it or change the parameter list in your subclass — the lookup is by name, with the struct layout matching the parameter list exactly.
BlueprintNativeEvent override for interface methods
NotifyInteractionSuccess is a BlueprintNativeEvent declared on the ICrimsonInteractableTarget interface, so the C++ override uses the _Implementation suffix in the subclass header — this is the interface override pattern and differs from the regular Crimson rule about non-interface BlueprintNativeEvents.
Source location
Plugins/CrimsonCore/Source/CrimsonCore/Public/Pickup/CrimsonCorePickupActor.h (+ matching .cpp under Private/Pickup/). Bag screen + VMs in Public/UserInterface/Pickup/ and Private/UserInterface/Pickup/.