Skip to content

Crow Equipment System

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

GoalHow the System Achieves It
No inheritance treesItem behavior is composed from USCEquipmentAction objects, not subclassed actors
Data-drivenAll configuration lives in USCEquipmentDefinition; zero C++ changes per new item
Slot-basedFGameplayTag drives slot identity — flexible and designer-friendly
Input-awareEnhanced Input contexts are automatically injected/removed with each equip/unequip cycle
DecoupledThe 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

  1. Designer creates a USCEquipmentDefinition asset and configures the slot, visual actor class, action classes, and input bindings.
  2. Gameplay code calls USCEquipmentComponent::EquipDefinition().
  3. The component creates a USCEquipmentInstance, which reads the definition, instantiates action objects, spawns the visual actor, and attaches it to the owner.
  4. Player input flows through Enhanced Input → HandleEquipmentInput()ExecuteEquipmentAction()USCEquipmentInstance::ExecuteAction() → matching USCEquipmentAction objects.

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

PropertyExample Value
EquipmentIdEquipment.Weapon.Pistol
EquipmentTypeEquipment.Type.Firearm
SlotTagSlot.Weapon.RightHand
AttachSocketNamehand_r
VisualActorClassBP_PistolActor
ActionClasses[ BP_FireAction, BP_ReloadAction ]
InputMappingContextIMC_Pistol
InputBindings[0].InputActionIA_Fire
InputBindings[0].ActionTagAction.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.

PropertyTypeDescription
EquipmentIdFGameplayTagStable identity tag. Used as the Primary Asset ID when valid.
EquipmentTypeFGameplayTagOptional classification tag (e.g. Equipment.Type.Firearm).
SlotTagFGameplayTagRequired. The inventory slot this item occupies.
AttachSocketNameFNameSocket on the attach component. NAME_None attaches at origin.
VisualActorClassTSubclassOf<AActor>Actor spawned in the world for visuals. Leave empty for buff-only items.
InstanceClassTSubclassOf<USCEquipmentInstance>Custom runtime instance class. Falls back to USCEquipmentInstance.
ActionClassesTArray<TSubclassOf<USCEquipmentAction>>Behavior objects created on the instance at equip time.
SlotsToClearOnEquipFGameplayTagContainerSlots unequipped automatically before this item is placed.
RequiredTagsFGameplayTagContainerTags that must be present for the item to be usable.
BlockedTagsFGameplayTagContainerTags that block usage.
InputMappingContextUInputMappingContext*Enhanced Input context added to the local player while this item is active.
InputMappingPriorityint32Priority passed to the Enhanced Input subsystem (default: 1).
InputBindingsTArray<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
MethodVisibilityDescription
Initialize(Owner, Definition, Payload)C++ InternalInitializes 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()BlueprintNativeEventOverride hook called when the equip process is complete.
OnUnequipped()BlueprintNativeEventOverride hook called when the unequip process is started.
GetDefinition()Blueprint PureReturns the source static configuration dataset.
GetPayload()Blueprint PureReturns 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.

MethodBlueprintDescription
EquipDefinition(Def, Payload)✅ YesEquips definition configurations and purges blocking configurations.
UnequipBySlot(SlotTag, bChildren)✅ YesUnequips items inside the designated slot structure.
GetEquippedInstance(SlotTag)✅ YesReturns active instance objects inside the queried slot.
TryInitializeEquipmentInput()✅ YesBinds 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.

MethodSignatureDescription
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) -> boolDetermines if this action can handle the provided execution tag.
ExecuteAction(AActor*, Instance*, Tag, IA, Value) -> boolExecutes 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:

  1. OwnerActor->GetRootComponent() (cast to USceneComponent)
  2. OwnerActor->FindComponentByClass<USceneComponent>()

5.2 IUSCEquipmentActorInterface

File: Interfaces/USCEquipmentActorInterface.h

Implement on the spawned visual actor to receive lifecycle callbacks from the equipment system:

MethodExecution 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 EventResulting 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
);