Inventory Component¶
What You'll Learn¶
- Setting up and configuring inventory components
- Understanding network replication and authority patterns
- Managing item collections with automatic merging and validation
- Implementing search and filtering systems
- Using the event-driven notification system
- Handling server-client synchronization
- Integrating with save systems
- Performance optimization techniques
Quick Start¶
// Add component to actor
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Inventory")
class TObjectPtr<UMounteaInventoryComponent> InventoryComponent;
// Add item to inventory
bool Success = InventoryComponent->Execute_AddItem(InventoryComponent, Item);
// Find specific items
FInventoryItem Found = InventoryComponent->Execute_FindItem(InventoryComponent, SearchParams);
// Listen for changes
InventoryComponent->OnItemAdded.AddDynamic(this, &AMyActor::OnItemAdded);
Result
Fully networked inventory system with automatic replication, validation, and event notifications for seamless multiplayer inventory management.
Core Concepts¶
What Is the Inventory Component?¶
The Inventory Component is the central manager for collections of item instances. Think of it as a smart container that not only stores items but also handles validation, networking, merging, searching, and notifications.
Real-World Analogy
Like a warehouse management system - it doesn't just store boxes (items), but tracks what's inside each box, where everything is located, who has access, and automatically updates all connected systems when anything changes.
Component Architecture¶
The component extends UActorComponent and implements IMounteaAdvancedInventoryInterface:
class UMounteaInventoryComponent : public UActorComponent, public IMounteaAdvancedInventoryInterface
{
// Core Properties
FInventoryItemArray InventoryItems; // Replicated item storage
EInventoryType InventoryType; // Player, Chest, Vendor, etc.
EInventoryFlags InventoryTypeFlag; // Private, Public, ReadOnly
TObjectPtr<UUserWidget> InventoryWidget; // Associated UI widget
// Event Delegates
FOnItemAdded OnItemAdded;
FOnItemRemoved OnItemRemoved;
FOnItemQuantityChanged OnItemQuantityChanged;
FOnItemDurabilityChanged OnItemDurabilityChanged;
FOnNotificationProcessed OnNotificationProcessed;
};
Key Features
- Network Replication: Automatic client-server synchronization
- Smart Merging: Automatically combines stackable items
- Validation: Prevents invalid operations before they happen
- Event System: Reactive notifications for UI and gameplay
- Save Integration: Seamless persistence across sessions
- Search Engine: Powerful filtering and finding capabilities
Component Setup¶
Basic Configuration¶
// In your actor's constructor
AMyActor::AMyActor()
{
// Create inventory component
InventoryComponent = CreateDefaultSubobject<UMounteaInventoryComponent>(TEXT("InventoryComponent"));
// Configure replication is done automatically
// InventoryComponent->SetIsReplicatedByDefault(true);
}
// In BeginPlay or when needed
void AMyActor::BeginPlay()
{
Super::BeginPlay();
// Bind to inventory events if needed
if (InventoryComponent)
{
InventoryComponent->OnItemAdded.AddDynamic(this, &AMyActor::OnItemAdded);
InventoryComponent->OnItemRemoved.AddDynamic(this, &AMyActor::OnItemRemoved);
InventoryComponent->OnItemQuantityChanged.AddDynamic(this, &AMyActor::OnItemQuantityChanged);
}
}
Network Replication Setup¶
The component handles replication automatically but understanding the pattern helps with debugging:
// Replication configuration
void UMounteaInventoryComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// Only replicate to owner and initially
DOREPLIFETIME_CONDITION(UMounteaInventoryComponent, InventoryItems, COND_InitialOrOwner);
}
// Authority checking
bool UMounteaInventoryComponent::IsAuthority() const
{
const AActor* Owner = GetOwner();
if (!Owner || !Owner->GetWorld())
return false;
// Standalone games are always authoritative
if (const ENetMode NetMode = Owner->GetWorld()->GetNetMode(); NetMode == NM_Standalone)
return true;
// Check server authority
return Owner->HasAuthority();
}
Network Authority
Only the server can make authoritative changes to inventory. Clients send requests via Server RPCs, and the server validates and executes them.
Item Management¶
Adding Items¶
The component provides intelligent item addition with automatic merging:
bool UMounteaInventoryComponent::AddItem_Implementation(const FInventoryItem& Item)
{
// Validation first
if (!Execute_CanAddItem(this, Item))
{
// Send failure notification
Execute_ProcessInventoryNotification(this,
UMounteaInventoryStatics::CreateNotificationData(
MounteaInventoryNotificationBaseTypes::ItemNotUpdated,
this, Item.GetGuid(), 0
)
);
return false;
}
// Client requests server action
if (!IsAuthority())
{
AddItem_Server(Item); // Server RPC
return true;
}
// Server logic: Try to find existing item to merge with
auto existingItem = Execute_FindItem(this, FInventoryItemSearchParams(Item.GetGuid()));
if (!existingItem.IsItemValid())
existingItem = Execute_FindItem(this, FInventoryItemSearchParams(Item.GetTemplate()));
if (existingItem.IsItemValid())
{
// Check if items can be merged
if (CanMergeItems(existingItem, Item))
{
// Calculate how much can be added
const int32 AvailableSpace = Item.GetTemplate()->MaxQuantity - existingItem.GetQuantity();
const int32 AmountToAdd = FMath::Min(Item.GetQuantity(), AvailableSpace);
// Transfer from source inventory if needed
if (Item.IsItemInInventory())
{
Execute_DecreaseItemQuantity(Item.GetOwningInventory().GetObject(),
Item.GetGuid(), AmountToAdd);
}
// Add to this inventory
Execute_IncreaseItemQuantity(this, existingItem.GetGuid(), AmountToAdd);
return true;
}
else
{
// Cannot merge (unique items, different types, etc.)
return false;
}
}
else
{
// Create new item entry
FInventoryItem newItem = Item;
// Transfer from source if needed
if (Item.IsItemInInventory())
{
Execute_DecreaseItemQuantity(Item.GetOwningInventory().GetObject(),
Item.GetGuid(), Item.GetQuantity());
}
// Add to collection
InventoryItems.Items.Add(newItem);
InventoryItems.Items.Last().SetOwningInventory(this);
InventoryItems.MarkArrayDirty(); // Trigger replication
// Notify observers
OnItemAdded.Broadcast(newItem);
PostItemAdded_Client(newItem);
return true;
}
}
Smart Merging Logic
The system automatically combines compatible items while respecting unique flags, stack limits, and template differences. Non-mergeable items create new entries.
Unique items are automatically discarded as unique items can exist only once in the Inventory.
Removing Items¶
Item removal handles cleanup and notifications:
bool UMounteaInventoryComponent::RemoveItem_Implementation(const FGuid& ItemGuid)
{
if (!IsActive()) return false;
// Find item to remove
const int32 ItemIndex = Execute_FindItemIndex(this, FInventoryItemSearchParams(ItemGuid));
if (ItemIndex == INDEX_NONE)
return false;
const FInventoryItem RemovedItem = InventoryItems.Items[ItemIndex];
// Notify immediately (before removal for safety)
OnItemRemoved.Broadcast(RemovedItem);
// Client requests server action
if (!IsAuthority())
{
RemoveItem_Server(ItemGuid);
return true;
}
// Server removes and notifies clients
PostItemRemoved_Client(RemovedItem);
InventoryItems.Items.RemoveAt(ItemIndex);
InventoryItems.MarkArrayDirty();
return true;
}
Quantity Modification¶
Safe quantity changes with bounds checking:
bool UMounteaInventoryComponent::IncreaseItemQuantity_Implementation(const FGuid& ItemGuid, const int32 Amount)
{
if (!IsActive() || !IsAuthority())
return false;
const int32 index = Execute_FindItemIndex(this, FInventoryItemSearchParams(ItemGuid));
if (index != INDEX_NONE && InventoryItems.Items.IsValidIndex(index))
{
auto& inventoryItem = InventoryItems.Items[index];
const int32 OldQuantity = inventoryItem.GetQuantity();
// Use item's built-in validation
if (inventoryItem.SetQuantity(OldQuantity + Amount))
{
// Mark for replication
InventoryItems.MarkItemDirty(inventoryItem);
// Notify observers
OnItemQuantityChanged.Broadcast(inventoryItem, OldQuantity, inventoryItem.GetQuantity());
PostItemQuantityChanged(inventoryItem, OldQuantity, inventoryItem.GetQuantity());
return true;
}
}
return false;
}
bool UMounteaInventoryComponent::DecreaseItemQuantity_Implementation(const FGuid& ItemGuid, const int32 Amount)
{
if (!IsActive() || !IsAuthority())
return false;
const int32 index = Execute_FindItemIndex(this, FInventoryItemSearchParams(ItemGuid));
if (index != INDEX_NONE && InventoryItems.Items.IsValidIndex(index))
{
auto& inventoryItem = InventoryItems.Items[index];
const int32 OldQuantity = inventoryItem.GetQuantity();
const int32 NewQuantity = OldQuantity - Amount;
// Auto-remove if quantity reaches zero
if (NewQuantity <= 0)
return Execute_RemoveItem(this, ItemGuid);
if (inventoryItem.SetQuantity(NewQuantity))
{
InventoryItems.MarkItemDirty(inventoryItem);
OnItemQuantityChanged.Broadcast(inventoryItem, OldQuantity, NewQuantity);
PostItemQuantityChanged(inventoryItem, OldQuantity, NewQuantity);
return true;
}
}
return false;
}
Automatic Cleanup
When item quantity reaches zero through DecreaseItemQuantity, the item is automatically removed from the inventory to prevent invalid entries.
Search and Filtering¶
Search Parameters¶
The component provides a powerful search system using FInventoryItemSearchParams:
// Search by unique ID
FInventoryItemSearchParams SearchByGuid(const FGuid& ItemGuid)
{
FInventoryItemSearchParams Params;
Params.bSearchByGuid = true;
Params.ItemGuid = ItemGuid;
return Params;
}
// Search by template
FInventoryItemSearchParams SearchByTemplate(UMounteaInventoryItemTemplate* Template)
{
FInventoryItemSearchParams Params;
Params.bSearchByTemplate = true;
Params.Template = Template;
return Params;
}
// Search by category
FInventoryItemSearchParams SearchByCategory(const FString& Category)
{
FInventoryItemSearchParams Params;
Params.bSearchByCategory = true;
Params.CategoryId = Category;
return Params;
}
// Search by gameplay tags
FInventoryItemSearchParams SearchByTags(const FGameplayTagContainer& Tags, bool bRequireAll = false)
{
FInventoryItemSearchParams Params;
Params.bSearchByTags = true;
Params.Tags = Tags;
Params.bRequireAllTags = bRequireAll;
return Params;
}
Search Implementation¶
FInventoryItem UMounteaInventoryComponent::FindItem_Implementation(const FInventoryItemSearchParams& SearchParams) const
{
const auto foundItem = InventoryItems.Items.FindByPredicate([&SearchParams](const FInventoryItem& Item)
{
// GUID search (exact match)
if (SearchParams.bSearchByGuid && Item.GetGuid() != SearchParams.ItemGuid)
return false;
// Template search (same item type)
if (SearchParams.bSearchByTemplate && Item.GetTemplate() != SearchParams.Template)
return false;
// Tag search (custom data matching)
if (SearchParams.bSearchByTags)
{
if (SearchParams.bRequireAllTags)
return Item.GetCustomData().HasAll(SearchParams.Tags);
else
return Item.GetCustomData().HasAny(SearchParams.Tags);
}
// Category search (item classification)
if (SearchParams.bSearchByCategory && Item.GetTemplate()->ItemCategory != SearchParams.CategoryId)
return false;
// Rarity search (item tier)
if (SearchParams.bSearchByRarity && Item.GetTemplate()->ItemRarity != SearchParams.RarityId)
return false;
return true;
});
return foundItem ? *foundItem : FInventoryItem();
}
Batch Search Operations¶
// Find multiple items matching criteria
TArray<FInventoryItem> UMounteaInventoryComponent::FindItems_Implementation(const FInventoryItemSearchParams& SearchParams) const
{
TArray<FInventoryItem> returnResult;
// If no search criteria specified, return all items
if (!SearchParams.bSearchByGuid && !SearchParams.bSearchByTemplate &&
!SearchParams.bSearchByTags && !SearchParams.bSearchByCategory &&
!SearchParams.bSearchByRarity)
{
return InventoryItems.Items;
}
// Filter items using search criteria
Algo::CopyIf(InventoryItems.Items, returnResult, [&SearchParams](const FInventoryItem& Item) -> bool
{
if (!Item.IsItemValid())
return false;
// Any matching criteria includes the item
if (SearchParams.bSearchByGuid && Item.GetGuid() == SearchParams.ItemGuid)
return true;
if (SearchParams.bSearchByTemplate && Item.GetTemplate() == SearchParams.Template)
return true;
if (SearchParams.bSearchByCategory && Item.GetTemplate()->ItemCategory == SearchParams.CategoryId)
return true;
if (SearchParams.bSearchByRarity && Item.GetTemplate()->ItemRarity == SearchParams.RarityId)
return true;
if (SearchParams.bSearchByTags)
{
return SearchParams.bRequireAllTags
? Item.GetCustomData().HasAll(SearchParams.Tags)
: Item.GetCustomData().HasAny(SearchParams.Tags);
}
return false;
});
return returnResult;
}
// Practical search examples
TArray<FInventoryItem> FindAllWeapons()
{
return Execute_FindItems(this, SearchByCategory(TEXT("Weapon")));
}
TArray<FInventoryItem> FindBrokenItems()
{
FGameplayTagContainer BrokenTag;
BrokenTag.AddTag(FGameplayTag::RequestGameplayTag("Condition.Broken"));
return Execute_FindItems(this, SearchByTags(BrokenTag));
}
bool HasHealthPotions()
{
auto FoundPotion = Execute_FindItem(this, SearchByCategory(TEXT("Consumable")));
return FoundPotion.IsItemValid();
}
Search Use Cases
- Equipment Systems: Find all equippable weapons
- Crafting: Locate required materials
- Trading: Filter items by rarity or category
- Repair: Find damaged items needing attention
- Quests: Check for specific quest items
Event System¶
Event Delegates¶
The component provides comprehensive event notifications:
// Core item events
UPROPERTY(BlueprintAssignable, Category="Events")
FOnItemAdded OnItemAdded; // Item added to inventory
UPROPERTY(BlueprintAssignable, Category="Events")
FOnItemRemoved OnItemRemoved; // Item removed from inventory
UPROPERTY(BlueprintAssignable, Category="Events")
FOnItemQuantityChanged OnItemQuantityChanged; // Stack size changed
UPROPERTY(BlueprintAssignable, Category="Events")
FOnItemDurabilityChanged OnItemDurabilityChanged; // Item condition changed
UPROPERTY(BlueprintAssignable, Category="Events")
FOnNotificationProcessed OnNotificationProcessed; // General notifications
Event Binding Examples¶
// C++ binding
void APlayerCharacter::BeginPlay()
{
Super::BeginPlay();
if (InventoryComponent)
{
// Bind to specific events
InventoryComponent->OnItemAdded.AddDynamic(this, &APlayerCharacter::OnItemAdded);
InventoryComponent->OnItemRemoved.AddDynamic(this, &APlayerCharacter::OnItemRemoved);
InventoryComponent->OnItemQuantityChanged.AddDynamic(this, &APlayerCharacter::OnItemQuantityChanged);
// General notification handler
InventoryComponent->OnNotificationProcessed.AddDynamic(this, &APlayerCharacter::OnInventoryNotification);
}
}
// Event handlers
UFUNCTION()
void APlayerCharacter::OnItemAdded(const FInventoryItem& Item)
{
// Update UI, play sound, show notification
if (InventoryWidget)
InventoryWidget->RefreshInventoryDisplay();
PlayItemAddedSound(Item.GetTemplate()->ItemCategory);
ShowNotification(FText::Format(
NSLOCTEXT("Inventory", "ItemAdded", "Added {0} x{1}"),
Item.GetItemName(),
Item.GetQuantity()
));
}
UFUNCTION()
void APlayerCharacter::OnItemRemoved(const FInventoryItem& Item)
{
// Check if removed item was equipped
if (EquipmentComponent && EquipmentComponent->IsItemEquipped(Item.GetGuid()))
{
EquipmentComponent->UnequipItem(Item.GetGuid());
}
// Update displays
if (InventoryWidget)
InventoryWidget->RefreshInventoryDisplay();
}
UFUNCTION()
void APlayerCharacter::OnItemQuantityChanged(const FInventoryItem& Item, int32 OldQuantity, int32 NewQuantity)
{
// Handle stack changes
if (NewQuantity > OldQuantity)
{
// Items added to stack
OnItemAdded(Item);
}
else
{
// Items removed from stack
const int32 RemovedAmount = OldQuantity - NewQuantity;
ShowNotification(FText::Format(
NSLOCTEXT("Inventory", "ItemUsed", "Used {0} x{1}"),
Item.GetItemName(),
RemovedAmount
));
}
}
Notification System¶
The component includes a sophisticated notification system:
void UMounteaInventoryComponent::ProcessInventoryNotification_Implementation(const FInventoryNotificationData& Notification)
{
// Check if notifications are enabled
if (!Notification.NotificationConfig.bIsEnabled)
return;
// Handle based on network context
if (!IsAuthority() || (IsAuthority() && UMounteaInventorySystemStatics::CanExecuteCosmeticEvents(GetWorld())))
{
// Direct broadcast (client or server with cosmetics enabled)
OnNotificationProcessed.Broadcast(Notification);
}
else if (IsAuthority() && !UMounteaInventorySystemStatics::CanExecuteCosmeticEvents(GetWorld()))
{
// Server without cosmetics - send to client
ProcessInventoryNotification_Client(Notification.ItemGuid, Notification.Type, Notification.DeltaAmount);
}
}
// Client notification handler with timing safety
void UMounteaInventoryComponent::ProcessInventoryNotification_Client_Implementation(const FGuid& TargetItem, const FString& NotifType, const int32 QuantityDelta)
{
// Wait for next tick to ensure replication has completed
GetWorld()->GetTimerManager().SetTimerForNextTick([this, NotifType, TargetItem, QuantityDelta]()
{
auto targetItem = Execute_FindItem(this, FInventoryItemSearchParams(TargetItem));
OnNotificationProcessed.Broadcast(UMounteaInventoryStatics::CreateNotificationData(
NotifType,
this,
targetItem.GetGuid(),
QuantityDelta
));
});
}
Network Timing
Client notifications are delayed by one tick to ensure replicated data has arrived before processing events. This prevents race conditions.
Network Patterns¶
Server RPCs¶
Client actions are validated and executed on the server:
// Client requests item addition
UFUNCTION(Server, Reliable)
void AddItem_Server(const FInventoryItem& Item);
// Client requests item removal
UFUNCTION(Server, Reliable)
void RemoveItem_Server(const FGuid& ItemGuid);
// Client requests quantity change
UFUNCTION(Server, Reliable)
void ChangeItemQuantity_Server(const FGuid& ItemGuid, const int32 DeltaAmount);
// Implementation with validation
void UMounteaInventoryComponent::AddItem_Server_Implementation(const FInventoryItem& Item)
{
// Server validates and executes
Execute_AddItem(this, Item);
}
void UMounteaInventoryComponent::ChangeItemQuantity_Server_Implementation(const FGuid& ItemGuid, const int32 DeltaAmount)
{
// Route to appropriate function based on delta sign
if (DeltaAmount < 0)
Execute_DecreaseItemQuantity(this, ItemGuid, FMath::Abs(DeltaAmount));
else if (DeltaAmount > 0)
Execute_IncreaseItemQuantity(this, ItemGuid, FMath::Abs(DeltaAmount));
}
Client RPCs¶
Server notifies clients of changes and events:
// Unreliable notifications (can be lost without breaking gameplay)
UFUNCTION(Client, Unreliable)
void ProcessInventoryNotification_Client(const FGuid& TargetItem, const FString& NotifType, const int32 QuantityDelta);
UFUNCTION(Client, Unreliable)
void PostItemAdded_Client(const FInventoryItem& Item);
UFUNCTION(Client, Unreliable)
void PostItemRemoved_Client(const FInventoryItem& Item);
// Event broadcasting with authority checks
void UMounteaInventoryComponent::PostItemAdded_Client_Implementation(const FInventoryItem& Item)
{
// Only broadcast if server allows cosmetic events
if (IsAuthority() && UMounteaInventorySystemStatics::CanExecuteCosmeticEvents(GetWorld()))
{
Execute_ProcessInventoryNotification(this, UMounteaInventoryStatics::CreateNotificationData(
MounteaInventoryNotificationBaseTypes::ItemAdded,
this,
Item.GetGuid(),
Item.Quantity
));
}
// Always broadcast the core event
OnItemAdded.Broadcast(Item);
}
RPC Reliability
- Server RPCs: Reliable to ensure critical operations reach the server
- Client RPCs: Unreliable for notifications since data replication provides the authoritative state
Advanced Features¶
Validation System¶
bool UMounteaInventoryComponent::CanAddItem_Implementation(const FInventoryItem& Item) const
{
if (!IsActive())
return false;
// Basic item validation
if (!Item.IsItemValid() || !Item.GetTemplate())
return false;
// Check for existing item to merge with
auto existingItem = Execute_FindItem(this, FInventoryItemSearchParams(Item.GetGuid()));
if (!existingItem.IsItemValid())
existingItem = Execute_FindItem(this, FInventoryItemSearchParams(Item.GetTemplate()));
if (existingItem.IsItemValid())
{
// Can merge if not at max quantity
return existingItem.GetQuantity() < existingItem.GetTemplate()->MaxQuantity;
}
// New item can be added (implement capacity checks here if needed)
return true;
}
// Template-based validation
bool UMounteaInventoryComponent::CanAddItemFromTemplate_Implementation(UMounteaInventoryItemTemplate* const Template, const int32 Quantity) const
{
if (!IsValid(Template))
return false;
// Create temporary item for validation
return Execute_CanAddItem(this, FInventoryItem(Template, Quantity, Template->BaseDurability, nullptr));
}
Durability Management¶
bool UMounteaInventoryComponent::ModifyItemDurability_Implementation(const FGuid& ItemGuid, const float DeltaDurability)
{
if (!IsActive() || !IsAuthority())
return false;
const int32 index = Execute_FindItemIndex(this, FInventoryItemSearchParams(ItemGuid));
if (index != INDEX_NONE && InventoryItems.Items.IsValidIndex(index))
{
auto& inventoryItem = InventoryItems.Items[index];
const float OldDurability = inventoryItem.GetDurability();
// Clamp to valid range (0 to max durability)
const float NewDurability = FMath::Clamp(
OldDurability + DeltaDurability,
0.f,
inventoryItem.GetTemplate()->MaxDurability
);
if (inventoryItem.SetDurability(NewDurability))
{
InventoryItems.MarkItemDirty(inventoryItem);
OnItemDurabilityChanged.Broadcast(inventoryItem, OldDurability, NewDurability);
PostItemDurabilityChanged(inventoryItem, OldDurability, NewDurability);
return true;
}
}
return false;
}
Inventory Clearing¶
void UMounteaInventoryComponent::ClearInventory_Implementation()
{
if (!IsAuthority())
return;
// Notify for each item being removed
for (const auto& Item : InventoryItems.Items)
{
OnItemRemoved.Broadcast(Item);
}
// Clear the collection
InventoryItems.Items.Empty();
InventoryItems.MarkArrayDirty();
}
Save System Integration¶
The component supports automatic save/load through UE's save system:
// Properties marked with SaveGame automatically persist
UPROPERTY(SaveGame, EditAnywhere, BlueprintReadOnly, Category="Inventory")
EInventoryType InventoryType;
UPROPERTY(SaveGame, EditAnywhere, BlueprintReadOnly, Category="Inventory")
EInventoryFlags InventoryTypeFlag;
UPROPERTY(SaveGame, VisibleAnywhere, BlueprintReadOnly, Category="Inventory")
FInventoryItemArray InventoryItems;
UPROPERTY(SaveGame, EditAnywhere, BlueprintReadOnly, Category="Inventory")
TObjectPtr<UUserWidget> InventoryWidget;
Save Integration
All inventory data automatically saves with your game save system. No additional code required for persistence.
Performance Optimization¶
Replication Efficiency¶
// Use COND_InitialOrOwner for player inventories
DOREPLIFETIME_CONDITION(UMounteaInventoryComponent, InventoryItems, COND_InitialOrOwner);
// Mark specific items dirty rather than entire array
InventoryItems.MarkItemDirty(inventoryItem);
// Batch operations to reduce RPC calls
void BatchAddItems(const TArray<FInventoryItem>& Items)
{
for (const auto& Item : Items)
{
Execute_AddItem(this, Item);
}
// Single replication update for all changes
}
Search Optimization¶
// Cache search results for repeated queries
TMap<FString, TArray<FInventoryItem>> SearchCache;
TArray<FInventoryItem> GetCachedCategoryItems(const FString& Category)
{
if (auto* CachedResult = SearchCache.Find(Category))
return *CachedResult;
auto Results = Execute_FindItems(this, SearchByCategory(Category));
SearchCache.Add(Category, Results);
return Results;
}
// Invalidate cache when inventory changes
void InvalidateSearchCache()
{
SearchCache.Empty();
}
Memory Management¶
// Avoid storing unnecessary references
void CleanupInvalidReferences()
{
InventoryItems.Items.RemoveAll([](const FInventoryItem& Item)
{
return !Item.IsItemValid();
});
}
// Use object pooling for frequently created search parameters
FInventoryItemSearchParams* GetPooledSearchParams()
{
static TArray<FInventoryItemSearchParams> SearchParamsPool;
// Pool management logic...
}
Common Patterns¶
Pattern 1: Inventory Transfer¶
Use Case
Moving items between player inventory and storage chests
bool TransferItemBetweenInventories(
UMounteaInventoryComponent* SourceInventory,
UMounteaInventoryComponent* TargetInventory,
const FGuid& ItemGuid,
int32 Quantity = -1)
{
// Find source item
auto SourceItem = SourceInventory->Execute_FindItem(SourceInventory, FInventoryItemSearchParams(ItemGuid));
if (!SourceItem.IsItemValid())
return false;
// Determine transfer amount
const int32 TransferAmount = (Quantity <= 0) ? SourceItem.GetQuantity() : FMath::Min(Quantity, SourceItem.GetQuantity());
// Check if target can accept the item
FInventoryItem TransferItem = SourceItem;
TransferItem.SetQuantity(TransferAmount);
if (!TargetInventory->Execute_CanAddItem(TargetInventory, TransferItem))
return false;
// Perform transfer
if (TargetInventory->Execute_AddItem(TargetInventory, TransferItem))
{
// Remove from source (AddItem handles this automatically for items with OwningInventory)
return true;
}
return false;
}
Pattern 2: Conditional Item Access¶
Use Case
Restricting inventory access based on game state
class UConditionalInventoryComponent : public UMounteaInventoryComponent
{
protected:
virtual bool CanAddItem_Implementation(const FInventoryItem& Item) const override
{
// Base validation first
if (!Super::CanAddItem_Implementation(Item))
return false;
// Custom conditions
if (bIsLocked)
return false;
if (RequiredLevel > 0 && GetOwnerLevel() < RequiredLevel)
return false;
if (RestrictedCategories.Contains(Item.GetTemplate()->ItemCategory))
return false;
return true;
}
private:
UPROPERTY(EditAnywhere, Category="Access Control")
bool bIsLocked = false;
UPROPERTY(EditAnywhere, Category="Access Control")
int32 RequiredLevel = 0;
UPROPERTY(EditAnywhere, Category="Access Control")
TArray<FString> RestrictedCategories;
};
Pattern 3: Smart Auto-Sorting¶
Use Case
Automatically organizing inventory by type and rarity
void AutoSortInventory()
{
auto AllItems = Execute_GetAllItems(this);
// Sort by category, then rarity, then name
AllItems.Sort([](const FInventoryItem& A, const FInventoryItem& B)
{
// Primary: Category
if (A.GetTemplate()->ItemCategory != B.GetTemplate()->ItemCategory)
return A.GetTemplate()->ItemCategory < B.GetTemplate()->ItemCategory;
// Secondary: Rarity (higher rarity first)
if (A.GetTemplate()->ItemRarity != B.GetTemplate()->ItemRarity)
return GetRarityValue(A.GetTemplate()->ItemRarity) > GetRarityValue(B.GetTemplate()->ItemRarity);
// Tertiary: Name
return A.GetTemplate()->DisplayName.ToString() < B.GetTemplate()->DisplayName.ToString();
});
// Clear and re-add in sorted order
InventoryItems.Items.Empty();
for (const auto& Item : AllItems)
{
InventoryItems.Items.Add(Item);
}
InventoryItems.MarkArrayDirty();
}
Troubleshooting¶
Common Issues¶
Items Not Replicating
Problem: Changes don't appear on clients
Solution: Ensure operations run on server authority, check COND_InitialOrOwner replication
Event Timing Issues
Problem: UI updates before item data arrives
Solution: Use timer delays in client RPCs, bind to OnRep functions
Performance Degradation
Problem: Frame drops during inventory operations
Solution: Batch operations, use Unreliable RPCs for notifications, cache search results
Save/Load Problems
Problem: Inventory doesn't persist between sessions
Solution: Verify SaveGame tags on properties, check save system integration
Debug Commands¶
// Console command for debugging
UFUNCTION(CallInEditor=true, Category="Debug")
void DebugPrintInventory()
{
LOG_WARNING(TEXT("Inventory Contents:"));
for (int32 i = 0; i < InventoryItems.Items.Num(); ++i)
{
const auto& Item = InventoryItems.Items[i];
LOG_WARNING(TEXT("[%d] %s"), i, *Item.ToString());
}
}
// Validate inventory integrity
UFUNCTION(CallInEditor=true, Category="Debug")
void ValidateInventoryIntegrity()
{
int32 ErrorCount = 0;
for (const auto& Item : InventoryItems.Items)
{
if (!Item.IsItemValid())
{
UE_LOG(LogTemp, Error, TEXT("Invalid item found: %s"), *Item.ToString());
ErrorCount++;
}
}
LOG_WARNING(TEXT("Validation complete. %d errors found."), ErrorCount);
}
Best Practices¶
Design Guidelines
- Authority Pattern: Always validate on server, execute on authority
- Event Driven: Use delegates for UI updates and game logic reactions
- Validation First: Check CanAddItem before attempting operations
- Batch Operations: Group related changes to reduce network traffic
- Cache Smartly: Store expensive search results but invalidate appropriately
Performance Tips
- Use Unreliable RPCs for cosmetic notifications
- Implement search result caching for repeated queries
- Mark individual items dirty rather than entire arrays
- Consider inventory size limits to prevent unbounded growth
Common Pitfalls
- Don't forget authority checks in custom implementations
- Avoid circular dependencies between inventory and equipment systems
- Remember to handle edge cases like zero quantities and invalid items
- Test thoroughly in networked environments
Next Steps¶
- Equipment System: Equip and manage worn items from inventory
- UI System: Create responsive inventory interfaces
- Trading System: Enable item exchange between players