📐

Built-in Pitch Replication And Axis Compression in Unreal Engine

Replicating Pitch for using in aim offsets is a quite a common task to come across while building almost any kind of multiplayer game, you want to replicate the look view of other players to know where they are looking or aiming at.

Quite a lot of young developers will search about this on internet and come across tutorials that manually replicate the pitch value from control rotation, this is all well and good and I myself was one of those guys back in university days starting to learn Unreal I also replicated the pitch using the same way, it was not until I came across Shooter Game where upon seeing the source code I realized that a better and more efficient solution already comes with Unreal Engine.

The Pawn Class

You can use the function called GetBaseAimRotation() which exists inside the pawn class to get the replicated pitch in blueprints as well.

Follow these steps:

In C++ you can either use the functions provided by Kismet library or the C++ native functions both do the same thing, If using Kismet Library remember to add it’s respective header.

Let’s create a function which we can use to get our replicated pitch value:

FRotator GetReplicatedPitch() const;

In the implementation file:

float AMyCharacter::GetReplicatedPitch() const
{
	const FVector AimDirectionWorld = UKismetMathLibrary::GetForwardVector(GetBaseAimRotation());
	const FVector AimDirectionLocal = UKismetMathLibrary::InverseTransformDirection(GetActorTransform(), AimDirectionWorld);
	const FRotator AimRotationLocal = UKismetMathLibrary::MakeRotFromX(AimDirectionLocal);
	
	return AimRotationLocal.Pitch;
}

Or if you don’t want to use Kismet Math Library you can easily do the same thing with native functions:

float AMyCharacter::GetReplicatedPitch() const
{
	const FVector AimDirectionWorld = GetBaseAimRotation().Vector();
	const FVector AimDirectionLocal = ActorToWorld().InverseTransformVectorNoScale(AimDirectionWorld);
	const FRotator AimRotationLocal = AimDirectionLocal.Rotation();
	
	return AimRotationLocal.Pitch;
}

Under The Hood:

You might be curious that how this works under the hood, you can use the same method for replicating other axis or even other values.

As you have seen the method at the core is called GetBaseAimRotation() this function is divided into two paths, if the player has the controller and if the controller isn’t present AKA If it is a simulated proxy.

In case of controller being value the method executes a very simple set of instructions:

FVector POVLoc;
FRotator POVRot;
if( Controller != nullptr && !InFreeCam() )
{
	Controller->GetPlayerViewPoint(POVLoc, POVRot);
	return POVRot;
}

We are just getting the player’s eyes or direction at which the player is looking at. However if the controller is not valid:

// If we have no controller, we simply use our rotation
POVRot = GetActorRotation();

// If our Pitch is 0, then use a replicated view pitch
if( FMath::IsNearlyZero(POVRot.Pitch) )
{
			// use the RemoteViewPitch
			const UWorld* World = GetWorld();
			// Simulated network driver for recording and playing back game sessions
			const UDemoNetDriver* DemoNetDriver = World ? World->GetDemoNetDriver() : nullptr;
			// This is for replay and recording skip this if statement
			if (DemoNetDriver && DemoNetDriver->IsPlaying() && (DemoNetDriver->GetPlaybackEngineNetworkProtocolVersion() < FEngineNetworkCustomVersion::PawnRemoteViewPitch))
			{
				POVRot.Pitch = RemoteViewPitch;
				POVRot.Pitch = POVRot.Pitch * 360.0f / 255.0f;
			}
			else
			{
				// This is the main part, we are just assigning Remote View pitch to pitch and returning it
				POVRot.Pitch = FRotator::DecompressAxisFromByte(RemoteViewPitch);
			}
}

return POVRot;

You might get a bit overwhelmed at first but most of this code doesn’t even get the chance to execute in normal circumstances because it is for replay and recording features, the thing we are concerned with is the last else of the if else statements:

// This is the main part, we are just assigning Remote View pitch to pitch and returning it
POVRot.Pitch = FRotator::DecompressAxisFromByte(RemoteViewPitch);

variable called RemoteViewPitch is a replicated member of the class here is how it is declared in the header:

UPROPERTY(replicated)
uint8 RemoteViewPitch;

It’s just a replicated property and another thing that catches the eye is the unsigned integer, in this variable is where our Pitch value is stored in a compressed format.

We do the compression in the pre-replication stage, pre-replication is a virtual function that you can override and this is called on classes of type Actors right before the actual replication occurs.

void APawn::PreReplication( IRepChangedPropertyTracker & ChangedPropertyTracker )
{
	Super::PreReplication( ChangedPropertyTracker );

	if (GetLocalRole() == ROLE_Authority && GetController())
	{
		SetRemoteViewPitch(GetController()->GetControlRotation().Pitch);
	}
}

The SetRemoteViewPitch is another function inside the Pawn class which just compresses the float axis value into a single byte.

void APawn::SetRemoteViewPitch(float NewRemoteViewPitch)
{
	// Compress pitch to 1 byte
	RemoteViewPitch = FRotator::CompressAxisToByte(NewRemoteViewPitch);
}

That is all for this topic, I do recommend to go into the pawn class and take a look at these functions again, even if you can’t grasp these fear not as most of time you just will be using the built-in pitch replication for pawns and if you want to compress some other axis the best way to do this is in the pre-replication function.

Good Luck!