I'm trying to make a grid-based movement game but when character jumps, it's affected by gravity; too, the character doesn't receive instructions by the keyboard, but it receive instructions step by step when the players push "GO" button
I have the gravity affecting the character, but I don't know how to move the character step by step on grid-based movement. Someone has an idea or some video tutorial
The var "moviendose" is changed to true when the player press GO
`
extends KinematicBody2D
var moviendose = false
var lista_habilidades_jugador = []
onready var tweene = $AnimatedSprite
const GRAVITY = 9.8
var velocity = Vector2.ZERO
const def_habilidades = {
"Habilidad1": Vector2(16,0),
"Habilidad2": Vector2(-16,0),
"Habilidad3": Vector2(0,(-GRAVITY*16))
}
func _physics_process(delta):
velocity.y += GRAVITY
if lista_habilidades_jugador.size() == 0:
moviendose = false
if moviendose == true:
movimiento()
velocity = move_and_slide(velocity)
func movimiento():
for mov in lista_habilidades_jugador:
if mov == "Habilidad1":
velocity = velocity.move_toward(def_habilidades[mov] , 5)
tweene.flip_h = false
tweene.play("correr")
if mov == "Habilidad2":
velocity = lerp(velocity + def_habilidades[mov], Vector2.ZERO,20)
tweene.flip_h = true
if mov == "Habilidad3":
velocity = velocity + def_habilidades[mov]
self.lista_habilidades_jugador.pop_front()
` [bar of movement] (https://i.sstatic.net/cnwqA.png)
I'll break this into issues:
Presumably, this is an earth-like gravity:
const GRAVITY = 9.8
Which means this is an acceleration, in meters per second square.
But this is a 2D enviroment:
extends KinematicBody2D
So movement units are pixels, not meters. Thus the velocity here is in pixels per seconds:
velocity = move_and_slide(velocity)
And thus here you are adding meters per second squared to pixels per second, and that is not right:
velocity.y += GRAVITY
I bet it looks about right anyway, I'll get back to why.
Do you remember physics? Velocity is displacement over time (speed is distance over time). And we have that time, we call it delta. So this would be meters per second:
GRAVITY * delta
And if we add a conversion constant for pixels to meters, we have:
GRAVITY * delta * pixels_per_meter
How much is that constant? How much you want. Presumably something noticiable, you need to check the size of your sprites to come up with some pixel scaling that works for you.
That means the constant is big, but the time between frames we call detal is small, less than a second. So delta makes the value small and the pixel to meters constant makes it big. And I expect these effects to compensate each other, so it kind of looks good anyway… But having them 1. makes it correct, 2. makes it frame rate independent, 3. gives you control over the scaling.
The alternative is to come up with the gravity value in pixels per second squared. In other words, instead of figuring out a conversion constant from pixels to meters, tweak the gravity value until it works like you want (which is equivalent to having it pre-multiplied by the constant), and just keep delta:
GRAVITY * delta
Now, what is this?
velocity = velocity.move_toward(def_habilidades[mov] , 5)
We are changing the velocity towards some known value at steps of 5. That 5 is a delta velocity (delta meaning change here). You would want an acceleration to make this frame rate independent. Something like this:
velocity = velocity.move_toward(def_habilidades[mov] , 5 * delta)
Which would also imply to pass delta to the movement method.
And what the heck is this?
velocity = lerp(velocity + def_habilidades[mov], Vector2.ZERO,20)
I'll assume the value you are adding to the velocity is also a velocity. I'll challenge that later, it is not the point here.
The point is that you are using lerp weird. The lerp function is meant to give you a value between the first two arguments. The third arguments controls how close is the result towards the first or second argument. When the third argument is zero, you get the first argument as result. And when the third argument is one, you get the second argument. But the third argument is 20?
I don't know what the intention here is. Do you want to go to zero and overshoot?
I don't know. But as far as units are concerned, you are changing the velocity but not by a delta, but by a factor. This is not an acceleration, this is a jerk (look it up).
Here you have an unusual structure:
func movimiento(): # you probably will add delta as parameter here
for mov in lista_habilidades_jugador:
# PROCESS THE MOVEMENT
self.lista_habilidades_jugador.pop_front()
You iterate over a list, and each iteration - after processing the movement - you remove an element from the list. Yes, the same list.
If the list stars with three items:
Don't do that. In general you want to one element, and process it. The minimum would be like this:
func movimiento():
var mov = self.lista_habilidades_jugador.pop_front()
# PROCESS THE MOVEMENT
The next time you call the movement method it will pull the next element, and so on.
Ah, but you are calling the movement method each physics frame. Except presumably the motion should take more than one physics frame. So you should hold up until the current movement finishes.
This is how I would structure it:
func _physics_process(delta):
velocity.y += GRAVITY
movimiento()
velocity = move_and_slide(velocity)
Then the movement method can set the variable:
func movimiento():
# if there are no movements we do nothing
if lista_habilidades_jugador.size() == 0:
return
# get the first movement
var mov = self.lista_habilidades_jugador[0]
moviendose = true
# PROCESS THE MOVEMENT
if not moviendose: # we will come back to this
# we are done moving
self.lista_habilidades_jugador.pop_front()
To process each movement we need to update the velocity and know when it reached the destination. We can't do that if we just have a velocity.
In general we don't want to define velocities for grid movement. But displacements. Which will be an issue for the jump but we will get to that.
Thus, I'll assume that the values you have in your dictionary are not velocities but displacements. In fact, we are going to store a target:
var target := Vector2.ZERO
So we can keep track to were we have to move towards. And we need to update that when we pull a new movement from the list:
func movimiento():
# if there are no movements we do nothing
if lista_habilidades_jugador.size() == 0:
return
# get the first movement
var mov = self.lista_habilidades_jugador[0]
if not moviendose:
# just starting a new movement
var displacement = def_habilidades[mov]
target = position + displacement
moviendose = true
# PROCESS THE MOVEMENT
if not moviendose: # we will come back to this
# we are done moving
self.lista_habilidades_jugador.pop_front()
The other thing we need is how long it should take to move. I'll come up with some value, you tweak it as you see fit:
var step_time := 0.5
Do you remember physics (again)? the velocity is displacement over time.
velocity = displacement / step_time
So:
func movimiento():
# if there are no movements we do nothing
if lista_habilidades_jugador.size() == 0:
return
# get the first movement
var mov = self.lista_habilidades_jugador[0]
if not moviendose:
# just starting a new movement
var displacement = def_habilidades[mov]
velocity = displacement / step_time
target = position + displacement
moviendose = true
# PROCESS THE MOVEMENT
if not moviendose: # we will come back to this
# we are done moving
self.lista_habilidades_jugador.pop_front()
And what is left is finding out if we reached the target. A first approximation is this:
func movimiento():
# if there are no movements we do nothing
if lista_habilidades_jugador.size() == 0:
return
# get the first movement
var mov = self.lista_habilidades_jugador[0]
if not moviendose:
# just starting a new movement
var displacement = def_habilidades[mov]
velocity = displacement / step_time
target = position + displacement
moviendose = position.distance_to(target) > 0
if not moviendose:
# we are done moving
self.lista_habilidades_jugador.pop_front()
And we are done. Right? RIGHT?
We are not done. We move some distance each frame, so the distance to the target will likely not hit zero. And that is without talking about floating point errors.
Thus, instead of finding out if we reached the target, we will find out if we will overshoot the target. And to do that we need to compute how we will move. Start by bringing delta in (I'm also adding type information):
func _physics_process(delta:float) -> void:
velocity.y += GRAVITY
movimiento(delta)
velocity = move_and_slide(velocity)
func movimiento(delta:float) -> void:
# if there are no movements we do nothing
if lista_habilidades_jugador.size() == 0:
return
# get the first movement
var mov:String = self.lista_habilidades_jugador[0]
if not moviendose:
# just starting a new movement
var displacement:Vector2 = def_habilidades[mov]
velocity = displacement / step_time
target = position + displacement
moviendose = position.distance_to(target) > 0
if not moviendose:
# we are done moving
self.lista_habilidades_jugador.pop_front()
Now we can compute how much we will move this frame:
velocity.length() * delta
And compare that with the distance to the target:
moviendose = position.distance_to(target) > velocity.length() * delta
Now that we know we will overshoot, we should prevent it. The first idea is to snap to the target:
if not moviendose:
# we are done moving
position = target
self.lista_habilidades_jugador.pop_front()
For reference I'll also mention that we can compute the velocity to reach the target this frame (within floating point error):
if not moviendose:
# we are done moving
velocity = velocity.normalized() * position.distance_to(target) / delta
self.lista_habilidades_jugador.pop_front()
Here we normalize the velocity to get just its direction. And do you remember physics? Yes, yes. That is distance over time.
However we will not use this one, because it would mess with the jump…
But the jump does not work like that. You cannot define a jump the same way. There are six ways to define a vertical jump:
Defining the jump by gravity and speed is very common, at least among beginners, because you already have a gravity and then you define some upward velocity and you have a jump.
It is, however, a good idea to define the gravity using height because then you know - by design - how high it can jump which can be useful for scenario design.
In fact, in the spirit of keeping the representation of movement by displacement, using a definition by gravity and height is convenient. So I'll go with that. Yet, we will still need to compute the velocity, which we will do like this:
half_jump_time = sqrt(-2.0 * max_height / gravity)
jump_time = half_jump_time * 2.0
vertical_speed = -gravity * half_jump_time
That comes from the equations of motion. I'll spare how to come up with that, feel free to look it up.
However, note that the vertical component will not be the destination vertical position, but the height of the jump. Which is half way through the jump. So for consistency sake, when you specify a jump the displacement will be to the highest point of the jump…
We will know it is a jump because it has a vertical component at all.
if not moviendose:
# just starting a new movement
var displacement:Vector2 = def_habilidades[mov]
target = position + displacement
if displacement.y == 0:
velocity = displacement / step_time
else:
var half_jump_time = sqrt(-2.0 * displacement.y / gravity)
var jump_time = half_jump_time * 2.0
velocity.y = -gravity * half_jump_time
velocity.x = displacement.x / half_jump_time
And that highlights another problem: We cannot say the motion ended when reached the target, we also need to check if the character is on the ground.
For that effect, I'll have one extra variable (add at the top of the file):
var target_reached := false
So we can say:
var distance:float = velocity.length() * delta
target_reached = target_reached or position.distance_to(target) > distance
moviendose = not (target_reached and is_on_floor())
if not moviendose:
# we are done moving
target_reached = false
position = target
self.lista_habilidades_jugador.pop_front()
Unless I forgot to include something, that should be it.
Addendum: Yes, I forgot something. When the character landed that is not the target position (because the target position is the mid point of the jump). You could, in theory, snap mid jump, but I don't think that is of much use. Instead consider snapping the position (or issuing a move) once landed to the nearest position of the grid. You can use the method snapped
on the vector to find out where it aligns to the grid size.