The Multiplayer Vehicle Control Dilemma in UE5.6: Breaking the Pawn-Possession Paradigm

Issues with Using ChaosVehicles Plugin in Unreal 5.6 C++

Problem Background

While using the ChaosVehicles plugin in Unreal Engine 5.6 C++, I’ve encountered some unresolved issues.

Since a Vehicle is a Pawn, when we want to control a vehicle, we must have the PlayerController possess the VehiclePawn and attach the Character to the VehiclePawn:

PlayerController->Possess(VehiclePawn);
Character->AttachToActor(VehiclePawn, AttachmentRules, SocketName);
Character->DisableCollision();
graph LR
    PlayerController -->|Possess| VehiclePawn
    Character -->|Attach| VehiclePawn

Desired Game Design Pattern

However, many modern games implement vehicle functionality differently:

  • RV There Yet? (made with Unreal Engine 5)
  • GTA Series
  • Red Dead Redemption Series
  • Many open-world games

Their vehicle functionality works like this:

graph LR
    PlayerController -->|Possess| Character
    Character -->|Possess| VehiclePawn
    Character -->|Attach| VehiclePawn

The specific behavior is: after the player controls the Character to enter a vehicle, the player still controls the Character, still views through the Character’s camera, and can still use the Character’s functions like interaction, free look, etc., but certain Character functions are disabled, such as walking, jumping, etc.

My Solution: VehicleControllerComponent

To achieve this, I created a VehicleControllerComponent that inherits from UActorComponent and can only be attached to a Character. The purpose is to give the Character the ability to control vehicles. Why not attach it to the PlayerController? Because it’s not the PlayerController that has the ability to control vehicles, but the Character. The PlayerController controls the Character, and the Character has the ability to control vehicles, thus giving the player indirect control over vehicles.

VehicleControllerComponent Implementation Details

  1. Automatically gets the Character and binds inputs in BeginPlay:

    TWeakObjectPtr<ACharacter> Character = nullptr;
    
    void UVehicleControllerComponent::BeginPlay()
    {
        Super::BeginPlay();
    
        Character = Cast<ACharacter>(GetOwner());
    
        SetupInput();
    }
    
  2. VehicleControllerComponent has a ControlVehicle API that automatically queries for the VehicleMovementComponent:

    TWeakObjectPtr<APawn> VehiclePawn = nullptr;
    TWeakObjectPtr<UChaosVehicleMovementComponent> VehicleMovementComponent = nullptr;
    
    bool USingularisVehicleControllerComponent::ControlVehicle(APawn* NewVehiclePawn)
    {
        if (!NewVehiclePawn) return false;
    
        UChaosVehicleMovementComponent* NewVehicleMovementComponent =
            NewVehiclePawn->FindComponentByClass<UChaosVehicleMovementComponent>();
    
        if (!IsValid(NewVehicleMovementComponent)) return false;
    
        VehiclePawn = NewVehiclePawn;
        VehicleMovementComponent = NewVehicleMovementComponent;
    
        return true;
    }
    
  3. Key binding callbacks (Throttle example only):

    void UVehicleControllerComponent::HandleThrottle(const FInputActionValue& Value)
    {
    	if (!VehicleMovementComponent.IsValid()) return;
    
    	VehicleMovementComponent->SetThrottleInput(Value.Get<float>());
    	VehicleMovementComponent->SetBrakeInput(0.0f);
    }
    

This implementation worked well for my single-player needs. I really like this VehicleControllerComponent approach, but unfortunately, if it had completely solved my requirements, I wouldn’t be seeking help here.

Multiplayer Issues

When I added multiplayer functionality to my game, I encountered unsolvable problems:

  • A Vehicle is a Pawn. On NM_ListenServer, the Vehicle has the role ROLE_Authority, but on NM_Client the Vehicle has the role ROLE_SimulatedProxy. Therefore, when a client player calls Input functions on VehiclePawn (like SetThrottleInput) through the VehicleControllerComponent, the VehiclePawn doesn’t move because ROLE_SimulatedProxy has no authority.

Attempted Solutions

Solution 1: Forwarding Input via Server RPC

I made the VehicleControllerComponent replicable (SetIsReplicatedByDefault(true);) and forwarded input from NM_Client to NM_ListenServer, letting the VehicleControllerComponent on NM_ListenServer control the vehicle:

void HandleThrottle(const FInputActionValue& Value);

UFUNCTION(Server, Reliable)
void HandleThrottle_Server(const float& Throttle);
void VehicleControllerComponent::HandleThrottle(const FInputActionValue& Value)
{
	if (!VehicleMovementComponent.IsValid()) return;

	HandleThrottle_Server(Value.Get<float>());
}

void VehicleControllerComponent::HandleThrottle_Server_Implementation(const float& Throttle)
{
	VehicleMovementComponent->SetThrottleInput(Throttle);
}

This worked! But it’s a poor solution and not best practice. The issues are:

  • No movement prediction on the client
  • In multiplayer games, the player driving the vehicle should have a local-like driving experience, while other players have a slightly worse experience due to network latency
  • In this solution, the driving player needs to forward input to the server, which then replicates the vehicle’s state back to the player, resulting in no local-like driving experience
  • This driving experience is unacceptable to players

Solution 2: Overriding VehicleMovementComponent

I extended UChaosWheeledVehicleMovementComponent and overrode various functions to break the forced binding between VehicleMovementComponent and Pawn, allowing VehicleMovementComponent to work even when attached to an Actor:

USingularisWheeledVehicleMovementComponent::USingularisWheeledVehicleMovementComponent()
{
	bRequiresControllerForInputs = false;
	SetIsReplicatedByDefault(true);
}

void USingularisWheeledVehicleMovementComponent::PreTickGT(const float DeltaTime)
{
	// Super::PreTickGT(DeltaTime);

	// movement updates and replication
	if (PVehicleOutput && UpdatedComponent)
		UpdateState(DeltaTime);
    
    //......
}

Now VehicleMovementComponent can be attached to an Actor and work.

When I was about to test this in multiplayer, I realized that breaking the original logic would make the code difficult to maintain and goes against its design philosophy. Additionally, VehicleActor would lack many features compared to VehiclePawn, such as AIController support.

Conclusion and Request for Help

These are the issues I’ve encountered while using the Unreal 5.6 C++ ChaosVehicles plugin and the solutions I’ve attempted. I’m very grateful to the Unreal Engine community and Epic Games for providing such powerful tools and support.

I would greatly appreciate any suggestions or solutions to these problems. I hope this issue can be resolved. Thank you very much!