[iris] Why are the references to SequencePlayer and LevelSequenceAsset replicated in ALevelSequenceActor?

Hi,

We’ve added some engine level functionality to Iris that lets us reset the replicated state client side for an actor and it’s replicated sub-objects as part of our effort to support truly seamless server travel without having to tear down and spin up a new world. Things have worked pretty well for a long time until we threw a level sequence into the mix, and now our reset code nulls out the replicated references inside ALevelSequenceActor. Seeing as how SequencePlayer is a default sub object, I wouldn’t think it’s reference should be replicated. As for LevelSequenceAsset I don’t see any need for it be replicated at all. Do you see any glaring issue with me removing the “replicated” attribute from the specifier for these references? The only thing I could think of was maybe this would break the ability to spawn ALevelSequenceActors at runtime, but is that really a recommended use case anyway? We’re placing ALevelSequenceActors in the map and setting their LevelSequenceAsset accordingly.

Thanks,

Nick

[Attachment Removed]

I’m not Epic, but just to add some observations.

SequencePlayer needs to be replicated, because properties within it are replicated(in the UMovieSceneSequencePlayer base class). Also note that the UPROPERTY of SequencePlayer makes it transient as well. Meaning it doesn’t save with the actor.

Is it being reset to zero because your code is resetting based on the CDO? It will be null in the CDO(transient objects don’t serialize out) I don’t know why it’s transient, as a default sub object, but it is. If I had to guess, I would guess that it could be because a sequence player is too complex to save all its state out. And really, it doesn’t make a lot of sense to save out the full UObject state of what is effectively a complex multi-actor controlling animation. It’s too complex to ensure validity in all cases, and its too niche to try.

If it’s not something to do with the CDO, I’m drawing a blank as to any other logic that makes much sense that would zero out property references. If it’s not that, maybe a little more detail on why the zeroing out is occurring would help, since you said it’s your custom reset code.

  • What does it mean to reset replicated state in this context?
  • Why is zeroing out object references an element of this?
  • How is this not effecting other replicated UObject properties in other actors? (My guess it’s because of transient, instanced, or both in this case)

[Attachment Removed]

Hey Jeremy thank you for your time and input.

My understanding is that the SequencePlayer reference does NOT need to be replicated because it is a default sub object. Take replicated actor components for example, the component itself replicates but you don’t mark the component reference on the actor as being replicated in the case that it’s a default sub object. Resetting a replicated object reference to it’s default value sets it to null, as expected. What’s unexpected is that in this case we’ve marked a reference to a default sub object as being replicated. As for it being marked transient, the ALevelSequenceActor instantiates the SequencePlayer in it’s constructor (as a default sub object) so I’m not sure I understand how that’s relevant here.

[Attachment Removed]

“SequencePlayer is replicated explicitly in ALevelSequenceActor::BeginPlay(), via AddReplicatedSubObject”

“Replication isn’t really just a default feature of sub objects in all circumstances.”

“The sequence player isn’t a component, so it can’t rely on that hidden automatic replication.”

This is exactly right, and is the basis for the question that started this thread. ALevelSequenceActor takes the necessary steps to replicate SequencePlayer as a replicated default sub object. Therefore, why does the reference need to be replicated? It likely does not.

“sounds like what you are trying to do is fundamentally unsafe”

“You can’t assume that any replicated UObject property is safe to null out.”

We’re resetting the replicated state as part of client travel. The location at which we do this makes it safe, and we can make that assumption because the default state of a replicated reference is null.

We have 2 engine level features we’ve added to support the game we’re making. The first is the ability to pool UObjects on both client and server in order to reduce the CPU overhead of having to spawn a lot of complex Actors at runtime (a requirement for our game). This pooling and reuse of objects requires that we’re able to reset the state of an object (including replicated state) upon returning it to the pool. Secondly, we’ve implemented the ability to do a hidden (purely seamless) travel from one server to the next with no loading screens or tearing down of UWorlds. This required that we have the ability to reset the replicated state of net startup actors placed in the level.

ALevelSequenceActor is a net startup actor and we need to reset the replicated state for all instances of it in the map during a hidden travel. The replicated state of the SequencePlayer object is reset appropriately, however the ALevelSequenceActor’s REFERENCE to the SequencePlayer is also reset back to null since it is marked replicated. The argument is, it shouldn’t need to be marked replicated due to the fact that it’s a default sub object.

[mention removed]​ Would you be able weigh in? Have I made an invalid assumption with respect to how object references need to be marked when replicated as default sub objects?

Thanks,

Nick

[Attachment Removed]

Hi,

We’re still looking into this, and we’ll soon hopefully have some more info on why the SequencePlayer and LevelSequenceAsset pointers are set to replicate. In the meanwhile, I can add some info here on replicated subobjects and hopefully clarify some things.

From the documentation for replicating UObjects:

"You create a default subobject in the owning object’s constructor with CreateDefaultSubobject<T>. The owning actor on the server and client both create their own instances of the default subobject when the actor is spawned.

You do not need to replicate a reference to the subobject since the object exists on both the server and the client.

The subobject starts replicating its properties when you call ReplicateSubobject or AddReplicatedSubObject on the object and it is registered. You can still reference the object over the network even if the subobject is not replicating its properties yet since it is stably named relative to its outer object."

So it is correct that a reference to a replicated default subobject does not need to be marked as replicated, as this reference is set locally on the server and clients when the owning object is constructed. The reference property itself should be replicated if it is intended for this to point to a dynamically spawned, replicated subobject.

It’s possible that this is why the SequencePlayer reference property is marked as replicated, in case users want to dynamically spawn a new ULevelSequencePlayer object at runtime and set this on the level sequence actor. If you only ever need the default SequencePlayer subobject, then it does seem as though marking the SequencePlayer property as non-replicated would be okay. Again though, we are getting some more input from those more familiar with level sequences to confirm what the intention is behind having these properties replicated.

Thanks,

Alex

[Attachment Removed]

Thank you [mention removed]​ and [mention removed]​ for your input.

“So it is correct that a reference to a replicated default subobject does not need to be marked as replicated, as this reference is set locally on the server and clients when the owning object is constructed. The reference property itself should be replicated if it is intended for this to point to a dynamically spawned, replicated subobject.”

…this was my understanding but I wasn’t fully confident so thank you for putting my mind at ease. And with that, I’m at the very least comfortable with the change we’ve made as not being something that goes against the fundamental principals of replication (assuming we don’t do as you said and repoint the reference to another dynamically spawned object which would be very surprising and in fact I’ll probably just add an ensure to be safe).

Thanks again,

Nick

[Attachment Removed]

SequencePlayer is replicated explicitly in ALevelSequenceActor::BeginPlay(), via AddReplicatedSubObject(GetSequencePlayer());

Actor Components have a lot of hidden code for managing their replication, giving the appearance of automatic sub object replication, when really it’s just code looping the components and for any GetIsReplicated(), setting up replication. See calls to AActor::AddComponentForReplication. Replication isn’t really just a default feature of sub objects in all circumstances.

The sequence player isn’t a component, so it can’t rely on that hidden automatic replication.

The reason I suspect transient is just based on the intuition that because the field doesn’t save/load, the UPROPERTY is losing its value at some point in the process, and needs to be replicated to ensure the client points to a valid object. I can’t pinpoint where that occurs, so it may be more complicated than that. Or something else entirely. It looks like ALevelSequenceActor::PostLoad() assumes the presence of a SequencePlayer, so that kinda undermines my line of thinking.

Whatever Epics rationale is for the setup of this property, and whether or not it has anything at all to do with the transient flag, it sounds like what you are trying to do is fundamentally unsafe.

Resetting a replicated object reference to it’s default value sets it to null, as expected. What’s unexpected is that in this case we’ve marked a reference to a default sub object as being replicated.

You can’t assume that any replicated UObject property is safe to null out. We replicate UObject properties that point to cosmetic assets for the character. It would break quite a bit to have something null those out just because they are replicated. Can you explain what the purpose behind this(seemingly) indiscriminate nulling of replicated properties? You didn’t really explain the scope of this operation, or what deciding logic might precede this operation, but if you’re talking about server travel I assume you’re just doing this on select actors involved in the travel? In what circumstance does a level sequence need to travel? Even on a narrow set of actors, like characters, it sounds very unsafe.

For example, our inventory component is object based, which means we have UItemInstance. The inventory has an array of references to them, It replicates them as subobject, like sequencer and components, and it has replicated TObjectPtr property fields to point to specific ones that are equipped in various slots. A characters inventory would need to travel, but no system should be nulling anything out in there.

Just doing a regex of UPROPERTY with replicated and TObjectPtr, there are quite a few in the engine alone, and virtually all of them would probably crash if some code came in and just nulled out their object pointers underneath them. We have many more at the project level.

[Image Removed]

[Attachment Removed]

AddReplicatedSubObject in ALevelSequenceActor::BeginPlay is just saying, replicate this object. That alone has no knowledge of the fact that the SequencePlayer property is supposed to reference that object. Where are you thinking that the SequencePlayer property would get its value from, if not by replication? ReplicateSubObject replicates the object, and the SequencePlayer property will be associated with that object through a FNetworkGUID replicated in association with that property. Sub objects don’t magically associate with the UPROPERTYs that reference them. It may look that way because normal component references and stuff don’t need to be marked replicated, but that’s for a different reason. They don’t need the property replicated because it’s already guaranteed to be duplicated(and instanced if necessary) properly from the CDO on creation of the object(and serialized if it wasn’t transient).

Not to muddy up this thread, but “why would a default sub object be stored in a transient field” has like 2 pages of interesting explanation(for what it’s worth) in ChatGPT that maybe communicate the concerns better than I am.

I’ll say no more now and let a real Epic response correct me if I’m off base. I don’t intend to come across argumentative.

[Attachment Removed]

From above:

“As for it being marked transient, the ALevelSequenceActor instantiates the SequencePlayer in it’s constructor (as a default sub object) so I’m not sure I understand how that’s relevant here.”

…the SequencePlayer is a default sub object, meaning it is instantiated independently on both server and client via the constructor.

[Attachment Removed]

Hi,

A colleague reached out to the dev team, and after getting their input, it does seem as though these properties are marked as replicated in order to support dynamically spawning/changing the LevelSequenceAsset and SequencePlayer at runtime on the server. As long as you do not need to change these at runtime, then I believe it should be fine to mark these properties as not replicated.

That being said, if you do run into any further problems, please don’t hesitate to reach out. Please note though that support will be limited for the next couple of weeks due to the company break.

Thanks,

Alex

[Attachment Removed]

OK great, that’s an interesting use case I haven’t seen before. I guess repointing the reference to a new object would result in the default sub object being garbage collected? Either way this is good to know and I appreciate the follow up.

[Attachment Removed]