Proposal to fix turret rotation and barrel elevation
After doing the code for barrel and turret rotation, when starting to play, the turret/barrel begins to jitter when not moving the tank and not aiming and the barrel reaches its final aim position, making my tanks look like dogs wagging their tails. The video below shows the effect
So going for more debugging:
BTW: DrawDebugLine is your friend, so drawing 2 debug lines starting at projectile socket location towards
- hit location from crosshair scan
- aim direction derived from GetSightRayHitLocation(), 3m long
After not having any clue on the root cause, I decided to clone the course project from github. Running on UE4.22.1 shows the same behaviour, so an error in my code does not seem to be responsible.
One important fact: I always reduce the framerate while editing using
t.maxfps 10
console command to limit system resources (this does extremely reduce GPU/CPU load on my system keeping my fans calm). Might be interesting for others as well, especially when working on a notebook.
First observation: Changing maxfps back to 0 (no limit) or other values shows that the effect vanishes at around 27fps. At a 100fps I might never have noticed that effect. So you might need to increase speed or drop framerate to even see that problem at all.
Here we go with the solution I found to fix these issues. Actually, this is a story with 2 parts, as I found 2 different issues leading to same effect, making the first solution not being sufficient for all cases.
First solution iteration
I explain the root cause for this wagging tail effect, then show, how this can easily be fixed in a first change of code.
What causes the problem?
In the original implementation, the turret rotation uses the Yaw delta angle as speed. The larger the angle, the more way to go and the higher the speed. Therefore this value is clamped to -1.0 … +1.0 and the result is used as a factor multiplied with the maximum allowed rotation speed value.
The problem with this approach for the turret rotation is as follows:
For low frame rate and high rotation speed, the rotation delta might be quite high. E.g. 40 degrees/second max speed with 10fps results in 4 degrees per frame. While aiming around with the crosshair, no effects show up. But lets stop aiming and let the barrel move. At some point we get close to the tartet, let’s say, we are only 2 degrees away from the hit angle.
So UTankTurret::Rotate()
gets passed 2.0 as “angle to go”. This gets clamped to 1.0 and resulting rotation delta is
40 degrees/s * 1.0 * 100ms = 4 degrees
Applying this rotation leads to the turret now being -2 degrees off.
In the next frame, this results in a -4 degrees rotation, resulting in initial 2 degrees off again. And so on forever, as long as we do not move the tank or change the aiming.
The result is the turret jumping endlessly between 2 positions, like a dog wagging its tail as soon as it reaches its final position.
Original code in UTankTurret::Rotate()
float RelativeSpeed = FMath::Clamp(TargetHitYawAngle, -1.0f, 1.0f);
float RotationDelta = MaxRotationSpeed * RelativeSpeed * GetWorld()->DeltaTimeSeconds;
float NewRotation = RelativeRotation.Yaw + RotationDelta;
SetRelativeRotation(FRotator(0.0f, NewRotation, 0.0f));
How can it be solved?
Instead of interpreting the YAW component parameter as “relative speed” and clamping this to -1…+1 range, use it as what it actually is: the required delta rotation angle for the turret to make it point towards the hit location.
If this is larger than the allowed angle for the current speed limit just apply the maximum allowed (4 degrees in our example case).
If not, just apply this rotation, as it does not impose a speed limit exceed. So we have reached our final rotation towards the hit location. The next frame will now find a (almost) zero required angle.
New Code for UTankTurret::Rotate():
float MaxLimitedAngle = MaxRotationSpeed * FMath::Sign(TargetHitYawAngle) * GetWorld()->DeltaTimeSeconds;
SetRelativeRotation(FRotator(0.0f,
FMath::Abs(TargetHitYawAngle) > FMath::Abs(MaxLimitedAngle) ?
RelativeRotation.Yaw + MaxLimitedAngle :
RelativeRotation.Yaw + TargetHitYawAngle,
0.0f));
Barrel elevation
For the Barrel elevation, we do a similar code change in UTankeBarrel::Elevate()
to implement the same logic.
Original Code in UTankBarrel::Elevate():
float RelativeSpeed = FMath::Clamp(TargetHitPitchAngle, -1.0f, 1.0f);
float ElevationDelta = MaxElevationSpeed * RelativeSpeed * dtime;
float RawNewElevation = RelativeRotation.Pitch + ElevationDelta;
float ClampedElevation = FMath::Clamp(RawNewElevation, MinimumElevation, MaximumElevation);
SetRelativeRotation(FRotator(ClampedElevation, 0.0f, 0.0f));
New Code in UTankBarrel::Elevate():
float MaxLimitedAngle = MaxElevationSpeed * FMath::Sign(TargetHitPitchAngle) * GetWorld()->DeltaTimeSeconds;
float RawNewElevation = FMath::Abs(TargetHitPitchAngle) > FMath::Abs(MaxLimitedAngle) ?
RelativeRotation.Pitch + MaxLimitedAngle :
RelativeRotation.Pitch + TargetHitPitchAngle;
SetRelativeRotation(FRotator(FMath::Clamp(RawNewElevation, MinimumElevation, MaximumElevation), 0.0f, 0.0f));
It would have been too simple
At a first glance, the problem seems to be solved and I was happy - but only for a short time. When aiming down or up, the effect appears again. see below video
So going to debug again…
Second iteration
Again, showing the root cause for the effect and how I did solve it
What causes this problem?
After quite some debugging, the problem turns out to be caused by the fact, that the Yaw value passed to UTankTurret::Rotate()
is calculated based on the AimDirection
vector returned by SuggestProjectileVelocity()
.
I found, that the AimDirection.Rotation().Yaw
value returned by SuggestProjectileVelocity()
function more and more goes crazy when aiming towards ground or sky. This starts slowly but raises until values really jump. Passing the values to the UTankTurret::Rotate()
function result in the wagging tail effect, but when removing the angle limitation, the turrets just rotate/jump like crazy.
How it can be solved
I use SuggestProjectileVelocity()
resulting aimdirection only for the barrel elevation, for which it is really necessary to get the elevation angle and drop it for the turret rotation.
The turret rotation instead can be simply calculated from the barrel location to hitlocation vector
and the barrel rotator. So doing these calculations on our own.
BTW: I also decided to drop the UTankAimComponent::MoveBarrelTowards()
function and defactor the rotation and elevation functionality into UTankAimComponent::AimAt() directly.
The new code replacing the call to UTankAimComponent::MoveBarrelTowards()
:
// NEW CODE - Barrel Elevation
FRotator BarrelRotator = Barrel->GetForwardVector().Rotation();
FRotator AimRotator = AimDirection.Rotation();
Barrel->Elevate((AimRotator - BarrelRotator).Pitch);
// NEW CODE - Turret Rotation - calculate Yaw ourselves instead of using Yaw from AimRotator
FVector BarrelLocation = Barrel->GetComponentLocation();
FRotator YawRotator = (hitLocation - BarrelLocation).Rotation();
FRotator DeltaRotator = YawRotator - BarrelRotator;
Turret->Rotate(FMath::Abs(DeltaRotator.Yaw) < 180 ? DeltaRotator.Yaw : -DeltaRotator.Yaw);
Happy now
For now, this code seems to work perfectly, at least I did not find any issues. It might be interesting to go deeper into the SuggestProjectileVelocity()
source code, but I did not (yet?) go into that. Maybe it’s even a bug in that function.
You might want to check my Github Repo to have a closer look. Changes are in these 3 classes:
https://github.com/herb64/BattleTank/blob/master/Source/BattleTank/Private/TankAimComponent.cpp
https://github.com/herb64/BattleTank/blob/master/Source/BattleTank/Private/TankTurret.cpp
https://github.com/herb64/BattleTank/blob/master/Source/BattleTank/Private/TankBarrel.cpp