Trouble getting accurate collision detection using shape casts

Godot Version

4.5

Question

I am having difficulties getting the trajectory to draw correctly when it collides with an object, especially at the edges. The trajectory (yellow line) is the path that a ball will follow after being launched from a cannon.

In the image below the trajectory shows that the ball will pass the object as if it isn’t going to collide with it. However, when testing the ball will hit this object. This is the case when aiming near the edges of most objects.

extends Node2D
@export var ball_pool: NodePath
@export var trajectory_line: Line2D
@export var launch_speed: float = 900.0
@export var trajectory_points: int = 50
@export var use_high_arc: bool = false
@export var ball_radius: float = 6.0

var pool: Node
var remaining_balls: int = 999
var gravity: float
var space_state: PhysicsDirectSpaceState2D

func _ready():
	pool = get_node(ball_pool)
	gravity = ProjectSettings.get_setting("physics/2d/default_gravity")

func _process(_delta):
	space_state = get_world_2d().direct_space_state
	update_trajectory(get_global_mouse_position())

func _input(event):
	if event.is_action_pressed("shoot"):
		shoot_ball(get_global_mouse_position())

func get_launch_angles(start: Vector2, target: Vector2, speed: float) -> Array:
	var dx = target.x - start.x
	var dy = start.y - target.y
   
	var v2 = speed * speed
	var g = gravity
	if abs(dx) < 1e-6:
		if dy > v2 / (2 * g):
			return []
		return [3.0 * PI / 2.0]
   
	var disc = v2 * v2 - g * (g * dx * dx + 2.0 * dy * v2)
	if disc < 0:
		return []
   
	var sqrt_d = sqrt(disc)
	var low = atan((v2 - sqrt_d) / (g * dx))
	var high = atan((v2 + sqrt_d) / (g * dx))
	if dx < 0:
		low += PI
		high += PI
   
	return [low, high]

func angle_to_velocity(angle: float, speed: float) -> Vector2:
	return Vector2(cos(angle) * speed, -sin(angle) * speed)

func update_trajectory(target_pos: Vector2):
	trajectory_line.clear_points()
	var angles = get_launch_angles(global_position, target_pos, launch_speed)
	if angles.is_empty():
		return
   
	var angle = angles[1] if use_high_arc else angles[0]
	var velocity = angle_to_velocity(angle, launch_speed)
	var pos = global_position
	var vel = velocity
	var dt = 0.016
	
	var shape = CircleShape2D.new()
	shape.radius = ball_radius
	
	for i in range(trajectory_points):
		trajectory_line.add_point(pos - global_position)
		
		var next_vel = vel + Vector2(0, gravity * dt)
		var next_pos = pos + vel * dt

		var query = PhysicsShapeQueryParameters2D.new()
		query.shape = shape
		query.transform = Transform2D(0, pos)
		query.motion = next_pos - pos
		query.margin = 0.001
		
		var results = space_state.cast_motion(query)
		
		if results.size() > 0 and results[0] < 1.0:
			var collision_pos = pos + (next_pos - pos) * results[0]
			trajectory_line.add_point(collision_pos - global_position)
			break
		
		vel = next_vel
		pos = next_pos
		
		if pos.y > get_viewport_rect().size.y + 100:
			break

func shoot_ball(target_pos: Vector2):
	if remaining_balls <= 0:
		return
	var angles = get_launch_angles(global_position, target_pos, launch_speed)
	if angles.is_empty():
		return
   
	var ball = pool.get_ball()
	if ball:
		ball.global_position = global_position
		var angle = angles[1] if use_high_arc else angles[0]
		ball.linear_velocity = angle_to_velocity(angle, launch_speed)
		remaining_balls -= 1

What exactly are you trying to accomplish? Is there a reason you don’t want to use a RigidBody2D and let one of the physics engines handle this?

I don’t know how the physics engine is handling this (assuming the balls are rigidbodies), but theoretically it is more precise to use the average of pre-acceleration and post-acceleration velocity for constant accelerations like gravity:

	# in update_trajectory()

		var next_vel = vel + Vector2(0, gravity * dt)
		var next_pos = pos + (vel + next_vel) / 2.0 * dt

(Not sure if the internal physics engine does the same though.)

Also, have you made sure there are no other effects (like linear damping) affecting the ball?
Have you tried to print the trajectory points and ball positions to compare them? (You should probably change dt to be exactly 1.0 / physics_ticks_per_second for that.)

:man_facepalming: I forgot to set linear dampening to 0. I also implemented your other suggestions. Thank you for the help!

1 Like

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