Multiplayer RPC Routing through UObjects

I’m working on a game at the moment whereby we need to have player-controlled objects which multiple users can interact with at the same time. For example, think of a field gun or artillery piece which has both a gunner and a loader.

I don’t want the players that are interacting with the object to have to possess another pawn in order to do so, as this would mean rewriting huge chunks of the project. I’m at a bit of a dilemma here - I don’t want a convoluted system where everything is routed through the character pawn to any possible item they could interact with, but I also can’t use a “Proxy Pawn” that the player can possess while they are in control of the object.

So… I thought I’d found a workaround but I seem to have been fooled at the last minute. What I’ve done is created a new UObject class called “Seat”, which is created by the artillery piece in the constructor as a default sub-object. This is the relevant networking code for that “Seat” class:



//////////////////////
///// Networking /////
//////////////////////

bool USeat::IsSupportedForNetworking() const
{
    return true;
}

bool USeat::CallRemoteFunction(UFunction * Function, void * Parms, FOutParmRec * OutParms, FFrame * Stack)
{
    AActor* Owner = CurrentOccupant ? CurrentOccupant : Cast<AActor>(GetOuter());
    if (!Owner)
    {
        return false;
    }

    UNetDriver* NetDriver = Owner->GetNetDriver();
    if (!NetDriver)
    {
        return false;
    }

    NetDriver->ProcessRemoteFunction(Owner, Function, Parms, OutParms, Stack, this);

    return true;
}

int32 USeat::GetFunctionCallspace(UFunction * Function, void * Parameters, FFrame * Stack)
{
    AActor* Owner = CurrentOccupant ? CurrentOccupant : Cast<AActor>(GetOuter());
    return (Owner ? Owner->GetFunctionCallspace(Function, Parameters, Stack) : FunctionCallspace::Local);
}

void USeat::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME(USeat, CurrentOccupant);
    DOREPLIFETIME_CONDITION(USeat, SeatOwner, COND_InitialOnly)
}


What you can see is that if the seat has a current “occupant” (the player characters), then I use that occupant to determine the “caller” of the RPC. This all compiles fine and it even sends the RPC (yay!) - but once an occupant tries to call an RPC they are kicked, because the Occupant is not an “Outer” of the UObject. Now obviously, there isn’t really much I can do to change that (perhaps unless I change the “Outer” of the UObject at runtime or use AddToRoot()? That’s one part I didn’t try, but I don’t know if it’s even possible.

So I’m pretty bummed because this was a nice clean system - but I’ve been called out by the networking system at the last minute. The engine kicks the client because the “Seat” is not in the “Outer” chain of the occupant. I’m not really sure how/if I can workaround that?

Note: I’ve since rewritten this system to use an “Actor” instead of a UObject, and I set the “Owner” of the Actor to whoever the Occupant is - which kind of mimics what Pawn Possession does anyway. The thing is, it’s much more messy because I have to spawn actors, and they have additional networking overhead of their own. I’d like to revert to a UObject method if I can, it just feels much cleaner.

In single player or even split-screen this would be extremely easy and wouldn’t require any special workarounds, everything would just work.

Server Authority isn’t the problem - players can’t call an RPC on anything they don’t own (by default). I managed to get around the “Ownership” limitation, but the next barrier is that the player seemingly can’t call an RPC on an Object that isn’t in their “Outer” chain. I’m looking for a way to workaround that without changing engine code - but it doesn’t seem feasible.

The Actor Proxy system I’ve built in the meantime works just fine - but I don’t want to use actors because of all the extra overhead they create. Guess I’m stuck with it for now though.

Why not make “Seat” be sub-object of Player and make Artillery reference that instead of owning the Object?

That’s an interesting idea, I could probably make that work quite easily but I’d prefer to keep the seats as being “Owned” by the item the player is sitting in, rather than having to create new seat objects when a player gets in/out of a given seat.

For now I’ve stuck with the Actor system, since it doesn’t look like I can get around this without source changes which are out of scope for this project. God knows what kind of problems it would cause anyway.

Seats as actors is how many previous Unreal Engine games have handled things like this in the past (including UT’s vehicles).

Maybe a dumb question, but could you not change the owner of the seat at runtime? That would essentially amount to your “Occupant” system, but in a way that the engine understands (the notion of owner).

With UObject the problem isn’t the ‘Owner’ it’s the ‘Outer’ (UObject has no concept of Owner natively, that’s an Actor thing).

The Outer is the object it was created “in” and is relevant for garbage collection and suchlike. I’d likely introduce a whole host of other problems if I tried to mess with that (I’m not even sure it can be changed).

Avoiding Actors would just make the system a lot less heavy and cleaner. In a game with a lot of players that can make a big impact.