Please help me fix the error in smoothing out bends. In sharp turns, parallel curves become very curved.

I’m working on a tower defense game for my younger brother. I added parallel paths to my curves, but there’s an issue: when the path turns sharply, the parallel curves get distorted and bend too much. help please

@tool
extends Path2D
class_name Pathway

@export var offset_distance    : float = 15.0
@export var num_parallel_paths : int = 2
@export_enum("Both", "Left", "Right") var duplicate_side : String = "Both"

var _parallel_paths : Array[Path2D] = []
var way             : Array[Path2D] = []

func _ready():
   if curve:
   	curve.connect("changed", Callable(self, "_update_parallel_paths"))
   _update_parallel_paths()

func _update_parallel_paths():
   way.clear()
   _clear_parallel_paths()

   if not curve or curve.point_count <= 1:
   	return

   match duplicate_side:
   	"Both":
   		for i in range(1, num_parallel_paths + 1):
   			_create_parallel_path("parallel_pos_", i, offset_distance * i)
   			_create_parallel_path("parallel_neg_", i, offset_distance * -i)
   	"Left":
   		for i in range(1, num_parallel_paths + 1):
   			_create_parallel_path("parallel_neg_", i, offset_distance * -i)
   	"Right":
   		for i in range(1, num_parallel_paths + 1):
   			_create_parallel_path("parallel_pos_", i, offset_distance * i)

   way.append(self)

func _create_parallel_path(name_prefix: String, index: int, offset: float):
   var parallel_path : Path2D = Path2D.new()
   parallel_path.name = name_prefix + str(index)
   parallel_path.curve = _generate_parallel_curve(offset)
   add_child(parallel_path)
   _parallel_paths.append(parallel_path)
   way.append(parallel_path)

func _generate_parallel_curve(offset : float) -> Curve2D:
   var new_curve : Curve2D = Curve2D.new()

   for i in range(curve.point_count):
   	var point_pos := curve.get_point_position(i)
   	var direction : Vector2

   	if i == 0:
   		direction = (curve.get_point_position(i + 1) - point_pos).normalized()
   	elif i == curve.point_count - 1:
   		direction = (point_pos - curve.get_point_position(i - 1)).normalized()
   	else:
   		var prev := (point_pos - curve.get_point_position(i - 1)).normalized()
   		var next := (curve.get_point_position(i + 1) - point_pos).normalized()
   		direction = (prev + next).normalized()

   	var normal = direction.rotated(PI / 2)
   	var offset_point = point_pos + normal * offset

   	new_curve.add_point(offset_point)

   	if curve.get_point_in(i) != Vector2.ZERO:
   		var in_control = curve.get_point_in(i)
   		new_curve.set_point_in(new_curve.get_point_count() - 1, _offset_control(offset, point_pos, curve.get_point_position(i), in_control))
   	if curve.get_point_out(i) != Vector2.ZERO:
   		var out_control = curve.get_point_out(i)
   		new_curve.set_point_out(new_curve.get_point_count() - 1, _offset_control(offset, point_pos, curve.get_point_position(i), out_control))

   return new_curve
func _offset_control(offset : float, point_pos : Vector2, curve_pos : Vector2, control_pos : Vector2) -> Vector2:
   var control_vector = control_pos - point_pos
   var new_control_pos = curve_pos + control_vector
   var direction = (curve_pos - point_pos).normalized()
   var normal = direction.rotated(PI / 2)
   return new_control_pos + normal * offset

func _clear_parallel_paths():
   for path in _parallel_paths:
   	if is_instance_valid(path):
   		path.queue_free()
   _parallel_paths.clear()

The screenshot shows how the script works.
I would greatly appreciate your help.

What is a parallel path/curve? Could you show an example of the input and output curve?

Make sure to format your code pastes between three ticks ```

1 Like

thanks

Rather than doing multiple paths like this, what I’d suggest is do a single path and then calculate the other “paths” as an offset from your path.

For something on the main path, you can get the position and use it raw. For something that’s one “path” to the left, you can get the position on the path, but also get a position just slightly forward of that point, and one slightly behind. Use the two flanking points to generate a vector (subtract behind from in_front), normalize it, rotate it 90 degrees, and scale it by the distance between tracks.

That gives you a single track with as many synthetic parallel tracks as you please. It will still potentially have hairy behaviour on the inside of tight corners, but that’s a level design problem.

It was very helpful, thank you! Could you explain how to eliminate the curve’s breaks?

@tool
extends Path2D
class_name PathWithOffsets

@export var offset_distance: float = 32.0
@export var num_offsets: int = 1
@export var step: float = 20.0

var offset_curves: Array[Curve2D] = []

func _ready() -> void:
	if Engine.is_editor_hint():
		_generate_offsets()

func _notification(what):
	if what != NOTIFICATION_ENTER_TREE:
		_generate_offsets()

func _generate_offsets() -> void:
	if not curve:
		return
	offset_curves.clear()
	
	var length := curve.get_baked_length()
	for side in [-1, 1]: 
		for i in range(1, num_offsets + 1):
			var offset_curve := Curve2D.new()
			var d = offset_distance * i * side
			var pos := 0.0
			while pos <= length:
				var p  = curve.sample_baked(pos)
				var p_f = curve.sample_baked(min(pos + step, length))
				var p_b = curve.sample_baked(max(pos - step, 0))

				var dir = p_f - p_b
				if dir.length() < 0.001:
					pos += step
					continue

				var tangent = dir.normalized()
				var normal = tangent.rotated(PI/2) * d

				if offset_curve.point_count > 0:
					var prev = offset_curve.get_point_position(offset_curve.point_count - 1)
					var blended = (prev + (p + normal)) * 0.5
					offset_curve.set_point_position(offset_curve.point_count - 1, blended)
					offset_curve.add_point(p + normal)
				else:
					offset_curve.add_point(p + normal)

				pos += step
			offset_curves.append(offset_curve)

	queue_redraw()

func _draw():
	if curve:
		draw_polyline(curve.get_baked_points(), Color.YELLOW, 2.0)
	for oc in offset_curves:
		draw_polyline(oc.get_baked_points(), Color.CYAN, 1.0)

The first thing I’d suggest is “don’t do sharp corners”. :slight_smile:

If you do want to have sharp corners, my suggestion would be to see if you can keep track of where the sharp corners are, and always send the synthetic paths to the outside of the corners rather than the inside. This does potentially mean the paths will cross though.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.