1
\$\begingroup\$

Currently I have the player moving towards the mouse position , using this example :

https://kidscancode.org/godot_recipes/3d/click_to_move/

now how would I achieve jumping to the mouse position, currently my script is like this:

  void Player::move(const float &delta) {
            velocity.y += gravity * delta;
    
            if (target != godot::Vector3::ZERO) {
    
                look_at(target, godot::Vector3::UP);
                rotate_x(0.0);
                velocity = -get_transform().basis.z * speed;
    
                auto distance_to_target = get_transform().origin.distance_to(target);
                if (distance_to_target < 0.5) {
                    velocity = target = godot::Vector3::ZERO;
                }
    
// weird behavior not what I'm looking for

                if (godot::Input::get_singleton()->is_action_pressed("Jump") && is_on_floor()) {
                    godot::Godot::print(std::to_string(distance_to_target).c_str());
                    velocity.y = 100;
                }
            }
    
            velocity = move_and_slide(velocity, godot::Vector3::UP);
        }

I tried messing around with velocity.y , but it always result in weird behaviors and the player when landing point/look at the floor.

how would I do it correctly ?

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

This is the version in the article, I'll work from it:

extends KinematicBody

export var speed = 5
export var gravity = -5

var target = null
var velocity = Vector3.ZERO

func _physics_process(delta):
    velocity.y += gravity * delta
    if target:
        look_at(target, Vector3.UP)
        rotation.x = 0
        velocity = -transform.basis.z * speed
        if transform.origin.distance_to(target) < .5:
            target = null
            velocity = Vector3.ZERO
    velocity = move_and_slide(velocity, Vector3.UP)

I suggest (against what the linked article says) to check the distance to the target instead of checking with ZERO (ZERO could be a valid target). And, I'll set target to transform.origin for initialization. Since we are already checking for the distance, refactor the conditional that does that:

extends KinematicBody

export var speed = 5
export var gravity = -5

var target = transform.origin
var velocity = Vector3.ZERO

func _physics_process(delta):
    velocity.y += gravity * delta
    if transform.origin.distance_to(target) < 0.5:
        # perhaps you want to flip this line
        target = transform.origin
        velocity = Vector3.ZERO
    else:
        look_at(target, Vector3.UP)
        rotation.x = 0
        velocity = -transform.basis.z * speed

    velocity = move_and_slide(velocity, Vector3.UP)

There is another problem with the article code: when we write velocity we are erasing the vertical velocity we set with gravity (which, of course, you would not notice until the physics object is not on the floor). So let us fix that:

extends KinematicBody

export var speed = 5
export var gravity = -5

var target = transform.origin
var velocity = Vector3.ZERO

func _physics_process(delta):
    velocity.y += gravity * delta
    if transform.origin.distance_to(target) < 0.5:
        # perhaps you want to flip this line
        target = transform.origin
        velocity = Vector3(0, velocity.y, 0)
    else:
        look_at(target, Vector3.UP)
        rotation.x = 0
        var vel = -transform.basis.z * speed
        velocity = Vector3(vel.x, velocity.y, vel.z)

    velocity = move_and_slide(velocity, Vector3.UP)

Now, let us work on the jump. First of all, do not use is_on_floor before using move_and_slide. Let us try at the end:

extends KinematicBody

export var speed = 5
export var gravity = -5

var target = transform.origin
var velocity = Vector3.ZERO

func _physics_process(delta):
    velocity.y += gravity * delta
    if transform.origin.distance_to(target) < 0.5:
        # perhaps you want to flip this line, maybe just set x and z
        target = transform.origin
        velocity = Vector3(0, velocity.y, 0)
    else:
        look_at(target, Vector3.UP)
        rotation.x = 0
        var vel = -transform.basis.z * speed
        velocity = Vector3(vel.x, velocity.y, vel.z)

    velocity = move_and_slide(velocity, Vector3.UP)
    if is_on_floor() and Input.is_action_pressed("Jump"):
        velocity.y = 10

Did you see that? We have a fixed jump velocity, it might undershoot. Or it might overshoot which is worse because the code snaps to the target horizontal position mid-air.

So that snapping must only happen when is_on_floor() is true. Which means we need to put move_and_slide before all that:

extends KinematicBody

export var speed = 5
export var gravity = -5

var target = transform.origin
var velocity = Vector3.ZERO

func _physics_process(delta):
    velocity.y += gravity * delta
    velocity = move_and_slide(velocity, Vector3.UP)
    if is_on_floor():
        if Input.is_action_pressed("Jump"):
            velocity.y = 10
        if transform.origin.distance_to(target) < 0.5:
            # perhaps you want to flip this line, maybe just set x and z
            target = transform.origin
            velocity = Vector3(0, velocity.y, 0)
        else:
            look_at(target, Vector3.UP)
            rotation.x = 0
            var vel = -transform.basis.z * speed
            velocity = Vector3(vel.x, velocity.y, vel.z)

Now it jumps, falls to the ground, then it adjusts directions.

Perhaps you want it to compute how much to jump to get there. With the help of good old motion equations we can do that. We will need the horizontal distance. It is also a good idea to check against horizontal distance, just in case the target is on a different elevation.

And there you go, this code jumps to the target:

extends KinematicBody

export var speed = 5
export var gravity = -5

var target = transform.origin
var velocity = Vector3.ZERO

func _physics_process(delta):
    velocity.y += gravity * delta
    velocity = move_and_slide(velocity, Vector3.UP)
    if is_on_floor():
        var h_distance = Vector2(transform.origin.x, transform.origin.z).distance_to(Vector2(target.x, target.z))
        if Input.is_action_pressed("Jump"):
            var expected_time = h_distance / speed
            var v_velocity = -0.5 * expected_time * gravity
            velocity.y = v_velocity
        if h_distance < 0.5:
            # perhaps you want to flip this line, maybe just set x and z
            target = transform.origin
            velocity = Vector3(0, velocity.y, 0)
        else:
            look_at(target, Vector3.UP)
            rotation.x = 0
            var vel = -transform.basis.z * speed
            velocity = Vector3(vel.x, velocity.y, vel.z)

By the way, if you change the target mid-air, it will complete the jump, then change direction and go. Similarly, if it fails to land on the target because of an obstacle, it will change direction after landing then go. There is no path-finding or obstacle avoidance (other than sliding on walls because move_and_slide) of course.

If this is not the behavior that you want, hopefully you can modify it from there. Remember:

  • When setting velocity you are overriding velocity.y
  • Use is_on_floor after move_and_slide (and move_and_slide after applying gravity). The KinematicBody might be on the floor, but if it didn't slide on it, it will not register.
  • Consider if you want it to be able to change direction mid-air or not. If the target is directly below it will suddenly stop the jump arc and fall vertically.
  • Do not assume that ZERO is not a valid target.

I'm letting translating that to C++ to you, because, honestly, it is too much setup to test it (I'm being lazy), and I'm not confident to get it right without testing it. Yet you should be able to do it with little effort.


Note: The value 0.5 on h_distance < 0.5 might be too large depending on the size of the scene. Which results in the motion stopping too short. If that happens, try an smaller value.


Addendum: Making jump time constant

Look again at this code:

var expected_time = h_distance / speed
var v_velocity = -0.5 * expected_time * gravity
velocity.y = v_velocity

It only sets the vertical velocity, and uses the speed it has. If you want the jump to take a constant time, you would have to change the speed. Solve the equation for speed:

var expected_time = h_distance / speed
=>
speed * expected_time = h_distance
=>
speed = h_distance / expected_time

It also means you would have to do the following again:

look_at(target, Vector3.UP)
rotation.x = 0
var vel = -transform.basis.z * speed
velocity = Vector3(vel.x, velocity.y, vel.z)

Thus:

var jump_speed = h_distance / jump_time
var v_velocity = -0.5 * jump_time * gravity
look_at(target, Vector3.UP)
rotation.x = 0
var vel = -transform.basis.z * jump_speed
velocity = Vector3(vel.x, v_velocity, vel.z)

And put jump_time (it is in seconds, by the way) as an exported variable alongside speed. That should do it… Well, almost. The last block will undo it by setting the velocity again. We can simply put the second if statement in the else of the first one. However, as you have seen we have some code repetition. Instead, I suggest the following refactor:

if is_on_floor():
    var h_distance = Vector2(transform.origin.x, transform.origin.z).distance_to(Vector2(target.x, target.z))
    var h_velocity = speed
    var v_velocity = 0
    if Input.is_action_pressed("Jump"):
        h_velocity = h_distance / jump_time
        v_velocity = -0.5 * jump_time * gravity
    if h_distance < 0.05:
        # perhaps you want to flip this line, maybe just set x and z
        target = transform.origin
        velocity = Vector3(0, v_velocity, 0)
    else:
        look_at(target, Vector3.UP)
        rotation.x = 0
        var vel = -transform.basis.z * h_velocity
        velocity = Vector3(vel.x, v_velocity, vel.z)

Addendum: Fixing both time and jump height

A way to make the object reach higher is by increasing the initial vertical velocity. However, the object will also take longer to fall down. Thus, we need to change the rate at which the object falls down, which means tweaking the gravitational acceleration.

As soon as the object begins it upwards motion, its vertical velocity begins to decrease according to the gravitational acceleration. Eventually it reaches a point where the vertical velocity is zero. At which point it cannot continue to move up. Thus, that is the highest point of the jump.

The rate at which the object lost its upwards velocity, is the same rate at which it gains downwards velocity: the gravitational acceleration. Since it is the same rate, it takes the same time for the object to get to highest than it gets to fall down from it.

Thus, we can consider the motion in two halves. For the second half, the initial velocity is zero and the object accelerates down for half the jump time. The final velocity has the same magnitud as the initial velocity of the jump, but in the opposite direction. Thus: v_velocity = -0.5 * jump_time * gravity, which is what we were using before.

Remember that gravity is a negative value.

Now, how high the object gets depends on its initial velocity and the gravitational acceleration. Which are related by the jump time as shown above.

And I'm not going to bother finding the specific equation. Instead, I'll pick a general one:

y = v_velocity * time + 0.5 * gravity * time * time

We can figure out how high the object is at half way into the jump by replacing time with 0.5 * jump_time:

jump_height = v_velocity * (0.5 * jump_time) + 0.5 * gravity * (0.5 * jump_time) * (0.5 * jump_time)

=>

jump_height = v_velocity * 0.5 * jump_time + 0.5 * gravity * 0.25 * jump_time * jump_time

=>

jump_height = v_velocity * 0.5 * jump_time + 0.125 * gravity * jump_time * jump_time

We can replace v_velocity by the formula we had before:

jump_height = (-0.5 * jump_time * gravity) * 0.5 * jump_time + 0.125 * gravity * jump_time * jump_time

And we got to solve for gravity:

jump_height = -0.25 * gravity * jump_time * jump_time + 0.125 * gravity * jump_time * jump_time

=>

jump_height = (-0.25 + 0.125) * gravity * jump_time * jump_time

=>

jump_height = -0.125 * gravity * jump_time * jump_time

=>

-0.125 * gravity * jump_time * jump_time = jump_height

=>

gravity * jump_time * jump_time = (1/-0.125) * jump_height

=>

gravity * jump_time * jump_time = -8 * jump_height

=>

gravity = -8 * jump_height / (jump_time * jump_time)

And thus we can update the code:

func _physics_process(delta):
    velocity.y += gravity * delta
    velocity = move_and_slide(velocity, Vector3.UP)
    if is_on_floor():
        var h_distance = Vector2(transform.origin.x, transform.origin.z).distance_to(Vector2(target.x, target.z))
        var h_velocity = speed
        var v_velocity = 0
        if Input.is_action_pressed("Jump"):
            h_velocity = h_distance / jump_time
            gravity = -8 * jump_height / (jump_time * jump_time)
            v_velocity = -0.5 * jump_time * gravity
        if h_distance < 0.05:
            # perhaps you want to flip this line, maybe just set x and z
            target = transform.origin
            velocity = Vector3(0, v_velocity, 0)
        else:
            look_at(target, Vector3.UP)
            rotation.x = 0
            var vel = -transform.basis.z * h_velocity
            velocity = Vector3(vel.x, v_velocity, vel.z)

And yes, that means that each jump will have a different gravity.


Addendum: Thank you Mario, but the floor is in another elevation

So far we have been ignoring the vertical component of the target (when we compute the horizontal distance - h_distance - we only use target.x and target.z).

As a consequence, if the target is above, the jump will not be able to complete the parabola, and thus will end up short of the target (we found floor before we expected it). Similarly, if the target is below, the jump parabola will overextend, and thus will overshoot the target (the floor wasn't were we expected it, so the jump arc kept going).

Alright, so what do we know? We know that after jump_time, we should have reached target coordinates:

position(time = jump_time) = (target.x, target.y, target.z)

To make it easier, I'll call the initial coordinate zero, and we will be working on jump arc as if it were 2D. Our dimensions are h and v:

position(time = 0) = (0, 0)
position(time = jump_time) = (h_distance, v_distance)

Where:

h_distance = Vector2(transform.origin.x, transform.origin.z).distance_to(Vector2(target.x, target.z))
v_distance = target.y - transform.origin.y

The sign of v_distance is important.

We also know that the motion will follow the motion equations. This is the one we had previously:

y = v_velocity * time + 0.5 * gravity * time * time

Which I will rename to v for vertical:

v(time) = v_velocity * time + 0.5 * gravity * time * time

Let me restate the points we had before:

v(time) = v_velocity * time + 0.5 * gravity * time * time

v(0) = 0
v(jump_time) = v_distance

Actually v(0) = 0 is trivial, we can see that evaluating v(time) when time = 0 will gives us 0. Let us not worry about that one:

v(time) = v_velocity * time + 0.5 * gravity * time * time
v(jump_time) = v_distance

And this gives us - drum roll - infinite solutions. There are infinite possible jump arcs that satisfy those equations. The jump arc could go higher, and have higher gravity, or go lower but with less gravity.

We dealt with that before. We will be modifying gravity based on the jump_height. However, we want to make sure the jump always goes above both start and end points. If it were below, we would end with negative gravity, and we don't want that.

So, I'll say that height = max(v_distance, 0) + jump_height where jump_height > 0.

Thus, let us have it like this:

v(time) = v_velocity * time + 0.5 * gravity * time * time
v(jump_time/2) = height
v(jump_time) = v_distance

By the way, to keep these equations short, I will be using:

tt = jump_time * jump_time
gtt = gravity * jump_time * jump_time

Now let us solve the equation system. Starting with:

v(jump_time) = v_distance

We have:

v_distance = v(jump_time) = v_velocity * jump_time + 0.5 * gtt

=>

v_distance = v_velocity * jump_time + 0.5 * gtt

=>

v_distance - 0.5 * gtt = v_velocity * jump_time

=>

(v_distance - 0.5 * gtt)/jump_time = v_velocity

And starting with:

v(jump_time/2) = height

We have:

height = v(jump_time/2) = v_velocity * jump_time/2 + 0.5 * gtt/4

=>

height = v_velocity * 0.5 * jump_time + 0.125 * gtt

=>

height - 0.125 * gtt = v_velocity * 0.5 * jump_time

=>

(height - 0.125 * gtt) / (0.5 * jump_time) = v_velocity

Thus:

v_velocity
= (height - 0.125 * gtt) / (0.5 * jump_time)
= (v_distance - 0.5 * gtt)/jump_time

=>

height * 2.0 - 0.125 * gtt * 2.0 = v_distance - 0.5 * gtt

=>

height * 2.0 - 0.25 * gtt = v_distance - 0.5 * gtt

=>

height * 2.0 = v_distance - 0.5 * gtt + 0.25 * gtt

=>

height * 2.0 = v_distance - 0.25 * gtt

=>

height * 2.0 + 0.25 * gtt = v_distance

=>

0.25 * gtt = v_distance - height * 2.0

=>

gtt = (v_distance - height * 2.0) / 0.25

=>

gtt = v_distance/0.25 - height * 2.0/0.25

=>

gtt = 4 * v_distance - 8 * height

=>

gravity * tt = 4 * v_distance - 8 * height

=>

gravity = (4 * v_distance - 8 * height)/tt

And we replace that here:

(v_distance - 0.5 * gtt)/jump_time = v_velocity

Getting:

v_velocity = (v_distance - 0.5 * ((4 * v_distance - 8 * height)/tt) * tt)/jump_time 

=>

v_velocity = (v_distance - 0.5 * (4 * v_distance - 8 * height))/jump_time

=>

v_velocity = (v_distance - (2 * v_distance - 4 * height))/jump_time

=>

v_velocity = (v_distance - 2 * v_distance + 4 * height)/jump_time

=>

v_velocity =  (4 * height - v_distance)/jump_time

Alright, update the code:

func _physics_process(delta):
    velocity.y += gravity * delta
    velocity = move_and_slide(velocity, Vector3.UP)
    if is_on_floor():
        var h_distance = Vector2(transform.origin.x, transform.origin.z).distance_to(Vector2(target.x, target.z))
        var v_distance = target.y - transform.origin.y
        var h_velocity = speed
        var v_velocity = 0
        if Input.is_action_pressed("Jump"):
            var height = max(v_distance, 0) + jump_height
            gravity = (4 * v_distance - 8 * height) / (jump_time * jump_time)
            h_velocity = h_distance / jump_time
            v_velocity = (4 * height - v_distance) / jump_time
        if h_distance < 0.05:
            # perhaps you want to flip this line, maybe just set x and z
            target = transform.origin
            velocity = Vector3(0, v_velocity, 0)
        else:
            look_at(target, Vector3.UP)
            rotation.x = 0
            var vel = -transform.basis.z * h_velocity
            velocity = Vector3(vel.x, v_velocity, vel.z)

Tested.

\$\endgroup\$
28
  • \$\begingroup\$ thank you very very much, for this detailed and amazing answer, I have implemented it in C++ , but there is an issue , I feel like the player is jumping half the distance, and when jumping diagonally , the y becomes higher, that the play jump up high, I have added a link to let you see how it behaves : youtube.com/watch?v=cnFUi7weKnU , maybe you can spot what is wrong, thank you again. \$\endgroup\$
    – Abanoub
    May 18, 2021 at 9:15
  • \$\begingroup\$ @Abanoub what I notice is the following: 1. You are using global_transform, does not seem to be the source of the problem (I tested with global_transform in GDScript and it isn't a problem). 2. rotate_x(0) and rotation.x = 0 is not the same (at least in GDScript, rotate_x(0) does nothing). 3. It is not sliding, it should be moving on the ground until it reaches the destination. Now, it could be jumping less if the KinematicBody is scaled down. But then it would slide to the target. Its as if the the target is wrong. I suggest to make a marker for the target, to discard. \$\endgroup\$
    – Theraot
    May 18, 2021 at 10:16
  • \$\begingroup\$ @Abanoub Something else: enable Visible Collision Shapes in the Debug menu. I'm wondering if it is not sliding because something is wrong with the collision shape. \$\endgroup\$
    – Theraot
    May 18, 2021 at 10:19
  • 1
    \$\begingroup\$ @Abanoub Added a version that lets you specify the height of the jump under "Addendum: Fixing both time and jump height". \$\endgroup\$
    – Theraot
    Jun 2, 2021 at 10:20
  • 1
    \$\begingroup\$ @Abanoub I guess it is not accurate enough and overshoots ever so slightly. Perhaps a matter of tweaking that 0.05 again. The other thing that comes to mind is to switch to least Semi-implicit Euler. I haven't tried that with Godot, if you want to try, I think it would be like this: velocity = move_and_slide(Vector3(velocity.x, velocity.y + gravity * delta * 0.5, velocity.z), Vector3.UP); velocity.y += gravity * delta, it is trying to compensate for the motion not being aware of the acceleration. \$\endgroup\$
    – Theraot
    Jun 5, 2021 at 12:11

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .