Attachment Containers¶
What You'll Learn¶
- Setting up attachment container components
- Managing slot collections and states
- Implementing network replication patterns
- Using server RPCs for attachment operations
- Handling container events and notifications
- Finding slots with tag-based queries
- Performance optimization techniques
Quick Start¶
// Add attachment container to actor
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Equipment")
class UMounteaAttachmentContainerComponent* AttachmentContainer;
// Try to attach object to slot
bool Success = AttachmentContainer->Execute_TryAttach(AttachmentContainer, SlotName, AttachableObject);
// Find compatible slot automatically
FName FreeSlot = AttachmentContainer->Execute_FindFirstFreeSlotWithTags(AttachmentContainer, RequiredTags);
Result
Network-replicated container managing attachment slots with validation, events, and smart slot finding capabilities.
Component Architecture¶
Core Responsibilities¶
UMounteaAttachmentContainerComponent
serves as the central manager for attachment slots:
class UMounteaAttachmentContainerComponent : public UActorComponent, public IMounteaAdvancedAttachmentContainerInterface
{
// Slot Management
TArray<TObjectPtr<UMounteaAdvancedAttachmentSlot>> AttachmentSlots;
// Target Configuration
FName DefaultAttachmentTarget; // Component name for attachment
USceneComponent* DefaultAttachmentTargetComponent; // Resolved component reference
// State Management
EAttachmentSlotState State; // Container state
// Events
FOnAttachmentChanged OnAttachmentChanged;
FOnSlotStateChanged OnSlotStateChanged;
FOnContainerCleared OnContainerCleared;
};
Key Features
- Slot Collection Management: Add, remove, and organize attachment slots
- Network Replication: Automatic slot state synchronization
- Event Broadcasting: React to attachment changes
- Tag-Based Queries: Find compatible slots automatically
- Validation System: Prevent invalid attachment operations
Component Setup¶
Basic Configuration¶
// In actor constructor
AEquippableActor::AEquippableActor()
{
// Create attachment container
AttachmentContainer = CreateDefaultSubobject<UMounteaAttachmentContainerComponent>(TEXT("AttachmentContainer"));
// Configure default attachment target
AttachmentContainer->DefaultAttachmentTarget = TEXT("StaticMeshComponent");
// Replication enabled by default
AttachmentContainer->SetIsReplicatedByDefault(true);
}
Slot Configuration¶
Slots are configured in the editor through the AttachmentSlots array:
// Editor configuration creates slots like:
AttachmentSlots = {
// Weapon slot
{
SlotName: "MainHand",
SlotTags: "Equipment.Weapon.Melee",
SlotType: EAttachmentSlotType::EAST_Socket,
SocketName: "weapon_socket",
State: EAttachmentSlotState::EASS_Empty
},
// Shield slot
{
SlotName: "OffHand",
SlotTags: "Equipment.Shield",
SlotType: EAttachmentSlotType::EAST_Socket,
SocketName: "shield_socket",
State: EAttachmentSlotState::EASS_Empty
}
};
Initialization Process¶
void UMounteaAttachmentContainerComponent::BeginPlay()
{
Super::BeginPlay();
// Resolve default attachment target
const auto attachmentTargetComponent = UMounteaAttachmentsStatics::GetAvailableComponentByName(
Execute_GetOwningActor(this),
DefaultAttachmentTarget
);
if (!IsValid(attachmentTargetComponent))
LOG_ERROR(TEXT("Default attachment target component '%s' is not valid!"), *DefaultAttachmentTarget.ToString());
Execute_SetDefaultAttachmentTargetComponent(this, attachmentTargetComponent);
// Initialize all slots
Algo::ForEach(AttachmentSlots, [this](const auto& AttachmentSlot)
{
if (IsValid(AttachmentSlot))
{
AttachmentSlot->InitializeAttachmentSlot(this);
AttachmentSlot->BeginPlay();
}
});
}
Target Resolution
If the default attachment target component isn't found, attachment operations may fail. Ensure the target component name matches an existing component.
Attachment Operations¶
Try Attach¶
The primary method for attaching objects with validation:
bool UMounteaAttachmentContainerComponent::TryAttach_Implementation(const FName& SlotId, UObject* Attachment)
{
if (!Attachment) return false;
// Client sends request to server
if (!GetOwner()->HasAuthority())
{
ServerTryAttach(SlotId, Attachment);
return true;
}
// Server validates and executes
const bool bSuccess = TryAttachInternal(SlotId, Attachment);
if (bSuccess)
OnAttachmentChanged.Broadcast(SlotId, Attachment, nullptr);
return bSuccess;
}
// Internal validation and attachment
bool TryAttachInternal(const FName& SlotId, UObject* Attachment)
{
auto foundSlot = AttachmentSlots.FindByPredicate([SlotId](const UMounteaAdvancedAttachmentSlot* Slot) {
return Slot && Slot->SlotName == SlotId;
});
return foundSlot && (*foundSlot)->Attach(Attachment);
}
Force Attach¶
Bypass validation for administrative operations:
bool UMounteaAttachmentContainerComponent::ForceAttach_Implementation(const FName& SlotId, UObject* Attachment)
{
if (!Attachment) return false;
auto foundSlot = *AttachmentSlots.FindByPredicate([SlotId](const UMounteaAdvancedAttachmentSlot* Slot) {
return Slot->SlotName == SlotId;
});
if (!foundSlot) return false;
// Direct state assignment
foundSlot->Attachment = Attachment;
foundSlot->State = EAttachmentSlotState::EASS_Occupied;
return true;
}
Detachment Operations¶
bool UMounteaAttachmentContainerComponent::TryDetach_Implementation(const FName& SlotId)
{
auto foundSlot = *AttachmentSlots.FindByPredicate([SlotId](const UMounteaAdvancedAttachmentSlot* Slot) {
return Slot->SlotName == SlotId;
});
if (!foundSlot) return false;
// Client requests server action
if (!GetOwner()->HasAuthority())
{
ServerTryDetach(SlotId);
return true;
}
return foundSlot->Detach();
}
// Clear all attachments
void UMounteaAttachmentContainerComponent::ClearAll_Implementation()
{
for (auto& attachmentSlot : AttachmentSlots)
{
attachmentSlot->Detach();
}
}
Network Replication¶
Server RPCs¶
All attachment operations use server authority:
UFUNCTION(Server, Reliable)
void ServerTryAttach(const FName& SlotId, UObject* Attachment);
UFUNCTION(Server, Reliable)
void ServerTryDetach(const FName& SlotId);
// Implementations
void UMounteaAttachmentContainerComponent::ServerTryAttach_Implementation(const FName& SlotId, UObject* Attachment)
{
Execute_TryAttach(this, SlotId, Attachment);
}
void UMounteaAttachmentContainerComponent::ServerTryDetach_Implementation(const FName& SlotId)
{
Execute_TryDetach(this, SlotId);
}
Sub-Object Replication¶
Slots replicate as sub-objects for efficient delta synchronization:
bool UMounteaAttachmentContainerComponent::ReplicateSubobjects(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags)
{
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1
bool wroteSomething = Super::ReplicateSubobjects(Channel, Bunch, RepFlags);
// Register slots for replication
for (auto& Slot : AttachmentSlots)
{
if (IsValid(Slot))
AddReplicatedSubObject(Slot);
}
return wroteSomething;
#else
// Legacy replication support
bool wroteSomething = true;
for (UORReplicatedObject* ReplicatedObject : ReplicatedObjects)
{
if (IsValid(ReplicatedObject))
wroteSomething |= Channel->ReplicateSubobject(ReplicatedObject, *Bunch, *RepFlags);
}
return wroteSomething;
#endif
}
Replication Properties¶
void UMounteaAttachmentContainerComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UMounteaAttachmentContainerComponent, State);
DOREPLIFETIME(UMounteaAttachmentContainerComponent, AttachmentSlots);
}
Replication Strategy
Container state and slot collection replicate, while individual slot states use sub-object replication for granular updates.
Slot Management¶
Slot Queries¶
// Check if slot exists and is valid
bool UMounteaAttachmentContainerComponent::IsValidSlot_Implementation(const FName& SlotId) const
{
const auto foundSlot = *AttachmentSlots.FindByPredicate([SlotId](const UMounteaAdvancedAttachmentSlot* Slot) {
return Slot->SlotName == SlotId;
});
return foundSlot && foundSlot->IsSlotValid();
}
// Get slot reference
UMounteaAdvancedAttachmentSlot* UMounteaAttachmentContainerComponent::GetSlot_Implementation(const FName& SlotId) const
{
const auto foundSlot = *AttachmentSlots.FindByPredicate([SlotId](const UMounteaAdvancedAttachmentSlot* Slot) {
return Slot->SlotName == SlotId;
});
return foundSlot ? foundSlot : nullptr;
}
// Check occupancy status
bool UMounteaAttachmentContainerComponent::IsSlotOccupied_Implementation(const FName& SlotId) const
{
const auto foundSlot = *AttachmentSlots.FindByPredicate([SlotId](const UMounteaAdvancedAttachmentSlot* Slot) {
return Slot->SlotName == SlotId;
});
return foundSlot && foundSlot->IsOccupied();
}
Smart Slot Finding¶
// Find first compatible slot with tags
FName UMounteaAttachmentContainerComponent::FindFirstFreeSlotWithTags_Implementation(const FGameplayTagContainer& RequiredTags) const
{
const auto* found = Algo::FindByPredicate(AttachmentSlots, [&](const UMounteaAdvancedAttachmentSlot* slot) {
return slot != nullptr && slot->CanAttach() && slot->MatchesTags(RequiredTags, true);
});
return found ? (*found)->SlotName : NAME_None;
}
// Find first empty slot
FName UMounteaAttachmentContainerComponent::GetFirstEmptySlot_Implementation() const
{
const auto* Found = Algo::FindByPredicate(AttachmentSlots, [&](const UMounteaAdvancedAttachmentSlot* Slot) {
return Slot != nullptr && !Slot->IsOccupied();
});
return Found ? (*Found)->SlotName : NAME_None;
}
// Find slot by attachable object
FName UMounteaAttachmentContainerComponent::GetSlotIdForAttachable_Implementation(const UMounteaAttachableComponent* Attachable) const
{
if (!Attachable) return NAME_None;
const auto* Found = Algo::FindByPredicate(AttachmentSlots, [&](const UMounteaAdvancedAttachmentSlot* Slot) {
return Slot != nullptr && Slot->IsOccupied() && Slot->Attachment == Attachable;
});
return Found ? (*Found)->SlotName : NAME_None;
}
Event System¶
Event Delegates¶
// Attachment change events
UPROPERTY(BlueprintAssignable, Category="Attachment Container")
FOnAttachmentChanged OnAttachmentChanged;
UPROPERTY(BlueprintAssignable, Category="Attachment Container")
FOnSlotStateChanged OnSlotStateChanged;
UPROPERTY(BlueprintAssignable, Category="Attachment Container")
FOnContainerCleared OnContainerCleared;
Event Binding¶
// Bind to container events
void AEquippedCharacter::BeginPlay()
{
Super::BeginPlay();
if (AttachmentContainer)
{
AttachmentContainer->OnAttachmentChanged.AddDynamic(this, &AEquippedCharacter::OnAttachmentChanged);
AttachmentContainer->OnSlotStateChanged.AddDynamic(this, &AEquippedCharacter::OnSlotStateChanged);
AttachmentContainer->OnContainerCleared.AddDynamic(this, &AEquippedCharacter::OnContainerCleared);
}
}
// Event handlers
UFUNCTION()
void AEquippedCharacter::OnAttachmentChanged(const FName& SlotId, UObject* NewAttachment, UObject* OldAttachment)
{
if (NewAttachment)
{
// Handle new attachment
ProcessNewAttachment(SlotId, NewAttachment);
}
if (OldAttachment)
{
// Handle removed attachment
ProcessRemovedAttachment(SlotId, OldAttachment);
}
}
Common Patterns¶
Pattern 1: Equipment Auto-Assignment¶
Use Case
Automatically equipping items to the best available slot
bool AutoEquipItem(UObject* Equipment, const FGameplayTagContainer& ItemTags)
{
// Find best matching slot
FName BestSlot = AttachmentContainer->Execute_FindFirstFreeSlotWithTags(AttachmentContainer, ItemTags);
if (BestSlot.IsNone())
{
// No compatible slots available
return false;
}
// Attempt attachment
return AttachmentContainer->Execute_TryAttach(AttachmentContainer, BestSlot, Equipment);
}
Pattern 2: Slot Validation¶
Use Case
Validating attachment operations before execution
bool ValidateAttachment(const FName& SlotId, UObject* Equipment)
{
// Check slot exists
if (!AttachmentContainer->Execute_IsValidSlot(AttachmentContainer, SlotId))
{
LOG_WARNING(TEXT("Slot %s does not exist"), *SlotId.ToString());
return false;
}
// Check slot availability
if (AttachmentContainer->Execute_IsSlotOccupied(AttachmentContainer, SlotId))
{
LOG_WARNING(TEXT("Slot %s is already occupied"), *SlotId.ToString());
return false;
}
// Check equipment compatibility
if (auto AttachableComp = Equipment->FindComponentByClass<UMounteaAttachableComponent>())
{
auto Slot = AttachmentContainer->Execute_GetSlot(AttachmentContainer, SlotId);
if (!Slot->MatchesTags(AttachableComp->GetTags_Implementation(), true))
{
LOG_WARNING(TEXT("Equipment not compatible with slot %s"), *SlotId.ToString());
return false;
}
}
return true;
}
Pattern 3: Batch Operations¶
Use Case
Equipping multiple items efficiently
void EquipItemSet(const TArray<TPair<FName, UObject*>>& EquipmentPairs)
{
// Disable events temporarily
bool bEventsEnabled = AttachmentContainer->bBroadcastEvents;
AttachmentContainer->bBroadcastEvents = false;
// Perform all attachments
for (const auto& Pair : EquipmentPairs)
{
AttachmentContainer->Execute_TryAttach(AttachmentContainer, Pair.Key, Pair.Value);
}
// Re-enable events and send batch notification
AttachmentContainer->bBroadcastEvents = bEventsEnabled;
OnBatchEquipmentChanged.Broadcast(EquipmentPairs);
}
Performance Optimization¶
Slot Caching¶
// Cache frequently accessed slots
class UOptimizedAttachmentContainer : public UMounteaAttachmentContainerComponent
{
private:
mutable TMap<FName, UMounteaAdvancedAttachmentSlot*> SlotCache;
public:
virtual UMounteaAdvancedAttachmentSlot* GetSlot_Implementation(const FName& SlotId) const override
{
if (auto* CachedSlot = SlotCache.Find(SlotId))
return *CachedSlot;
auto* Slot = Super::GetSlot_Implementation(SlotId);
if (Slot)
SlotCache.Add(SlotId, Slot);
return Slot;
}
};
Efficient Queries¶
// Pre-filter slots by common tags
TArray<UMounteaAdvancedAttachmentSlot*> GetSlotsByTag(const FGameplayTag& Tag) const
{
TArray<UMounteaAdvancedAttachmentSlot*> FilteredSlots;
for (auto* Slot : AttachmentSlots)
{
if (Slot && Slot->HasTag(Tag))
{
FilteredSlots.Add(Slot);
}
}
return FilteredSlots;
}
Memory Management¶
Optional memory management would look like this, however, Unreal native reflection system should handle the memory for you.
// Clean up container and slots
void CleanupContainer()
{
// Clear all attachments first
Execute_ClearAll(this);
// Clean up slot references
for (auto& Slot : AttachmentSlots)
{
if (IsValid(Slot))
{
Slot->MarkPendingKill();
}
}
AttachmentSlots.Empty();
}
Troubleshooting¶
Slots Not Replicating
- Verify sub-object replication setup
- Check slot validation passes
- Ensure server authority for operations
Attachment Failures
- Validate slot names and tags
- Check component target resolution
- Verify attachment interface implementation
Performance Issues
- Cache frequent slot lookups
- Batch attachment operations
- Limit unnecessary event broadcasts
Memory Leaks
- Clean up slot references properly
- Unbind event delegates
- Clear attachment references
Best Practices¶
Design Guidelines
- Server Authority: Always validate on server for multiplayer
- Event Efficiency: Batch operations to reduce event spam
- Validation Early: Check compatibility before attempting attachment
- Clean Separation: Keep container logic separate from slot implementation
- Performance Aware: Cache expensive operations and validate optimization
Configuration Tips
- Use descriptive slot names for debugging
- Group related slots with consistent tag prefixes
- Configure attachment targets carefully
- Test slot configurations in editor validation
Next Steps¶
- Attachment Slots: Configure individual attachment points
- Attachable Components: Create attachable items and objects
- Equipment Component: Extend containers for equipment systems