btm2424
btm2424

Reputation: 641

Steering motion in Godot Engine

I would like to learn about game development in Godot Engine. I'm trying to make a mobile game similar to the game Missiles: Missiles

Right now I have a functioning joystick. I get the value as a normalized Vector2:

var joystick_value = joystick.get_value()

But I can't figure out how to change the velocity of the plane based on the joystick value. Plus to set some limit on how much the plane can turn (or the max angle).

(The plane is a KinematicBody2D)

Any ideas?

Upvotes: 2

Views: 1390

Answers (1)

Theraot
Theraot

Reputation: 40355

Velocity

If we are talking about KinematicBody2D and velocity we are talking of a script something like this, give or take:

extends KinematicBody2D

var velocity:Vector2 = Vector2.ZERO # pixels/second

func _physics_process(_delta:float) -> void:
    move_and_slide(velocity)

Perhaps you are better of working with speed and direction instead of velocity. We can do that too:

extends KinematicBody2D

var speed:float = 0                # pixels/second
var direction:Vector2 = Vector2.UP # pixels

func _physics_process(_delta:float) -> void:
    var velocity = direction.normalized() * speed
    move_and_slide(velocity)

What if we wanted an angle instead of a direction vector? Sure:

extends KinematicBody2D

var speed:float = 0 # pixels/second
var angle:float = 0 # radians

func _physics_process(_delta:float) -> void:
    var velocity = Vector2.RIGHT.rotated(angle) * speed
    move_and_slide(velocity)

Rotation

Since we will do steering, we want to rotate the KinematicBody2D according to its velocity.

Sure, we can get a rotation angle from a velocity:

extends KinematicBody2D

var velocity:Vector2 = Vector2.ZERO # pixels/second

func _physics_process(_delta:float) -> void:
    rotation = velocity.angle()
    move_and_slide(velocity)

Similarly with a direction vector, or if you have an angle you can use that directly.


Steering

For steering we will be keeping the speed and changing angle. So we want the speed and angle version I showed above. With rotation, of course:

extends KinematicBody2D

var speed:float = 0 # pixels/second
var angle:float = 0 # radians

func _physics_process(_delta:float) -> void:
    rotation = angle
    var velocity = Vector2.RIGHT.rotated(angle) * speed
    move_and_slide(velocity)

And now we will have a target_angle which will come from user input. In your case that means:

var target_angle = joystick.get_value().angle()

Now, notice we don't know in what direction is the rotation. Doing target_angle - angle does not work, because it could be shorter to rotate the other way around. Thus, we will do this:

var angle_difference = wrapf(target_angle - angle, -PI, PI)

What does wrapf do? It "wraps" the value to a range. For example, wrapf(11, 0, 10) is 1, because it went over 10 by 1, and 1 + 0 is 1. And wrapf(4, 5, 10) is 9 because it went below 5 by 1 and 10 - 1 is 9. Hopefully that makes sense.

We are wrapping in the range from -PI to PI so it gives the angle difference in the direction that is shorter to make the rotation.


We will also need angular_speed. That is, how much the angle changes per unit of time (the unit is in angle/time). Notice that is not the same as how much the angle changes (the unit is in angles). To convert, we multiply by the elapsed time since last time:

var delta_angle = angular_speed * delta

Ah, actually, we need that in the direction of angle_difference. Thus, its sign:

var delta_angle = angular_speed * delta * sign(angle_difference)

And we do not want to overshoot. Thus, if delta_angle has greater absolute value than angle_difference, we need to set delta_angle to angle_difference:

var angle_difference = wrapf(target_angle - angle, -PI, PI)
var delta_angle= angular_speed * delta * sign(angle_difference)
if abs(delta_angle) > abs(angle_difference):
    delta_angle = angle_difference

We can save one call to abs there:

var angle_difference = wrapf(target_angle - angle, -PI, PI)
var delta_angle_abs = angular_speed * delta
var delta_angle = delta_angle_abs * sign(angle_difference)
if delta_angle_abs > abs(angle_difference):
    delta_angle = angle_difference

Put it all together:

extends KinematicBody2D

var speed:float = 0         # pixels/second
var angle:float = 0         # radians
var angular_speed:float = 0 # radians/second

func _physics_process(delta:float) -> void:
    var target_angle = joystick.get_value().angle()
    var angle_difference = wrapf(target_angle - angle, -PI, PI)
    var delta_angle_abs = angular_speed * delta
    var delta_angle = delta_angle_abs * sign(angle_difference)
    if delta_angle_abs > abs(angle_difference):
        delta_angle = angle_difference

    angle += delta_angle
    rotation = angle
    var velocity = Vector2.RIGHT.rotated(angle) * speed
    move_and_slide(velocity)

And finally, some refactoring, including but not limited to extracting that chunk of code to another function:

extends KinematicBody2D

var speed:float = 0         # pixels/second
var angle:float = 0         # radians
var angular_speed:float = 0 # radians/second

func _physics_process(delta:float) -> void:
    var target_angle = joystick.get_value().angle()
    angle = apply_rotation_speed(angle, target_angle, angular_speed, delta)
    rotation = angle
    var velocity = Vector2.RIGHT.rotated(angle) * speed
    move_and_slide(velocity)

static func apply_rotation_speed(from:float, to:float, angle_speed:float, delta:float) -> float:
    var diff = wrapf(to - from, -PI, PI)
    var angle_delta = angle_speed * delta
    if angle_delta > abs(diff):
        return to

    return from + angle_delta * sign(diff)

Here is a version with angular acceleration:

extends KinematicBody2D

var speed:float = 0                # pixels/second
var angle:float = 0                # radians
var angular_speed:float = 0        # radians/second
var angular_acceleration:float = 0 # radians/second^2

func _physics_process(delta:float) -> void:
    var target_angle = joystick.get_value().angle()
    if angle == target_angle:
        angular_speed = 0
    else:
        angular_speed += angular_acceleration * delta
        angle = apply_rotation_speed(angle, target_angle, angular_speed, delta)

    rotation = angle
    var velocity = Vector2.RIGHT.rotated(angle) * speed
    move_and_slide(velocity)

static func apply_rotation_speed(from:float, to:float, angle_speed:float, delta:float) -> float:
    var diff = wrapf(to - from, -PI, PI)
    var angle_delta = angle_speed * delta
    if angle_delta > abs(diff):
        return to

    return from + angle_delta * sign(diff)

And the shiny version with angular easing:

extends KinematicBody2D

var speed = 10
var angle:float = 0
var angular_speed:float = 0
export(float, EASE) var angular_easing:float = 1

func _physics_process(delta:float) -> void:
    var target_angle = (get_viewport().get_mouse_position() - position).angle()
    angle = apply_rotation_easing(angle, target_angle, angular_easing, delta)

    rotation = angle
    var velocity = Vector2.RIGHT.rotated(angle) * speed
    move_and_slide(velocity)

static func apply_rotation_easing(from:float, to:float, easing:float, delta:float) -> float:
    var diff = wrapf(to - from, -PI, PI)
    var diff_norm = abs(diff)
    var angle_speed = ease(diff_norm / PI, easing)
    var angle_delta = angle_speed * delta
    if angle_delta > diff_norm:
        return to

    return from + angle_delta * sign(diff)

Set angular_easing to some value between 0 and 1 to have it accelerate as it begins to rotate and decelerate as it approaches the target angle. With a value of 0 it does not rotate. With a value of 1 it rotates with constant velocity. See ease.


I tested the code in this answer (with some non-zero values), and this for mouse control:

var target_angle = (get_viewport().get_mouse_position() - position).angle()

It works.

Upvotes: 4

Related Questions