How to Rotate Turret Bone to Camera (Fast Snappy Rotation without Jitter) in Unreal?

I’m building a tank player character, and I have a skeletal mesh with a turret bone.
The goal is to rotate the turret based on the camera yaw direction.

I don’t want the turret to slowly rotate like a tank — I want it to snap quickly to the camera direction, but without jittering when the tank moves.

2025-04-28 16-02-32_2

Here’s what I’m doing right now (roughly):





Any advice, examples, or best practices would be amazing!
Thanks a lot!

understood. so if you are doing that animation to target rotation with modify bone

there is an interp option on that node where you can set speed of interpolation to target rotations.

I see that it is already snapping very fast and doing its job. however there is no interpolation between start point and end point. So that you see that as jittering between frames. A higher frame rate probably will see this less however proper way is to interpolate the rotation like above.

Thanks for the suggestion! But sadly it doesn’t solve the problem.
2025-04-28 16-02-32_3

I understand what you mean about interpolating with Modify Bone.

However, the problem is not just a lack of interpolation.
The real issue is that the tank body itself is rotating suddenly (e.g., when turning while moving),
and the turret rotation is calculated relative to the world or control rotation.

Because the base (the tank) moves sharply, it causes the turret rotation to jitter even if I interpolate.
So even with smoothing, the target rotation is constantly changing due to the tank’s own sharp movements.

Interpolation alone doesn’t solve it because the “starting point” itself shifts too violently frame-to-frame.

I think I might need a way to:

  • Either stabilize the reference frame for the turret
  • Or decouple the turret rotation logic from the tank’s movement frame updates

Thanks again for the help, any ideas for solving this properly?

Well decoupling is the idea since it will always look at camera direction. I would just bind it to camera forward look at angle if thats the case.

By the way just want to mention something that I would beleive can be handy for you, since you ask the proper way :smiley: Since this is a TPS after all, having single trace channel to world you will need, now or in the future development.

Basically what is the point of target you have.

That enables you to

  • Use it visually accurate turret pointing
  • If there is an obstacle you can inform player about it in crosshair. Since look at angles are different as you know probably.
  • If you are using in future like a very big curve projectile you can use the system for landing point.

So better use it with targeting system if you don’t have already can be good to create. Just sayin

Here is what I mean by a targeting system thats what you should be targeting for. You can start as simple and build on it depending on the game’s needs and by design shooting experience you want. Wanted to help since it looks like a nice little tank game :smiley:

123

Thanks a lot for the detailed explanation and advice!
Also your art style looks incredibly cool, just from the little bit I can see in your GIF file. :grinning_face_with_smiling_eyes:
If you’re curious, the game we’re working on is called DroneTanks: https://www.youtube.com/watch?v=Prs47aeXpwM.
Initially, we had the tank body, turret base, and turret barrel all split up into separate meshes for rotation handling.

Recently though, we decided to move toward single skeletal mesh setups instead (because we’re adding designs with hammers, wings, and more complex moving parts),
so we thought sticking to a skeletal mesh would be cleaner for animation blending.

Anyway, I tried decoupling the turret rotation calculation into the AnimBP (like you suggested), but I’m still getting jitter.

I’m not using a raycast; I’m using the camera’s forward vector directly to determine the aim.


2025-04-28 16-02-32_4
And it still jitters :frowning:

My problem now: Even with everything moved into the AnimBP, the turret keeps jittering when the tank body moves sharply.

I’m stuck with this one :thinking:

Well thanks but it’s not my game or I don’t work on it as a developer. It’s Valve’s Deadlock.

Your game looks nice, subscribed to your channel :slight_smile:

allright, think this is more solveable and. Can you check your tick groups? Play with them a little if not you can use Add Tick Prerequisite Component. Basically it willl say which tick before what actor.

Try checking skeletal mesh component tick too. This is common on VFX too to have accurate tick order so when you jump etc, the projectile VFX displays where it should be. Think its similar problem.

1 Like

You can decouple turret rotation from tank body by (ab)using behavior from SpringArmComponent.
See example here How to handle child components - #2 by Chatouille
Attach the turret to body via a zero length spring arm, disable rotation inheritance, and disable its other features. Now the turret shouldn’t try to rotate with the body anymore.

Hey, thanks for joining in with the conversation! In our case, the turret isn’t a separate mesh, but the entire tank is a single skeletal mesh, and the turret is controlled through a bone. I read your response in the other thread where you suggested using a SpringArmComponent to decouple turret rotation, which seems like a smart approach for separate components, but our setup doesn’t really match that.

If you’ve worked with bone-driven turret rotation before, I’d be really interested to hear how you handled it!

Appreciate you sharing the approach though, if you’ve dealt with bone-based turret setups before, I’d be super interested in any best practices you’ve come across!

the tank cpp

#include "Player/MainPlayer.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "Player/PlayerWeaponSystem.h"

AMainPlayer::AMainPlayer()
{
	PrimaryActorTick.bCanEverTick = true;

	bUseControllerRotationYaw = false;
	JumpMaxHoldTime = JumpHoldTime;

	// Capsule Component
	SetRootComponent(GetCapsuleComponent());
	GetCapsuleComponent()->SetCapsuleHalfHeight(80.0f);
	GetCapsuleComponent()->SetCapsuleRadius(60.0f);
	
	// Base Mesh
	GetMesh()->SetupAttachment(GetRootComponent());
	GetMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	GetMesh()->SetRelativeLocation(FVector(0.0f, 0.0f, -75.0f));

	// Spring Arm
	CameraSpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraSpringArm"));
	CameraSpringArm->SetupAttachment(GetCapsuleComponent());
	CameraSpringArm->TargetArmLength = 600.0f;
	CameraSpringArm->TargetOffset = FVector(0.0f, 0.0f, 150.0f);
	CameraSpringArm->bUsePawnControlRotation = true;

	// Camera
	Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
	Camera->SetupAttachment(CameraSpringArm);

	// Movement Component
	GetCharacterMovement()->MaxWalkSpeed = MovementSpeed;
	GetCharacterMovement()->MaxAcceleration = Acceleration;
	GetCharacterMovement()->BrakingDecelerationWalking = BrakingDecelerationWalking;
	GetCharacterMovement()->RotationRate = FRotator(0.0f, RotationRate, 0.0f);
	GetCharacterMovement()->BrakingFrictionFactor = BrakingFrictionFactor;
	
	GetCharacterMovement()->GravityScale = Gravity;
	GetCharacterMovement()->JumpZVelocity = JumpForce;
	GetCharacterMovement()->BrakingDecelerationFalling = BrakingDecelerationWhileFalling;
	GetCharacterMovement()->AirControl = AirControl;

	GetCharacterMovement()->GroundFriction = 8.0f;
	GetCharacterMovement()->MinAnalogWalkSpeed = 20.0f;
	GetCharacterMovement()->bUseSeparateBrakingFriction = true;
	GetCharacterMovement()->bOrientRotationToMovement = true;

	// Weapon system component
	WeaponComponent = CreateDefaultSubobject<UPlayerWeaponSystem>(TEXT("WeaponComponent"));
}

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

	ApplyPlayerVariables();

	if (WeaponComponent)
	{
		WeaponComponent->InitializeWeaponComponent(this);
	}
}

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

	if (bShouldDampenZVelocity && bIsGliding)
	{
		GlideDampElapsed += DeltaTime;

		// Lerp from the speed the player was falling to the glide down speed
		const float Alpha = FMath::Clamp(GlideDampElapsed / GlideDampTime, 0.0f, 1.0f);
		const float NewZ = FMath::Lerp(InitialZVelocity, GlideDownSpeed, Alpha);

		// Update velocity with new Z value
		FVector Velocity = GetCharacterMovement()->Velocity;
		Velocity.Z = NewZ;
		GetCharacterMovement()->Velocity = Velocity;

		if (Alpha >= 1.0f)
		{
			bShouldDampenZVelocity = false;
		}
	}

	if (bIsGliding)
	{
		// Gradually increase the forward glide speed up to the maximum glide speed
		CurrentGlideSpeed = FMath::Clamp(CurrentGlideSpeed + (GlideAcceleration * DeltaTime), 0.0f, GlideSpeed);

		// Set forward velocity and keep Z velocity updated
		FVector ForwardVelocity = GetActorForwardVector() * CurrentGlideSpeed;
		ForwardVelocity.Z = GetCharacterMovement()->Velocity.Z; // Maintain the updated Z velocity
		GetCharacterMovement()->Velocity = ForwardVelocity;
	}

	if (TimeSinceDash < TimeBetweenDashes) TimeSinceDash += DeltaTime;
}

void AMainPlayer::ApplyPlayerVariables() const
{
	GetCharacterMovement()->MaxWalkSpeed = MovementSpeed;
	GetCharacterMovement()->MaxAcceleration = Acceleration;
	GetCharacterMovement()->BrakingDecelerationWalking = BrakingDecelerationWalking;
	GetCharacterMovement()->RotationRate = FRotator(0.0f, RotationRate, 0.0f);
	GetCharacterMovement()->BrakingFrictionFactor = BrakingFrictionFactor;
	
	GetCharacterMovement()->GravityScale = Gravity;
	GetCharacterMovement()->JumpZVelocity = JumpForce;
	GetCharacterMovement()->BrakingDecelerationFalling = BrakingDecelerationWhileFalling;
	GetCharacterMovement()->AirControl = AirControl;
}

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

	const FRotator ControlRotation = GetControlRotation();
	const FRotator YawRotation(0.f, ControlRotation.Yaw, 0.f);

	const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
	const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
	
	AddMovementInput(RightDirection, MovementVector.X);
	AddMovementInput(ForwardDirection, MovementVector.Y);

	TankTreadMovement();
}

// Looking
void AMainPlayer::Look(const FInputActionValue& Value)
{
	const FVector2D LookAxisVector = Value.Get<FVector2D>();
	
	AddControllerYawInput(LookAxisVector.X);
	AddControllerPitchInput(-LookAxisVector.Y);
}

void AMainPlayer::Landed(const FHitResult& Hit)
{
	Super::Landed(Hit);

	StopGlide();
}

// Jumping
void AMainPlayer::StartJump()
{
	PlayAnimMontage(JumpMontage);
	Jump();
}

void AMainPlayer::StopJump()
{
	StopJumping();	
}

void AMainPlayer::StartGlide()
{
	if (GetCharacterMovement()->IsFalling())
	{
		const FVector PlayerVelocity = GetCharacterMovement()->Velocity;

		StopJumping();

		// Here we check if the player is moving forward
		const float InitialSpeed = FVector::DotProduct(PlayerVelocity, GetActorForwardVector());
		CurrentGlideSpeed = FMath::Max(InitialSpeed, 0.0f);

		// Start Z velocity damp
		InitialZVelocity = PlayerVelocity.Z;
		GlideDampElapsed = 0.0f;

		// Disable gravity temporarily to prevent it from interfering with glide downward speed
		GetCharacterMovement()->GravityScale = 0.0f;

		// Enable glide-specific movement settings
		GetCharacterMovement()->AirControl = GlideAirControl;
		GetCharacterMovement()->RotationRate = FRotator(0.0f, GlideRotationRate, 0.0f);

		bIsGliding = true;
		bShouldDampenZVelocity = true;
	}
}

void AMainPlayer::StopGlide()
{
	GetCharacterMovement()->GravityScale = Gravity; 
	GetCharacterMovement()->AirControl = AirControl;
	GetCharacterMovement()->RotationRate = FRotator(0.0f, RotationRate, 0.0f);

	bIsGliding = false;
	CurrentGlideSpeed = 0.0f;
	bShouldDampenZVelocity = false;
	InitialZVelocity = 0.0f;
	GlideDampElapsed = 0.0f;
}

void AMainPlayer::Dash()
{
	if (TimeSinceDash >= TimeBetweenDashes)
	{
		const FVector MovementInput = GetLastMovementInputVector();

		bIsDashing = true;

		const FVector UpVector = this->GetActorUpVector();
		const FVector DashVector = FMath::Lerp(MovementInput, UpVector, DashAngle / 90.0f);

		this->LaunchCharacter(DashVector * DashStrength, true, true);
		TimeSinceDash = 0.0f;
	}
}


You’re an absolute hero! :smiley: Really appreciate the help, this was driving me crazy.

Setting the tick group on BP_MainPlayer to Post Update Work actually did the trick! That seems to have fixed the main issue! I just hope it won’t cause problems elsewhere down the line? Before I can fully call this solved though, when rotating the camera really fast, it still feels a tiny bit laggy, so I might try to make it even snappier.
giphy
Do you have any suggestions on how I could push it further to make it more responsive?

Thanks again, you’ve been a huge help!

1 Like

=) i am flattered also happy to see tiny little tank move nicely.

It would not cause any issues and if you have issues around it in further development like a vfx not appearing right on muzzle, its all about tick groups and can be always ordered and solved. So you don’t have to worry too much about it.

To be more precise how I solve these kind of issues.

  • As a designer I prefer to make movements on PrePhyiscs
  • As a designer I make movements in PrePhysics so that I can arrange appearence of projectiles, VFX with ease on first person, with different needs. Traces, Spawns etc.
  • As a designer I tend to do gameplay related movements as component in pawn itself so that Camera, Weapon Motions such as inertia, bop, sway, swap becomes fluid and gameplay specific movements in animBP so it has meaningfull variations.

So basically I solve this problems not in animation but actual hierarchy. If I was doing a tank, I would probably have 2 skeletal meshes in my pawn class as body and turret. Would solve all movement as specific components like turret movement component etc. So it becomes something extendable for me.
Like there could be modification of turret changes in gameplay, armor additions etc. So it can become more handy in that terms to arrange a modular hierarchy. All the other things like the palette movement etc can be anim bp specific to the palette model.

However these are my thoughts on it, you don’t really have to do like this, I just too much think sometimes about the next steps of development and vision. Can be handy for you aswell so sharing.

Happy development.

Some detail info about Tick Groups and Official Document also Tick Prerequisite Actor

1 Like

Setting the tick group to Post Update Work solved a lot, but the cherry on top was adding a tick prerequisite to ensure the skeletal mesh (and the AnimBP) ticks after the player actor. That really fixed the issue cleanly.

:smiley: I’m flattered! Also just really glad to see the tiny tank finally move properly with a single skeletal mesh!

Thanks a lot for sharing your insights and advice. It’s been super helpful, and I appreciate the clarity on how you approach tick groups and gameplay logic.

Also, happy to connect, if you ever need any VFX advice, feel free to reach out. I’m always available to help where I can!

1 Like

Hey Grimnir and the rest of the community! Quick follow-up, we added antennas to the tank and enabled physics on the skeletal mesh so the physics asset can drive the antenna movement. But when physics is enabled, we’re seeing jitter again on the turret bone.

It probably makes sense: enabling physics seems to force the skeletal mesh to tick in the physics tick group, which breaks the AddTickPrerequisiteActor setup we used before to fix the turret jitter (by making the mesh tick after the player actor).

Disabling physics fixes the jitter, but of course then the antenna physics stops working.
Is there a way to keep the physics simulation for the antenna bones without causing tick order issues for the turret control? Or some workaround to make the mesh still respect custom tick order even with physics enabled?

Thanks in advance!
2025-05-06 14-50-52_1
2025-05-06 14-50-52

1 Like

Hi there,

First :slight_smile: it’s looking cool, love the animations and glide :smiley:

“Set All Bodies Below Simulate Physics” are you enabling like this from the antenna bone? or full skeletal mesh ?