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.
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.
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.
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.
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.
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.
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.
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.