Best practices for clean role separation in multiplayer (c++)?

In multiplayer actors you may have code/variables that:

  1. Are exclusive to server
  2. Are exclusive to client
  3. Are shared

This looks like a SOC (separation of concerns) issue.
Sprinkling authority checks throughout an actor seems inadequate, and prone to role mistakes (clients doing server stuff and vice versa).

it gets more complicated if you want to support both dedicated servers and listen servers, which seems like a common scenario (because single-player is essentially a listen server).

Have any of you developed practices to counteract this weak point? Like for example naming conventions for roles, or using components within an actor for each role?

I’d love to hear. Thanks

I don’t know the “right” answer, but i can share a bit of personal experience:

Sprinkling authority checks throughout an actor seems inadequate

We(team of 3 devs) have started developing as a dedicated-server-only game and authority checks was manageable approach to some extent.

it gets more complicated if you want to support both dedicated servers and listen servers

But here you are absolutely right, the real sht started a year after when we decided we need a listen-server support as well. At this point i was still sometimes missusing HasAuthority() (hell, i’m even now not really able to tell all the corner cases of the top of my head), so we moved to custom side checks named ShouldRunServerLogic(), ShouldRunClientLogic() & etc.
Aand, it’s appeared this approach for us was way more manageable than default one: no need to remember authority’s corner cases, just keep in mind which side you want to be executed on. We did managed to add listen server support without much pain this way.

(Internally those functions are just switches over GetWorld()->GetNetMode() value)

With this, we using no extra conventions on top of it, as such parts of code are quite a self-describing and 90% of code will have all the shared\server-only and client-only parts you mentioned.

I also have netmode switches:

bool TSS_CORE_API IsAnyKindOfServer(const UWorld* World);
bool TSS_CORE_API IsAnyKindOfClient(const UWorld* World);
bool TSS_CORE_API IsDedicatedServer(const UWorld* World);
bool TSS_CORE_API IsDedicatedClient(const UWorld* World);

One stumbling block for listen servers is that OnRep functions are not called automatically. So I’m heading towards these practices:

  • Always implement the OnRep function
  • Always call the OnRep function after the value changes on the server
  • Use a client role check inside the OnRep function for stuff that only the client should do.

This should handle both dedi and listen cases.

Also I notice that Unreal code does not consistently use setters and getters. It sometimes makes class variables public. I think particularly for replicated variables this should be avoided, because the setter is a good place to call the OnRep function.

i.e.

  • Always use a setter for a replicated variable, and never change the variable directly outside of the setter. The setter calls the OnRep.