True FPS & Aim offsets - how to handle extremes

Hi all,

I’m building a game which features a true FPS perspective via attaching a camera component to my pawn’s head, and I’m trying to figure out how to handle aim offsets.

I guess since my pawn is always going to be rotating to face the mouse cursor, as is standard in most FPS games I’d only need a one dimensional aim offset for pitch. Has anyone experimented using an aim offset for the players yaw and then rotating the player to the mouse cursor after they pass the extent of the aim offset?

My main question is this - once I approach the maximum / min bounds of my aim offset, how can I prevent the aim offset from causing the character animation to flip flop between positive and negative axes? I tried clamping the pitch value to -90 / 90 to match my aim offset axis bounds, but this didn’t seem to work. Any other ideas?

Thanks!

I figured out the answer to my main question - answer was in the player camera manager, needed to make sure I set ViewPitchMin/Max appropriately.

Still interested in the first question I posed - if anyone has used aim offset yaw in a FPS context, and figured out a way to make the character realistically rotate when the max / min extent of the aim offset is reached.

Hey,

Im using true first person for my game with free aim.
But im using a custom aim pitch / yaw cause for multiplayer only aim pitch is replicated and seems using GetAimOffsets like in the ShooterGame example giving issues like the flipping camera when looking down all the way and loosing control when switching to weapon attached camera for ADS view.

I’m using the AimPitch/AimYaw variables directly in the animation BP.
In the character BP i also disable UseControllerRotationPitch/Yaw/Roll (need to override FaceRotation when doing this)

For the head attached camera, enable the “Use Pawn Control Rotation”

Character Header file




	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Transient, Replicated, Category = "Character")
	float AimPitch;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Transient, Replicated, Category = "Character")
	float AimYaw;



Character Source file




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

	if (IsLocallyControlled())
	{
		// Save the current aim pitch and yaw
		float SavedAimPitch = AimPitch;
		float SavedAimYaw = AimYaw;

		// Update the aim offset
		FRotator ControlRotation = GetControlRotation();
		FRotator ActorRotation = GetActorRotation();
		FRotator DeltaRotation = ControlRotation - ActorRotation;
		DeltaRotation.Normalize();

		// Interpolate the current and new aim offsets for smoothing
		FRotator CurrentAimOffset(AimPitch, AimYaw, 0.0f);
		FRotator UpdatedAimOffset = FMath::RInterpTo(CurrentAimOffset, DeltaRotation, DeltaTime, 15.0f);
		AimPitch = UpdatedAimOffset.Pitch;
		AimYaw = UpdatedAimOffset.Yaw;

		// Notify the server about the new aim offset
		if (Role < ROLE_Authority && (AimPitch != SavedAimPitch || AimYaw != SavedAimYaw))
		{
			//TODO: This might give some network overhead, see if we can replicate this trough the movement component
			//		see RemoteViewPitch (how its set and replicated)
			ServerSetAimOffset(AimPitch, AimYaw);
		}
	}
}


void ASwatCharacter::FaceRotation(FRotator NewControlRotation, float DeltaTime)
{
	// Only continue if it's not game over
	static const FName NAME_GameOver = FName(TEXT("GameOver"));
	auto SwatPlayerController = Cast<ASwatPlayerController>(Controller);
	if (SwatPlayerController && SwatPlayerController->GetStateName() == NAME_GameOver)
	{
		return;
	}

	// Update the actor rotation based on the aim offset
	FRotator ControlRotation = GetControlRotation();
	FRotator ActorRotation = GetActorRotation();
	FRotator ControlYawRotation(0.0f, ControlRotation.Yaw, 0.0f);
	FRotator ActorYawRotation(0.0f, ActorRotation.Yaw, 0.0f);
	FRotator YawDeltaRotation = ControlYawRotation - ActorYawRotation;
	YawDeltaRotation.Normalize();
	FRotator AimRotation = FMath::RInterpTo(FRotator::ZeroRotator, YawDeltaRotation, DeltaTime, 5.0f);
	if (FMath::Abs(YawDeltaRotation.Yaw) >= 70.0f || bIsInAimRotation || GetVelocity().Size() > 0.0f)
	{
		AddActorWorldRotation(AimRotation);
		bIsInAimRotation = !FMath::IsNearlyEqual(YawDeltaRotation.Yaw, 0.0f, 2.0f);
	}

	// Update the actor rotation
	const FRotator CurrentRotation = GetActorRotation();
	if (!bUseControllerRotationPitch)
	{
		NewControlRotation.Pitch = CurrentRotation.Pitch;
	}
	if (!bUseControllerRotationYaw)
	{
		NewControlRotation.Yaw = CurrentRotation.Yaw;
	}
	if (!bUseControllerRotationRoll)
	{
		NewControlRotation.Roll = CurrentRotation.Roll;
	}
	SetActorRotation(NewControlRotation);
}


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

	DOREPLIFETIME_CONDITION(ASwatCharacter, AimPitch, COND_SkipOwner);
	DOREPLIFETIME_CONDITION(ASwatCharacter, AimYaw, COND_SkipOwner);
}