Aim offset that follows mouse cursor

Hi all,

I’m trying to figure out a twin stick control scheme, and I want the players upper torso to move independently from the lower torso. I have my aim offsets working, my aim offset on both axes takes a value of -0.5 to 0.5 on both axes. For now in my code, I’m manually setting the pitch to 0.

Here’s what my code looks like -


FRotator ADerelictCharacter::GetAimOffsets() const
{
	if (Controller)
	{
		ADerelictPlayerController* PC = Cast<ADerelictPlayerController>(GetController());
		FRotator currentRotation = GetActorRotation();
		FVector target = PC->GetMouseLocationInWorldSpace();
		FVector start = GetActorLocation();
		FVector direction = -(target - start);
		FRotator targetRot = FRotationMatrix::MakeFromX(direction).Rotator();

		const FVector RotationDir = targetRot.Vector();
		const FVector LocalDir = ActorToWorld().InverseTransformVectorNoScale(RotationDir);
		const FRotator LocalRotation = LocalDir.Rotation();
		return FRotator(currentRotation.Pitch, -(LocalRotation.Yaw + currentRotation.Yaw), currentRotation.Roll);
	}
	return FRotator::ZeroRotator;
}

I know this isn’t the correct way to get my character to always face the mouse cursor - it is currently axis dependent. As soon as I change the rotation of my character, it breaks.

Can anyone help me out with some math here? I’d like the aim offset to always take into account my characters facing, I just don’t know how to do it. 3d math fail.

Here’s examples of what I get -

(correct)

http://giant.gfycat.com/UnfitUnlinedCaudata.gif

(incorrect)

http://giant.gfycat.com/WeeCompetentBobolink.gif

Alright I’m much closer to something workable.

Since I got stuck with the aim offset I figured I’d start working on the other code I’m going to need to make this control scheme work. I implemented code so that my pawn is always rotating towards my mouse cursor. I’m slerping to the rotation so that you can still see some of the aim offset.

Now this looked great until I adjusted my characters yaw. When he had a yaw of > 90 degrees or < -90 degrees the aim offset stopped working and he just kind of snapped between two angles for any degrees between there.

So to alleviate this issue I started checking in my getaimoffset function for the actors current rotation like so -



FRotator ADerelictCharacter::GetAimOffsets() const
{
	if (Controller)
	{
		ADerelictPlayerController* PC = Cast<ADerelictPlayerController>(GetController());
		FRotator currentRotation = GetActorRotation();
		FVector target = PC->GetMouseLocationInWorldSpace();
		FVector start = GetActorLocation();
		FVector direction = -(target - start);
		FRotator targetRot = FRotationMatrix::MakeFromX(direction).Rotator();

		GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("Yaw " + FString::SanitizeFloat(currentRotation.Yaw)));

		const FVector RotationDir = targetRot.Vector();
		const FVector LocalDir = ActorToWorld().InverseTransformVectorNoScale(RotationDir);
		FRotator LocalRotation = LocalDir.Rotation();

		if (currentRotation.Yaw > 90 || currentRotation.Yaw < -90) {
			return FRotator(currentRotation.Pitch, (LocalRotation.Yaw + currentRotation.Yaw), currentRotation.Roll);
		}
		else {
			return FRotator(currentRotation.Pitch, -(LocalRotation.Yaw + currentRotation.Yaw), currentRotation.Roll);
		}

		
	}
	return FRotator::ZeroRotator;
}


I get decent results with this solution - but still not optimal. I’m still encountering deadzones where the character just snaps to an angle - which is to be expected the way I’m handling this. As soon as the pawn’s rotation reaches a certain value, I invert the offset, which isn’t correct. I’m missing something when calculating the offset, I’m pretty sure.

Any ideas?

http://giant.gfycat.com/ImaginaryFirsthandBlackrhino.gif

Hey,

I’m doing something simular, reimplemented a blueprint sample/tutorial i found for doing this in C++ in a bit different way …
i assume your pawn blueprint has allready UseControllerRotationPitch/Yaw/Roll turned off?

If u are making a single player game, remove the ServerSetAimOffset()
I’m using this cause i dont use the GetAimOffset method used in the animation blueprint.
For some reasson the method used in the ShooterGame example causing issues when i change the view target to my weapon camera (ADS view)

I use the AimPitch/AimYaw variables directly in my animation blueprint

Header File




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

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

	/** Flags used by the aim offset calculation. */
	uint32 bIsInAimRotation : 1;



Source file



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

	// Calculate the aim offsets
	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, DeltaSeconds, 15.0f);
		AimPitch = UpdatedAimOffset.Pitch;
		AimYaw = UpdatedAimOffset.Yaw;

		// Notify the server about the new aim offset
		if (Role < ROLE_Authority && (AimPitch != SavedAimPitch || AimYaw != SavedAimYaw))
		{
			ServerSetAimOffset(AimPitch, AimYaw);
		}
	}
}

bool ASwatCharacter::ServerSetAimOffset_Validate(float AimPitch, float AimYaw)
{
	return true;
}

void ASwatCharacter::ServerSetAimOffset_Implementation(float InAimPitch, float InAimYaw)
{
	AimPitch = InAimPitch;
	AimYaw = InAimYaw;
}



Finally i update the pawn rotation by overriding the FaceRotation method of ACharacter
do not call Super::FaceRotation, from the comment line “Update the actor rotation”, the code is copied from the ACharacter class.
In the base class, when non of the UseControlRotations are used, the SetRotation will not be called.



void ASwatCharacter::FaceRotation(FRotator NewControlRotation, float DeltaTime)
{
	// 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);
}


Thanks a lot! I’ll read this over here in a few and give it a shot.

Good luck with implementing the code.

I need to change the ServerSetAimOffset() some day cause i’m sure this is giving some network overhead :frowning:

I’m still struggling with this -

I’m not really sure how to implement the solution you shared with me Fallonsnest - doesn’t seem to be using the mouse cursors position at all to calculate an aim offset. I’m not sure what GetControlRotation returns and what the delta you’re calculating between that and your Actor’s rotation represents.

I have no problem calculating an aim offset when my character is facing forwards… Once I start messing with his yaw, the calculation falls apart. This answerhub thread - Aiming on any actor using only upper body (Aim Offset) - Programming & Scripting - Epic Developer Community Forums - is the closest I’m getting to a solution. He even hints at

I just have no idea how to do that.

What you’re probably missing is that you want the aim offset to be in “player local space” rather than world space. I’ll let you figure out how you prefer to do that (there are different methods, but it will likely involve the FTransform or the FRotator). The reason you want it in local space, is that then it doesnt matter which way the player is facing, the aim offset is relative to him. So what you do is you calculate the aim point (mouse cursor) and transform it into his local frame of reference. Then the aim offsets will always blend properly.

I thought that’s what this method call was - const FVector LocalDir = ActorToWorld().InverseTransformVectorNoScale(RotationDir);

I may just be misunderstanding you - here’s an up to date copy of my code / gif of results. Hopefully with comments added someone can point out my mistake.



 ADerelictCharacter::GetAimOffsets() const {
	if (Controller)
	{
		ADerelictPlayerController* PC = Cast<ADerelictPlayerController>(GetController());
		if (PC) {
			FVector MouseLocation;
			FVector MouseDirection;

                        // Deproject mouse location & directino to world space
			PC->DeprojectMousePositionToWorld(MouseLocation, MouseDirection);
                        // Calculate in world space, a look at rotation
			const FRotator LookAtRotation = FRotationMatrix::MakeFromX(MouseDirection).Rotator();

                        // Get a direction vector from the look at rotation - still in world space
			const FVector LookAtDirection = LookAtRotation.Vector();

                        // Convert direction vector of desired rotation to actors local space
			const FVector LocalLookAtDirection = GetTransform().InverseTransformVector(LookAtDirection);
                        // Get local rotation
			const FRotator LocalLookAtRotation = LocalLookAtDirection.Rotation();


			UE_LOG(LogDerelict, Warning, TEXT("Look at rotation yaw is %f"), LocalLookAtRotation.Yaw);

			const FRotator CurrentActorRotation = GetActorRotation();

			return FRotator(CurrentActorRotation.Pitch, LocalLookAtRotation.Yaw, CurrentActorRotation.Roll);
		}
		return FRotator::ZeroRotator;
	}
	return FRotator::ZeroRotator;
}


Also this is the code in my player controller to rotate the character -



void ADerelictPlayerController::PlayerTick(float DeltaTime)
{
	Super::PlayerTick(DeltaTime);
	APawn* const Pawn = GetPawn();
	if (Pawn)
	{
		// Get mouse position on screen
		float xMouse;
		float yMouse;
		GetMousePosition(xMouse, yMouse);

		// Get Character position on screen
		FVector CharLoc = Pawn->GetActorLocation();
		FVector2D CharInScreen;
		ProjectWorldLocationToScreen(CharLoc, CharInScreen);

		// Get mouse position relative to the Character.
		FVector2D Result;
		Result.X = -(yMouse - CharInScreen.Y);
		Result.Y = xMouse - CharInScreen.X;

		// Get angle of rotation from mouse position based on character position
		float angle = FMath::RadiansToDegrees(FMath::Acos(Result.X / Result.Size()));

                // Clamp between 0 & 360
		if (Result.Y < 0)
			angle = 360 - angle;

                // Get character's denormalized yaw
		const float CharacterYaw = Pawn->GetActorRotation().GetDenormalized().Yaw;

                // Compare to our desired angle of rotation
		const float delta = angle - CharacterYaw;

                // If the character is more than 90 degrees off of our desired rotation, start to lerp towards desired rotation.
		if (delta > 90 || delta < -90) {
			FRotator rot(0, angle, 0);

			Pawn->SetActorRotation(FMath::RInterpTo(Pawn->GetActorRotation(), rot, DeltaTime, 1.0));
		}
	}
}


Here’s what all this ends up looking like :

http://giant.gfycat.com/FickleFailingArgali.gif

Hmm, reading your aim offset code it does look like you are getting the local offset vector properly. But then you are returning the rotation value for that in the offset, rather than a re-transformed version. Remember that the angle you are getting is in player local space. Do you want that? Or do you expect it in world space? simple way to check would be to add the characters yaw onto the offsets yaw and see if that helps. Normally you’d do the blend in player space, so you should be able to use that angle raw, but then I don’t know how you’re using it.

Other than that, go through the math and check that everything has the correct values. Feed a set of values into the method that you know are good (i.e. feed a forward vector into the character and a right vector into the offset and make sure you get the right angles, then do the same for a left vector etc).

Oh and you might want to actually draw a debug point for the mouse world location, just to make sure that part of the math is correct.

Thanks for the suggestions - I shouldn’t have been deprojecting the mouse position - it kind of worked but the better way to do it was to raytrace.



FRotator ADerelictCharacter::GetAimOffsets() {
	if (Controller)
	{
		ADerelictPlayerController* PC = Cast<ADerelictPlayerController>(GetController());
		if (PC) {
			FHitResult HitResult;
			PC->GetHitResultUnderCursorByChannel(UEngineTypes::ConvertToTraceType(ECC_Camera), true, HitResult);

			FVector PlayerLocation = GetActorLocation() * FVector(1, 1, 0);
			FVector LocationDelta = HitResult.Location - PlayerLocation;
			LocationDelta.Normalize();

			const FRotator ActorRotation = GetActorRotation();

			FVector RightVector = FRotationMatrix(GetActorRotation()).GetScaledAxis(EAxis::Y) * FVector(1, 1, 0);
			RightVector.Normalize();

			const float yaw = FVector::DotProduct(RightVector, LocationDelta);

			return FRotator(ActorRotation.Pitch, yaw, ActorRotation.Roll);
				
		}
		return FRotator::ZeroRotator;
	}
	return FRotator::ZeroRotator;
}


I got rid of the code that rotates my character based on mouse position - They player can use WASD / another joystick to rotate the character and just use this one to aim.

The only thing I need to fix at this point is - when the mouse cursor goes behind the character, so his forward vector might be (1,0,0) and the cursor could be behind him and he’ll still aim. Ideally I’d like to only allow him to aim when the mouse cursor is within a 180 degree arc in front of him. I’ll fix this later, for now this gets me where I need to be.

To do that, you want to check the sign of the dot product between the players direction vector and the target (or it might be the left/right vector, can’t remember, math failure). Basically dot product gives you the angle, but its signed so +ve and -ve mean in front or behind (again, can’t remember which way round it is). So just check the sign of the return value from the dot product of the two vectors.

Thanks zoombapup! I’ll give a shot at implementing that solution this evening.