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()
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.
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.
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.