1

Below is my code (ignore the input variable it is just for movement):

var input = Vector3()
    if Input.is_action_pressed("move_forward") and Input.is_action_pressed("move_left"):
        rotation_degrees.y = lerp_angle(rotation_degrees.y, atan2(1, -1), delta * 7)
        input[2] = 1
        input[0] = 1
    elif Input.is_action_pressed("move_forward") and Input.is_action_pressed("move_right"):
        rotation_degrees.y = lerp_angle(rotation_degrees.y, atan2(1, 1), delta * 7)
        input[2] = 1
        input[0] = -1
    elif Input.is_action_pressed("move_backward") and Input.is_action_pressed("move_left"):
        rotation_degrees.y = lerp_angle(rotation_degrees.y, atan2(-1, -1), delta * 7)
        input[2] = -1
        input[0] = 1
    elif Input.is_action_pressed("move_backward") and Input.is_action_pressed("move_right"):
        rotation_degrees.y = lerp_angle(rotation_degrees.y, atan2(-1, 1), delta * 7)
        input[2] = -1
        input[0] = -1
    else:
        if Input.is_action_pressed("move_forward"):
            rotation_degrees.y = lerp_angle(rotation_degrees.y, atan2(1, 0), delta * 7)
            input[2] = 1
        if Input.is_action_pressed("move_backward"):
            rotation_degrees.y = lerp_angle(rotation_degrees.y, atan2(-1, 0), delta * 7)
            input[2] = -1
        if Input.is_action_pressed("move_left"):
            rotation_degrees.y = lerp_angle(rotation_degrees.y, atan2(0, -1), delta * 7)
            input[0] = 1
        if Input.is_action_pressed("move_right"):
            rotation_degrees.y = lerp_angle(rotation_degrees.y, atan2(0, 1), delta * 7)
            input[0] = -1

For some reason, the player is hardly moving at all. Before I just set rotation_degrees.y to the direction but that didn't make a smooth movement. Any help is appreciated!

3
  • It is unclear how you wish this to work. If delta is the argument to _process then it makes little sense, as it is not a sequence value in the range 0..100. (It is only the number of milliseconds since the last invocation of _process. You need to create a Tween and use that to update your rotation. (Or use an Animation.) Commented Nov 3, 2021 at 11:16
  • @Dúthomhas As far as I have been able to figure out, this practice comes from Unity, and then picked up by some Godot tutorials. Due to the fact that they use delta, it is not really linear. I'm not sure if it stable across different frame rates. But it would be smooth. At least OP attempts to use delta, other don't (under the idea that _physics_process is magically stable), which I consider pure cargo cult. This is still cargo cult adjacent (i.e. writing code without fully understanding it because somebody else does it that way). Commented Nov 3, 2021 at 15:07
  • @Theraot What really ticks me off is people who write tutorials without knowing what the double-hockeysticks they are talking about, not bothering to even try the code they tell people to use. There do exist a few good tutorials for Godot, though, explaining tweening and other elements of the animation system. (I personally tend to do a lot of animation in code with just a few lines, instead of using the Animation editor, etc, but I’ll use the tool most useful for the situation.) I think OP has better choices to achieve what he wants here than playing around in _process. Commented Nov 3, 2021 at 16:04

1 Answer 1

7

Getting Input

We have a better way to get a vector from input, let us start there:

var input := Vector3(
    Input.get_action_strength("move_left") - Input.get_action_strength("move_right"),
    0,
    Input.get_action_strength("move_forward") - Input.get_action_strength("move_backward")
)

Addendum: This is the order you had. However, notice that we usually thing of x growing to the right, and - in Godot - forward is negative z. So this is backwards, I don't know if that is intentional.

What is going on here is that get_action_strength gives us the "strength" of the input, which is a number between 0 and 1. It will be 0 or 1 for a digital input, but it could be a value in between for analog input.

Then to get a component of the vector I take the input that would make it positive and subtract the negative. That gives you a value in the range from -1 to 1. Which also means we don't get a unit vector, depending on what you are doing, you might or might not want to normalize it.


Next you want the angle on the xz plane. Well, if you just used a Vector2, that would be trivial. So, instead of the code above, let us use a Vector2:

var input := Vector2(
    Input.get_action_strength("move_left") - Input.get_action_strength("move_right"),
    Input.get_action_strength("move_forward") - Input.get_action_strength("move_backward")
)

By the way, in Godot 3.4+ and Godot 4.0+ you will be able to use Input.get_vector, which simplifies this further.

It should not be hard to get a Vector3 from that if you need it for something else: Vector3(input.x, 0, input.y)

And now get the angle:

var input_angle := input.angle()

Addendum: You may need to do an operation on the angle to make it usable in 3D. The reason being that the angle is measured from the x axis, but no rotation is looking down the z. I had overlooked that when I wrote this answer. Although I tested all the code here. Without motion I didn't notice the orientation didn't match the direction of motion.


Smooth Rotation

Now, we want to smoothly change rotation_degrees.y to input_angle. There are multiple ways to do it, here are a few.

Before any of them. You are going to need to declare the target angle as a field, so the rotation does not reset when there is no input.

var _target_angle:float

And we are going to store it, whenever there is input:

var input := Vector2(
    Input.get_action_strength("ui_left") - Input.get_action_strength("ui_right"),
    Input.get_action_strength("ui_up") - Input.get_action_strength("ui_down")
)
if input.length_squared() > 0:
    _target_angle = input.angle()

Note: I'm checking length_squared instead of length to avoid a square root.


lerp_angle

Yes, you should be able to use lerp_angle. Even thought, I don't advocate that.

I'll go ahead and add that 7 magic constant you have:

const _rotation_amount:float = 7.0

Then you would do it something like this (which I admit is convenient and short code):

rotation.y = lerp_angle(rotation.y, _target_angle, delta * _rotation_amount)

Please notice I changed rotation_degrees to rotation, that is because you get the angle in radians. Which would have been a problem using atan2 too. You can use deg2rad and rad2deg to do the conversions if you prefer.

With this approach, you have very little control over the interpolation. You do not specify how fast it rotates, nor how much time the rotation should take.

Please notice that the weight of a linear interpolation is not time. You are not telling Godot "go from this value to this value in this amount of time" (you can do that with a Tween, see below).

Also notice that it will advance a more or less stable proportion of the difference of the values each frame. Thus it moves more at the start, and less at the end. That is, this is not linear at all. It has a deceleration effect.


Angular speed

If we are going to work with angular speed, it follows that we need a field to hold the angular speed:

const _angular_speed:float = TAU

Here TAU (which is PI * 2) represent one rotation per second (four rotations per second would be 4 * TAU, and a quarter rotation per second could be 0.25 * TAU, you can associate TAU with one Turn as mnemonic).

Now, we are going to figure out the angle difference (shortest path), to do that we can do this:

var angle_diff := wrapf(_target_angle - rotation.y, -PI, PI)

The wrapf function will "wrap" the value in the range (-PI to PI in this case), so that if the values goes beyond that range, it is as if it entered from the opposite edge (if you say wrapf(14, 0, 10) you get 4, and if you say wrapf(-4, 0, 10) you get 6). I hope that makes sense.

To apply the angular speed, in theory, we would only need the sign:

rotation.y += delta * _angular_speed * sign(angle_diff)

However, we don't want to overshoot. We can solve this with clamp:

rotation.y += clamp(delta * _angular_speed, 0, abs(angle_diff)) * sign(angle_diff)

Notice I have separated the sign from the magnitude of angle_diff. I'm clamping how much the angle would change according to angular speed to the magnitude of angle_diff (so it is never more than angle_diff, i.e. it does not overshoot). Once clamped, I apply the sign so it turns in the correct direction.

And, of course, if you really wanted to, you could work with angular acceleration and deceleration too.


Tween

Using a Tween node is the most powerful option sans using an AnimationPlayer. And it is also very easy to use.

We, of course, need to define a Tween object and add it as a child:

var _tween:Tween

func _ready() -> void:
    _tween = Tween.new()
    add_child(_tween)

This is because the Tween object will continue to gradually change the rotation each frame (so we don't have to worry about it). And to do that, it needs to persist and to get frame notifications, so it needs to be in the scene tree somewhere.

In Godot 4.0+, you will be able to do get_tree().create_tween() which returns a rooted Tween, so you don't have to worry about keeping a reference or adding it to the scene tree.

And then you can do this:

var input := Vector2(
    Input.get_action_strength("ui_left") - Input.get_action_strength("ui_right"),
    Input.get_action_strength("ui_up") - Input.get_action_strength("ui_down")
)
if input.length_squared() > 0:
    var rotation_time := 0.2
    _tween.interpolate_property(self, "rotation:y", rotation.y, input.angle(), rotation_time)
    _tween.start()

Do not forget to call start. Well, In Godot 4.0+, all tween will automatically start by default.

As you can see, the code is telling the Tween to interpolate the y of the rotation from the value of rotation.y (its current value) to the value of input.angle(), and we specify a rotation time (which I made a variable just so I can give it a name). And that's it.

In Godot 4.0+, interpolate_property is gone in favor of the new tween_property API, with which you don't need to specify the start value.


Oh, wait, we can specify how do you want to ease these values:

_tween.interpolate_property(
    self,
    "rotation:y",
    rotation.y,
    input.angle(),
    rotation_time,
    Tween.TRANS_QUAD,
    Tween.EASE_OUT
)

See this cheatsheet by wandomPewlin to pick your easing:

Tween cheatsheet by wandomPewlin

I believe Tween.TRANS_QUAD and Tween.EASE_OUT gives you something close to what you get from lerp_angle, except stable. And look, you have more options!

I'll also mention that you can ask Tween what interpolation it currently is doing, and also remove them, which is useful in some cases. Yet, you don't need to go into that for simple cases like this one. And, well, there is AnimationPlayer if you need something more complex. By the way, yes, AnimationPlayer can operate on a subproperty (such as rotation.y, I have an explanation elsewhere).

Sign up to request clarification or add additional context in comments.

2 Comments

Wow, thank you so much that is amazing I'm going to start going through that and trying those options what a legend!
Hi Theraot, I'm having some problems with the code so I would really appreciate if you look at this question I posted. I tried to send it as a comment but it is too long. stackoverflow.com/questions/69831997/…

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.