draw preview path before lunch a ball from A to Mouse position

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By newold

Hey. How i can do this?

i want draw a line with first point in A, then calculate a line that passes through the position of the mouse and describe the entire route including bounces against the obstacles as in the image. The line has a fixed size, but I can’t figure out how to do this.

Any help?

:bust_in_silhouette: Reply From: gmaps

Since you are already using physics, just use Raycast query when the mouse moves. Get the vector pointing from A to mouse loctaion (position of A - mouse location). multiply it by some large value, e.g. length of diagonal of the field and raycast a point from A to that far point. The result will be a collision with a normal. You mirror (EDITED) your vector against the normal and get your next raycasting direction. YOu use the collision location the same way you used A. Multipy the normal so you get a very far point and raycast from current collision location to that far point. You get another collision location and it’s normal, and you repeat the steps until you’ve reached the max number of steps, or exit condition happens (e.g. collision with the bottom line). Prettiest way would be to implement it as a recursion.

Pseudocode would look like this:

0. raycast_count = 0
1.  start point = A, end_point = (Mouse_loc - A) * 100
2. raycast from A to end_point, raycast_count += 1, Draw rectangle from A to raycast.result.position
3. If raycast.result is bottom line, finish
4. Else Go to number 2, start_point = raycast.result.location,  end_point = raycast.result.normal.rotate(angle_to_normal) * 100 + raycast.result.location

I try it, but i get wrong results:

Black line in the picture is drawn with the points obtained with a raycast but I expected to get the green line.

I use this code to get that points:

func get_preview_path(a, b):
# Node $Player call this method in his parent
        # a = start_point
        # b = end_point (current mouse position)
        a += $Player.position
        var points = []
        var start_point = a
        var end_point = b * diagonal # diagonal = diagonal of screen
        var l = 0 # counter for steps for the loop
        var last_collider = null

while l < diagonal: # length of diagonal of screen
points.append(start_point)
raycast.clear_exceptions()
if last_collider != null: raycast.add_exception(last_collider)
raycast.position = start_point
raycast.cast_to = end_point
raycast.force_raycast_update()
if raycast.is_colliding():
var p = raycast.get_collision_point()
var n = (raycast.get_collision_normal() * diagonal + p)
start_point = p
end_point = n
 l += 30
last_collider = raycast.get_collider()
else:
break

newold | 2019-10-01 18:14

I wrote that you should use Raycast2D, but what I really meant was raycast 2d query, sorry about that. Although both should work, Raycast2D is usually used when we want to do only one raycast which is fixed to it’s parent’s location/rotation, character aim for example. In your case, it could be used only to retrieve the first point. But since you need unknown number of raycasts (Technically it could be infinite) it’s better to use Raycast querying.

So, you don’t need Raycast2D, you just need to initialize space state like this:

var space_state = get_world_2d().direct_space_state

And then check the collisions with this function

var result = space_state.intersect_ray(point_a, point_b)

It should work like a charm, If it doesn’t leave another comment. I edited the answer to include link to raycast query instead of Raycast2D.

gmaps | 2019-10-02 10:33

I can’t make this work.

i change my code at this:

func get_preview_path(a, b):
a += $Player.position
var points = []
var start_point = a
var end_point = b * diagonal
var l = 0
var last_collider = null


# use World2D
var space_state = get_world_2d().direct_space_state	
var result

while l < diagonal:
	points.append(start_point)
	result = space_state.intersect_ray(start_point, end_point, [last_collider])
	if result:
		print([result.position, result.normal])
		start_point = result.position
		end_point = result.normal * diagonal + start_point
		last_collider = result.collider
	else:
		break
	l += 12


return (points)

i only can get the first point as before using the raycast, rest of points are wrong

newold | 2019-10-02 14:35

Aah, I see, you should use the angle between previous vector and normal to mirror the vector across the normal and get next direction. Something like this:

    var angle_to_normal = (start_point - end_point).angle_to(result.normal)
    start_point = result.position
    end_point = result.normal.rotate(angle_to_normal) * diagonal + start_point

It should give you a similar result, you want. But the normals seem odd in your case… Can you print them and comment them here? I think they should be perpendicular to the wall, but they’re not. In documentation it says:

normal: The object’s surface normal at the intersection point.

And the surface normal of a wall is always perpendicular.

I suggest you try mirroring it, if it’s not the same as ball bounce, post another question with “collision normals don’t reflect surface normal”.

I also see another issue you will be dealing with… One raycast would not be enough, as it may go beside a wall, but the ball would hit it because it’s wider, so the ball would bounce, but ray would continue. One option would be to use more than one raycast (3-5, or so that the distance between the raycast is bigger than the smallest object), and check which one is the shortest (and use it’s normal). Alternative is to try actually simulating another ball, with higher speed, it should be simpler than raycasting, and could produce better results, but… everytime the mouse moves, you shoot another ball, high speed, just to get the angles and draw it’s position. The issue with this approach is that with very high speeds, it may happen that collisions are not properly detected, because the ball could be in las vegas instead of colliding with your wall.

There also exists the Shape query 2D. First set all the Shape query parameters, so motion and shape (your ball and it’s starting direction). And use cast_motion function. It seems to me that it doesn’t return any normals, nor does it simulate anything. But you might need it, because with raycasts, it’s difficult to determine collisions with edges (maybe by recognizing when neighbouring normals point in different directions).

gmaps | 2019-10-03 07:57

Thanks for your help. It works at last:

I only need this code to make it work

var angle_to_normal = (start_point - end_point).angle_to(result.normal)
start_point = result.position
end_point = result.normal.rotate(angle_to_normal) * diagonal + start_point

This is the final code:

func get_preview_path(a, b):
a += $Player.position
var points = []
var start_point = a
var end_point = b * diagonal
var l = 0
var last_collider = null

var space_state = get_world_2d().direct_space_state	
var result
var normal
var angle_to_normal

while l < diagonal:
	points.append(start_point)
	result = space_state.intersect_ray(start_point, end_point, [last_collider])
	if result:
		normal = result.normal
		angle_to_normal = (start_point - end_point).angle_to(normal)
		start_point = result.position
		end_point = normal.rotated(angle_to_normal) * diagonal + start_point
		last_collider = result.collider
		if last_collider.is_in_group("wall_bottom"):
			break
	else:
		break
	l += 30


return (points)

newold | 2019-10-03 08:21