How do I fix my dive and slide moves?

Godot Version

I’m using Godot 4.5

Question

I’m learning game dev for the first time and I’m trying my best to make a Sonic Colors DS-esque game as practice. I put my issues in the double hashtags above the problem code, but I’ll put it here too: The dive (or Stomp in Sonic) for some reason doesn’t work if I’m traveling right while holding Jump. And the slide move doesn’t decelerate when it is initiated while boosting. I can’t for the life of me understand what’s going wrong here.

Here’s my code (any ideas on how to make it better in other ways is welcome!):

extends CharacterBody2D

var speed = 300.0
var maxspeed = 1200.0
var decel = 50.0
var force = 500.0
const jump = -3000.0
const weight = 5.0
var slide = false

@onready var sprite: Sprite2D = $Sprite2D
@onready var collbox: CollisionShape2D = $CollisionShape2D


func _physics_process(delta: float) -> void:
	# Add the gravity.
	velocity += get_gravity() * delta * weight

	#variable jump height
	if Input.is_action_just_released("Jump") and velocity.y < 0:
		velocity.y = jump/4
	#speed-based jump
	if Input.is_action_pressed("Jump") and is_on_floor() and slide == false:
		if velocity.x > 0:
			velocity.y += jump + -velocity.x/4
		if velocity.x < 0:
			velocity.y += jump + velocity.x/4  
		if velocity.x == 0:
			velocity.y += jump
	
	#dive jump
	##fix: doesn't work while holding jump and traveling right
	if Input.is_action_pressed("Down") and is_on_floor() == false:
			velocity.y += -jump
			slide = true
			
	#slide
	##fix: doesn't decelerate while holding boost, even after releasing boost
	if Input.is_action_pressed("Down") and is_on_floor():
		slide = true
	if Input.is_action_just_released("Down"):
		slide = false
		
	
	#move right
	if Input.is_action_pressed("Right"):
	#boost right
		if Input.is_action_pressed("Boost") and force > 0 and slide == false:
			decel = 0
			if is_on_floor():
				velocity.x += speed
				force -= 1
			
		#move right
		else:
			if velocity.x > maxspeed:
				decel = 50.0
				velocity.x -= decel
				
			else:
				velocity.x += speed
		
	#move left
	if Input.is_action_pressed("Left"):
		#boost left
		if Input.is_action_pressed("Boost") and force > 0 and slide == false:
			decel = 0
			if is_on_floor():
				velocity.x -= speed
				force -= 1
			
		#move left
		else:
			if velocity.x < -maxspeed:
				decel = 50.0
				velocity.x += decel
				
			else:
				velocity.x -= speed
		
	#deceleration
	else:
		if velocity.x > 0:
			velocity.x -= decel
			
		if velocity.x < 0:
			velocity.x += decel
		
	#flip sprite
	if velocity.x < 0:
		sprite.flip_h = true
	if velocity.x > 0:
		sprite.flip_h = false
		

	move_and_slide()

Hi there!

I would get the input not in _physics_process but within _input() (or even better _unhandled_input()).

Also I would create a character controller class that expresses the possible moves explicitly, for example with an enum. It just makes it nicer to read in general. On top, pulling this code out, you could also reduce the overall line count of your individual script files by grouping stuff into categories. I think less lines of code per script is one of the best way to reduce the cognitive load when reading code later.

This goes into the direction of lose coupling. But for code that handles the input in a real time game like Sonic and similar you want the code to execute fast without any perceivable lag.

These are improvements I would see and things I would look out for while improving.

Why would _unhandled_input() be better than _input()?

There is a bunch of input functions (_input, _unhandled_input, _gui_input). None of them is better in all contexts, but in gameplay context I would avoid _input for handling controls. There is one really good doc page about the differences of these functions, it also has links to all those functions. There it says the following:

First of all, the standard Node._input() function will be called in any node that overrides it (and hasn’t disabled input processing with Node.set_process_input()). If any function consumes the event, it can call Viewport.set_input_as_handled(), and the event will not spread any more. This ensures that you can filter all events of interest, even before the GUI. For gameplay input, Node._unhandled_input() is generally a better fit, because it allows the GUI to intercept the events.

The problem is in the movement logic, not _input() versus _unhandled_input().

decel is set to 0 while boosting and is not reliably restored when sliding starts. Your slide also still runs the normal right/left acceleration code, so it can accelerate and decelerate in the same frame. Finally, jump and dive use is_action_pressed(), which applies them every physics frame while held; these should be one-shot actions.

Replace the jump/dive/horizontal-movement section with one mutually exclusive flow:


const WALK_SPEED := 300.0
const BOOST_SPEED := 1200.0
const ACCELERATION := 3500.0
const GROUND_FRICTION := 2500.0
const SLIDE_FRICTION := 900.0
const DIVE_SPEED := 1200.0

var slide := false

func _physics_process(delta: float) -> void:
    velocity += get_gravity() * delta * weight

    if Input.is_action_just_pressed("Jump") and is_on_floor() and not slide:
        velocity.y = jump - absf(velocity.x) * 0.25

    if Input.is_action_just_released("Jump") and velocity.y < 0.0:
        velocity.y *= 0.25

    if Input.is_action_just_pressed("Down"):
        slide = true
        if not is_on_floor():
            velocity.y = DIVE_SPEED

    if Input.is_action_just_released("Down"):
        slide = false

    var direction := Input.get_axis("Left", "Right")

    if slide:
        velocity.x = move_toward(
            velocity.x, 0.0, SLIDE_FRICTION * delta
        )
    elif direction != 0.0:
        var boosting := Input.is_action_pressed("Boost") and force > 0.0
        var target_speed := BOOST_SPEED if boosting else WALK_SPEED
        velocity.x = move_toward(
            velocity.x,
            direction * target_speed,
            ACCELERATION * delta
        )
        if boosting:
            force = maxf(force - 60.0 * delta, 0.0)
    else:
        velocity.x = move_toward(
            velocity.x, 0.0, GROUND_FRICTION * delta
        )

    if velocity.x < 0.0:
        sprite.flip_h = true
    elif velocity.x > 0.0:
        sprite.flip_h = false

    move_and_slide()

This makes slide, boost and normal movement mutually exclusive. Holding Jump no longer retriggers a jump on landing, and the dive sets a downward speed once instead of adding thousands of pixels per second every frame. Tune the speed and friction constants after the logic works.

Uncertainty: the constants need tuning for the project’s scale.

I’ll give some small fix recommendations:

if velocity.x > 0:
			velocity.y += jump + -velocity.x/4
		if velocity.x < 0:
			velocity.y += jump + velocity.x/4  
		if velocity.x == 0:
			velocity.y += jump

For these three conditions, you can use if, elif, elif. It would run a tad faster as if one condition is true, the remaining others are skipped.

is_on_floor() == false

instead of this, you can shorten this to

not is_on_floor()

and it would be a bit more readable.

I believe the deceleration issue is coming from these lines of code

if Input.is_action_pressed("Boost") and force > 0 and slide == false:
			decel = 0
			if is_on_floor():
				velocity.x += speed
				force -= 1
			
		#move right
		else:
			if velocity.x > maxspeed:
				decel = 50.0
				velocity.x -= decel
				
			else:
				velocity.x += speed

When your velocity.x is lower than maxspeed, you are not setting decel back to 50. You got to set it back to 50, or never set it to 0 in the first place and adjust the boost to be more powerful to overcome deceleration.