What is the recommended Node type for projectiles in 2D that bounce off other Nodes?

Godot Version

4.6.2

Question

There are some posts about the topic but I couldn’t find something conclusive.

I have a projectile class which currently extends Area2D, which is cool, but for my custom bounce logic here:

func bounce_off_wall() -> void:
	var normal_on_colliding_body = VectorUtil.calculate_normal_relative_to_direction(
		$RayCast2D, width
	)
	direction = direction.bounce(normal_on_colliding_body.rotated(randf_range(0, PI / 8)))

	var tween = create_tween()
	tween.tween_property(self, "speed", 0, 0.1).set_ease(Tween.EASE_OUT)
	tween.parallel().tween_property(self, "rotation", rotation + randf_range(-PI / 2, PI / 2), 0.1)
	tween.tween_property(self, "modulate:a", 0, 0.3)
	tween.tween_callback(queue_free)

	can_hurt_player = false
	can_hurt_non_player = false

	has_bounced_off_wall = true

It works fine most off the time, but from time to time I’m getting errors, that the normal is zero and bounce doesn’t work accordingly. And have a look at this beautiful workaround (which is only working most of the time):

## Use the center as a starting point for detecting the colliding body.
## If it doesn't match, search from the center outward.
static func calculate_normal_relative_to_direction(raycast: RayCast2D, width: float):
	var normal_from_center = raycast.get_collision_normal()
	if normal_from_center:
		return normal_from_center

	# The distance to move the ray each step (within the shape's boundaries).
	var step_size = min(5.0, width)

	var initial_position = raycast.position
	var initial_target_position = raycast.target_position
	var normal = Vector2.ZERO

	# to move the raycast to probe collisions away from the tip in the center
	var perpendicular_direction = (
		(raycast.target_position - raycast.position).orthogonal().normalized()
	)

	# to start probing from the center outwards (at least one step)
	var offsets = []
	for i in range(1, max(2, int(width / step_size))):
		offsets.append(i * step_size)
		offsets.append(-i * step_size)

	for offset in offsets:
		raycast.position = initial_position + perpendicular_direction * offset
		raycast.target_position = initial_target_position + perpendicular_direction * offset
		raycast.force_raycast_update()

		if raycast.is_colliding():
			normal = raycast.get_collision_normal()
			break  # Stop once we find a collision

	raycast.position = initial_position  # Reset position
	return normal

Is there a recommended Node type for cases like this - so there is no need for custom raycasting to find that normal.

Thanks a million!

You could make it a StaticBody2D and use move_and_collide() to handle movement and bouncing. Or if the projectile is small, you could just use raycasting for all collisions.

1 Like

No, don’t use Area2D or StaticBody2D. The first is for detection and can be used for slow moving projectiles, but not if you want them to interact with the environment. If you move an Area2D too fast, it will go through walls and not collide with them. StaticBody2D is used for things that do not move, like walls and floors.

While we are on the subject, AnimateableBody2D is for platforms like elevators, windmills, or conveyors. But that’s not what you want either.

You want a RigidBody2D. You give it an impulse, and then let it go. It has all these built-in physics properties you can tweak:

Then, just add the bounce amount you want with the slider. (You’ll have to add a new physics material from the dropdown.) Play around with the settings until it does what you expect.

No need to even have a _physics_process() function.

4 Likes

Thank you both for the quick answer!

I’m going to try the RigidBody2D and tell you how it went. I was hesitant, because I thought a RigidBody2D was just for letting physics do their thing without much custom control (e.g. somthing simple like fading out and freeing after the first bounce).

You can control it as much as you want, but try to use impulses if you can. Don’t update it every physics frame because it does that once it has an impulse.

1 Like

move_and_collide() was moved to PhysicsBody2D as a way to get rid of the old KinematicBody2D, so using StaticBody2D for moving objects is intended even if it’s mostly used for static objects.

1 Like

Perhaps you have intimate knowledge of that change that I don’t, so I can’t really speak to the reasoning behind the change.

However my understanding was that AnimateableBody2D was specifically added to 4.0 to affect other bodies in its path when moved, whereas StaticBody2D does not. Hence making it good for platforms.

So I guess you’re right, it can be used for projectiles that do not impact the movement of anything it hits. But I feel like the OP is still going to be writing a lot of physics code when hopefully RigidBody2D gives most, if not all of what’s needed for free.

2 Likes

Thanks again for the support!

Converting the Area2D to RigidBody2D was mostly painless (using freeze to keep it in place while animating its growth) but now I have a problem with the “first” collision:

  • When the projectile comes out of a wall it bounces off immediately.
  • When the projectile is thrown by the player it collides in a curious way as well.

Do I need to work with changing the collision mask? How would I know when to switch the collision mask back? With the Area2D I had the luxury to ignore e.g. the first wall collision.

A few options:

Multiple Layers

The Layer something is on determines where it can be seen. The Mask indicates what layers it can see.

So, If your wall is on Layer 1 and can see Layer2; and your Player is on Layer2 and can see Layer 1 they will collide correctly. Then a projectile shot from the Player which is on and can see Layer 1 won’t hit the player, and vice-versa.

Waiting

Default to no layers and masks for the object. Create a float timer variable, and decrement it by delta every frame. When it gets to zero, turn on the masks. That number will depend on how big your object is.

@export var spawn_timer: float = 0.25


func _process(delta: float) -> void:
	spawn_timer -= delta
	if spawn_timer <= 0:
		_set_collisions() #Create this function or do this inline
		set_process(false)

Spawn Point

Create a Marker2D outside the player/wall. Use that to spawn the projectile on. If it’s far enough away, it will never intersect with the player. It can also be used to get the trajectory vector for initial impulse.

1 Like