1. Overview
The Crow Equipment System is a data-driven, modular equipment management system for Unreal Engine 5. It enables characters or vehicles to equip, swap, and use items — weapons, tools, accessories — without writing per-item actor logic.
Design Goals
| Goal | How the System Achieves It |
|---|---|
| No inheritance trees | Item behavior is composed from USCEquipmentAction objects, not subclassed actors |
| Data-driven | All configuration lives in USCEquipmentDefinition; zero C++ changes per new item |
| Slot-based | FGameplayTag drives slot identity — flexible and designer-friendly |
| Input-aware | Enhanced Input contexts are automatically injected/removed with each equip/unequip cycle |
| Decoupled | The system talks to its host via interfaces, never through concrete class casts |
2. Architecture
The system has four primary classes and two optional interfaces. The diagram below shows how they relate at runtime.
┌──────────────────────────────────────────────────────────┐
│ Your Character / Vehicle │
│ │
│ ┌────────────────────────┐ ┌──────────────────────┐ │
│ │ USCEquipmentComponent │ │ ISCEquipmentAttach- │ │
│ │ (Actor Component) │ │ Provider (Interface) │ │
│ └───────────┬────────────┘ └──────────────────────┘ │
└──────────────│──────────────────────────────────────────┘
│ owns (1 : N, keyed by SlotTag)
▼
┌────────────────────────┐ reads ┌────────────────────────────┐
│ USCEquipmentInstance │ ────────► │ USCEquipmentDefinition │
│ (Runtime UObject) │ │ (Primary Data Asset) │
└───────────┬────────────┘ └────────────────────────────┘
│ owns (1 : N)
├──────────────────────────────────────────┐
▼ ▼
┌────────────────────────┐ ┌────────────────────────────┐
│ USCEquipmentAction │ │ Visual Actor (AActor) │
│ (Behavior Objects) │ │ IUSCEquipmentActorInterface │
└────────────────────────┘ └────────────────────────────┘
Data Flow
- Designer creates a
USCEquipmentDefinitionasset and configures the slot, visual actor class, action classes, and input bindings. - Gameplay code calls
USCEquipmentComponent::EquipDefinition(). - The component creates a
USCEquipmentInstance, which reads the definition, instantiates action objects, spawns the visual actor, and attaches it to the owner. - Player input flows through Enhanced Input →
HandleEquipmentInput()→ExecuteEquipmentAction()→USCEquipmentInstance::ExecuteAction()→ matchingUSCEquipmentActionobjects.
3. Quick Start
Step 1 — Add the Component to Your Character
// Copyright (c) 2026 SuspectCrow. All rights reserved.
// MyCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Components/SCEquipmentComponent.h"
#include "MyCharacter.generated.h"
UCLASS()
class AMYCHARACTER_API AMyCharacter : public ACharacter
{
GENERATED_BODY()
public:
AMyCharacter();
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Equipment")
TObjectPtr<USCEquipmentComponent> EquipmentComponent;
};
// Copyright (c) 2026 SuspectCrow. All rights reserved.
// MyCharacter.cpp
#include "MyCharacter.h"
AMyCharacter::AMyCharacter()
{
EquipmentComponent = CreateDefaultSubobject<USCEquipmentComponent>(TEXT("EquipmentComponent"));
}
Step 2 — Connect Enhanced Input
Call this once your pawn’s InputComponent is ready:
// Copyright (c) 2026 SuspectCrow. All rights reserved.
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
EquipmentComponent->TryInitializeEquipmentInput();
}
Alternative — pass the component explicitly if you control input setup elsewhere:
EquipmentComponent->SetupEquipmentInputComponent(Cast<UEnhancedInputComponent>(InputComponent));
Step 3 — Implement ISCEquipmentAttachProvider
This interface tells the system which USceneComponent the visual actor should attach to:
// Copyright (c) 2026 SuspectCrow. All rights reserved.
// MyCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Interfaces/SCEquipmentAttachProvider.h"
#include "MyCharacter.generated.h"
UCLASS()
class AMYCHARACTER_API AMyCharacter : public ACharacter, public ISCEquipmentAttachProvider
{
GENERATED_BODY()
public:
virtual USceneComponent* GetEquipmentAttachComponent_Implementation(FGameplayTag SlotTag) const override;
};
// Copyright (c) 2026 SuspectCrow. All rights reserved.
// MyCharacter.cpp
#include "MyCharacter.h"
USceneComponent* AMyCharacter::GetEquipmentAttachComponent_Implementation(FGameplayTag SlotTag) const
{
static const FGameplayTag RightHandTag = FGameplayTag::RequestGameplayTag("Slot.Weapon.RightHand");
if (SlotTag.MatchesTag(RightHandTag))
{
return GetMesh();
}
return GetRootComponent();
}
Step 4 — Create an Equipment Definition Asset
In the Content Browser: Right-click → Miscellaneous → Data Asset → USCEquipmentDefinition
| Property | Example Value |
|---|---|
EquipmentId | Equipment.Weapon.Pistol |
EquipmentType | Equipment.Type.Firearm |
SlotTag | Slot.Weapon.RightHand |
AttachSocketName | hand_r |
VisualActorClass | BP_PistolActor |
ActionClasses | [ BP_FireAction, BP_ReloadAction ] |
InputMappingContext | IMC_Pistol |
InputBindings[0].InputAction | IA_Fire |
InputBindings[0].ActionTag | Action.Fire |
Step 5 — Equip at Runtime
C++:
// EquipmentComponent loaded and initialized EquipmentComponent->EquipDefinition(PistolDefinition);
Blueprint: Call the Equip Definition node on the Equipment Component.
4. Core Classes
4.1 USCEquipmentDefinition
File: DataAssets/SCEquipmentDefinition.h | Base: UPrimaryDataAsset
The single source of truth for one equipment type. This asset is read-only at runtime — it is never modified during play.
| Property | Type | Description |
|---|---|---|
EquipmentId | FGameplayTag | Stable identity tag. Used as the Primary Asset ID when valid. |
EquipmentType | FGameplayTag | Optional classification tag (e.g. Equipment.Type.Firearm). |
SlotTag | FGameplayTag | Required. The inventory slot this item occupies. |
AttachSocketName | FName | Socket on the attach component. NAME_None attaches at origin. |
VisualActorClass | TSubclassOf<AActor> | Actor spawned in the world for visuals. Leave empty for buff-only items. |
InstanceClass | TSubclassOf<USCEquipmentInstance> | Custom runtime instance class. Falls back to USCEquipmentInstance. |
ActionClasses | TArray<TSubclassOf<USCEquipmentAction>> | Behavior objects created on the instance at equip time. |
SlotsToClearOnEquip | FGameplayTagContainer | Slots unequipped automatically before this item is placed. |
RequiredTags | FGameplayTagContainer | Tags that must be present for the item to be usable. |
BlockedTags | FGameplayTagContainer | Tags that block usage. |
InputMappingContext | UInputMappingContext* | Enhanced Input context added to the local player while this item is active. |
InputMappingPriority | int32 | Priority passed to the Enhanced Input subsystem (default: 1). |
InputBindings | TArray<FSCEquipmentInputBinding> | Maps input actions to dynamic action tags. |
FSCEquipmentInputBinding
// Copyright (c) 2026 SuspectCrow. All rights reserved.
struct FSCEquipmentInputBinding
{
UInputAction* InputAction;
bool bRouteAllTriggerEvents;
ETriggerEvent TriggerEvent;
FGameplayTag ActionTag;
};
4.2 USCEquipmentInstance
File: Objects/SCEquipmentInstance.h | Base: UObject
Created by USCEquipmentComponent when a slot is filled. One instance exists per occupied slot. Subclass this to store runtime state (ammo, heat, durability).
Initialize()
│
▼
Equip() ──────────────────────────────────────────────────────────────────► Unequip()
│ │
├─ Spawn & attach visual actor ├─ OnUnequipped() [Blueprint hook]
├─ Notify each USCEquipmentAction → OnEquipped() ├─ Notify visual actor → OnUnequipped()
├─ Notify visual actor → OnEquipped() ├─ Notify each action → OnUnequipped()
└─ OnEquipped() [Blueprint hook] └─ Destroy visual actor
Public API
| Method | Visibility | Description |
|---|---|---|
Initialize(Owner, Definition, Payload) | C++ Internal | Initializes the instance with targeting data. Do not call manually. |
Equip() | Blueprint / C++ | Spawns visual actors, attaches components, sets up behaviors. |
Unequip() | Blueprint / C++ | Tears down visual actors, destroys internal state. |
ExecuteAction(ActionTag, IAInstance, Value) | Blueprint / C++ | Dispatches the routed gameplay tag to running action logic. |
OnEquipped() | BlueprintNativeEvent | Override hook called when the equip process is complete. |
OnUnequipped() | BlueprintNativeEvent | Override hook called when the unequip process is started. |
GetDefinition() | Blueprint Pure | Returns the source static configuration dataset. |
GetPayload() | Blueprint Pure | Returns optional initialization parameter references. |
Subclassing Example
// Copyright (c) 2026 SuspectCrow. All rights reserved.
// MyWeaponInstance.h
#pragma once
#include "CoreMinimal.h"
#include "Objects/SCEquipmentInstance.h"
#include "MyWeaponInstance.generated.h"
UCLASS(Blueprintable)
class AMYCHARACTER_API UMyWeaponInstance : public USCEquipmentInstance
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadWrite, Category = "Weapon")
int32 CurrentAmmo = 30;
virtual void OnEquipped_Implementation() override;
virtual void OnUnequipped_Implementation() override;
};
// Copyright (c) 2026 SuspectCrow. All rights reserved.
// MyWeaponInstance.cpp
#include "MyWeaponInstance.h"
void UMyWeaponInstance::OnEquipped_Implementation()
{
Super::OnEquipped_Implementation();
}
void UMyWeaponInstance::OnUnequipped_Implementation()
{
Super::OnUnequipped_Implementation();
}
4.3 USCEquipmentComponent
File: Components/SCEquipmentComponent.h | Base: UActorComponent
The core component that manages slots and routes input commands across the system.
| Method | Blueprint | Description |
|---|---|---|
EquipDefinition(Def, Payload) | ✅ Yes | Equips definition configurations and purges blocking configurations. |
UnequipBySlot(SlotTag, bChildren) | ✅ Yes | Unequips items inside the designated slot structure. |
GetEquippedInstance(SlotTag) | ✅ Yes | Returns active instance objects inside the queried slot. |
TryInitializeEquipmentInput() | ✅ Yes | Binds the cached enhanced input component structure. |
Slot Conflict Resolution
EquipDefinition(PistolDef)
│
├─ UnequipBySlot("Slot.Weapon.LeftHand") ← from SlotsToClearOnEquip
├─ UnequipBySlot("Slot.Weapon.RightHand") ← existing item in target slot
├─ NewInstance = NewObject<USCEquipmentInstance>(...)
├─ NewInstance->Initialize(...)
├─ NewInstance->Equip()
└─ BindEquipmentInputForSlot("Slot.Weapon.RightHand", PistolDef)
4.4 USCEquipmentAction
File: Objects/SCEquipmentAction.h | Base: UObject
Instanced behavior objects managed inside USCEquipmentInstance. Actions isolate logic for tasks like firing, reloading, or toggling equipment. They use the specifiers: Abstract, Blueprintable, EditInlineNew, and DefaultToInstanced.
| Method | Signature | Description |
|---|---|---|
OnEquipped | (AActor*, USCEquipmentInstance*) | Called when equipping starts. Set up GAS properties and listeners. |
OnUnequipped | (AActor*, USCEquipmentInstance*) | Called when unequipping starts. Clean up GAS and listeners. |
CanExecuteAction | (AActor*, Instance*, Tag) -> bool | Determines if this action can handle the provided execution tag. |
ExecuteAction | (AActor*, Instance*, Tag, IA, Value) -> bool | Executes the core action logic. |
C++ Action Example
// Copyright (c) 2026 SuspectCrow. All rights reserved.
// MyFireAction.h
#pragma once
#include "CoreMinimal.h"
#include "Objects/SCEquipmentAction.h"
#include "MyFireAction.generated.h"
UCLASS()
class AMYCHARACTER_API UMyFireAction : public USCEquipmentAction
{
GENERATED_BODY()
public:
virtual bool CanExecuteAction_Implementation(AActor* Owner, USCEquipmentInstance* Instance, FGameplayTag Tag) const override;
virtual bool ExecuteAction_Implementation(AActor* Owner, USCEquipmentInstance* Instance, FGameplayTag Tag, const FInputActionInstance& IAInstance, FInputActionValue Value) override;
};
// Copyright (c) 2026 SuspectCrow. All rights reserved.
// MyFireAction.cpp
#include "MyFireAction.h"
#include "MyWeaponInstance.h"
bool UMyFireAction::CanExecuteAction_Implementation(AActor* Owner, USCEquipmentInstance* Instance, FGameplayTag Tag) const
{
static const FGameplayTag FireTag = FGameplayTag::RequestGameplayTag("Action.Fire");
return Tag.MatchesTagExact(FireTag);
}
bool UMyFireAction::ExecuteAction_Implementation(AActor* Owner, USCEquipmentInstance* Instance, FGameplayTag Tag, const FInputActionInstance& IAInstance, FInputActionValue Value)
{
UMyWeaponInstance* WeaponInstance = Cast<UMyWeaponInstance>(Instance);
if (!WeaponInstance || WeaponInstance->CurrentAmmo <= 0)
{
return false;
}
--WeaponInstance->CurrentAmmo;
return true;
}
5. Interfaces
5.1 ISCEquipmentAttachProvider
File: Interfaces/SCEquipmentAttachProvider.h
Implement on the actor that owns the USCEquipmentComponent to determine which attachment component to target based on slot tags:
// Copyright (c) 2026 SuspectCrow. All rights reserved. USceneComponent* GetEquipmentAttachComponent(FGameplayTag SlotTag) const;
Fallback Chain: If the provider interface is absent or returns null, the system resolves components in this order:
OwnerActor->GetRootComponent()(cast toUSceneComponent)OwnerActor->FindComponentByClass<USceneComponent>()
5.2 IUSCEquipmentActorInterface
File: Interfaces/USCEquipmentActorInterface.h
Implement on the spawned visual actor to receive lifecycle callbacks from the equipment system:
| Method | Execution Condition |
|---|---|
OnEquipped(Instance) | Called after the visual actor attaches and USCEquipmentInstance::Equip() completes. |
OnUnequipped() | Called before the visual actor is detached and destroyed during Unequip(). |
OnEquipmentActionExecuted(Tag) | Fires when an action tag is routed through ExecuteAction(). |
6. Input System Integration
The system integrates natively with UE5’s Enhanced Input plugin. Inputs are automatically registered during the equip lifecycle.
[Player presses key]
│
▼
Enhanced Input System
│ fires event for UInputAction (e.g. IA_Fire)
▼
USCEquipmentComponent::HandleEquipmentInput(FInputActionInstance)
│ looks up InputRouteMap by UInputAction pointer
│ finds: [ { SlotTag = "Slot.Weapon.RightHand", ActionTag = "Action.Fire" } ]
▼
USCEquipmentComponent::ExecuteEquipmentAction(SlotTag, ActionTag, ...)
│
▼
USCEquipmentInstance::ExecuteAction(ActionTag, ...)
│ iterates Actions[]
▼
USCEquipmentAction::CanExecuteAction() → true?
│
▼
USCEquipmentAction::ExecuteAction()
Configuring Input inside Equipment Definitions
InputMappingContext: IMC_Pistol
InputBindings:
[0] InputAction: IA_Fire
bRouteAllTriggerEvents: true
ActionTag: Action.Fire
[1] InputAction: IA_Reload
bRouteAllTriggerEvents: false
TriggerEvent: Triggered
ActionTag: Action.Reload
Multi-Slot Input Sharing
If two equipped items reference the same UInputAction asset, the event is dispatched to both slots. The internal route mapping handles multiple routes per action, removing them independently when unequipping.
Input Lifecycle Summary
| System Event | Resulting Action |
|---|---|
EquipDefinition() | InputMappingContext is injected into the Player Subsystem; input listeners are registered. |
UnequipBySlot() | The mapping context is removed and slot routes are cleared. Shared delegates remain active for other slots. |
SetupEquipmentInputComponent() | Any already-equipped items are synchronized and bound immediately. |
7. Extending the System
Custom Equipment Instances
Subclass USCEquipmentInstance to manage specific item properties:
// Copyright (c) 2026 SuspectCrow. All rights reserved.
// MyMeleeInstance.h
#pragma once
#include "CoreMinimal.h"
#include "Objects/SCEquipmentInstance.h"
#include "MyMeleeInstance.generated.h"
UCLASS(Blueprintable)
class AMYCHARACTER_API UMyMeleeInstance : public USCEquipmentInstance
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadWrite, Category = "Melee")
float SwingProgress = 0.f;
UPROPERTY(BlueprintReadOnly, Category = "Melee")
bool bIsComboActive = false;
};
Custom Equipment Definitions
Subclass USCEquipmentDefinition to expose designer properties for categories of equipment:
// Copyright (c) 2026 SuspectCrow. All rights reserved.
// MyFirearmDefinition.h
#pragma once
#include "CoreMinimal.h"
#include "DataAssets/SCEquipmentDefinition.h"
#include "MyFirearmDefinition.generated.h"
UCLASS(BlueprintType)
class AMYCHARACTER_API UMyFirearmDefinition : public USCEquipmentDefinition
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, Category = "Firearm")
int32 MagazineCapacity = 30;
UPROPERTY(EditDefaultsOnly, Category = "Firearm")
float FireCooldownSeconds = 0.1f;
};
Using the Payload Object
The system supports passing arbitrary runtime context during the equip cycle:
// E.g., passing inventory item structures to equipment instances EquipmentComponent->EquipDefinition(PistolDefinition, InventoryItemPayload);
8. Common Patterns & Recipes
Pattern 1 — Two-Handed Weapons
Configure two-handed equipment definitions to automatically clear both hand slots upon equipping:
SlotTag: Slot.Weapon.TwoHanded SlotsToClearOnEquip: [ Slot.Weapon.RightHand, Slot.Weapon.LeftHand ]
Pattern 2 — Querying Active Equipment
External systems (like UIs or abilities) can query active equipment information directly through slot configurations:
// Copyright (c) 2026 SuspectCrow. All rights reserved.
if (USCEquipmentInstance* Inst = EquipmentComponent->GetEquippedInstance(RightHandSlotTag))
{
if (UMyWeaponInstance* Weapon = Cast<UMyWeaponInstance>(Inst))
{
HUDWidget->SetAmmoCount(Weapon->CurrentAmmo);
}
}
Pattern 3 — Executing Actions Without Input
Useful for executing equipment actions from Animation Notifies, Ability Tasks, or server-side events:
// Copyright (c) 2026 SuspectCrow. All rights reserved.
FInputActionValue DummyValue;
FInputActionInstance DummyInstance;
EquipmentComponent->ExecuteEquipmentAction(
FGameplayTag::RequestGameplayTag("Slot.Weapon.RightHand"),
FGameplayTag::RequestGameplayTag("Action.Fire"),
DummyInstance,
DummyValue
);
Pattern 4 — Invisible / Buff-Only Equipment
Leave VisualActorClass empty. The system will still create instances and action objects. Use OnEquipped / OnUnequipped to apply or remove active attributes, such as GAS Gameplay Effects:
// Copyright (c) 2026 SuspectCrow. All rights reserved.
void UMyArmorBuffAction::OnEquipped_Implementation(AActor* Owner, USCEquipmentInstance* Instance)
{
if (UAbilitySystemComponent* ASC = Owner->FindComponentByClass<UAbilitySystemComponent>())
{
ArmorEffectHandle = ASC->ApplyGameplayEffectToSelf(...);
}
}
void UMyArmorBuffAction::OnUnequipped_Implementation(AActor* Owner, USCEquipmentInstance* Instance)
{
if (UAbilitySystemComponent* ASC = Owner->FindComponentByClass<UAbilitySystemComponent>())
{
ASC->RemoveActiveGameplayEffect(ArmorEffectHandle);
}
}
Pattern 5 — Late Input Initialization
On networked pawns, the controller input component may not be ready at equip time. TryInitializeEquipmentInput() is safe to call multiple times to synchronize equipped items:
// Copyright (c) 2026 SuspectCrow. All rights reserved.
void AMyCharacter::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
EquipmentComponent->TryInitializeEquipmentInput();
}
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
EquipmentComponent->TryInitializeEquipmentInput();
}
Pattern 6 — Category-Wide Unequipping
Unequip entire categories of slots simultaneously by enabling bIncludeChildren with parent tags:
// Copyright (c) 2026 SuspectCrow. All rights reserved.
// Unequips RightHand, LeftHand, TwoHanded, etc.
EquipmentComponent->UnequipBySlot(
FGameplayTag::RequestGameplayTag("Slot.Weapon"),
true
);
EN
TR