Determining the exact global position of a collision with Rigidbody2D body_shape_entered

Godot Version

4.2.2

Question

I’m currently detecting a collision correctly with my rigidbody2d signal body_shape_entered. I’m struggling though to figure out what method to use to determine the precise global_position of the collision point from the standard signal outputs, (body_rid:RID, body: Node, body_shape_index: int, local_shape_index: int)
From a usage perspective, I just want to instantiate a scene that produces a spark at the point of collision

I’m not really sure whether _body_shape_entered() is what you want to use for this use case. The only way I’ve found to get contact points (point of collision) is through the state in _integrate_forces().

Example:

func _integrate_forces(state):
    var contact_point = state.get_contact_collider_position(0)

While you can use this approach to place scenes at every contact point, you rarely want to do this; collisions often happen in rapid succession due to minor changes in the state of your physical object. Without countermeasures, you are going to spawn objects almost every physics update.

No countermeasures produces a shower of particles.
With countermeasures, the expected result is achieved

Left: Simple implementation – Right: Implementation with countermeasures.

This is not desirable.

Solution

In order to achieve the desired result, we have to somehow filter the contact points so only the most influential collisions are used in our system. One way to think about this is: whenever the body (e.g. RigidBody) is moving fast, occurring collisions are of use. This means comparing the velocity, at the point of contact, to an arbitrary threshold such that a limited range of collision points are used.

# Pseudo-code example
var threshold = 10
if velocity.length > threshold
    # Spawn particle

However, such a comparison is, by default, not possible. At the time of contact, the velocity has already changed in response; the velocity is no longer what is was and any comparison will be based on “false” data.

Built-in function for collision velocity (Docs):
state.get_contact_collider_velocity_at_position()

Long story short, I’ve implemented a system that uses the previous state of the object to compute the current velocity at the contact point (compute_velocity_at_point()), and spawn a particle instance.

It’s a little rough around the edges and the object’s center of mass is currently not taken into account – but I think it works well enough.

extends RigidBody2D

@export var particle_prefab: PackedScene
@export var effect_speed_threshold = 100.0

# State history
var transform_old: Transform2D
var velocity_old: Vector2
var angular_velocity_old: float
# NOTE: For extended use, it would be best to wrap this kind of data in a class

# Testing variables
var queue_reset: bool
var reset_transform: Transform2D

func _ready():
	reset_transform = global_transform

func _process(delta):
	if (Input.is_action_just_pressed("ui_accept")):
		queue_reset = true
		sleeping = false

func _integrate_forces(state):
	
	if (queue_reset):
		state.transform = reset_transform
		state.linear_velocity = Vector2.ZERO
		state.angular_velocity = 0
		queue_reset = false
	
	for i in range(state.get_contact_count()):
		# NOTE: state.get_contact_local_position() is in the local space of
		#		the body that is collided with. Therefore, it is not useful.
		var to_collision_point = state.get_contact_collider_position(i) - state.transform.origin
		var collision_velocity = compute_velocity_at_point(to_collision_point, state, transform_old, velocity_old, angular_velocity_old)
		
		print("Collision speed(%d): %s" % [i, collision_velocity.length()])
		
		if (collision_velocity.length() >= effect_speed_threshold):
			# Spawn, position and play the particle instance
			var p = particle_prefab.instantiate()
			get_tree().get_root().add_child(p)
			p.global_position = state.get_contact_collider_position(i)
			if (p is CPUParticles2D):
				p.restart()
				
	# Update the state-history variables
	transform_old = state.transform
	velocity_old = state.linear_velocity
	angular_velocity_old = state.angular_velocity

func compute_velocity_at_point(local_point: Vector2, state: PhysicsDirectBodyState2D, old_transform: Transform2D, old_velocity: Vector2, old_angular_velocity: float) -> Vector2:
	#====================================================================================
	# Compute point velocity based on the previous state (velocity, and angular velocity)
	# NOTE: The immediate state of the object is not useful for this use-case; the collision
	# 		forces have already been applied to the rigidbody.
	#====================================================================================
	
	## Compute the velocity resulting purely from the angular velocity
	### The difference in rotation between the two states (current and old state)
	var rot_diff = state.transform.get_rotation() - old_transform.get_rotation()
	### The vector that is "tangent" to the position (from the local point provided).
	var tangent = local_point.rotated(old_transform.get_rotation() + PI / 2.0).normalized()
	### The velocity at the point modulated by the angular_velocity and distance from the center.
	var velocity_rot = tangent.rotated(rot_diff) * old_angular_velocity * local_point.length()
	
	return old_velocity + velocity_rot


Hopefully, the code provided will help you solve your issue.

1 Like

Thanks so much for this detailed example! I will definitely use it for more complex collisions - out of interest, while I was waiting for some help I applied an ‘in-place’ shape2d cast at the point of collision - for my current use case they’re just enemy missiles hitting objects so it’s a one-time collision and destruct - which seems to work pretty well, although shapecasts are obviously quite expensive, but I figured as it’s not going anywhere, (it’s just a one-shot in-place forced update to collect collision info) it was a workable solution. Your approach will definitely help, thanks :pray::+1:

1 Like

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