Client PlayerController is never created if client calls a server RPC too early?

This is an issue I’ve actually already resolved (such as it is) but I’d like to better understand what actually happened and why.

The setup

I have a custom Pawn class which grabs player input from its InputComponent in order to move around the world. (I realize you’re “supposed to” do this in the PlayerController and not the Pawn but… reasons.)

I have a listen server and one client connected to it. The pawn has bReplicates and bReplicatesMovement both set to true, so when I move around on the server, the client sees that movement replicated automatically.

As for client-to-server, there was an earlier iteration of the code in which the client built a giant struct of input data which it passed to a server RPC. The server then unpacked that struct and updated the client’s pawn accordingly; the updated values were then replicated back to the client automatically. This worked, more or less, but it was kind of dumbly implemented and had some weird edge cases owing to the fact that I wrote that code months ago when I was still very new to this engine and didn’t have any idea what I was doing.

So I refactored that code to a new scheme in which the client simply passes its transform to a server RPC, the server applies the received transform to the pawn, and the new transform gets replicated back to the client automatically. (For the sake of discussion, let’s leave aside issues of network bandwidth and cheating, as this is a LAN-only project in which we don’t really have to worry about either of those things. I fully recognize this is not a valid network architecture for a live, internet-based, competitive game.)

The problem

After the refactor, I suddenly couldn’t control my client pawn at all. Some logging revealed that the client pawn never got possessed by a PlayerController and thus never got an InputComponent assigned.

So I stashed that version and reverted to the previous pawn implementation, replicated my logging there, and found that the client did get a PlayerController at startup.

Then I studied the diff between the two implementations, trying to identify what change would’ve broken PlayerController assignment. But none of my code changes had anything to do with any gameplay framework classes; it was all very specific to my custom pawn class. Like, the only methods whose contents were changed were methods that are declared by this class, not overrides from APawn or AActor. I also didn’t make any changes to the object configuration (e.g. to a blueprint class or in my class constructor).

I was stumped by this for hours, and then…

The solution

Recall that the new client updates its state by passing its local transform to a server RPC. On a whim – I still don’t know why this occurred to me – I wrapped that RPC call with:

if(IsValid(InputComponent)) { ... }

This fixed everything.

The question

What the hell just happened? O_o

As far as I can tell, the client was calling a server RPC (from Tick) before it had ever gotten possessed (which, okay, dumb)… and doing that somehow prevented it from ever getting possessed at all, forever. I don’t really understand why that would, like… be a thing?

I mean, I’m sure there’s some good reason for it, and like I said at the top, this is a closed issue for me now (my code works again), but I’m really curious to understand this incident a bit better.

Did you have any custom serialization in the RPC?

There is a mechanism which turns off specific RPCs if certain kinds of errors with it are detected (e.g. not all bits were read). Can’t remember where this is in code but I could probably look it up if you’re interested. IIRC its hidden a bit due to being wrapped inside a macro.

I believe one of the logs will dump this error, but you won’t see it unless you’ve turned on finer granularity. Even then it gets pretty spammy.

There’s nothing wrong with getting input in a Pawn. That’s why it has it’s own SetupPlayerInputComponent function, it’s there for that reason.

Typically you send input from a client to the Server via a movement component.