Character cannot travel backwards on a slope correctly!

Godot Version

4.5.1

Question

Hi all, I am an absolute first timer, but out of the blue was motivated to make a NES-esque skateboarding game. I have graphic and music experience so figured I could easily do the fun bits. I have been using chatGPT to help with script (I apologise if that’s frowned upon - I am wrapping my head around it the more I fiddle though). I’ve been at it for about 6 days. It’s been fairly successful until I wanted to add slopes.

The slope is made through a collision layer on a tile.

I wanted to have sprites change for forwards uphill and backwards downhill. Got it to work for uphill, but down hill has been a multi day battle now and I would truly appreciate any assistance or advice anybody can provide. It’s kind of the last physical action required, then I can actually make the game.

Attached is the full code and I’ll link a little video so you can see it in action. This is the closest I’ve gotten. You can see that if you stop travelling to the left while on the slope it holds the right sprite, but if moving, it flashes between the right sprite and one of the sprite for travelling left on flat ground. In other iterations it would flash between the correct sprite and the falling from a jump sprite - suggesting disconnection from the slope. I’m so confused.

https://imgur.com/a/ExXjpL8

If you share code, please wrap it inside three backticks or replace the code in the next block:

extends CharacterBody2D

# ---------------- MOVEMENT ----------------
@export var speed := 120
@export var gravity := 600
@export var jump_force := -300
@export var floor_friction := 0.85
@export var air_friction := 0.99

# ---------------- SPRITES ----------------
@export var roll_forward_1: Sprite2D
@export var roll_forward_2: Sprite2D
@export var roll_fakie_1: Sprite2D
@export var roll_fakie_2: Sprite2D
@export var uphill: Sprite2D
@export var downhill_fakie: Sprite2D
@export var Ollie_1: Sprite2D
@export var Ollie_2: Sprite2D
@export var sprite_ollie_fakie: Sprite2D
@export var sprite_ollie_fakie_fall: Sprite2D
@export var kickflip_1: Sprite2D
@export var kickflip_2: Sprite2D
@export var kickflip_3: Sprite2D
@export var bs360_1: Sprite2D
@export var bs360_2: Sprite2D
@export var bs360_3: Sprite2D

# ---------------- GRIND SPARKS ----------------
@export var RailTilemap: TileMap
@export var grind_spark_1: Sprite2D
@export var grind_spark_2: Sprite2D
var grind_sparks: Array[Sprite2D] = []
var spark_index := 0
var spark_timer := 0.0
var spark_interval := 0.1

# ---------------- COLLISION ----------------
@export var collision_shape: CollisionShape2D

# ---------------- ANIMATION ----------------
var animation_timer := 0.0
var animation_interval := 0.2
var forward_toggle := true
var fakie_toggle := true

# ---------------- DIRECTION ----------------
var facing_right := true

# ---------------- KICKFLIP ----------------
var kickflip_active := false
var kickflip_timer := 0.0
var kickflip_frame := 0
var last_jump_time := 1.0
var double_jump_window := 0.35

# ---------------- BS 360 ----------------
var bs360_active := false
var bs360_timer := 0.0
var bs360_frame := 0
var bs360_queued := false

# ---------------- DOWNHILL CONTEXT ----------------
var downhill_context := false


func _ready():
	grind_sparks = [grind_spark_1, grind_spark_2]
	for s in grind_sparks:
		if s:
			s.visible = false

	if uphill:
		uphill.position.y += 6
	if downhill_fakie:
		downhill_fakie.position.y += 6

	if roll_forward_1:
		roll_forward_1.visible = true


func _physics_process(delta):
	animation_timer += delta
	last_jump_time += delta

	var direction := 0
	if Input.is_action_pressed("ui_left"):
		direction -= 1
	if Input.is_action_pressed("ui_right"):
		direction += 1

	if direction != 0:
		facing_right = direction > 0

	var on_rail := _is_on_rail_floor()

	if direction != 0:
		velocity.x = direction * speed
	else:
		if is_on_floor():
			if on_rail:
				velocity.x = speed if facing_right else -speed
			else:
				velocity.x *= floor_friction
		else:
			velocity.x *= air_friction

	if not is_on_floor():
		velocity.y += gravity * delta
	else:
		velocity.y = 0

	if Input.is_action_just_pressed("ui_up"):
		if last_jump_time <= double_jump_window and velocity.y <= 0:
			kickflip_active = true
			kickflip_timer = 0
			kickflip_frame = 0
			if is_on_floor():
				velocity.y = jump_force
		elif is_on_floor():
			velocity.y = jump_force
		last_jump_time = 0

	if Input.is_action_just_pressed("ui_down"):
		if not is_on_floor() and velocity.y < 0 and not kickflip_active:
			bs360_queued = true

	if bs360_queued and velocity.y > -40 and velocity.y < 0:
		bs360_active = true
		bs360_queued = false
		bs360_timer = 0
		bs360_frame = 0

	# ---------------- DOWNHILL CONTEXT ----------------
	if is_on_floor() and not kickflip_active and not bs360_active:
		var normal: Vector2 = _get_floor_normal()
		var slope: bool = abs(normal.x) > 0.05

		# downhill true if moving along slope
		if slope:
			downhill_context = (normal.x > 0 and velocity.x > 0) or (normal.x < 0 and velocity.x < 0)
		else:
			downhill_context = false
	else:
		downhill_context = false

	move_and_slide()

	if kickflip_active:
		_handle_kickflip(delta)
	elif bs360_active:
		_handle_bs360(delta)
	else:
		_handle_normal_sprites()

	_handle_grind_sparks()


# ---------------- FLOOR NORMAL ----------------

func _get_floor_normal() -> Vector2:
	for i in range(get_slide_collision_count()):
		var collision := get_slide_collision(i)
		if collision.get_normal().y < -0.7:
			return collision.get_normal()
	return Vector2.UP


# ---------------- SPRITES ----------------

func _hide_all_sprites():
	for s in [
		roll_forward_1, roll_forward_2,
		roll_fakie_1, roll_fakie_2,
		uphill, downhill_fakie,
		Ollie_1, Ollie_2,
		sprite_ollie_fakie, sprite_ollie_fakie_fall,
		kickflip_1, kickflip_2, kickflip_3,
		bs360_1, bs360_2, bs360_3
	]:
		if s:
			s.visible = false


func _show_forward_roll():
	if animation_timer >= animation_interval:
		animation_timer = 0
		forward_toggle = !forward_toggle
	if forward_toggle:
		roll_forward_1.visible = true
	else:
		roll_forward_2.visible = true


func _show_fakie_roll():
	# only toggle fakie if NOT downhill
	if downhill_context:
		return
	if animation_timer >= animation_interval:
		animation_timer = 0
		fakie_toggle = !fakie_toggle
	if fakie_toggle:
		roll_fakie_1.visible = true
	else:
		roll_fakie_2.visible = true


func _handle_normal_sprites():
	_hide_all_sprites()

	if is_on_floor():
		var normal: Vector2 = _get_floor_normal()
		var slope: bool = abs(normal.x) > 0.05
		var uphill_here: bool = slope and normal.x < 0 and velocity.x > 0

		# DOWNHILL PRIORITY
		if downhill_context and downhill_fakie:
			downhill_fakie.visible = true
		elif uphill_here:
			uphill.visible = true
		else:
			if velocity.x >= 0:
				_show_forward_roll()
			else:
				_show_fakie_roll()
	else:
		if downhill_context and downhill_fakie:
			downhill_fakie.visible = true
		else:
			if velocity.x >= 0:
				Ollie_1.visible = velocity.y < -100
				Ollie_2.visible = velocity.y >= -100
			else:
				sprite_ollie_fakie.visible = velocity.y < -100
				sprite_ollie_fakie_fall.visible = velocity.y >= -100


func _handle_kickflip(delta):
	kickflip_timer += delta
	if kickflip_timer >= 0.1:
		kickflip_timer = 0
		kickflip_frame += 1

	_hide_all_sprites()
	match kickflip_frame:
		0: kickflip_1.visible = true
		1: kickflip_2.visible = true
		2: kickflip_3.visible = true
		_: kickflip_active = false; _handle_normal_sprites()


func _handle_bs360(delta):
	bs360_timer += delta
	if bs360_timer >= 0.12:
		bs360_timer = 0
		bs360_frame += 1

	_hide_all_sprites()
	match bs360_frame:
		0: bs360_1.visible = true
		1: bs360_2.visible = true
		2: bs360_3.visible = true
		_: bs360_active = false; _handle_normal_sprites()


# ---------------- RAIL CHECK ----------------

func _is_on_rail_floor() -> bool:
	if not is_on_floor():
		return false

	for i in range(get_slide_collision_count()):
		var collision = get_slide_collision(i)
		if collision.get_collider() == RailTilemap and collision.get_normal().y < -0.7:
			return true
	return false


# ---------------- GRIND SPARKS ----------------

func _handle_grind_sparks():
	if collision_shape == null or grind_sparks.size() == 0:
		return

	var sparks_active := false

	if is_on_floor():
		for i in range(get_slide_collision_count()):
			var collision = get_slide_collision(i)
			if collision.get_collider() == RailTilemap:
				sparks_active = true
				break

	spark_timer += get_process_delta_time()
	if spark_timer >= spark_interval:
		spark_timer = 0
		spark_index = (spark_index + 1) % grind_sparks.size()

	var bottom_pos = global_position + Vector2(0, collision_shape.shape.extents.y - 6)
	var offset = Vector2((-13) if facing_right else (13), 0)

	for i in range(grind_sparks.size()):
		var s = grind_sparks[i]
		s.global_position = bottom_pos + offset
		s.visible = sparks_active and i == spark_index
		s.flip_h = not facing_right

It is frowned upon, but not for ethical reasons (mostly). It’s frowned upon because it confidently gives wrong answers to programming questions - especially Godot, which changes so quickly the models cannot keep up. They confuse versions and they make up function names that do not exist. As convenient as it is, you are making your life more difficult by using it.

So that’s the other problem with ChatGPT. It is so confident in its assurances that it can solve your problem, you’ll spend days going down rabbit-holes.

The answer is, it has nothing to do with your code.

Go to the CharacterBody2D in the Inspector and change the Snap Length from 1 to something larger, like 10 (as seen in the screen shot above).

1 Like

Hi, I really appreciate you taking the time.

I actually already had the snap length at 10 px (an experimented with every number around it). It seems like it should be so simple. If it works forwards, why wouldn’t it work backwards?

Your code is kinda hard to decipher. I don’t understand why you use facing_right sometimes, and direction other times. I don’t know why you aren’t using get_axis() to determine direction. (I’m assuming because ChatGPT told you to.)

It appears from your code that the player can face right or left. So how is it they can travel backwards? I kinda see how that could happen if they’re on a rail, but otherwise, I’m confused about how that would happen.

Again, I really appreciate you taking the time to interact with someone who is obviously clueless. Anything dumb in the code is ChatGPT - I just assumed that if it was practically working (which everything has up to the leftward slope challenge) that the code must’ve been ok. Damn…

When I say backwards (or fakie) that’s probably just me using skateboarding language when I say - what I mean is travelling to the left. You could think of it as a car reversing down a hill rather driving up it forwards, turning around and driving down it forwards.

If you wanted a character to travel right and left, up and down slopes, and have a sprite change for those slope moments. How would you approach it? Thank you so much for your time.

So, to start, this is the default script for a CharacterBodyt2D, provided by the editor:

extends CharacterBody2D


const SPEED = 300.0
const JUMP_VELOCITY = -400.0


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

	# Handle jump.
	if Input.is_action_just_pressed("ui_accept") and is_on_floor():
		velocity.y = JUMP_VELOCITY

	# Get the input direction and handle the movement/deceleration.
	# As good practice, you should replace UI actions with custom gameplay actions.
	var direction := Input.get_axis("ui_left", "ui_right")
	if direction:
		velocity.x = direction * SPEED
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)

	move_and_slide()

In it, you’ll see an example of how to use get_axis(). Note the comment above it that talks about good practice of not using ui_left and ui_right, but replacing them with your own actions.

The fact that you didn’t do that tells me that you started with ChatGPT without any grounding in how to use Godot to begin with. So before we start discussing how to solve your problem, you need the vocabulary to understand the answer. Start with the tutorial in the documentation: Your first 2D game — Godot Engine (stable) documentation in English It should take an hour or so. Then do Your first 3D game — Godot Engine (stable) documentation in English - even though you’re not doing a 3D game. Doing these things will help you understand the basics of the editor.


Yes, anything dumb is ChatGPT. But if I don’t understand why it made a decision, and you don’t understand why it made a decision, then ripping code out is going to break your game in weird ways I cannot predict.

Also, my goal here is to help you understand what’s going on so that you learn how your game works. And to understand how Godot woks so you can make informed decisions about your game. Me just rewriting your code is A) Going to take me a lot of time I don’t want to spend, and B) not going to help you when it breaks.

Let’s take one small piece as an example:

Grind Sparks

# ---------------- GRIND SPARKS ----------------
@export var RailTilemap: TileMap
@export var grind_spark_1: Sprite2D
@export var grind_spark_2: Sprite2D
var grind_sparks: Array[Sprite2D] = []
var spark_index := 0
var spark_timer := 0.0
var spark_interval := 0.1

func _ready():
	grind_sparks = [grind_spark_1, grind_spark_2]
	for s in grind_sparks:
		if s:
			s.visible = false

func _physics_process(delta):
	_handle_grind_sparks()

# ---------------- GRIND SPARKS ----------------

func _handle_grind_sparks():
	if collision_shape == null or grind_sparks.size() == 0:
		return

	var sparks_active := false

	if is_on_floor():
		for i in range(get_slide_collision_count()):
			var collision = get_slide_collision(i)
			if collision.get_collider() == RailTilemap:
				sparks_active = true
				break

	spark_timer += get_process_delta_time()
	if spark_timer >= spark_interval:
		spark_timer = 0
		spark_index = (spark_index + 1) % grind_sparks.size()

	var bottom_pos = global_position + Vector2(0, collision_shape.shape.extents.y - 6)
	var offset = Vector2((-13) if facing_right else (13), 0)

	for i in range(grind_sparks.size()):
		var s = grind_sparks[i]
		s.global_position = bottom_pos + offset
		s.visible = sparks_active and i == spark_index
		s.flip_h = not facing_right

This is a lot of code to handle what could be handled by a GPUParticle2D node being turned on and off. The code could look like this:

# ---------------- GRIND SPARKS ----------------
@onready var grind_sparks: GPUParticle2D = $GrindSparks
@onready var wheels: Area2D: = $Wheels


func _ready() -> void:
	wheels.area_entered(_handle_grind_sparks)
	wheels.area_exited(_stop_grind_sparks)


func _handle_grind_sparks():
	grind_sparks.emitting = true


func _stop_grind_sparks():
	grind_sparks.emitting = false

You’d put the rail_tile_map (renamed from RailTilemap because it is not a class) on its own collision layer, so that the wheels (and the player) could detect it. The player and wheels would have their mask set to the layer. You’d place the wheels (or whatever it is that actually makes the sparks - that could be a bad variable name) on the same layer mask. Their whole job is to detect when a grind should happen. If they are colliding with the rail, turn them on. If they are not, turn them off.

Of course then you have to learn how GPUParticle2D works so that you can get those cool sparks using the two textures you have.

A Note on LLMs

If you ask a glorified guessing machine to tell you how to write code to do something, it’s not going to to tell you that it is impossible, or that there’s a much easier way. It’s going to write code that may or may not work. LLMs are not intelligent.

If you’re going to use an LLM to answer your Godot questions, stop using ChatGPT and use Cladue.ai instead. It is much more knowledgeable and will send you on fewer wild goose chases. Also, learn how to step back from your question. Don’t ask how you can code something in Godot first - instead, ask how you can do it in Godot. Crafting prompts is an art, and the more you know about Godot, the better able you are to get questions answered.

You are pretty much my hero. You have been so great in taking time to explain these things to me. I will take your advice, and I think I will take the game and myself all the way back to the start and try to understand much more as I can see that issues will only compound if I continue working out of ignorance.

Thank you Dragonforge

1 Like

Always nice to hear from a princess. :rofl:

I think that’s a good plan.

Also, check out the GDScript Style Guide. Try to adhere to it as you learn. It will help keep your code organized, and as your files get longer, it will help you know where to look for things.

Once you’ve done the tutorials, check out AnimatedSprite2D. I think it’s the best way for you to handle your roll, fakie, ollie, kickflip, bs360 and normal movement sprites. I recommend finding a YouTube video or two on it.

Then take a look at some videos on GPUParticle2D. Check out a tutorials on smoke, fire, and explosions. What you learn there will be enough for you to make cool grinding sparks.

Just learning those two things will significantly reduce the amount of code you have.

Also, use snake_case for all your folders and file names. If you don’t, you will have export problems on Windows and Mac.

1 Like