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
| Plugin | What it provides |
|---|---|
CrimsonInventory | ACrimsonPickupActorBase, UCrimsonInventoryRoutingComponent::TakePickupSlot/All, FCrimsonInventoryPickup |
CrimsonCommon | ICrimsonInteractableTarget, FCrimsonInteractionOption, UCrimsonPrimaryGameLayout |
CrimsonUI | UCrimsonActivatableWidget 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().
UCLASS(Blueprintable, ClassGroup = "Crimson|Pickup")class CRIMSONCORE_API ACrimsonCorePickupActor : public ACrimsonPickupActorBase, public ICrimsonInteractableTarget{// BP-tunable visualsUPROPERTY(VisibleAnywhere)TObjectPtr<USphereComponent> InteractionSphere;// BP-tunable bag UIUPROPERTY(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 slotsUPROPERTY(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
| Condition | Option text | On success |
|---|---|---|
| Single item / template / currency | Pick Up <Name> x N | Routing->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.
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).
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
| Class | Purpose |
|---|---|
UCrimsonCorePickupBagScreenViewModel | Owns the Entries list; rebuilds from ICrimsonPickupable::GetCrimsonPickupInventory() on every contents change. RequestTakeAll() mirror. |
UCrimsonCorePickupBagEntryViewModel | One 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
| Step | Action |
|---|---|
| 1 | Subclass ACrimsonCorePickupActor as e.g. BP_LootBag. |
| 2 | Set BagMeshOverride (inherited from the base) to a sack / chest mesh — used when the pickup is multi-item. |
| 3 | Subclass 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. |
| 4 | Set BagScreenClass on BP_LootBag to WBP_LootBagScreen. |
| 5 | Optional: set BagScreenLayerTag if your project uses a layer other than UI.Layer.Menu. |
End-to-end flow (multi-item bag)
[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
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
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.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.Plugins/CrimsonCore/Source/CrimsonCore/Public/Pickup/CrimsonCorePickupActor.h (+ matching .cpp under Private/Pickup/). Bag screen + VMs in Public/UserInterface/Pickup/ and Private/UserInterface/Pickup/.