I’m starting to work with Unreal Engine 5 multiplayer and I’m trying to serialize my equipment items to send them back to the client using RPC. However, I’ve noticed that the client’s PostReplicateAdd() and PostReplicatedChange() functions are never being called.
In other words, the replicated array (or struct) doesn’t seem to trigger replication callbacks on the client side.
Here’s the relevant part of my class code:
EquipmentComponent.h
USTRUCT(BlueprintType)
struct FRPGEquipmentEntry : public FFastArraySerializerItem
{
GENERATED_USTRUCT_BODY()
UPROPERTY(BlueprintReadOnly)
FGameplayTag EntryTag = FGameplayTag();
UPROPERTY(BlueprintReadOnly)
FGameplayTag RarityTag = FGameplayTag();
private:
UPROPERTY()
TObjectPtr Instance = nullptr;
};
DECLARE_MULTICAST_DELEGATE_OneParam(FOnEquipmentEntrySignature, const FRPGEquipmentEntry& /*Equipment Entry*/);
USTRUCT()
struct FRPGEquipmentList : public FFastArraySerializer
{
GENERATED_USTRUCT_BODY()
FRPGEquipmentList() :
OwnerComponent(nullptr)
{}
FRPGEquipmentList(UEquipmentManagerComponent* InComponent) :
OwnerComponent(InComponent)
{}
URPGAbilitySystemComponent* GetAbilitySystemComponent();
UEquipmentInstance* AddEntry(const TSubclassOf& InEquipmentDefinition, const TArray& StatEffects);
void RemoveEntry(UEquipmentInstance* InEquipmentInstance);
// FFastArraySerializer Contract
void PreReplicatedRemove(const TArrayView RemovedIndices, int32 FinalSize);
void PostReplicateAdd(const TArrayView AddedIndices, int32 FinalSize);
void PostReplicatedChange(const TArrayView ChangedIndices, int32 FinalSize);
bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParams)
{
return FastArrayDeltaSerialize(Entries, DeltaParams, *this);
}
FOnEquipmentEntrySignature EquipmentEntryDelegate;
FOnUnEquippedEntry UnEquippedEntryDelegate;
private:
UPROPERTY(NotReplicated)
TObjectPtr OwnerComponent;
UPROPERTY()
TArray Entries;
};
template<>
struct TStructOpsTypeTraits : TStructOpsTypeTraitsBase2
{
enum
{
WithNetDeltaSerializer = true
};
};
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class RPGSYSTEMS_API UEquipmentManagerComponent : public UActorComponent
{
GENERATED_BODY()
public:
UPROPERTY(Replicated)
FRPGEquipmentList EquipmentList;
EquipmentComponent.cpp
UEquipmentManagerComponent::UEquipmentManagerComponent() :
EquipmentList(this)
{
PrimaryComponentTick.bCanEverTick = true;
SetIsReplicatedByDefault(true);
}
void UEquipmentManagerComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UEquipmentManagerComponent, EquipmentList);
}
UEquipmentInstance* FRPGEquipmentList::AddEntry(const TSubclassOf& InEquipmentDefinition,
const TArray& StatEffects)
{
check(InEquipmentDefinition);
check(OwnerComponent);
check(OwnerComponent->GetOwner()->HasAuthority());
const UEquipmentDefinition* EquipmentCTO = GetDefault(InEquipmentDefinition);
TSubclassOf InstanceType = EquipmentCTO->InstanceType;
if (!IsValid(InstanceType))
{
InstanceType = UEquipmentInstance::StaticClass();
}
for (auto EntryIt = Entries.CreateIterator(); EntryIt; ++EntryIt)
{
FRPGEquipmentEntry& Entry = *EntryIt;
if (Entry.SlotTag.MatchesTagExact(EquipmentCTO->SlotTag))
{
RemoveEntry(Entry.Instance);
break;
}
}
FRPGEquipmentEntry& NewEntry = Entries.AddDefaulted_GetRef();
NewEntry.EntryTag = EquipmentCTO->ItemTag;
NewEntry.RarityTag = EquipmentCTO->RarityTag;
NewEntry.SlotTag = EquipmentCTO->SlotTag;
NewEntry.Instance = NewObject(OwnerComponent->GetOwner(), InstanceType);
MarkItemDirty(NewEntry);
EquipmentEntryDelegate.Broadcast(NewEntry);
return NewEntry.Instance;
}
void FRPGEquipmentList::PostReplicateAdd(const TArrayView AddedIndices, int32 FinalSize)
{
for (const int32 Index : AddedIndices)
{
const FRPGEquipmentEntry& Entry = Entries[Index];
EquipmentEntryDelegate.Broadcast(Entry);
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red,
FString::Printf(TEXT("Equipped Item: %s"), *Entry.EntryTag.ToString()));
}
}
this component is attached to PlayerController
ARPGPlayerController::ARPGPlayerController()
{
bReplicates = true;
EquipmentComponent = CreateDefaultSubobject(TEXT("EquipmentManagerComponent"));
EquipmentComponent->SetIsReplicated(true);
}
void ARPGPlayerController::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ARPGPlayerController, EquipmentComponent);
}