Private Properties in AActor causing headaches with Custom Replication!

Just updated to 4.11 and having quite a few problems carrying over network code from 4.10. I realize that 4.11 is in Preview, but this issue also existed in 4.10 with some properties as well (Such as ‘Owner’ and ‘RemoteRole’ for example).

I have a projectile class which inherits from AActor. In the interests of keeping it as lightweight as possible (essential for a large volume of objects that update quickly over a network!), I have overriden some of the functions in AActor and am trying to avoid replicating properties that I don’t need. There is a huge opportunity for me to regain some bandwidth here. Unfortunately, more and more of these properties are becoming private and it’s becoming a pain to override the stock Actor replication and use your own, or tell properties that you know you will never need to not replicate.

The DOREPLIFETIME and DOREPLIFETIME_ACTIVE_OVERRIDE macros do NOT allow you to access private members of the AActor class - so some properties are forced to replicate whether you want them to or not! For my Ordnance class I have a custom SendInitialReplication function to handle the properties that I need, and all other parameters can be determined from these on the other side (such as Owner, Instigator etc). In order to do this, you have to override GetLifetimeReplicatedProps() and do NOT call ‘Super’. For example:



void ABZGame_Ordnance::GetLifetimeReplicatedProps(TArray< class FLifetimeProperty > & OutLifetimeProps) const
{
	// Don't Call Super - Get Replicated Props from Blueprint class.
	UBlueprintGeneratedClass* BPClass = Cast<UBlueprintGeneratedClass>(GetClass());
	if (BPClass != nullptr)
	{
		BPClass->GetLifetimeBlueprintReplicationList(OutLifetimeProps);
	}

	// Now Replicate the ONLY two properties from AActor that we actually need!
	DOREPLIFETIME(AActor, bTearOff);
	DOREPLIFETIME(AActor, AttachmentReplication); // TODO - Only when Necessary / when this projectile is a 'Sticky' one.

	DOREPLIFETIME(ABZGame_Ordnance, OwningWeapon); 	// Used to Determine if used with Caching and determine Instigator / Owner etc. No point copying three pointers pointing to the same thing!
	DOREPLIFETIME(ABZGame_Ordnance, bExploded);
	DOREPLIFETIME(ABZGame_Ordnance, bOrdnanceActive);
	DOREPLIFETIME_CONDITION(ABZGame_Ordnance, OrdnanceReplicatedMovement, COND_SimulatedOrPhysics);
}


You can see from the comments why I have made these optimizations. If I decide I want to replicate ‘Owner’ however (from AActor) - I can’t without calling Super. DOREPLIFETIME(AActor, Owner) throws a compiler error.

I could just call Super of course, but then ALL properties from AActor that are set to replicate will also replicate when I don’t want them too. I CANNOT however override PreReplication() and use DOREPLIFETIME_ACTIVE_OVERRIDE(AActor, RemoteRole, false) for example, because RemoteRole is private and the macro can’t access it. I also get use ‘GetRemoteRole()’ or ‘GetOwner()’ either.

My Projectile works similarly to UT’s one. It has a custom Replication struct and a few minor properties and that’s ALL it needs. To correctly replicate the movement, I have also overriden GatherCurrentMovement() - but as of 4.11 AttachmentReplication is now private so I can’t modify that either, even with the accessor function :frowning:



void ABZGame_Ordnance::GatherCurrentMovement()
{
	if (RootComponent != nullptr)
	{
		// If attached, replicate only attachment position!
		if (RootComponent->AttachParent != nullptr)
		{
			if (AttachmentReplication.AttachParent != nullptr)
			{
				AttachmentReplication.LocationOffset = RootComponent->RelativeLocation;
				AttachmentReplication.RotationOffset = RootComponent->RelativeRotation;
				AttachmentReplication.RelativeScale3D = RootComponent->RelativeScale3D;
			}
		}
		else
		{
			OrdnanceReplicatedMovement.Location = RootComponent->GetComponentLocation();
			OrdnanceReplicatedMovement.Rotation = RootComponent->GetComponentRotation();
			OrdnanceReplicatedMovement.LinearVelocity = GetVelocity();
		}
	}
}


I’m all for encapsulation, but this is in my opinion yet another nail in the coffin for Multiplayer - which is becoming an increasingly overlooked topic with every release.

I think trying to avoid calling the base class implementation, even if you find a way to do it, is asking for trouble. It’s definitely not intended use and is the kind of thing that’s liable to break with future engine changes.

Is the primary motivation to save on network performance, because you have heaps of projectiles? If so, I’d say the way to go would be not using an actor for each projectile. You could have a single actor class which manages all projectiles (maybe one manager for your entire game, or one for each player/projectile launcher). Maybe then have an instanced static mesh component for instancing projectiles. Any extra per-projectile data you need you could store in an array/map inside your actor class, and replicate as needed.

How would that work with physics, invisible object removal and collision detection though? (curious)

Haven’t really used ISMs specifically, so not sure if/how they support physics.
In general though, you obviously lose a lot of functionality when you go from having an actor for every projectile to something more lightweight. Some of that stuff you may need, in which case you’d need to roll your own. If performance is super important though, then it might be worth that extra effort. In the case of projectiles for example, it’s unlikely you’d need full overlap shape collision testing - simulating them as point particles should be sufficient and will be faster.

Yeah the difficulty then becomes tracking collisions etc, but it is something I’m open to exploring. I currently cache/pool a lot of them. Not calling the base implementation was just something I copied from UT’s implementation, which does this so that it doesn’t have to use the Engines FRepMovement (which has a lot of unnecessary data for something like this), and a bunch of other properties. I too have made my own new Replicated-movement struct and am trying to reuse as much data as possible to avoid replicating it.

I would really like to explore the lightweight projectile option, since I could have hundreds if not thousands of the things in the world in some extreme cases, and I need to make sure I can scale to that. Would you have any suggestions on how to go about it? I currently pool them, but it’s not really enough. All they need is a few basic parameters:

  • Particle System
  • Point-Collision / Very basic Sphere Collision
  • Audio Component (not always, but sometimes)
  • Basic movement

Using UObject as a base would certainly improve the speed of things and cutdown on a lot of wasted memory real-estate, but I wouldn’t know where to start with adding things like Particles and most importantly collision to such a thing :confused:

its not like i’m understand clearly what are you doing, but game dev programmer 101 says
if something too hard to implement, poke the Game Designer.

Modern system should have no problem handling 20000 objects that can potentially collide (Sweep and Prune algorithm), however, I’m not sure how would I go about handling network load in this case and I’d expect PhysX to already do something like that.

The reason why I asked kamrann about collisions and invisible object removal is because that’s area of the engine I have not properly investigated yet. For example, if physics and occlusion is decoupled from AActor, you might get away with using one AActor-based class for all projectiles. However, if occlusion is handled on AActor level and not on, say, component level, then you’ll hit lots of trouble if you do that. You could end up trying to draw lots of invisible objects, for example, and you might end up reimplementing lots of physx-related stuff that already exists in the engine.

Either way, I think the easiest way to go about it is probably forking engine, and moving some of the private methods into “protected” section of the AActor. Maintaining your own fork might be a hassle, but hey, at least you’ll have full control in that situation.

If you’re talking 100’s, maybe 1000’s, but not more, then I’d guess an AActor-based solution should probably be able to cope. In that case, as NegInfinity says your easiest solution would be an engine mod to fix your current issue.

As for the other approach - I’m pretty certain the bulk (if not all) of the code handling both physics and occlusion is at the level of UPrimitiveComponent. So you could I think have a single actor, containing an array of components for the projectiles. Of course, you’re still then replicating component data for every projectile, but I guess that’s considerably more lightweight than actor data.

Having said that, I just checked now and it looks like there is no component-level control of network relevancy, meaning at any time, you’d be replicating either all the projectiles or none of them. That’s not really gonna fly. Presumably you just want replication for purposes of visualizing the projectiles to each client? In that case you only want to be replicating those that are within some range of the client camera, right?

You could go super low level: have a manager that simulates the projectiles on the server, but manually tracks network relevancy and replicates a custom array of projectile position/state to individual clients (and doesn’t replicate any components), with the clients then building their own scene components from the received data. That would be getting a bit crazy though, you’d have to be sure the projectile replication was totally killing your game before wanting to do that.

Actually I suspect that physics code might be decoupled from even component, because skeletal mesh might be controlled by several rigid bodies (PhysicsAsset) and I vaguely remember seiing some kind of FSomethingPhysXRelated structure somewhere. If you search through engine code for physx references/subroutines you should find that one - all object settings, like use of CCD are stored in it.

I think you may be right on that. There have been a few times where I’ve been waiting for engine-level fixes in the binary version. I suppose forking and having my own version would save me some trouble. using sourcetree it shouldn’t be too much work to update to later engine versions, and just modify a few properties here and there. Hell it would even allow me to avoid the use of some components entirely…

I suppose the other factor for consideration is the general overhead of Actor spawning and the fact that it can’t be done on anything other than the main game thread right now. Each ‘Weapon’ object in my scene is an Actor, which is spawned by the owning ‘Vehicle’ or ‘Character’ whenever that too spawns into the world. At this point, any cached/pooled ordnance is also spawned.

For something like say a machine gun, each weapon caches around 50 ordnance each. This means that when I spawn that vehicle, I’m spawning nearly 150 actors in total with all weapons accounted for, in that same frame - which gives me a big-old hitch and lag spike. Since there are a few sub-classes of ordnance with behaviour of their own and different firing mechanisms for various weapon classes, pooling/caching before play begins becomes quite difficult. Using some kind of lightweight UObject class would be very beneficial in the long run.

I suppose the next most ideal approach would be implementing a very simple lockstep deterministic movement solution for the projectiles. Each client/object only sends the RPC out for when it starts and ends firing, and the projectiles then behave identically on all clients regardless of what happens to them in the world.

For something Planetside-2 / Battlezone scale like what I’m trying to do (I know the latter uses a fully deterministic multi-world lockstep system) - that might also be my go to option.

I’m all of them so it’s okay ;D

Not sure about source tree, but something like that is fairly easy to do with git which has “rebase” commands for situation where you want to follow official changes, but reapply your own modifications on top of them.

Hi TheJamsh,

Thanks for the feedback, your point is valid. The stock UE4 MP framework sort of relies on the base properties of actors to replicate to properly function. It’s hard to say what would happen when you start turning those off, but it’s totally plausible that you can get things working by inferring the state, and setting it manually locally as you are doing.

The idea of not calling super, and manually adding properties as you are doing could work, but as you mentioned, getting access to private properties is an issue. It would be hard to justify exposing some of those, since by design they are protected from direct access.

One idea might be for something like Owner, is to make a duplicate property on the child class, and mirror Owner property in PreReplication. Then when that property changes on the client, restore Owner from that. The only issue here, is if the client needs Owner set within the de-serialization code to work properly (before OnReps are called for example). I can’t think of anything off the top of my head though.

This is an interesting problem, and something we’ll give more thought towards!

Hey TheJamsh, I made the change to make AttachmentReplication private. There were some good reasons, namely that with it being public some people were trying to set values on it directly thinking that would actually have an effect on the replication, when in fact GatherCurrentMovement() is all that matters. I just added a comment to make this more clear.

Another important reason is that SceneComponent used to fill in AttachComponent and AttachSocket, but that has been refactored to just happen in Actor now as well. In your example override, you don’t fill in these variables, so you might have an error there. I think in your case though you can still use the Super implementation in place of the block where you fill in AttachmentReplication. Unless the root component is simulating physics (when AttachmentReplication wouldn’t be used anyway), this should result in the correct behavior (plus correctly set the aforementioned variables).

As for the DOREPLIFETIME macros etc… that is an unfortunate side effect that we are thinking about how to address. I believe it comes from the GET_MEMBER_NAME_CHECKED macro used by the network replication macros. While it might be possible to make AttachmentReplication protected (I’d like input on this), the macros will still have this issue.

Hey Zak / John, thanks for posting!

Thanks for the explanation also, I can see why the change was made. I was essentially following from UT’s example, where they do a lot of overriding of the stock GetLifetimeReplicatedProps() functions to prevent replicating some values. I believe this is how they get around not requiring an ‘Owner’ in order to send RPC’s / receive replicated props. In my case (large scale RTS / FPS mix) I’m trying to save on whatever bandwidth I can. I guess though if the properties are never changed, they don’t have any cost anyway other than the initial send.

The Macros do seem to be able to access protected members, but not private at all. I also noticed that RemoteRole and Role are themselves replicated properties, are they only sent from server with the initial replication bunch, like when an actor is initially spawned or something? If so I guess I don’t need to worry about those either… maybe I’m just being over-zealous with my optimizations.

What would help tremendously is if it were possible to add basic replication functionality to a UObject, but currently I can’t figure a way to do that. Is it even possible?

If it’s a subobject inside an actor, then yes. See this wiki page.

Ah interesting. Actor is still needed whatever you do it seems. Creating a super-lightweight ordnance / projectile class seems to be pretty difficult :confused:

I’m bumping this because, really, private and protected property/functions are getting really tiresome…
The best part is when you make a request to have something made public, but nobody accepts a pull or change an important method to be public in engine source code.

Bumping again because its now 2019 and this is still an issue.

I ran into this same problem today. Here’s how I solved it (admittedly using a bit of a hack, but nothing too extreme).

First note how DOREPLIFETIME is defined:



#define DOREPLIFETIME_WITH_PARAMS(c,v,params) \
{ \
   FProperty* ReplicatedProperty = GetReplicatedProperty(StaticClass(), c::StaticClass(),GET_MEMBER_NAME_CHECKED(c,v)); \
   RegisterReplicatedLifetimeProperty(ReplicatedProperty, OutLifetimeProps, params); \
}

#define DOREPLIFETIME(c,v) DOREPLIFETIME_WITH_PARAMS(c,v,FDoRepLifetimeParams())


DOREPLIFETIME_CONDITION, DOREPLIFETIME_CONDITION_NOTIFY etc. are similar. They all go to DOREPLIFETIME_WITH_PARAMS. The culprit here is GET_MEMBER_NAME_CHECKED, which was written with good intentions - but has the unfortunate side effect of disallowing access to private and protected members:



// Returns FName(TEXT("MemberName")), while statically verifying that the member exists in ClassName
#define GET_MEMBER_NAME_CHECKED(ClassName, MemberName) \
   ((void)sizeof(UE4Asserts_Private::GetMemberNameCheckedJunk(((ClassName*)0)->MemberName)), FName(TEXT(#MemberName)))


Fortunately, the only thing this macro really does is convert the member name to an FName. My solution therefore was to replace this chain of macros, placing the following in my common header:



#define GET_MEMBER_NAME_UNCHECKED(ClassName, MemberName) \
   FName(TEXT(#MemberName))

#define DOREPLIFETIME_WITH_PARAMS_UNCHECKED(c,v,params) \
{ \
   FProperty* ReplicatedProperty = GetReplicatedProperty(StaticClass(), c::StaticClass(),GET_MEMBER_NAME_UNCHECKED(c,v)); \
   RegisterReplicatedLifetimeProperty(ReplicatedProperty, OutLifetimeProps, params); \
}

#define DOREPLIFETIME_UNCHECKED(c,v) DOREPLIFETIME_WITH_PARAMS_UNCHECKED(c,v,FDoRepLifetimeParams())


The ‘_UNCHECKED’ suffix indicates that the member access is, well, unchecked. This way you can use the ‘_UNCHECKED’ macros where needed (which shouldn’t be very often) and continue to use the regular macros elsewhere. Final usage:



void MyActor::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const
{
   // Not calling Super::GetLifetimeReplicatedProps()
   // ...but we still want to replicate private variable bCanBeDamaged
   DOREPLIFETIME_UNCHECKED(AActor, bCanBeDamaged);
}


1 Like