Search code examples
game-physicsgodot

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?


Solution

  • 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.