Item Actions¶
What You'll Learn¶
- Understanding the item action system powered by Gameplay Ability System
- Creating custom actions for inventory items
- Implementing visibility and permission logic
- Using Blueprint and C++ action patterns
- Integrating with GAS for cooldowns, costs, and effects
- Building common action types (consume, equip, drop, split)
Quick Start¶
// Execute an action on an item
bool Success = ItemAction->ExecuteInventoryAction(TargetItem);
// Check if action is available
bool CanUse = ItemAction->IsActionVisible(Item) && ItemAction->IsAllowed(Item);
// Get user-friendly error message
FText WhyNot = ItemAction->GetDisallowedReason(Item);
Result
Context-sensitive actions that appear dynamically based on item properties, with full GAS integration for complex gameplay mechanics.
Core Concepts¶
What Are Item Actions?¶
Item actions define what players can do with inventory items - use a potion, equip a sword, drop an item, or split a stack. Think of them as the verbs in your inventory system: the actions give meaning to the nouns (items).
Real-World Analogy: Like context menus on your computer - right-click a file and see relevant actions (Open, Copy, Delete, Rename). The available options change based on what type of file you selected.
Action System Architecture¶
The system builds on Unreal's Gameplay Ability System (GAS), inheriting powerful features while adding inventory-specific functionality:
// Base action class extends UGameplayAbility
class UMounteaInventoryItemAction : public UGameplayAbility
{
// Inventory-specific properties
FText ActionDisplayName; // "Consume Potion"
FText ActionDescription; // "Restores 50 health points"
TSoftObjectPtr<UTexture2D> ActionIcon; // Potion drinking icon
int32 ActionPriority = 0; // Sort order in UI
bool bRequiresConfirmation; // Show "Are you sure?" dialog
// Core functionality
virtual bool IsActionVisible(const FInventoryItem& TargetItem) const;
virtual bool IsAllowed(const FInventoryItem& TargetItem) const;
virtual bool ProcessAction(UObject* ActionInitiator, const FInventoryItem& TargetItem);
};
Why GAS Integration?
- Cooldowns: Prevent action spam (can't drink potions too quickly)
- Costs: Actions can consume resources (mana, stamina)
- Effects: Apply gameplay effects (healing, buffs, debuffs)
- Targeting: Support complex targeting systems
- Networking: Automatic client-server synchronization
- Modular: Easy to extend with custom logic
Action Lifecycle¶
Every action follows a predictable lifecycle when executed:
- Visibility Check: Should this action appear in the UI?
- Permission Check: Is the action currently allowed?
- Confirmation: Does the action need user confirmation?
- Execution: Run the action logic
- Effects: Apply any gameplay effects
- Cleanup: Handle post-action state
// Complete action execution flow
bool ExecuteItemAction(UMounteaInventoryItemAction* Action, const FInventoryItem& Item)
{
// 1. Check visibility
if (!Action->IsActionVisible(Item))
return false; // Action not applicable to this item
// 2. Check permissions
if (!Action->IsAllowed(Item))
{
ShowErrorMessage(Action->GetDisallowedReason(Item));
return false;
}
// 3. Handle confirmation if needed
if (Action->bRequiresConfirmation)
{
if (!ShowConfirmationDialog(Action->ActionDisplayName))
return false; // User cancelled
}
// 4. Execute the action
return Action->ExecuteInventoryAction(Item);
}
Built-in Action Types¶
Consume Action¶
The consume action handles using consumable items like potions, food, or temporary buffs:
// UMounteaConsumeItemAction - Predefined consume behavior
class UMounteaConsumeItemAction : public UMounteaInventoryItemAction
{
protected:
// Only show for consumable items
virtual bool IsActionVisible_Implementation(const FInventoryItem& TargetItem) const override
{
if (!TargetItem.IsItemValid())
return false;
// Check if item has consumable flag
return static_cast<uint8>(TargetItem.GetTemplate()->ItemFlags) &
static_cast<uint8>(EInventoryItemFlags::EIIF_Consumable);
}
// Allow if item has quantity > 0
virtual bool IsAllowed_Implementation(const FInventoryItem& TargetItem) const override
{
return TargetItem.GetQuantity() > 0;
}
// Reduce quantity and apply effects
virtual bool ProcessAction_Implementation(UObject* ActionInitiator, const FInventoryItem& TargetItem) override
{
// Reduce item quantity by 1
auto OwningInventory = TargetItem.GetOwningInventory();
if (!OwningInventory.GetObject())
return false;
bool Success = OwningInventory->Execute_DecreaseItemQuantity(
OwningInventory.GetObject(),
TargetItem.GetGuid(),
1
);
if (Success)
{
// Apply any configured gameplay effects
ApplyActionEffects();
// Trigger Blueprint event for custom logic
ReceiveActionCompleted(TargetItem);
}
return Success;
}
};
Typical Consume Effects
- Health potions: Restore HP over time
- Mana potions: Instantly restore MP
- Food items: Temporary stat bonuses
- Scrolls: Cast spells or grant abilities
Equip Action¶
The equip action handles putting on armor, weapons, and accessories:
// UMounteaEquipItemAction - Equipment functionality
class UMounteaEquipItemAction : public UMounteaInventoryItemAction
{
protected:
// Show for non-consumable items that can be equipped
virtual bool IsActionVisible_Implementation(const FInventoryItem& TargetItem) const override
{
if (!TargetItem.IsItemValid())
return false;
// Hide for consumable items
if (static_cast<uint8>(TargetItem.GetTemplate()->ItemFlags) &
static_cast<uint8>(EInventoryItemFlags::EIIF_Consumable))
return false;
// Show if item has a spawn actor (something to equip)
return TargetItem.GetTemplate()->SpawnActor.IsValid();
}
// Allow if equipment system is available and item isn't broken
virtual bool IsAllowed_Implementation(const FInventoryItem& TargetItem) const override
{
// Check for equipment interface
auto EquipmentInterface = GetEquipmentInterface(GetAvatarActorFromActorInfo());
if (!EquipmentInterface.GetObject())
return false;
// Don't equip broken items
if (TargetItem.GetTemplate()->bHasDurability &&
FMath::IsNearlyZero(TargetItem.GetDurability()))
return false;
return true;
}
// Spawn and attach item actor
virtual bool ProcessAction_Implementation(UObject* ActionInitiator, const FInventoryItem& TargetItem) override
{
auto EquipmentInterface = GetEquipmentInterface(ActionInitiator);
if (!EquipmentInterface.GetObject())
return false;
// Load the actor class to spawn
UClass* ActorClass = TargetItem.GetTemplate()->SpawnActor.LoadSynchronous();
if (!ActorClass)
return false;
// Spawn the equipment actor
AActor* EquipmentActor = GetWorld()->SpawnActor<AActor>(
ActorClass,
FVector::ZeroVector,
FRotator::ZeroRotator
);
if (EquipmentActor)
{
// Attach to equipment system
bool Success = EquipmentInterface->Execute_EquipItem(
EquipmentInterface.GetObject(),
TargetItem,
EquipmentActor
);
if (Success)
{
ReceiveActionCompleted(TargetItem);
return true;
}
else
{
// Failed to equip - clean up spawned actor
EquipmentActor->Destroy();
}
}
return false;
}
};
Creating Custom Actions¶
C++ Implementation Pattern¶
Custom actions follow a consistent pattern for maximum flexibility:
// Example: Repair Item Action
UCLASS(BlueprintType, Blueprintable, DisplayName="Repair Item Action")
class URepairItemAction : public UMounteaInventoryItemAction
{
GENERATED_BODY()
public:
URepairItemAction()
{
// Set display properties
ActionDisplayName = NSLOCTEXT("ItemActions", "RepairAction", "Repair");
ActionDescription = NSLOCTEXT("ItemActions", "RepairDesc", "Restore item durability");
ActionPriority = 10; // Lower priority than consume/equip
bRequiresConfirmation = true; // Confirm before spending resources
}
protected:
// Show only for damaged, repairable items
virtual bool IsActionVisible_Implementation(const FInventoryItem& TargetItem) const override
{
if (!TargetItem.IsItemValid())
return false;
auto Template = TargetItem.GetTemplate();
// Must support durability
if (!Template->bHasDurability)
return false;
// Must be damaged
return TargetItem.GetDurability() < Template->MaxDurability;
}
// Allow if player has repair materials
virtual bool IsAllowed_Implementation(const FInventoryItem& TargetItem) const override
{
// Check if player has repair kit in inventory
auto OwningInventory = TargetItem.GetOwningInventory();
if (!OwningInventory.GetObject())
return false;
FInventoryItemSearchParams SearchParams;
SearchParams.SearchByCategory = true;
SearchParams.CategoryToFind = TEXT("RepairKit");
return OwningInventory->Execute_HasItem(OwningInventory.GetObject(), SearchParams);
}
virtual FText GetDisallowedReason_Implementation(const FInventoryItem& TargetItem) const override
{
return NSLOCTEXT("ItemActions", "NoRepairKit", "You need a repair kit to fix this item");
}
// Repair logic
virtual bool ProcessAction_Implementation(UObject* ActionInitiator, const FInventoryItem& TargetItem) override
{
auto OwningInventory = TargetItem.GetOwningInventory();
if (!OwningInventory.GetObject())
return false;
// Find and consume repair kit
FInventoryItemSearchParams SearchParams;
SearchParams.SearchByCategory = true;
SearchParams.CategoryToFind = TEXT("RepairKit");
FInventoryItem RepairKit = OwningInventory->Execute_FindItem(
OwningInventory.GetObject(), SearchParams
);
if (!RepairKit.IsItemValid())
return false;
// Use one repair kit
bool KitUsed = OwningInventory->Execute_DecreaseItemQuantity(
OwningInventory.GetObject(), RepairKit.GetGuid(), 1
);
if (!KitUsed)
return false;
// Restore durability
float NewDurability = FMath::Min(
TargetItem.GetDurability() + 25.0f, // Repair 25 points
TargetItem.GetTemplate()->MaxDurability
);
bool RepairSuccess = OwningInventory->Execute_ModifyItemDurability(
OwningInventory.GetObject(),
TargetItem.GetGuid(),
NewDurability - TargetItem.GetDurability()
);
if (RepairSuccess)
{
ReceiveActionCompleted(TargetItem);
}
return RepairSuccess;
}
};
Blueprint Implementation¶
Actions can be implemented entirely in Blueprint for rapid prototyping:
// Blueprint events you can override
UFUNCTION(BlueprintImplementableEvent, DisplayName="On Execute Action")
void ReceiveExecuteAction(const FInventoryItem& TargetItem);
UFUNCTION(BlueprintImplementableEvent, DisplayName="On Action Completed")
void ReceiveActionCompleted(const FInventoryItem& TargetItem);
UFUNCTION(BlueprintImplementableEvent, DisplayName="On Action Failed")
void ReceiveActionFailed(const FInventoryItem& TargetItem, const FText& Reason);
Blueprint Action Example - Split Stack
- Is Action Visible: Check if item quantity > 1
- Is Allowed: Check if inventory has empty slots
- Process Action: Show quantity selection widget, split stack
- On Completed: Update UI, play sound effect
Advanced Patterns¶
Context-Sensitive Actions¶
Actions can change behavior based on game context:
// Smart equip action that handles different contexts
class USmartEquipAction : public UMounteaInventoryItemAction
{
protected:
virtual bool ProcessAction_Implementation(UObject* ActionInitiator, const FInventoryItem& TargetItem) override
{
auto EquipmentInterface = GetEquipmentInterface(ActionInitiator);
// Check if item is already equipped
if (IsItemCurrentlyEquipped(TargetItem))
{
// Unequip instead of equip
return EquipmentInterface->Execute_UnequipItem(
EquipmentInterface.GetObject(), TargetItem.GetGuid()
);
}
else
{
// Check if slot is occupied
FGameplayTag SlotTag = GetRequiredSlotForItem(TargetItem);
if (IsSlotOccupied(SlotTag))
{
// Swap with currently equipped item
return EquipmentInterface->Execute_SwapEquippedItem(
EquipmentInterface.GetObject(), TargetItem, SlotTag
);
}
else
{
// Normal equip
return EquipmentInterface->Execute_EquipItem(
EquipmentInterface.GetObject(), TargetItem, SlotTag
);
}
}
}
};
Multi-Step Actions¶
Complex actions can use GAS tasks for multi-step processes:
// Crafting action with confirmation and animation
class UCraftingAction : public UMounteaInventoryItemAction
{
protected:
virtual bool ProcessAction_Implementation(UObject* ActionInitiator, const FInventoryItem& TargetItem) override
{
// Start crafting animation
StartCraftingAnimation();
// Use GAS task for timed action
UAbilityTask_WaitDelay* DelayTask = UAbilityTask_WaitDelay::WaitDelay(
this, 3.0f // 3 second crafting time
);
DelayTask->OnFinish.AddDynamic(this, &UCraftingAction::OnCraftingComplete);
DelayTask->ReadyForActivation();
return true;
}
UFUNCTION()
void OnCraftingComplete()
{
// Consume materials and create result
ConsumeCraftingMaterials();
CreateCraftedItem();
ReceiveActionCompleted(GetTargetItem());
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, false);
}
};
Action Chaining¶
Actions can trigger other actions automatically:
// Consume action that triggers equipment repair
class UConsumableRepairAction : public UMounteaConsumeItemAction
{
protected:
virtual bool ProcessAction_Implementation(UObject* ActionInitiator, const FInventoryItem& TargetItem) override
{
// First consume the item normally
bool ConsumeSuccess = Super::ProcessAction_Implementation(ActionInitiator, TargetItem);
if (ConsumeSuccess)
{
// Then repair all equipped items
auto EquipmentInterface = GetEquipmentInterface(ActionInitiator);
if (EquipmentInterface.GetObject())
{
RepairAllEquippedItems(EquipmentInterface);
}
}
return ConsumeSuccess;
}
private:
void RepairAllEquippedItems(TScriptInterface<IMounteaAdvancedEquipmentInterface> Equipment)
{
TArray<FInventoryItem> EquippedItems = Equipment->Execute_GetAllEquippedItems(Equipment.GetObject());
for (FInventoryItem& Item : EquippedItems)
{
if (Item.GetTemplate()->bHasDurability)
{
float RepairAmount = Item.GetTemplate()->MaxDurability * 0.1f; // 10% repair
Equipment->Execute_ModifyEquippedItemDurability(
Equipment.GetObject(), Item.GetGuid(), RepairAmount
);
}
}
}
};
Action Management¶
Action Registration¶
Actions are typically registered with items through templates or dynamically:
// Static action registration in game mode
class AMyGameMode : public AGameModeBase
{
public:
virtual void BeginPlay() override
{
Super::BeginPlay();
RegisterItemActions();
}
private:
void RegisterItemActions()
{
// Get action manager
auto ActionManager = GetWorld()->GetSubsystem<UItemActionManager>();
// Register global actions (available to all items)
ActionManager->RegisterGlobalAction(UDropItemAction::StaticClass());
ActionManager->RegisterGlobalAction(USplitStackAction::StaticClass());
// Register category-specific actions
ActionManager->RegisterCategoryAction(TEXT("Weapon"), UEquipItemAction::StaticClass());
ActionManager->RegisterCategoryAction(TEXT("Armor"), UEquipItemAction::StaticClass());
ActionManager->RegisterCategoryAction(TEXT("Consumable"), UMounteaConsumeItemAction::StaticClass());
// Register template-specific actions
if (UMounteaInventoryItemTemplate* HealthPotionTemplate = LoadHealthPotionTemplate())
{
ActionManager->RegisterTemplateAction(HealthPotionTemplate, UHealingAction::StaticClass());
}
}
};
Dynamic Action Discovery¶
Actions can be discovered automatically based on gameplay tags:
// Action manager that finds actions based on item tags
TArray<UMounteaInventoryItemAction*> GetAvailableActions(const FInventoryItem& Item)
{
TArray<UMounteaInventoryItemAction*> AvailableActions;
if (!Item.IsItemValid())
return AvailableActions;
// Check all registered actions
for (auto ActionClass : RegisteredActionClasses)
{
UMounteaInventoryItemAction* ActionInstance = CreateActionInstance(ActionClass);
if (ActionInstance && ActionInstance->IsActionVisible(Item))
{
AvailableActions.Add(ActionInstance);
}
}
// Sort by priority
AvailableActions.Sort([](const UMounteaInventoryItemAction& A, const UMounteaInventoryItemAction& B)
{
return A.ActionPriority < B.ActionPriority;
});
return AvailableActions;
}
UI Integration¶
Action Menu Generation¶
Actions automatically populate context menus:
// Widget that shows available actions
class UItemActionMenuWidget : public UUserWidget
{
public:
void PopulateActionsForItem(const FInventoryItem& Item)
{
ActionButtonContainer->ClearChildren();
TArray<UMounteaInventoryItemAction*> Actions = GetAvailableActions(Item);
for (UMounteaInventoryItemAction* Action : Actions)
{
// Create button for this action
UButton* ActionButton = CreateWidget<UButton>(this, ActionButtonClass);
// Set button text and icon
auto ButtonText = ActionButton->FindChildOfType<UTextBlock>("ActionText");
if (ButtonText)
ButtonText->SetText(Action->ActionDisplayName);
auto ButtonIcon = ActionButton->FindChildOfType<UImage>("ActionIcon");
if (ButtonIcon && Action->ActionIcon.IsValid())
ButtonIcon->SetBrushFromTexture(Action->ActionIcon.LoadSynchronous());
// Bind click event
ActionButton->OnClicked.AddDynamic(this, &UItemActionMenuWidget::OnActionClicked);
// Store action reference
ActionButton->SetTag(FName(*FString::FromInt(Actions.Find(Action))));
// Enable/disable based on permissions
ActionButton->SetIsEnabled(Action->IsAllowed(Item));
if (!ActionButton->GetIsEnabled())
{
ActionButton->SetToolTipText(Action->GetDisallowedReason(Item));
}
ActionButtonContainer->AddChild(ActionButton);
}
}
UFUNCTION()
void OnActionClicked()
{
UButton* ClickedButton = Cast<UButton>(FSlateApplication::Get().GetUserFocusedWidget());
if (!ClickedButton)
return;
int32 ActionIndex = FCString::Atoi(*ClickedButton->GetTag().ToString());
if (CachedActions.IsValidIndex(ActionIndex))
{
CachedActions[ActionIndex]->ExecuteInventoryAction(CurrentItem);
}
// Close menu after action
SetVisibility(ESlateVisibility::Collapsed);
}
};
Best Practices¶
Action Design Principles¶
- Single Responsibility: Each action should do one thing well
- Consistent Interface: Use the same visibility/permission pattern
- Clear Feedback: Provide meaningful error messages
- Confirmation for Destructive Actions: Require confirmation for permanent changes
- Performance Awareness: Cache expensive checks, avoid heavy operations in visibility checks
Error Handling¶
// Robust action execution with error handling
bool ExecuteActionSafely(UMounteaInventoryItemAction* Action, const FInventoryItem& Item)
{
if (!Action || !Item.IsItemValid())
{
LOG_ERROR(TEXT("Invalid action or item"));
return false;
}
try
{
// Pre-execution validation
if (!Action->IsActionVisible(Item))
{
LOG_WARNING(TEXT("Action %s not visible for item %s"),
*Action->ActionDisplayName.ToString(),
*Item.ToString());
return false;
}
if (!Action->IsAllowed(Item))
{
FText Reason = Action->GetDisallowedReason(Item);
ShowUserMessage(Reason);
return false;
}
// Execute action
return Action->ExecuteInventoryAction(Item);
}
catch (const std::exception& e)
{
LOG_ERROR(TEXT("Action execution failed: %s"), *FString(e.what()));
return false;
}
}
Performance Optimization¶
Problem & Solution
- Action Pooling: Reuse action instances instead of creating new ones
- Lazy Loading: Load action classes only when needed
- Batch Updates: Group multiple action results for UI updates
- Async Operations: Use GAS tasks for time-consuming actions
Troubleshooting¶
Action Not Appearing¶
Problem & Solution
- Check
IsActionVisible
implementation - Verify item flags match expected values
- Ensure action is registered with the system
Action Disabled¶
Problem & Solution
- Review
IsAllowed
logic - Check prerequisites (inventory space, resources, etc.)
- Validate item state (durability, quantity)
GAS Integration Issues¶
Problem & Solution
- Ensure actor has AbilitySystemComponent
- Check ability tags and requirements
- Verify network authority for multiplayer
Performance Problems¶
Problem & Solution
- Profile visibility checks for expensive operations
- Cache action instances instead of recreating
- Use gameplay tags for efficient filtering
Next Steps¶
- Inventory Component: Manage collections of item instances
- Equipment System: Equip and manage worn items
- UI System: Create responsive inventory interfaces