Replicating TArrays crashes game

I created an ActorComponent “MyActorComponent” with an Array of UObjects “UMyObject” that need to be replicated. I overrode the IsSupportedForNetworking and the GetLifetimeRepProperties on the UObject.

(this is part psuedo code)

class MyUObject : public UObject
{

virtual void IsSupportedForNetworking() override
{
[INDENT]return true;
}

UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Replicated, Category=“My Object”
int32 bReplicatedUProperty;

void GetLifetimeReplicationProps(outLifetimeProps) const
{

Super::GetLifetimeReplicationProps(outLifetimeProps);

DOREPLIFETIME(MyUObject, bReplicatedUProperty);

}
[/INDENT]
}

Then in MyActorComponent I do pretty much the samething; SetIsReplicated(true) and override GetLifetimeReplicationProps and override the ReplicateSubobjects function

class MyActorComponent
{

MyActorComponent(SubobjectBuilderThinggy)
{
[INDENT]
this->SetIsReplicated(true);

}

UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Replicated, Category=“My Actor Component”)
TArray<UMyObject*> myObjects;

void GetLifetimeReplicationProps(outLifetimeProps) const
{

Super::GetLifetimeReplicationProps(outLifetimeProps);

DOREPLIFETIME(MyUObject, bReplicatedUProperty);

}

virtual bool ReplicateSubobjects(ActorChannel, Bunch, RepFlags) override
{

bool wroteSomething = Super::ReplicateSubobjects(ActorChannel, Bunch, RepFlags);

for(int32 i = 0; i < myObjects.Num(); ++i)
{
[INDENT]
wroteSomething |= ActorChannel->ReplicateSubobject(myObjects*, *Bunch, *RepFlags);

}
return wroteSomething
[/INDENT]
}

[/INDENT]
}

My Character Actor Creates the Component at BeginPlay and sets it to a property that is replicated and in the override of GetLifetimeReplicationProps is setup with DOREPLIFETIME. When I run the game the replication of the TArray crashes the game. When change the TArray to a single object and fix the corresponding objects, the game runs and replicates the value of the single object without problem.

Honestly I would really just love an overview of how anyone else has done TArray<UObject> replication.

Okay, I’m not entirely sure I follow you. I am not sure how I am changing what is replicated at Runtime. If I use this exact same setup with a single UObject instead of an array of UObjects the replication happens without a problem. It is when I make the “myObjects” variable to a TArray<UMyObject*> instead f a normal UMyObject * (In the component).

So ultimately my question is how do you properly replicate an array of UObjects?

Ah okay I see the confusion now. My owning actor has a UPROPERTY for the component setup before hand and that is set to be replicated from the get go. The property is initialized as NULL but is then created and set in Begin Play by the server. This seems to work fine as long as my component is not replicating a TArray within it. I setup a test in my ActorComponent where instead of an array I used a Single replicated UPROPERTY pointer that also was created on initialization of the component. This test was successful as the value was replicated to my client when it was changed on the server. This leads me to believe that as long as the property itself is set to be replicated then it can be null. My TArrays are always non pointers so having it exist before runtime is not an issue, it’s contents however are a different matter haha.

Okay I’ll try the DOREPTARRAY but I noticed the documentation you gave it looks like it requires the override of GetReplicationList() is that what you ment to put down?. Also a rep function OnRep_[MyArray] but no example of that function in the documentation.

My final confusion on this comes from the UAbilitySystemComponent that serializes and replicates a list of UAbility objects

Documentation - UAbilitySystemComponent - here there is a property that is replicated called AllReplicatedInstancedAbilities which is a TArray of UGameplayAbility.

In UAbilitySystemComponent::ReplicateSubobjects we see the list of AllReplicatedInstancedAbilities iterated through and added to the actor’s replication channel.

In the Documentation - UGameplayAbility I see no properties marked for replication or anything special about it… Just an object derrived from UObject.

I have just started by grand foray into replication so pardon me if my supplemental questions seem elementary :slight_smile: Thanks.

Okay so I finally successfully replicated a TArray of Objects, it is fairly simple once you get your head around Replication.
Begin with defining the UObject you want replicated. I actually create a UNetworkObject as a base class for all UObjects I want replicated. But the quick and dirty way is to simply override the IsSupportedForNetworking Then mark the properties you need replicated as UPROPERTY(Replicated)




class UNetworkObject : UObject
{
    virtual bool IsSupportedForNetworking() override;

    virtual bool ReplicateSubobjects(AActorChannel * Channel, FOutBunch * Bunch, FReplicationFlags * RepFlags); // note no override because this is the FIRST declaration of this function.
}



With your Replicatable UObject setup you can now replicate the property from a parent Component or Actor… But What if your parent object IS another UObject? You will need to provide an implementation of ReplicateSubobjects




bool UNetworkObject::ReplicateSubobjects(AActorChannel * Channel, FOutBunch * Bunch, FReplicationFlags * RepFlags)
{
    return false;
}



Now this base object can replicate it’s own subobjects if you derive new objects from it. A bit of confusion arose for me because I did not have to declare GetLifetimeReplicatedProps, but a compile time error will happen if you don’t define that function when you have a uproperty marked replicated.

The following example demonstrates how to Replicate a TArray of subobjects from within another UObject using an ActorComponent (whch uses an AActor object) for replication.

MyTestObject.h
My Test object is a simple UObject that needs to replicate some information, but is complex enough that it does not warrent a simple Struct to do the job. For demonstration purposes I chose a very simple object.




class UMyTestObject : UNetworkObject
{
    UPROPERTY(Replicated)
    float MyTestProperty;
}



MyTestObject.cpp





void UMyTestObject::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const
{
    DOREPLIFETIME(MyTestObject, MyTestProperty);
}



Now my Second UObject that is the outer to "MyTestObject.

MyTestObjectManager.h
The TestObjectManager simply contains a bunch of MyTestObjects. Ideally this class would also provide some accessor functionality but for demonstration purposes I have left that out.




class UMyTestObjectManager : UNetworkObject
{
    UPROPERTY(Replicated)
    TArray<UMyTestObject*> MyTestObjects;

    void BuildArrayOfTestObjects(int32 numObjects); // I'm not going to define this for demonstration sake, it would just create a bunch of UMyTestObjects and add them to the array.

    virtual bool ReplicateSubobjects(AActorChannel * Channel, FOutBunch * Bunch, FReplicationFlags * RepFlags) override; // override from the base class
}



MyTestObjectManager.cpp





void UMyTestObjectManager::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const
{
    DOREPLIFETIME(UMyTestObjectManager, MyTestObjects);
}

bool UMyTestObjectManager::ReplicateSubobjects(AActorChannel * Channel, FOutBunch * Bunch, FReplicationFlags * RepFlags)
{
    bool wroteSomething = Super::ReplicateSubobjects(Channel, Bunch, RepFlags);

    for(UMyTestObject * myObject : MyTestObjects)
    {
        if(myObject)
       {
           Channel->ReplicateObject(myObject, *Bunch, *RepFlags)
       }
    }
}



Okay now I don’t know exactly the details of what this is doing on the back end but think I’ve pretty much figured out why it works. First MyTestObject has a variable that freely replicates because it is a simple variable type (bool, int, float, struct). MyTestObjectManager contains an array of MyTestObjects. Now the TArray is marked to be replicated and TArrays support replication . The replication of THAT array occurs, but it’s contents do not because they are not simple variable types but instead pointers to UObjects. That is where ReplicateSubobjects comes into play. It iterates over each object in the array and replicates it to the client.

“But you said UObjects don’t replicate, your MyTestObjectManager is a UObject, How is it replicating?” - Well my friend we are not done.

The actual replication for this REQUIRES either an Actor OR a ActorComponent. Either way the procedure is the same.

MyActorComponent.h
MyActorComponent simply demonstrates how to do replication with a Component. You can just as easily use an AActor.




class UMyActorComponent : UActorComponent
{
    UMyActorComponent();

    virtual void InitializeComponent() override;

    UMyTestObjectManager * myTestObjectManager;

    virtual bool ReplicateSubobjects(AActorChannel * Channel, FOutBunch * Bunch, FReplicationFlags * RepFlags) override;
}



MyActorComponent.cpp




    UMyActorComponent::UMyActorComponent()
    {
        this->bWantsInitializeComponent = true;
        this->SetIsReplicated(true);
    }

    void UMyActorComponent::InitializeComponent()
    {
        this->myTestObjectManager = ConstructObject<UMyTestObjectManager>(UMyTestObjectManager::StaticClass(), this);
        if(this->myTestObjectManager)
        {
             this->myTestObjectManager->BuildArrayOfTestObjects(15);
        }
    }

    void UMyActorComponent::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const
    {
        DOREPLIFETIME(UMyActorComponent, myTestObjectManager);
    }

    bool UMyActorComponent::ReplicateSubobjects(AActorChannel * Channel, FOutBunch * Bunch, FReplicationFlags * RepFlags)
    {
        bool wroteSomething = Super::ReplicateSubobjects(Channel, Bunch, RepFlags);
        wroteSomething |= Channel->ReplicateSubobject(myTestObjectManager, *Bunch, *RepFlags); // replicate the subobject
        wroteSomething |= myTestObjectManager->ReplicateSubobjects(Channel, Bunch, RepFlags); // replicate the subobject's subobjects.

        return wroteSomething;
    }


Now one thing to note. Remote Procedure Calls on a UObject (Client or Server functions) cannot be performed (at least to my knowledge and without some major coding upgrades made by you). I have heard it theoretically possible… but the technical process currently is beyond where I want (or need) to go. So if you want to do RPC functions you must do them on an Actor or a UActorComponent. For instance lets say you want to set the value:



UMyActorComponent->myTestObjectManager->myTestObjects[0]->MyTestProperty = 25.22f


The cool thing is this will actually replicate up. But what if all these properties are not public and you ONLY want the setting of this to be performed on the Server? You’d have to setup a daisy chain starting from the UMyActorComponent.



void UMyActorComponent::SetValueAtObjectIndex(int32 objectIndex, float value)
{
    if(this->Role == RP_Authority)
    {
        this->myTestObjectManager->SetValueAtIndex(objectIndex, value);
    }
    else
    {
        this->ServerSetValueAtObjectIndex(objectIndex, value);
    }
}

bool UMyActorComponent::ServerSetValueAtObjectIndex_Validate(int32 objectIndex, float value)
{
    // do some logic to validate the user is not cheating
    return true; // return false if you want to automatically kick the cheater from your game.
}

void UMyActorComponent::ServerSetValueAtObjectIndex_Implementation(int32 objectIndex, float value)
{
    this->SetValueAtObjectIndex(objectIndex, value) // no point in duplicating code, this should be the authority now.
}


This will effectively change the value on the server, and then replicate the MyTestObject down to the client.

I wonder, will this replication take care of creating/destroying objects when tarray size is changed?

Yes it works the same as if an Array of actors or actor components are destroyed through replication. Actors and Actor components are both UObject based and the UObject provides Actors and Actor Components the interface for replication. The difference is that UObjects do not implement the interface they provide. There is a catch, Array replication will call Rep Notifies twice, once for when the array size is set or changed and gain when an element in the array has been added and removed. So when using OnReps that trigger other events I always check that my array does not have an empty element. I’ve actually built my entire game framework using this system of replicated UObjects now and it works very well. Also Epic’s Ability Subsystem leverages UObject replication and TArrays as well.

Hey, I am having trouble replicating an array of objects. I have an actor (Object manager) and a (NetworkObject). The Newtwork object has the IsSupportedForNetworking function override and the replicated properties using GetLifetimeReplicatedProps. The Object manager has an array of the NetworkObject that is replicated and using GetLifetimeReplicatedProps aswell. The array is not replicating properly. I get all null pointers in the client. I have not used the ReplicateSubobjects function in any of these ones. Any ideas?
NVM, i got it working. I needed to override ReplicateSubobjects in the manager. Thanks for this post. Really helped.

I have an update to this. If you need to make Blueprint versions of your Networked UObject with their own replicated properties you will need to add an additional line to the GetLifetimeReplicatedProps function to get the blueprint replicated properties. The function will need to look like the following:

void UMyTestObject::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	UBlueprintGeneratedClass * bpClass = Cast<UBlueprintGeneratedClass>(this->GetClass());
	if (bpClass != nullptr)
	{
		bpClass->GetLifetimeBlueprintReplicationList(OutLifetimeProps);
	}

	DOREPLIFETIME(UMytestObject, MyTestProperty);
}

@ ArcainOne:
I just tried your implementation, my UObject array was replicated and the rep notify function was called, the client array size is correct, but the contents were all null pointers.
Any clue for this? Thanks.

Yep, The array size will replicate first, so there will be nulls in the array if it has grown. Your rep notify will fire once for the array size change and again when the elements are populated. you can do a myArray.Contains(nullptr) to see if there are null values, or check during what ever operation you are performing that the retrieved element is not null.

Ok, It works! I forgot you mentioned that it will notify twice, BTW the AActionChannel type now changed to UActorChannel. Thanks.