Attempting to create 3rd person free-camera based movement

I couldn’t think of a proper name for this style of movement.

Basically, I want to create a Dark Souls type movement. It’s 3rd Person Camera, the camera is unlocked and can be rotated freely, and the player moves according to the camera view. Forward goes away from the camera, back goes towards the camera, and left/right is based on the camera as well.

https://youtu.be/RhiOxTfhFDc?t=11m49s - There is “narrative” audio. Sorry.

What I did was I created two functions, a MoveForward and MoveRight (that also handles back/left based on negative input). I’m terrible at vector math and 3D space stuff so I just did it some hacky way I came up with and basically they do the same thing. Turn the player according to the camera and then apply a forward movement to the character.

The problem is, when I add in the Lerp to smoothly rotate the character towards the direction rather than snapping them straight to the new rotation, the character begins behaving uncontrollably

Here’s my behavior https://youtu.be/4RhIH6vwwwM

In the video I’m moving forward okay, pressing right or left spins the character okay (although he always seems to take the long way around, which looks weird), but if I press both Forward and Right he’ll only SOMETIMES go the proper way, other times he turns behind me and runs backwards and left. Same thing if I do foward and left, he’ll sometimes run backwards and right.

Now I’ve pretty much figured out it’s the lerping function. It’s constantly getting two different inputs, and splitting the difference (which is what you’d want) but the lerp is rotating in such an odd way (the long way around bit) that it’s actually causing the actor to rotate the wrong way, and then it stops at the opposite direction of what I want.

I understand this isn’t a very good way of doing it, so I’m posting on here hopefully for someone to help steer me in the right direction as to a proper way to do this.



//========================= MOVEMENT =========================

/* MoveForward() - Takes the character and makes them rotate forward before applying a forward movement */
void ABLTPlayerPawn::MoveForward(float Value)
{
	if (Value == 0 || !IsLocallyControlled() || !GetMovementComponent())
		return;

	TurnCharacterToFront(Value); //Make the character face the proper direction towards/away from camera
	AddMovementInput(GetActorForwardVector(), FMath::Abs(Value)); //Apply an Absolute value onto movement because we will always be facing forward
}

/* MoveRight() - Turns the character to face camera left/right and then applies forward movement accordingly
*
*
*
*
*/
void ABLTPlayerPawn::MoveRight(float Value)
{
	if (Value == 0 || !IsLocallyControlled() || !GetMovementComponent())
		return;

	TurnCharacterToSide(Value); //Turn the character to the side
	AddMovementInput(GetActorForwardVector(), FMath::Abs(Value)); //Apply movement in the FORWARD direction because we just made their forward vector face the camera's side. FMath::Abs (AbsoluteOf(Value)) prevents a negative input (which would end up as a "move backwards" command)
}

/* TurnCharacterToFront() - Rotates the character to face the camera's direction or to face the camera itself
* @Param - Value: Whether we're going forward or backwards
*
*
*
*/
void ABLTPlayerPawn::TurnCharacterToFront(float Value)
{

	if (CameraComponent == NULL) //Return if we don't have a camera
		return;

	FVector LookAtVect = GetActorLocation() + (CameraComponent->GetForwardVector() * (192 * Value)); //Determine which direction the player wants to head based on camera direction and input direction (Value)
	FVector MyLocation = GetActorLocation(); //Get our pawn's location
	FRotator NewRotation = (LookAtVect - MyLocation).Rotation(); //Start the new Rotation by aiming our character towards where we're looking
	NewRotation.Pitch = GetActorRotation().Pitch; //Just make sure we aren't pitching or rolling the character accidently from my hacky way of determing rotation
	NewRotation.Roll = GetActorRotation().Roll;
	
	NewRotation = FMath::Lerp(GetActorRotation(), NewRotation, 0.1); //Interpolate current rotation to new rotation for smooth turning
	SetActorRotation(NewRotation); //Set new rotation


	ServerUpdateRotation(GetActorRotation()); //Tell the server we've turned
}

/* TurnCharacterToSide() - Rotates the character to the left or the right based on Camera's view
* @Param - Value: Simply whether we're going Left or Right (passed from MoveRight())
* This is literally the same as TurnCharacterToFront, except we're using the Camera's RightVector instead. So just read those comments
*
*
*/
void ABLTPlayerPawn::TurnCharacterToSide(float Value)
{
	if (CameraComponent == NULL) //Return if we don't have a camera
		return;

	FVector LookAtVect = GetActorLocation() + (CameraComponent->GetRightVector() * (192 * Value));
	FVector MyLocation = GetActorLocation();
	FRotator NewRotation = (LookAtVect - MyLocation).Rotation();
	NewRotation.Pitch = GetActorRotation().Pitch;
	NewRotation.Roll = GetActorRotation().Roll;

	NewRotation = FMath::Lerp(GetActorRotation(), NewRotation, 0.1);
	SetActorRotation(NewRotation);

	ServerUpdateRotation(GetActorRotation());
}

I believe if you use FMath::RInterpTo or FMath::RInterpConstantTo (based on whether you want acceleration or constant rotation), it will output a rotator lerped via the shortest path.

What is going wrong with using FMath::Lerp, I think, is that the rotators you pass in aren’t normalized: their pitch/yaw/roll aren’t guaranteed stored in the range (-180, 180). You would expect that if you feed the lerp’s result back into next frame’s lerp it will eventually reach the target, either via the short or long route, right? What’s preventing this from happening is that the output rotator from FMath::Lerp is normalized, but one of the input rotators (NewRotation) isn’t. For example if your target yaw is 230 (not within -180, 180) and your current yaw is 170 (almost there), lerp with alpha 0.5 would return 200, which normalzied becomes 200 - 360 = -160. So whenever your current rotation is about to reach the target rotation, the normalization puts it 360 degrees back again and the next lerp will put it somewhere inbetween not knowing that -180 == 180.

Long story short: NewRotation.Normalize() will help your character reach the target rotation (via long or short route), but FMath::RInterpTo/RInteroConstantTo will give you the same but always the short route. :wink:

That actually cleaned up 90% of my problems right there, and I really appreciate it!

I never thought of needing to normalize my rotation, but I ended up using FMath::RInterpTo and it’s working really nicely

EXCEPT one issue. I can now reliably run Forward/Backwards and Left/Right at the same time unless one condition happens. If I try and run the exact opposite way next move, it continues going the previous way.

Example: If I run Forward and Right, and then try to run Backwards and Left. It continues to run Forward and Right instead of rotating to the new direction. If I run Back and Right, it turns just fine and runs properly. But then trying to go Forward and Left from there causes it to continue running Back and Right.

Updated Code:



/* TurnCharacterToFront() - Rotates the character to face the camera's direction or to face the camera itself
* @Param - Value: Whether we're going forward or backwards
*
*
*
*/
void ABLTPlayerPawn::TurnCharacterToFront(float Value)
{

	if (CameraComponent == NULL) //Return if we don't have a camera
		return;

	FVector LookAtVect = GetActorLocation() + (CameraComponent->GetForwardVector() * (192 * Value)); //Determine which direction the player wants to head based on camera direction and input direction (Value)
	FVector MyLocation = GetActorLocation(); //Get our pawn's location
	FRotator NewRotation = (LookAtVect - MyLocation).Rotation(); //Start the new Rotation by aiming our character towards where we're looking
	NewRotation.Pitch = GetActorRotation().Pitch; //Just make sure we aren't pitching or rolling the character accidently from my hacky way of determing rotation
	NewRotation.Roll = GetActorRotation().Roll;
	
	NewRotation.Normalize();
	NewRotation = FMath::RInterpTo(GetActorRotation(), NewRotation,FApp::GetDeltaTime(), 10.5f);
	//NewRotation = FMath::Lerp(GetActorRotation(), NewRotation, 0.1); //Interpolate current rotation to new rotation for smooth turning
	SetActorRotation(NewRotation); //Set new rotation


	ServerUpdateRotation(GetActorRotation()); //Tell the server we've turned
}

/* TurnCharacterToSide() - Rotates the character to the left or the right based on Camera's view
* @Param - Value: Simply whether we're going Left or Right (passed from MoveRight())
* This is literally the same as TurnCharacterToFront, except we're using the Camera's RightVector instead. So just read those comments
*
*
*/
void ABLTPlayerPawn::TurnCharacterToSide(float Value)
{
	if (CameraComponent == NULL) //Return if we don't have a camera
		return;

	FVector LookAtVect = GetActorLocation() + (CameraComponent->GetRightVector() * (192 * Value));
	FVector MyLocation = GetActorLocation();
	FRotator NewRotation = (LookAtVect - MyLocation).Rotation();
	NewRotation.Pitch = GetActorRotation().Pitch;
	NewRotation.Roll = GetActorRotation().Roll;

	NewRotation.Normalize();
	NewRotation = FMath::RInterpTo(GetActorRotation(), NewRotation, FApp::GetDeltaTime(), 15.5f);
	//NewRotation = FMath::Lerp(GetActorRotation(), NewRotation, 0.1);
	SetActorRotation(NewRotation);

	ServerUpdateRotation(GetActorRotation());
}