Getting non-Actor subobject replication to work

I have got a state object in my custom player state that needs to be replicated across when it changes. This is something the engine can do, as referenced in the docs. Those are slightly out of date, but so far I have implemented ReplicateSubObject and something is happening.

The SubObject in question is UWeaponState, and is created in its parent state in a PostInitializeComponents() call.

void ARobotPlayerState::initWeaponStates()
{
	if (WeaponMounts.Num() == 0)
	{
		for (int i = 0; i < NumberOfWeaponMounts; i++)
		{
			FName key = FName(*FString::Printf(TEXT("mount_%02d"), i));

			UWeaponState  *state = NewObject<UWeaponState>();
			state->mountName = key;
			state->weaponName = NAME_None;
			state->RepObjId = i + 1;
			state->SetFlags(RF_RootSet | RF_WasLoaded);

			WeaponMounts.Add(key, state);
		}
	}
}

Like so. This function is called from PostInitializeComponents(). Setting the flags like that does two things - lets it be eligible for replication, and let it not be GCed. This is the bad way to do it. I know how to do the GC, how do I properly get it to not fire " Assertion failed: Object->IsSupportedForNetworking()" when I try to replicate without the RF_WasLoaded flag?

Here’s the function in RobotPlayerState that does the sub object replication:

bool ARobotPlayerState::ReplicateSubobjects(UActorChannel *Channel, FOutBunch *Bunch, FReplicationFlags *RepFlags)
{
	check(Channel);
	check(Bunch);
	check(RepFlags);

	bool WroteSomething = false;

	if (Channel->KeyNeedsToReplicate(0, WeaponMountsRepKey))	// Does the array need to replicate?
	{
		TArray<FName> keys;

		WeaponMounts.GetKeys(keys);

		for (auto wmIt = keys.CreateIterator(); wmIt; wmIt++)
		{
			FName key = *wmIt;

			UWeaponState * weaponState = WeaponMounts[key];
			if (Channel->KeyNeedsToReplicate(weaponState->RepObjId, weaponState->RepKey))
			{
				WroteSomething |= Channel->ReplicateSubobject(weaponState, *Bunch, *RepFlags);
			}
		}
	}

	return WroteSomething;
}

Now, when I try to replicate with those flags in place, I get an error and the connection is closed. It’s here:

LogNetTraffic:Error: UActorChannel::ReadContentBlockHeader: Sub-object not in parent actor 1. SubObj: WeaponState_1, Actor: RobotPlayerState_2
LogNet:Error: UActorChannel::ReceivedBunch: ReadContentBlockHeader FAILED. Bunch.IsError() == TRUE.  Closing connection.

What’s the proper way to set this up, so that it actually works?

EDIT: The header of UWeaponState so it’s clear what I am trying to replicate.

DECLARE_DELEGATE_OneParam(FWeaponStateChanged, UWeaponState *);

UCLASS()
class UWeaponState : public UObject
{
	GENERATED_UCLASS_BODY()
private:
	UFUNCTION()
	void OnRep_Weapon();
public:
	FWeaponStateChanged OnChange;

	~UWeaponState();

	int32 RepObjId;

	int32 RepKey;

	UPROPERTY(replicated, ReplicatedUsing=OnRep_Weapon)
	FName weaponName;

	UPROPERTY(replicated)
	FName mountName;

	bool IsSupportedForNetworking() const override;
};

You can just override UWeaponState::IsSupportedForNetworking and return true if all of your UWeaponStates are expected to be replicated.

I also wouldn’t set the RF_RootSet flag - the better way to keep them from being GC’d is to keep them in a TArray on the owning actor. You seem to be using a TMap (which is fine) but that is not a uproperty, so it won’t keep hard references to UObjects.

Finally, I would be careful in ARobotPlayerState::initWeaponStates, you should only create these subobjects on the server/authority. The server will then tell the client to create those subobjects when they are replicated.

Try that and see if you still get that disconnect message. If you do - are you getting it on the client or the server?

Finally, I would be careful in ARobotPlayerState::initWeaponStates, you should only create these subobjects on the server/authority. The server will then tell the client to create those subobjects when they are replicated.

How could it possibly do that? Since it’s not in a UPROPERTY there’s no magic. WHere’s the code that does this?

The docs I referenced mention a method to implement (OnSubobjectCreatedFromReplication) - I take it I would have to implement that?

Also, it seems very odd to create an objects sub objects on the server. I don’t make any kind of role check for the USkeletalMeshComponents that are subobjects of the player pawn. Those are created in the constructor and I imagine are created on the client side.

Ok, now after a few hours of tinkering.

I’ve changed from creating the UWeaponStates up front to creating them only when a weapon needs to be mounted.

I’ve implemented OnSubobjectCreatedFromReplication and OnSubobjectDestroyFromReplication.

I’ve restructured the code to work more as you’ve implied, with more role checks. This works differently than I expected. This model assumes that the server is authoritative over everything, even stuff that the client should in some models be able to handle itself.

In any case, the new subobject gets created and then gets replicated. However, it gets replicated with NAME_None in both fields.

void ARobotPlayerState::OnSubobjectCreatedFromReplication(UObject *NewSubobject)
{
	TRACE("Got new suboject!");

	UWeaponState *weaponState = Cast<UWeaponState>(NewSubobject);

	if (weaponState != NULL)
	{
		WeaponMounts.Add(weaponState);

		if (weaponState->mountName != NAME_None)
		{
			OnWeaponChanged.ExecuteIfBound(weaponState->mountName, weaponState->weaponName);
		} else
		{
			TRACE("Skipped informing pawn, mount name is NONE");
		}

	}
}

Those fields are initialized immediately after creation.

	TRACE("Creating new WeaponState %s/%s", *mountName.ToString(), *weaponName.ToString());

	weaponState = NewObject<UWeaponState>();
	weaponState->mountName = mountName;
	weaponState->weaponName = weaponName;
	weaponState->RepObjId = WeaponMounts.Num() + 1;
	weaponState->RepKey = 1;

Any ideas?

Are the UPROPERTY(replicated) fields on UWeaponState even doing anything, since it’s not an AActor? Do I need to be generating my own ObjectReplicator here?

I’m in a similar situation to you, but I’ve managed to successfully replicate an instanced UObject on each player pawn. Here are my findings so far, and what I think your problems could be.

//in RobotPlayerState.h

UCLASS()
class ARobotPlayerState 
{
    //....
    //TMap's are not supported for replication, so any thing you put in them will never be considered for replication. I haven't tested it, but IIRC the documentation says TArray's can be replicated, so try that (or better yet, try a single object and get it to work, then try arrays).
    UPROPERTY(Replicated)
    TArray<UWeaponState*> WeaponMounts;

}

//in RobotPlayerState.cpp

void ARobotPlayerState::initWeaponStates()
{
    //....
    // create with actor outer to avoid subobject not in actor error
    weaponState = NewObject<UWeaponState>(this);

    //It seems like you're just going to have to use this flag, as line 1361 of PackageMapClient.cpp doesn't allow you to have dynamic replicated UObjects, even though it still seems to work fine for me.
    weaponState->SetFlags(RF_WasLoaded);
    //...
}
bool ARobotPlayerState::ReplicateSubobjects(UActorChannel *Channel, FOutBunch *Bunch, FReplicationFlags *RepFlags){
    //...
    //try just always replicating everything to see if its working at all, then try optimising it with conditional replication
    UWeaponState * weaponState = WeaponMounts[key];
    //if (Channel->KeyNeedsToReplicate(weaponState->RepObjId, weaponState->RepKey))
    //{
        WroteSomething |= Channel->ReplicateSubobject(weaponState, *Bunch, *RepFlags);
    //}

    //...
}

//it seems like whenever a newsubobject is created through replication, it's created with default values from the constructor then passed to this function. It is assigned with the replicated values some time after this call. In my tests, overriding this function isn't necessary and you shouldn't use it.

void ARobotPlayerState::OnSubobjectCreatedFromReplication(UObject *NewSubobject)
{
}

So my guess is that your problems lie in a combination of TMap not being able to be replicated, and incorrect handling of KeyNeedsToReplicate. You should try replicating a single UObject without replication conditions and get it to work, then try arrays and conditions.

Edit:

For those who stumble across this answer in the future: if you’re using version 4.2 or lower; in order for your object to be supported it looks like you have to set the RF_WasLoaded flag or change UPackageMapClient itself. In 4.3, you can just override UObject::IsSupportedForNetworking. In 4.3 preview version I had to #include “Runtime/Engine/Classes/Engine/ActorChannel.h” for my overridden implementation of AActor::ReplicateSubobjects(), because UActorChannel was undefined.

Woo!

Actually, it was none of those, but your comment told me one piece of info that none of the docs did - the object in OnSubobjectCreatedFromReplication() is NOT initialized. Would have been nice to know that. I was getting there with a newly created object with uninitialized fields and thought it was messed up.

Instead, I just hadn’t wired the new object up correctly and now that i have it’s working. Once I did the second property replication step showed up and things are working. Thanks Beej and .

NB: It would be really, really nice to have a high level overview of how this low level stuff works, wouldn’t it? :slight_smile:

Glad you were able to sort things out. I apologize for the poor documentation/examples for this feature. Subobject replication really is one of the more advanced networking feature that hasn’t had much exposure or usability polish.

I’ll try to clear a few things up for you:

Static vs dynamic subobjects. You could create your objects as default subobjects, like actor components. This would require you to do it in the C++ constructor using functions like PCIP.CreateDefaultSubobject. This would give you more problems though since when the C++ constructor is run, we don’t have all blueprint data loaded or components initialized. This is why I think it is easier to just use dynamic subobjects (server creates, tells client to create).

Are the UPROPERTY(replicated) fields on UWeaponState even doing anything, since it’s not an AActor? Do I need to be generating my own ObjectReplicator here?

Your UPROPERTY fields on UWeaponState work the same as they would on the actor - they will replicate if you set them up to (replicated/ReplicatedUsing keyword and UWeaponState:GetLifetimeReplicationProps method).

Finally, setting the RF_WasLoaded flag should not be necessary as long as you override ::IsSupportedForNetworking on your UObject. This code has changed a bit recently, but I think if you grab the newest release, it should not be needed.