Disclaimer: It has been a good while since I've done anything in Java, and that is without considering LibGDX.
We need to make sure we have the shortest way to go from the current_angle
to the target_angle
. For that we will implement wrap
:
float wrap(float value, float lower_bound, float upper_bound)
{
float range = upper_bound - lower_bound;
return value - (range * Math.floor((value - lower_bound) / range));
}
Then we can get our angle offset like this:
float half_turn = 180f; // or Math.PI if we were using radians
float current_angle = ACTOR.getRotation();
float target_angle = destinationRotation;
float offset_angle = wrap(target_angle - current_angle, -half_turn, half_turn);
It should give us the angle difference, the shortest way.
I'm sure there are other ways to do this. I don't know if there is a better one for Java.
We want to apply an angular velocity. So we will compute how much we can rotate:
float delta = Gdx.graphics.getDeltaTime(); // seconds
float rotation_speed = 10f; // angle per second
float angle_delta = rotation_speed * delta; // angle
Now, notice that our angle_delta
is always positive. But not our offset_angle
. So we are going to decompose offset_angle
into sign
and magnitud
:
float offset_angle_magnitud = Math.abs(offset_angle);
float offset_angle_sign = Math.signum(offset_angle);
So we can limit how much we rotate so we don't overshoot, while preserving the sign:
float rotation_angle = Math.min(angle_delta, offset_angle_magnitud) * offset_angle_sign
Finally add that to the current_angle
:
ACTOR.setRotation(current_angle + rotation_angle);
I don't know if you would need to wrap at the end there (or if setRotation
handles that already):
ACTOR.setRotation(wrap(current_angle + rotation_angle, -half_turn, half_turn));
Or perhaps rotate
works better:
ACTOR.rotate(rotation_angle);
Addendum
The above code has the behavior of a sudden start/stop.
In a more conventional situation we would use an acceleration, but that would require to add state (either keep track of time or the current velocity). However, we cannot simply increase velocity with acceleration… Because we also want to decrease velocity and smoothing reach zero.
Thus, we want a solution with variable acceleration. How do we know if we should increase or reduce the acceleration? Intuition dictates that it should be some function of how far we are from the destination. Well, I'll cut the middle man and make the the velocity a function of the angle (this way our approach can remain stateless). This approach requires four parameters:
- We will specify a range of angles (minimum and maximum) over which we will do easing. Which I'll call
min_angle
and max_angle
.
- Similarly there will be a range of angular velocities (minimum and maximum). Which I'll call
min_rotation_speed
and max_rotation_speed
.
For practical proposes, the minimum angle and the minimum angular velocity will be zero. Yet, I'll leave the variables in for reference. Feel free to optimize once you have a solution that behaves the way you want.
We need to compute the size of the angle range:
float angle_range = max_angle - min_angle
I'm assuming that max_angle > min_angle
, if that is not the case you can swap them. We should have a positive angle_range
.
We will only do easing if this angle_range
is not zero:
if (angle_range > 0f)
{
// …
}
Now, we will map the offset_angle_magnitud
to a value between zero and one which indicates where it is in the angle_range
:
if (angle_range > 0f)
{
float x = Math.min(
max(offset_angle_magnitud - min_angle, 0.0) / angle_range,
1.0
);
}
This should give you zero below min_angle
and one above max_angle
, a number in between in the middle.
And now that we have a value between zero and one, we can interpolate:
if (angle_range > 0f)
{
float x = Math.min(
max(offset_angle_magnitud - min_angle, 0.0) / angle_range,
1.0
);
angle_delta = interpolate(min_rotation_speed, max_rotation_speed, x) * delta;
}
Notice than alternative is to use a smoothstep
function. But that is just one option, I'll go for something general.
We can write that interpolate
in terms of an ease function:
float interpolate(float lower_bound, float upper_bound, float weight)
{
return (upper_bound - lower_bound) * ease(weight) + lower_bound;
}
And there are many options for the ease
function. Be aware that this function controls the velocity as a function of the angle. You can start with simply this:
float ease(float x)
{
return x;
}
The above function would make interpolate
a linear interpolation (a.k.a lerp
).
With that, as the angle to the destination becomes larger, the velocity also becomes larger. Similarly as the angle to destination becomes smaller, the velocity also becomes smaller.
However, my intuition is that your function should have a quadratic behavior. You can try that too:
float ease(float x)
{
return x * x;
}
You can find more easing functions searching online. There are some "cheat sheets" such as this one: https://easings.net - pick, test, choose. Then - if want - you can inline these functions, and you may find some cancellations in the process.