Help on making grindrails

Godot Version

Godot Engine v4.4.1.stable.steam.49a5bc7b6

Question

I am trying to make a grind rail system in my 2D game, but I am having trouble to set it up prorpely. My Grind Rail logic was to set up a Path2D, a PathFollow2D and a RemoteTrasnform, it would work as the following: when the player enters the Rail area, it would add the RemoteTransform as a child of PathFollow2D and when the Path ended or the player Jumped, it would remove the RemoteTransform child of PathFollow2D.

The problem, however, is that if i have 2 Rails or more, it will always do the Path of one of the Rails. Example: I have a Rail 1, Rail 2, Rail 3, no matter which Rail area i enter, it will always do the PathFollow of Rail 1.

Here’s The Codes:

extends PathFollow2D
@onready var RemoteTransform = $RailRemoteTransform2D
@onready var RailAim = $"../RailAim"
func _ready() -> void:
	RailAim.hide()
	Global.ObjectPostitions.append(RailAim)
func _process(delta: float) -> void:
	if Global.CanRail:
		$".".add_child(RemoteTransform)
		var railSpeed := 10.0
		$".".progress += railSpeed
		if $".".progress_ratio == 1 :
			Global.ExitedRail = true
			$"../../Sonic".global_rotation = 0
			$".".remove_child(RemoteTransform)
			Global.CanRail = false
			$".".progress_ratio = 0
		if Input.is_action_just_pressed("jump"):
			$"../../Sonic".global_rotation = 0
			$".".remove_child(RemoteTransform)
			Global.CanRail = false
			$".".progress_ratio = 0
func _on_activate_path_body_entered(body: Node2D) -> void:
	Global.CanRail = true
	Global.ExitedRail = false
extends RemoteTransform2D
@onready var RemoteTransform = $"."
func _ready() -> void:
	pass

func _process(delta: float) -> void:
	if Global.CanRail:
		RemoteTransform.remote_path = $"../../../Sonic".get_path()

And my nodes are arranged like this:

If anyone have another way to implement this mechanic or a solution to this problem, i would appreciate the help

I think your problem is in your globals and caching for remote transforms.

If I were doing this, I would not use a remote transform at all. I think I would simple store a reference to the rail path and its curve in the player (doing away with the global), so something like:

On rail area entered:

  • emit signal to say rail available,
  • store reference to the rail in a node reference dictionary called ‘rails available’ ( or a simple variable, although you may want the player to have access to multiple rails at any one time)

If ‘jump’ or whatever input you use to say ‘get on rail’ (only if rail available of course, and if multiple used, perhaps jump to the nearest one?):

  • Change state to jumping on rail
  • get the curve of the now stored rail reference path
  • follow the curve with a mixture of get baked length and distance travelled.
  • Do any wobbling or ‘fall off criteria’ while on the rail, changing state to fallen if needed
  • At the end of the rail (from the curves length) change state to ‘jumping off rail’.

This is necessarily vague and it would be a lot of code to write as an example, so sorry for the code free answer. But in this setup your rails are independent, they are just really a path2D, your player has rail specific states so as not to interfere with your normal walking, running etc states, and the rail is passed by reference, so no caching problems and no reparenting issues (which I always consider a sign of a bad approach or architecture).

Look at all those gorgeous methods! And a handy comment from @jcobb48 and some real action from @Calinou - thanks Calinou.

You could now relatively simply introduce branching and rail switching just by swapping curves when a new rail become available, which could be interesting, jumping from one rail to another.

Anyway, I hope that helps in some way.

PS It really is worth spending the time to make your code more easily readable btw. I noticed that If your Global,can_rail is true, you will be adding a child in _physics_process which is multiple times a frame, this can’t be right surely?

func _process(delta: float) -> void:
	if Global.CanRail:
		$".".add_child(RemoteTransform)
		...
		if $".".progress_ratio == 1 :
			...
			Global.CanRail = false

		if Input.is_action_just_pressed("jump"):
			...
			Global.CanRail = false
# But neither of these conditions is forced on first pass, so how many children do you want to add exactly?

EDIT PS:

if $".".progress_ratio == 1 :

You could just say:

if progress_ratio == 1 :

The $“.” is superfluous. This just means self which is implied anyway.
Same with all the others too:

$".".add_child(RemoteTransform)

is the same as

add_child(RemoteTransform)

Edit 2:
Last thing I promise. But this is very bad idea:

$"../../Sonic".global_rotation = 0

Child nodes should not control parent nodes like this. Use signals to talk up a tree and references or signals to talk down the tree is normally best practice. I won’t go into all the reasons why a child trying to modify a parent in this way is a bad idea but the list is very, very long!

Anyway, I have rattled on about this for ages, sorry, and keep going, it sounds like a very interesting game project with rails and rail riding. Not a simple aim so good luck, I hope it goes well.

Thank you, I will take a look in the Curves2D and i will try to apply those changes you mentioned.