Godot Version
I use Godot 4.0
Question
So I’m making a 2D top-down bullet hell and the player will be playing on an icy map. I would like to know how i can change my movement code to make it feel like the player is sliding in ice. My plan for this is to have an upgrade that will increase your friction/ control over your movement each time its leveled up.
This is the code i currently use for the players movement.
@export var move_speed = 20.0
var direction : Vector2
func _physics_process(delta):
#Y direction variable
if Input.is_action_pressed("up"):
direction.y = -1 #+y is down
elif Input.is_action_pressed("down"):
direction.y = 1
else:
direction.y = 0
#X direction variable
if Input.is_action_pressed("right"):
direction.x = 1
elif Input.is_action_pressed("left"):
direction.x = -1
else:
direction.x = 0
velocity = direction * move_speed * delta * 200
move_and_slide()
I am open to changing my code as much is needed.
Thank you for your time.
There are many ways to do this. Heres one way. First set a var for current velocity.
var current_velocity: Vector2
Then set it at the end of each change:
current_velocity = velocity
now rather than setting velocity based on direction, lerp the velocity more slowly towards the given direction.
var input_velocity = direction * move_speed * delta * 200
velocity = current_velocity.lerp(input_velocity, 0.3)
I am only suggesting this from memory so hopefully it will help. (Change 0.3 to get different effects).
1 Like
Good advice, highly recommend move_toward over lerp to prevent framerate dependent movement; lerp would accelerate/decelerate much faster on high-framerate machines.
move_toward also allows for more sensible units in pixels per second, I’d recommend 4 times your speed, lower of more icy movement higher for snappier control. If 4000 is your move_speed (assumed from 20 * 200) then ACCELERATION = 6000 should be very icy.
var target_velocity = direction * move_speed * delta
velocity = velocity.move_toward(target_velocity, ACCELERATION * delta)
2 Likes
Lerp would actually converge slower on higher framerates, but if interpolation parameter is scaled by time slice (aka “multiplied by delta”) the difference would be tolerable in most cases within typical fps range. It’s almost framerate independent.
The irony is that using linear interpolation with percentage as the parameter and converging endpoints actually results in geometric and not linear progression. This non-linearity feels better in some use cases as it produces pleasant easing. So lerping may still be preferable there.
move_toward() uses absolute delta value as the parameter so its linearity is always guaranteed (if parameter is scaled by time slice)
2 Likes
lerp with *delta actually results in much worse framerate dependency; eitherway I think the most important difference is legibility of units for gameplay functions. Consider move towards, “accelerating X pixels per second”, but lerp without *delta is “accelerating X percent of the difference in target value per frame”, and finally with *delta this ends up being “accelerating X percent of the different in target value per frame-time-slice”. Since the frame-time-slice is used as a multiplier it will affect the value wildly if given different values and/or at different times, if it’s not already hard enough to measure.
Since the target unit is accelerating speed using move_toward for an actual linear increase ends up feeling very non-linear and smooth in the resulting position, acceleration → speed → position.
3 Likes
No, multiplying by delta stabilizes framerate dependency but as you say the motion is not really intuitively predictable. However, there are cases where lerping may be preferred feel-wise, especially for directly animating positions. Not saying this is that case though.
Just for fun, here’s a benchmark:
func _ready():
var fps = 30
while fps < 1000:
var output ="%3d:\t"%fps
for speed in range(1, 100, 10):
var t = 0.0
var dt = 1.0 / fps
var foo = 1.0;
while not is_zero_approx(foo):
foo = clamp(lerp(foo, 0.0, speed * dt), 0.0, 1.0)
t += dt
output += "\t%f"%t
print(output)
fps *= 2
And here’s the output that measures the convergence time from 1.0 to 0.0. Each column is for different convergence rate (in increasing order):
30: 11.333333 0.866667 0.333333 0.033333 0.033333 0.033333 0.033333 0.033333 0.033333 0.033333
60: 11.433333 0.950000 0.450000 0.266667 0.183333 0.116667 0.016667 0.016667 0.016667 0.016667
120: 11.466667 1.000000 0.500000 0.325000 0.233333 0.175000 0.141667 0.108333 0.091667 0.075000
240: 11.491667 1.025000 0.525000 0.350000 0.258333 0.204167 0.166667 0.137500 0.116667 0.104167
480: 11.502083 1.035417 0.537500 0.360417 0.268750 0.214583 0.177083 0.150000 0.131250 0.114583
960: 11.507292 1.040625 0.542708 0.365625 0.275000 0.219792 0.183333 0.156250 0.136458 0.120833
It’s noticeable that on slower convergence rates, the difference in total time is almost negligible across a wide fps range. As the convergence rate grows the difference becomes more significant (in relative terms in respect to total time).
It’s also worth mentioning that lerp used like this can potentially overshoot if you’re not careful and let the parameter go beyond 1. So clamping the parameter or the result is a wise thing to do.
2 Likes
That was very interesting.
I checked my original tank game prototype (that I abandoned) where I intended to do different ground types like ice and mud, (that I was recalling for the answer I gave) and in that prototype the movement did use lerp, and the lerp factor was traction_factor * delta. I also had an engine_drag * delta that also affected velocity, and I used an engine_thrust lerped with the traction factor to get the acceleration as well.
Anyway, even now the prototype still plays really well and the tank certainly slides like it is on ice, and does the coyote-time like effect of delayed acceleration like wheel-spin really well. However, looking at old code always amazes me as I think “well I wouldn’t do it that way now”.
Perhaps I will go back and finish that game one day.
1 Like
Thank you all so much. I realized that I have a lot more that I need to learn.
Pauldrewtt’s solution works best for my current situation, but I will definitely do more research on how to optimize my game to work on different frame rates.
I appreciate everyone who gave their input and I am happy I found a community to rely on.
2 Likes