Character Rotation Replication in a Planet surface

Hi,

I want to simply replicate the rotation of the third person character in a planet scene, changing the gravity when it is needed.

[PlanetGravity.cpp] → What I did was creating a script for the planet gravity, where it calculates the surface normal, and compares it to the current normal of the character and changes the gravity direction.

[PlanetUniversMediepolysCharacter.cpp] This script basically calculates the gravity of the character every tick, and sets it to the CharacterMovementComponent->SetGravityDirection(NewGravity), after that I set the calculated gravity direction to the currentGravityDirection (named as medielerGravityDirection) of the PUMC script

The problem is that the client does not read the replicated rotations from other clients, but the server can see the rotations for all the clients…

Some of the Logs I registered, but I do not understand what it is happening:

LogTemp: Warning: BP_ThirdPersonCharacter_C_2: Role = Server (Authority)
LogTemp: Warning: BP_ThirdPersonCharacter_C_2: Role = Client (Simulated)
LogTemp: Warning: BP_ThirdPersonCharacter_C_0: Role = Client (Owning Player)
LogTemp: Warning: BP_ThirdPersonCharacter_C_1: Role = Client (Simulated)
LogTemp: Warning: BP_ThirdPersonCharacter_C_2: Role = Client (Simulated)
LogTemp: Warning: Client (BP_ThirdPersonCharacter_C_1):attempted to update gravity for a character it does NOT own!

LogTemp: Warning: Surface Direction: X=-4085.765 Y=-3917.150 Z=-7664.812
LogTemp: Warning: Client (Client (Owning Player)): Sending gravity update to the server.
LogBlueprintUserMessages: [MyPlanet_C_UAID_D843AE2CBE806C0E02_1561671289] Server: Entra gravetat!
LogTemp: Warning: Client (BP_ThirdPersonCharacter_C_2):attempted to update gravity for a character it does NOT own!

#include "PlanetUniversMediepolysCharacter.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "EnhancedInputComponent.h"
#include "Net/UnrealNetwork.h"

APlanetUniversMediepolysCharacter::APlanetUniversMediepolysCharacter()
{
    Super::InitializeParamaters();

    PrimaryActorTick.bCanEverTick = true;

    planetGravity = CreateDefaultSubobject<UPlanetGravity>(TEXT("PlanetGravity"));

Camera configurations...
[...]

    bReplicates = true;
    SetReplicates(true);
    SetReplicateMovement(true);
}

void APlanetUniversMediepolysCharacter::BeginPlay()
{
    Super::BeginPlay();

    switch (GetLocalRole())
    {
    case ROLE_Authority:
        RoleString = "Server (Authority)";
        break;
    case ROLE_AutonomousProxy:
        RoleString = "Client (Owning Player)";
        break;
    case ROLE_SimulatedProxy:
        RoleString = "Client (Simulated)";
        
        break;
    default:
        RoleString = "Unknown Role";
    }

    UE_LOG(LogTemp, Warning, TEXT("%s: Role = %s"), *GetName(), *RoleString);

	SphericMovement = Cast<USphericalMovementComponent>(GetCharacterMovement());
    SphericMovement->SetIsReplicated(true);

}


void APlanetUniversMediepolysCharacter::UpdateCameraLook()
{
    FRotator PlayerRotation = CameraBoom->GetRelativeRotation();

	//clamp PlayerRotation.Yaw value to 40 degrees
    PlayerRotation.Pitch += LookAxisVector.Y;
	PlayerRotation.Pitch = FMath::Clamp(PlayerRotation.Pitch, -40.0f, 40.0f);

    PlayerRotation.Yaw += LookAxisVector.X;

    // Apply the rotation to the CameraBoom
    CameraBoom->SetRelativeRotation(PlayerRotation);

    CameraBoom->SetWorldLocation(GetActorLocation());
    
    AlignedToActor();    
}


void APlanetUniversMediepolysCharacter::AlignedToActor()
{
    if (planetGravity->GetGravityEnabled()) 
    {

        FVector CharacterLocation = GetActorLocation();
        FVector UpDirection = (planetGravity->GetPlanetCenter() - CharacterLocation).GetSafeNormal();

        // Step 2: Adjust the camera boom's rotation
        // Get the forward direction of the character (local X-axis)
        FVector CamForward = CameraRoot->GetForwardVector();
       

        // Create a rotation with the forward vector and the up direction
        FRotator DesiredRotation = FRotationMatrix::MakeFromZX(-UpDirection, CamForward).Rotator();

        // Smoothly interpolate to the desired rotation for a natural effect
        FRotator SmoothedRotation = FMath::RInterpTo(
            CameraRoot->GetComponentRotation(), // Current rotation
            DesiredRotation,                   // Target rotation
            GetWorld()->GetDeltaSeconds(),     // Delta time
            2.0f                               // Interpolation speed
        );

        CameraRoot->SetWorldRotation(SmoothedRotation);
    }
}

void APlanetUniversMediepolysCharacter::Move(const FInputActionValue& Value)
{
    FVector2D MovementVector = Value.Get<FVector2D>();


    FVector CalculateUpVector;

	if (!planetGravity->GetGravityEnabled()) {		
        CalculateUpVector = GetActorUpVector();            
    }
    else {
        CalculateUpVector = (GetActorLocation() - planetGravity->GetPlanetCenter()).GetSafeNormal();
    }   

    // Get the camera's forward vector
    FVector CameraForward = FollowCamera->GetForwardVector();
    FVector ForwardDirection = FVector::VectorPlaneProject(CameraForward, CalculateUpVector).GetSafeNormal();
    // Move the character forward direction relative to the camera
    AddMovementInput(ForwardDirection, MovementVector.Y);


    // Get the camera's right vector
    FVector CameraRight = FollowCamera->GetRightVector();
    FVector RightDirection = FVector::VectorPlaneProject(CameraRight, CalculateUpVector).GetSafeNormal();
    // Move the character relative to the camera
    AddMovementInput(RightDirection, MovementVector.X);
    		
}


void APlanetUniversMediepolysCharacter::AddAdditionalInputs()
{

}


void APlanetUniversMediepolysCharacter::CalculateGravity()
{
    if (IsValid(planetGravity)) {
        // Ensure the gravity is only applied if the component is active
        if (planetGravity->GetGravityEnabled())
        {
            if (planetGravity->ApplyGravity())
            {
                SphericMovement->UpdateGravityDirection(planetGravity->GetNewGravityDirection());
                
                if (GetLocalRole() == ROLE_AutonomousProxy && !HasAuthority())
                {
                    UE_LOG(LogTemp, Warning, TEXT("Client (%s): Sending gravity update to the server."), *RoleString);

                    //Server_SendGravityUpdate(planetGravity->GetNewGravityDirection());
                    Server_SendGravityUpdateToServer(planetGravity->GetNewGravityDirection());
                }

            }
        }
    }
}

/********************/
/* REPLICATED PART */
/********************/


void APlanetUniversMediepolysCharacter::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    //if (IsLocallyControlled())  //Only calculate locally
    if (HasAuthority() || (IsLocallyControlled() && GetLocalRole() == ROLE_AutonomousProxy))
    {
        
        CalculateGravity();
    }
}

void APlanetUniversMediepolysCharacter::AssignPlanetGravityToPlayer()
{
    medielerGravityDirection = planetGravity->GetNewGravityDirection();
}


// Enable replication for GravityDirection
void APlanetUniversMediepolysCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    DOREPLIFETIME(APlanetUniversMediepolysCharacter, medielerGravityDirection);
}

void APlanetUniversMediepolysCharacter::Server_SendGravityUpdateToServer_Implementation(const FVector& NewGravityDirection)
{
    UE_LOG(LogTemp, Warning, TEXT("Server: Received gravity update from client."));

    AssignPlanetGravityToPlayer();
    OnRep_GravityDirection();
}

bool APlanetUniversMediepolysCharacter::Server_SendGravityUpdateToServer_Validate(const FVector& NewGravityDirection)
{
    if (!IsLocallyControlled())
    {
        UE_LOG(LogTemp, Warning, TEXT("Client (%s):attempted to update gravity for a character it does NOT own!"), *GetName());
    }

    return true;
}

// Called when replicated gravity direction changes
void APlanetUniversMediepolysCharacter::OnRep_GravityDirection()
{
    UE_LOG(LogTemp, Warning, TEXT("Client(%S): Received replicated gravity direction update: %s"), *RoleString, *medielerGravityDirection.ToString());
    CalculateGravity();
}

And the header file:


#pragma once

#include "Net/UnrealNetwork.h"
#include "Engine/Engine.h"
#include "CoreMinimal.h"
#include "TimerManager.h"
#include "SphericalMovementComponent.h"
#include "UniversMediepolysCharacter.h"
#include "PlanetUniversMediepolysCharacter.generated.h"


UCLASS()
class UNIVERSMEDIEPOLYS_API APlanetUniversMediepolysCharacter : public AUniversMediepolysCharacter
{
	GENERATED_BODY()

	APlanetUniversMediepolysCharacter();

public:


protected:

	// To add mapping context
	virtual void BeginPlay();

	/** Camera boom positioning the camera behind the character */
	UPROPERTY(VisibleAnywhere, Category = Camera, meta = (AllowPrivateAccess = "true"))
	USpringArmComponent* CameraBoom;

	/** Follow camera */
	UPROPERTY(VisibleAnywhere, Category = Camera, meta = (AllowPrivateAccess = "true"))
	UCameraComponent* FollowCamera;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
	USceneComponent* CameraRoot;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "PlanetController")
	APlayerController* PController;

	USphericalMovementComponent* SphericMovement;


	UFUNCTION(Blueprintcallable)
	void UpdateCameraLook(); 

	UFUNCTION(Blueprintcallable)
	void AlignedToActor();

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
	UPlanetGravity* planetGravity;

	void AddAdditionalInputs() override;

public:

	UPROPERTY()
	FString RoleString;

	void Move(const FInputActionValue& Value) override;
	/** Returns CameraBoom subobject **/
	FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
	/** Returns FollowCamera subobject **/
	FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }

	void CalculateGravity();

	virtual void Tick(float DeltaTime) override;

	UFUNCTION(Blueprintcallable)
	void AssignPlanetGravityToPlayer();

#pragma region ReplicationFunctions


	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const;

	UFUNCTION(Server, Reliable, WithValidation)
	void Server_SendGravityUpdateToServer(const FVector& NewGravityDirection);

	UFUNCTION()
	void OnRep_GravityDirection();  // Called when GravityDirection is updated

	UPROPERTY(Editanywhere, ReplicatedUsing = OnRep_GravityDirection, Category = "PlanetController")
	FVector medielerGravityDirection;

#pragma endregion

};

And this some images of what happens:

Hey @RastaCat33 !

I check you code.
I would recommend not calculate the gravity in each client.
Just calculate it in the server or in the server and wait for the OnRep is calling.

Avoid using RPC if is possible.

In the function calculate gravity do you know if is being called on client?

void APlanetUniversMediepolysCharacter::CalculateGravity()
{
    if (IsValid(planetGravity)) {
        // Ensure the gravity is only applied if the component is active
        if (planetGravity->GetGravityEnabled())
        {
            if (planetGravity->ApplyGravity())
            {
                SphericMovement->UpdateGravityDirection(planetGravity->GetNewGravityDirection());
                
                if (GetLocalRole() == ROLE_AutonomousProxy && !HasAuthority())
                {
                    UE_LOG(LogTemp, Warning, TEXT("Client (%s): Sending gravity update to the server."), *RoleString);

                    //Server_SendGravityUpdate(planetGravity->GetNewGravityDirection());
                    Server_SendGravityUpdateToServer(planetGravity->GetNewGravityDirection());
                }

            }
        }
    }
}

Are those if valid in client?

  • IsValid(planetGravity)
  • planetGravity->GetGravityEnabled()
  • planetGravity->ApplyGravity()

Did you try to remove all related to replication and calculate in each client?

void APlanetUniversMediepolysCharacter::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    //Remove this if
    if (HasAuthority() || (IsLocallyControlled() && GetLocalRole() == ROLE_AutonomousProxy))
    {
        
        CalculateGravity();
    }
}

This will help you to find the issue :smiley:
Let me know if that help you to find the issue. If not I can help you go a lither further.

Will try as soon as I get home to test it.

Yes, those ‘IsValid’ , ‘planetGravity->GetGravityEnabled()’ … are executing in the client side, and what I wanted to try is to release the task of calculating the gravity from the server and giving this task to the clients.

I thought one solution was to change the Role of the client, because it was set as ‘Simulation’ automatically when starting the game. Don’t know if there is a solution by setting the local role as autonomus_proxy or something like that…

Will try simplify this solution and post the updates I get

Thx so much for the quick answer