First of all, sorry for the basic question.

I have an icon regularly following a Path2d, this way:

# from the Path2D GDScript file:
func _process(delta:float) -> void:
	t += delta
	$Path2D/PathFollow2D.progress = t * 200.0

This was only a basic test to see how a sprite can follow a path, but the end goal is the following: by clicking a button, I want the icon to move from a point of the path to another one.

For example, I have A, B, C and D points: when I click the button, the icon must go from A to B. If I click again it must go from B to C (or B to D), etc.

So, the first thing I did was looking for a way to get my points: thanks to @xyz, I ended up making this function:

func get_point_positions() -> PackedVector2Array:
	
	var points = PackedVector2Array()
	var total_points = curve.point_count
	
	for i in range(total_points):
		points.append(curve.get_point_position(i))
	
	return points

Problem is now I don't know how to move my icon.

I found the lerpfunction, but it was not very useful (it looks like it can't move over a path). Then, there are the Tweens, but in Godot 4 they work differently, and I only found resources about previous Godot versions (the only Godot 4 resource I found, doesn't mention Path2D).

So, how could I do?

  • @Megalomaniak @xyz

    Since I found too complicated - of course for me - to create separate curves the way you suggested (I'm very new to Godot and, for example, I have no idea of how to move my Sprite from a curve to another one), I tried investigating my latest idea, that is, tweening the progress_ratio of my PathFollow2D.

    My latest question was: how could I get the progress_ratio of the path in the position where I have a point?

    The progress_ratio is basically a float representing the distance between the first and the last point, in the range 0.0 - 1.0.

    By searching the docs, I found the Path2D curve has a get_baked_points method: summarizing a lot, this method returns an array of all the points in the curve.

    This led me to this function:

    func get_progress_at_point(point_coords:Vector2) -> float:
    	
    	var baked_points = curve.get_baked_points()
    	
    	var total_pixel_length = 0.0
    	var point_index = baked_points.find(point_coords)
    
    	# add control for find==-1
    
    	for i in range(0, point_index):
    		
    		var p2 = baked_points[i+1]
    		var p1 = baked_points[i]
    		
    		# sqrt(x2-x1)^2+(y2-y1)^2 or replace with distance_to
    
    		var d = sqrt(pow(p2[0] - p1[0], 2) + pow(p2[1] - p1[1], 2))
    		total_pixel_length += d
    	
    	return total_pixel_length

    I can use the function this way to get the ratio:

    var dist = get_progress_at_point(point_coords)
    var baked_length = curve.get_baked_length()
    var ratio = dist/baked_length

    and finally pass the ratio to the Tween function

[UPDATE]

I found a way to do something.

It looks like (correct me if I'm wrong) you can't tween between Path2D points coordinates.

I found you can, however, tween the progress_ratio property of the Pathfollow2D, this way:

var tween = get_tree().create_tween()
tween.tween_property($"../Path2D/PathFollow2D", "progress_ratio", 0.47, 2.0)

Problem is that at 0.47 there is not a point: 0.47 is just something I hardcoded for testing purposes.

If I hardcode any progress_ratio value of any point, by simply changing a little the appearence of my path I would break my code.

How could I get the progress_ratio of the path in the position where I have a point?

Megalomaniak This is generic, my situation is related to my Path2D curves. Or in your suggestion there's something I don't get. :-)

    Catapult You made it sound like lerps don't work at all, but they should. Still it's possible that lerp might not be the answer for what you want to achieve.

    Personally I suspect your best or perhaps rather, easiest option is to create multiple paths, each one would act as a segment of travel. Pressing the button the node would travel along the next path in queue/order.

    Split the segments of your one big curve into separate curves, each containing only one segment. Then do the path follow thing consecutively on those one-segment curves.

    @Megalomaniak @xyz

    Since I found too complicated - of course for me - to create separate curves the way you suggested (I'm very new to Godot and, for example, I have no idea of how to move my Sprite from a curve to another one), I tried investigating my latest idea, that is, tweening the progress_ratio of my PathFollow2D.

    My latest question was: how could I get the progress_ratio of the path in the position where I have a point?

    The progress_ratio is basically a float representing the distance between the first and the last point, in the range 0.0 - 1.0.

    By searching the docs, I found the Path2D curve has a get_baked_points method: summarizing a lot, this method returns an array of all the points in the curve.

    This led me to this function:

    func get_progress_at_point(point_coords:Vector2) -> float:
    	
    	var baked_points = curve.get_baked_points()
    	
    	var total_pixel_length = 0.0
    	var point_index = baked_points.find(point_coords)
    
    	# add control for find==-1
    
    	for i in range(0, point_index):
    		
    		var p2 = baked_points[i+1]
    		var p1 = baked_points[i]
    		
    		# sqrt(x2-x1)^2+(y2-y1)^2 or replace with distance_to
    
    		var d = sqrt(pow(p2[0] - p1[0], 2) + pow(p2[1] - p1[1], 2))
    		total_pixel_length += d
    	
    	return total_pixel_length

    I can use the function this way to get the ratio:

    var dist = get_progress_at_point(point_coords)
    var baked_length = curve.get_baked_length()
    var ratio = dist/baked_length

    and finally pass the ratio to the Tween function

    • xyz replied to this.

      Catapult Have you tested this? Looks like it may work but beware because this approach could introduce floating point precision bugs. You're searching for the exact point coordinates by value. For some numbers it can result in a miss even if point coordinates seemingly match, due to nature of floating point roundoff. It's generally not a good idea to compare float number using == operator.
      So you should test this excessively before using it in published code.

      Btw, splitting curves is pretty straightforward:

      func split_curve(curve: Curve2D):
      	var out_sub_curves = []
      	for i in curve.point_count-1:
      		var sub_curve = Curve2D.new()
      		sub_curve.add_point(curve.get_point_position(i), curve.get_point_in(i), curve.get_point_out(i) )
      		sub_curve.add_point(curve.get_point_position(i+1), curve.get_point_in(i+1), curve.get_point_out(i+1) )
      		out_sub_curves.push_back(sub_curve)
      	return out_sub_curves

        xyz I haven't found any problems with the code (yet).

        Not sure I completely understood what you say, maybe I should try to explain what I did (for example, I did no explicit == comparison). I don't understand this "seemlingly match": I don't choose randomly the main points coordinates, they come directly from Godot. The same for the baked array. So, I pass the function a point coming from get_point_position and I'm quite sure the point is inside the baked array (and this lets me find its index, etc.). For sure the function lacks some code for when the coordinate is not found, anyway.

        So, my situation was:

        • I had the (main) points coordinates as vectors and I needed to convert the vectors to "the progress_ratio at these points" (a float);

        • I found get_baked_length() that returns the entire length of the path (maybe doing at a low level the same thing I do, since I get about the same length by running my code. In the first test, the length returned by get_baked_length was 2453.138671875, while my code returned 2453.13806445297);

        • I found the curve is represented internally by an array of cached vectors (you could make the vector even more dense, by adding more points, but I decided not to do it);

        • I applied the classical distance formula to get the distance to any point I choose, and I got the distance from the start to my custom point;

        • since I knew the total length of the curve, it was easy to derive the progress_ratio;

        I must study your code (because of my incompetence, I can't understand it on the fly) :-)

          Catapult PackedVector2Array::find() likely uses == to check if values match. Floating point precision errors are nasty, chaotic and hard to reproduce after the fact. For example, everything could seemingly work fine when your coordinates are in the order of magnitude close to 1.0. Then after three months of development you decide to expand your maps so now coordinate ranges get much larger. Due to nature of floating point representation, you'll lose precision, comparison may start to fail sporadically and you end up going mad wondering how code that worked flawlessly for three months started exhibiting erratic behavior out of the blue.

            xyz A sample from my current coordinates:

            [(0, 0), (3.926271, -0.012693), (7.852598, -0.00889), (11.77886, 0.011278), (15.70493, 0.047683), (19.6307, 0.100194), (23.55603, 0.168685), (27.48082, 0.253023), (31.40493, 0.353082), (35.32825, 0.468731), (39.25066, 0.599841), (43.17204, 0.746282), (47.09224, 0.907927), (51.01118, 1.084645), (54.92871, 1.276307), (58.84473, 1.482784), (62.75911, 1.703947), (66.67172, 1.939666), (70.58245, 2.189812), (74.49117, 2.454257), (78.39777, 2.73287), (82.30212, 3.025522), (86.2041, 3.332085), (90.1036, 3.65243), (94.00047, 3.986425), (97.89462, 4.333943), (101.7859, 4.694855), (105.6742, 5.069031), (109.5595, 5.456342), (113.4415, 5.856658), (117.3201, 6.269851), (121.1954, 6.69579), (125.067, 7.134348), (128.9349, 7.585395), (132.799, 8.0488), (136.6592, 8.524435), (140.5153, 9.012174), (144.3672, 9.511883), (148.2148, 10.02343), (152.0579, 10.5467), (155.8966, 11.08155), (159.7305, 11.62785), (163.5597, 12.18548), (167.3839, 12.7543), (171.2031, 13.3342), (175.0172, 13.92503), (178.8259, 14.52666), (182.6293, 15.13898), (186.4272, 15.76185), (190.2194, 16.39514), ...

            And I still don't get the problem: what should change the coordinates? When I change the path, the coordinates change accordingly and are cached once again. And, if Godot returns my path points AND caches the points, along with many others, in a PackedVector2Array, how could a point be missed? The points are always part of the array set representing the curve.

            If you are suggesting the representation of the points could change anytime I start Godot, I'll look into this and see if the backed array and my points coordinates suddenly change. :-)

            get_baked_points() will return tessellated points. Tessellation involves calculation during which some precision could be lost, resulting in your tessellated (baked) points that should coincide with original curve points to end up slightly off. It's an extremely small precision error that could happen, depending on actual numbers and calculations involved. But when it happens it can cause the == operator to return false when you're expecting it to return true. Consider this example:

            var a = 1.0 / 3.0
            var v = Vector2()
            v.x = a
            print(v.x == a)

            You'd expect this to print true, right? Well, try to run it and see what happens.

              xyz

              But when it happens it can cause the == operator to return false when you're expecting it to return true

              Sorry, but, if the problem lies in the engine floats management, this can be easily solved by simply manually putting the (main) points (target of my tweens) at integer coords. For some reason, even if Vector2 expects two floats, you can pass in integers (in fact typeof returns 3, TYPE_REAL):

              var a = 45
              var v = Vector2()
              v.x = a
              print(v)
              print(v.x == a)  # true
              print(typeof(v.x))
              • xyz replied to this.

                Megalomaniak

                I know this problem, but from the beginning I have considered and accepted the possibility that there may be some inaccuracies when summing up floats. I am not even launching the Ariane 5 rocket :-)

                However, I was really surprised when I found Godot has a method that returns the length of a curve and doesn't have one to calculate the distance between two points in that curve. I'd like to know how they calculate the total length and if it always returns the same float.

                xyz

                func split_curve(curve: Curve2D):
                var out_sub_curves = []
                for i in curve.point_count-1:
                var sub_curve = Curve2D.new()
                sub_curve.add_point(curve.get_point_position(i), curve.get_point_in(i), curve.get_point_out(i) )
                sub_curve.add_point(curve.get_point_position(i+1), curve.get_point_in(i+1), curve.get_point_out(i+1) )
                out_sub_curves.push_back(sub_curve)
                return out_sub_curves

                Ok, so basically, correct me if I'm wrong, you create a new curve for every point in the main curve and you add two points to it (begin/end), then you return an array of subcurves.

                But this is a part of your design, how the sprite is supposed to move? Is there any sample I can look at?

                • xyz replied to this.

                  Catapult You're missing the point. Using integers carries exact same risks. It's not a matter of "engine's float management", but rather a side effect of floating point representation itself, and it happens across platforms and languages. The precision we're talking about here is negligible as far as most calculation results are concerned. You can safely lunch Ariane 5 with that kind of precision margins. However when comparison via operator == is involved, the precision mishaps can break your code. It's sort of a rule of thumb that if you want to produce bulletproof code you should never ever use == on floats, or rely on code that you suspect might use == on floats.

                  In Godot, all vector/matrix data components are 32 bit floats. Even if you assign integers to it, they'll be converted to floats. Doing calculations with 32 bit floats and then using == to compare results is always risky. Precisely because it appears to work without any problems almost all the time, until a case comes that it doesn't, producing hard to detect bugs.

                  There's a reason why all Godot's vector types have is_equal_approx() method. This method should always be used instead of == when comparing float data. Now PackedVector2Array::find() probably uses it internally but we don't have a guarantee on this. It could be implementation dependent. If it uses operator ==, the find() will fail in some cases where you think it shouldn't.

                    Catapult But this is a part of your design, how the sprite is supposed to move? Is there any sample I can look at?

                    It's simple. On each mouse click, just assign the next sub-curve to Path2D::curve and launch a tween that animates PathFollow2D::progress_ratio or PathFollow2D::progress

                      xyz

                      If it uses operator ==, the find() will fail in some cases where you think it shouldn't.

                      But I'm the engine user, not the engine developer: if the engine has a method find that works with Vector2 collections, I should use it safely, without caring about its implementation. Otherwise, in my humble opinion, the documentation should warn not to use this method at all, or to use it at your own risk.

                      • xyz replied to this.