Attachment Slots¶
What You'll Learn¶
- Configuring attachment slot properties and behaviors
- Understanding socket vs component attachment types
- Implementing tag-based compatibility filtering
- Managing slot states and replication
- Using editor integration and validation
- Optimizing slot performance and memory
Quick Start¶
// Slot configuration in container
{
SlotName: "MainHand",
SlotTags: "Equipment.Weapon.Melee",
SlotType: EAttachmentSlotType::EAST_Socket,
SocketName: "weapon_socket",
State: EAttachmentSlotState::EASS_Empty
}
// Runtime slot operations
auto Slot = Container->Execute_GetSlot(Container, "MainHand");
bool Success = Slot->Attach(WeaponActor);
Result
Configurable attachment points with tag filtering, state management, and automatic network replication.
Slot Architecture¶
Core Structure¶
UMounteaAdvancedAttachmentSlot
defines individual attachment points:
class UMounteaAdvancedAttachmentSlot : public UMounteaAdvancedAttachmentSlotBase
{
// Identity
FName SlotName; // Unique identifier
FGameplayTagContainer SlotTags; // Compatibility tags
FText DisplayName; // UI display name
// State
EAttachmentSlotState State; // Empty, Occupied, Locked
UObject* Attachment; // Currently attached object
// Configuration
EAttachmentSlotType SlotType; // Socket or Component
FName AttachmentTargetOverride; // Custom target component
FName SocketName; // Physical attachment point
// Relationships
TScriptInterface<IMounteaAdvancedAttachmentContainerInterface> ParentContainer;
};
Slot Types¶
Attachment Types
- Socket: Attaches to skeletal or static mesh sockets for visual positioning
- Component: Attaches to scene components for logical relationships
enum class EAttachmentSlotType : uint8
{
EAST_Socket, // Physical socket on mesh
EAST_Component // Scene component attachment
};
Slot Configuration¶
Basic Setup¶
Slots are configured as inline objects in attachment containers:
// Weapon slot configuration
{
SlotName: "MainHand",
SlotTags: "Equipment.Weapon.Melee",
DisplayName: "Main Hand",
SlotType: EAttachmentSlotType::EAST_Socket,
SocketName: "weapon_socket",
State: EAttachmentSlotState::EASS_Empty
}
// Armor slot configuration
{
SlotName: "Chest",
SlotTags: "Equipment.Armor.Body",
DisplayName: "Chest Armor",
SlotType: EAttachmentSlotType::EAST_Component,
AttachmentTargetOverride: "ArmorMeshComponent",
State: EAttachmentSlotState::EASS_Empty
}
Editor Integration¶
Slots provide dropdown helpers for configuration:
// Socket name dropdown
UFUNCTION()
TArray<FName> GetAvailableSocketNames() const
{
if (SlotType != EAttachmentSlotType::EAST_Socket || !IsValid(ParentContainer.GetObject()))
return TArray<FName>();
AActor* parentActor = ParentContainer->Execute_GetOwningActor(ParentContainer.GetObject());
if (!IsValid(parentActor))
return TArray<FName>();
FName targetName = AttachmentTargetOverride.IsNone() ?
ParentContainer->Execute_GetDefaultAttachmentTarget(ParentContainer.GetObject()) :
AttachmentTargetOverride;
return UMounteaAttachmentsStatics::GetAvailableSocketNames(parentActor, targetName);
}
// Component name dropdown
UFUNCTION()
TArray<FName> GetAvailableTargetNames() const
{
if (!IsValid(ParentContainer.GetObject()))
return TArray<FName>();
AActor* parentActor = ParentContainer->Execute_GetOwningActor(ParentContainer.GetObject());
return IsValid(parentActor) ? UMounteaAttachmentsStatics::GetAvailableComponentNames(parentActor) : TArray<FName>();
}
Auto-Configuration¶
Slots auto-populate from equipment settings when slot names are selected:
#if WITH_EDITOR
void UMounteaAdvancedAttachmentSlotBase::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
const FName propertyName = PropertyChangedEvent.Property ? PropertyChangedEvent.Property->GetFName() : NAME_None;
if (propertyName == GET_MEMBER_NAME_CHECKED(UMounteaAdvancedAttachmentSlotBase, SlotName))
{
if (auto EquipmentConfig = GetDefault<UMounteaAdvancedInventorySettings>()->EquipmentSettingsConfig.LoadSynchronous())
{
if (const FMounteaEquipmentSlotHeaderData* HeaderData = EquipmentConfig->AllowedEquipmentSlots.Find(SlotName))
{
SlotTags.Reset();
SlotTags.AppendTags(HeaderData->TagContainer);
DisplayName = HeaderData->DisplayName;
}
}
}
}
#endif
Configuration Dependencies
Slot auto-configuration requires valid equipment settings config. Missing config results in empty tags and display names.
State Management¶
Slot States¶
enum class EAttachmentSlotState : uint8
{
EASS_Empty, // Available for attachment
EASS_Occupied, // Has attached object
EASS_Locked // Disabled/unavailable
};
State Queries¶
// State checking methods
bool IsEmpty() const
{
return State == EAttachmentSlotState::EASS_Empty && Attachment == nullptr;
}
bool IsOccupied() const
{
return State == EAttachmentSlotState::EASS_Occupied && Attachment != nullptr;
}
bool IsLocked() const
{
return State == EAttachmentSlotState::EASS_Locked;
}
bool CanAttach() const
{
return IsSlotValid() && IsEmpty() && !IsLocked();
}
State Transitions¶
// Enable/disable slots
void DisableSlot()
{
if (!IsEmpty())
Detach();
State = EAttachmentSlotState::EASS_Locked;
}
// Attachment state changes
bool Attach(UObject* NewAttachment)
{
if (!CanAttach() || !IsValid(NewAttachment))
return false;
if (PerformAttachmentLogic(NewAttachment))
{
Attachment = NewAttachment;
State = EAttachmentSlotState::EASS_Occupied;
return true;
}
return false;
}
Attachment Logic¶
Physical Attachment¶
bool PerformPhysicalAttachment(UObject* Object, USceneComponent* Target) const
{
const FAttachmentTransformRules attachmentRules = FAttachmentTransformRules::SnapToTargetIncludingScale;
const FName attachmentName = GetAttachmentSocketName();
if (USceneComponent* sceneComp = Cast<USceneComponent>(Object))
{
sceneComp->AttachToComponent(Target, attachmentRules, attachmentName);
return true;
}
if (AActor* actor = Cast<AActor>(Object))
{
actor->AttachToComponent(Target, attachmentRules, attachmentName);
return true;
}
LOG_WARNING(TEXT("Unsupported attachment object type: %s"), *Object->GetName());
return false;
}
FName GetAttachmentSocketName() const
{
return SlotType == EAttachmentSlotType::EAST_Socket ? SocketName : NAME_None;
}
Attachment Target Resolution¶
void TryResolveAttachmentTarget()
{
if (AttachmentTargetOverride.IsNone() || !ParentContainer.GetObject())
return;
AActor* parentActor = ParentContainer->Execute_GetOwningActor(ParentContainer.GetObject());
if (IsValid(parentActor))
AttachmentTargetComponentOverride = UMounteaAttachmentsStatics::GetAvailableComponentByName(parentActor, AttachmentTargetOverride);
}
USceneComponent* GetAttachmentTargetComponent() const
{
return AttachmentTargetComponentOverride ?
AttachmentTargetComponentOverride :
ParentContainer->Execute_GetAttachmentTargetComponent(ParentContainer.GetObject());
}
Validation System¶
bool ValidateAttachmentSlot(const USceneComponent* Target) const
{
if (SlotType == EAttachmentSlotType::EAST_Socket && !Target->DoesSocketExist(SocketName))
{
LOG_WARNING(TEXT("Socket '%s' does not exist on target component '%s'!"), *SocketName.ToString(), *Target->GetName());
return false;
}
return true;
}
bool IsValidForAttachment(const UObject* NewAttachment)
{
if (!IsValid(NewAttachment))
{
LOG_ERROR(TEXT("Attachment is not valid!"));
return false;
}
if (!NewAttachment->Implements<UMounteaAdvancedAttachmentAttachableInterface>())
{
LOG_WARNING(TEXT("Attachable does not implement required interface!"))
return true; // Allow but warn
}
if (!IMounteaAdvancedAttachmentAttachableInterface::Execute_CanAttach(NewAttachment))
{
LOG_WARNING(TEXT("Attachable object is not compatible with selected Slot!"));
return false;
}
return true;
}
Tag Compatibility¶
Tag Matching¶
// Flexible tag matching
bool MatchesTags(const FGameplayTagContainer& Tags, const bool bRequireAll) const
{
return bRequireAll ? SlotTags.HasAll(Tags) : SlotTags.HasAny(Tags);
}
bool HasTag(const FGameplayTag& Tag) const
{
return SlotTags.HasTag(Tag);
}
Common Tag Patterns¶
// Hierarchical tags enable flexible matching
SlotTags: "Equipment.Weapon" // Accepts any weapon
ItemTags: "Equipment.Weapon.Sword" // Specific weapon type
SlotTags: "Equipment.Weapon.Melee" // Melee weapons only
ItemTags: "Equipment.Weapon.Ranged" // Won't match
SlotTags: "Equipment.Weapon.OneHanded" // Size restriction
ItemTags: "Equipment.Weapon.TwoHanded" // Size mismatch
Interface Integration¶
void HandleAttachableInterface(UObject* NewAttachment)
{
TScriptInterface<IMounteaAdvancedAttachmentAttachableInterface> attachableInterface = FindAttachableInterface(NewAttachment);
if (attachableInterface.GetObject())
IMounteaAdvancedAttachmentAttachableInterface::Execute_AttachToSlot(attachableInterface.GetObject(), ParentContainer, SlotName);
else
LOG_WARNING(TEXT("Attachment does not implement the attachable interface!"));
}
TScriptInterface<IMounteaAdvancedAttachmentAttachableInterface> FindAttachableInterface(UObject* Object)
{
TScriptInterface<IMounteaAdvancedAttachmentAttachableInterface> returnAttachable;
if (!IsValid(Object))
return returnAttachable;
// Check object directly
if (Object->GetClass()->ImplementsInterface(UMounteaAdvancedAttachmentAttachableInterface::StaticClass()))
{
returnAttachable.SetObject(Object);
returnAttachable.SetInterface(Cast<IMounteaAdvancedAttachmentAttachableInterface>(Object));
return returnAttachable;
}
// Check actor components
const AActor* targetActor = Cast<AActor>(Object);
if (IsValid(targetActor))
{
const auto actorComponents = UMounteaAttachmentsStatics::GetAvailableComponents(targetActor);
auto foundComponent = Algo::FindByPredicate(actorComponents, [](const UActorComponent* Comp) {
return IsValid(Comp) && Comp->GetClass()->ImplementsInterface(UMounteaAdvancedAttachmentAttachableInterface::StaticClass());
});
if (foundComponent)
returnAttachable = *foundComponent;
}
return returnAttachable;
}
Network Replication¶
Slot Replication¶
// Slot properties replicated automatically
UPROPERTY(ReplicatedUsing=OnRep_State, VisibleAnywhere, BlueprintReadWrite, Category="Settings")
EAttachmentSlotState State;
UPROPERTY(Replicated, BlueprintReadOnly, Category="Debug", AdvancedDisplay)
TObjectPtr<UObject> Attachment;
// Replication registration
void UMounteaAdvancedAttachmentSlotBase::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
UObject::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UMounteaAdvancedAttachmentSlotBase, Attachment);
DOREPLIFETIME(UMounteaAdvancedAttachmentSlotBase, State);
}
Replication Callbacks¶
UFUNCTION()
void OnRep_State()
{
switch (State)
{
case EAttachmentSlotState::EASS_Occupied:
if (Attachment)
ForceAttach(Attachment); // Re-attach on client
break;
case EAttachmentSlotState::EASS_Empty:
ForceDetach(); // Remove attachment on client
break;
}
// Notify container of change
if (ParentContainer.GetObject())
ParentContainer->GetOnAttachmentChangedEventHandle().Broadcast(SlotName, Attachment, LastAttachment);
}
Network Function Support¶
// Support for networked function calls
int32 GetFunctionCallspace(UFunction* Function, FFrame* Stack) override
{
if (HasAnyFlags(RF_ClassDefaultObject) || !IsSupportedForNetworking())
return GEngine->GetGlobalFunctionCallspace(Function, this, Stack);
return GetOuter()->GetFunctionCallspace(Function, Stack);
}
bool CallRemoteFunction(UFunction* Function, void* Parms, FOutParmRec* OutParms, FFrame* Stack) override
{
AActor* owningActor = GetOwningActor();
if (UNetDriver* netDriver = owningActor->GetNetDriver())
{
netDriver->ProcessRemoteFunction(owningActor, Function, Parms, OutParms, Stack, this);
return true;
}
return false;
}
Detachment Operations¶
Standard Detachment¶
bool Detach()
{
if (!IsOccupied())
return false;
PerformDetachment();
Attachment = nullptr;
State = EAttachmentSlotState::EASS_Empty;
return true;
}
bool ForceDetach()
{
PerformDetachment();
Attachment = nullptr;
State = EAttachmentSlotState::EASS_Empty;
return true;
}
Detachment Logic¶
void PerformDetachment()
{
UObject* targetAttachment = Attachment ? Attachment : LastAttachment;
// Physical detachment
if (USceneComponent* sceneComp = Cast<USceneComponent>(targetAttachment))
sceneComp->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
else if (AActor* actor = Cast<AActor>(targetAttachment))
actor->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
// Interface state update
if (IsValid(targetAttachment) && targetAttachment->Implements<UMounteaAdvancedAttachmentAttachableInterface>())
IMounteaAdvancedAttachmentAttachableInterface::Execute_SetState(targetAttachment, EAttachmentState::EAS_Detached);
}
Validation and Data Integrity¶
Editor Validation¶
#if WITH_EDITOR
EDataValidationResult IsDataValid(FDataValidationContext& Context) const override
{
if (!IsSlotValid())
Context.AddError(NSLOCTEXT("AttachmentSlot", "AttachmentSlot_SlotInvalid", "Slot is invalid! It must have a valid name and tags."));
if (SlotType == EAttachmentSlotType::EAST_Socket && SocketName.IsNone())
Context.AddError(NSLOCTEXT("AttachmentSlot", "AttachmentSlot_NoSocket", "Socket-type slots must specify a socket name."));
if (SlotTags.IsEmpty())
Context.AddWarning(NSLOCTEXT("AttachmentSlot", "AttachmentSlot_NoTags", "Slot has no tags - nothing will be able to attach."));
return Context.GetNumErrors() > 0 ? EDataValidationResult::Invalid : EDataValidationResult::Valid;
}
#endif
Runtime Validation¶
bool IsSlotValid() const
{
return ParentContainer.GetObject() != nullptr && !SlotName.IsNone() && !SlotTags.IsEmpty();
}
Performance Optimization¶
Efficient Queries¶
// Cache attachment target resolution
void BeginPlay_Implementation() override
{
Super::BeginPlay_Implementation();
TryResolveAttachmentTarget(); // Cache target component
}
// Inline state checks
FORCEINLINE bool CanAttach() const
{
return IsSlotValid() && IsEmpty() && !IsLocked();
}
FORCEINLINE bool HasTag(const FGameplayTag& Tag) const
{
return SlotTags.HasTag(Tag);
}
Memory Management¶
// Clean attachment references
void CleanupSlot()
{
if (Attachment)
{
Detach();
}
Attachment = nullptr;
LastAttachment = nullptr;
AttachmentTargetComponentOverride = nullptr;
}
Common Patterns¶
Pattern 1: Conditional Attachment¶
// Override attachment logic for special conditions
bool ConditionalAttach(UObject* NewAttachment)
{
// Check prerequisites
if (!CheckAttachmentPrerequisites(NewAttachment))
return false;
// Perform standard attachment
return Attach(NewAttachment);
}
bool CheckAttachmentPrerequisites(UObject* NewAttachment)
{
// Example: Two-handed weapons require empty off-hand
if (SlotName == "MainHand" && HasTag(FGameplayTag::RequestGameplayTag("Weapon.TwoHanded")))
{
auto OffHandSlot = ParentContainer->Execute_GetSlot(ParentContainer.GetObject(), "OffHand");
return OffHandSlot && OffHandSlot->IsEmpty();
}
return true;
}
Pattern 2: Smart Socket Selection¶
// Automatically select best socket for attachment type
FName SelectBestSocket(UObject* Attachment)
{
if (SlotType != EAttachmentSlotType::EAST_Socket)
return NAME_None;
// Get attachment size/type tags
if (auto AttachableComp = Attachment->FindComponentByClass<UMounteaAttachableComponent>())
{
auto Tags = AttachableComp->GetTags_Implementation();
// Select socket based on item size
if (Tags.HasTag(FGameplayTag::RequestGameplayTag("Size.Large")))
return "large_socket";
else if (Tags.HasTag(FGameplayTag::RequestGameplayTag("Size.Small")))
return "small_socket";
}
return SocketName; // Default socket
}
Troubleshooting¶
!!! warning "Socket Not Found - Verify socket exists on target mesh - Check attachment target component resolution - Validate socket name spelling
Tag Mismatch
- Check tag hierarchy compatibility
- Verify slot and item tag configuration
- Use tag debugging tools
Replication Problems
- Ensure proper sub-object registration
- Validate OnRep callbacks fire
- Check network authority
Validation Failures
- Review editor validation messages
- Check slot configuration completeness
- Verify parent container setup
Best Practices¶
Design Guidelines
- Descriptive Names: Use clear, descriptive slot names
- Hierarchical Tags: Design tag hierarchies for flexibility
- Socket Validation: Always validate socket existence
- Performance: Cache expensive operations
- Network Efficiency: Minimize replication frequency
Next Steps¶
- Attachable Components: Create attachable items and objects
- Equipment Component: Extend containers for equipment systems
- Tag Configuration: Set up compatibility tag hierarchies