Throw spear RigidBody2D and freeze it on impact

Godot Version

4.5.2

Question

Hello guys ! I m currently trying to develop a game which one the hero can throw and fight with a spear and i m on throwing mechanic now but i don’t know why but when the spear touch the ground or a wall it doesn’t be freeze correctly. The problem is that the rotation change before freeze i think but i don’t know how to solve this

Note : Spear is a RigidBody2D. I set the contact_monitor on true and the max_contacts_reported on 1. In addition i create a raycast to follow mouse position (mouse_dir) on the player and add a node that is the point where spear is launched (muzzle) as a child of the raycast

There is my code for the spear below :

extends RigidBody2D
@export var SPEED : float = 200

signal spear_impact(body:Node2D)

@onready var vector: Vector2 = Vector2(SPEED,0).rotated(rotation)
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	if not body_entered.is_connected(_on_body_entered):
		body_entered.connect(_on_body_entered)
	apply_central_impulse(vector)
	pass # Replace with function body.


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _physics_process(_delta: float) -> void:
	global_rotation = linear_velocity.angle()
	pass

func _on_body_entered(body:Node2D):
	if body.is_in_group("tilemap"):
		_freeze_object.call_deferred(body)
	pass

func _freeze_object(body:Node2D):
	set_physics_process(false)
	linear_velocity = Vector2.ZERO
	angular_velocity = 0
	freeze = true
	spear_impact.emit(body)
	pass
func _on_visible_on_screen_notifier_2d_screen_exited() -> void:
	print("instance free")
	queue_free.call_deferred()
	pass # Replace with function body.

And there is the code for instanciate the spear by the player :

func throw_spear():
	if mouse_dir.is_colliding() :
		pass
	else:
		print("try throw")
		var thrown_weapon = weapon.instantiate()

		thrown_weapon.position = muzzle.global_position

		thrown_weapon.look_at(get_global_mouse_position())

		get_parent().add_child(thrown_weapon)
	pass

A few general notes:

  1. pass literally means "do nothing and don’t throw an error bout it. It is a placeholder. Delete all of them.
  2. queue_free() is already a deferred call. Adding .call_deferred() on the end of it does nothing. Delete it.
  3. The fact that you’re checking if body_entered is connected before connecting it is weird. If indicates to me that you are doing something with the spear that is causing a bug and this is your fix. It could be a key to your actual bug that you are masking by doing this.
  4. You can simplify the second function:
func throw_spear():
	if not mouse_dir.is_colliding() :
		print("try throw")
		var thrown_weapon = weapon.instantiate()
		thrown_weapon.position = muzzle.global_position
		thrown_weapon.look_at(get_global_mouse_position())
		add_sibling(thrown_weapon)

As for your issue, I think the problem is that physics_process() is not stopping before the RigidBody2D collides and rebounds. My recommendation would be to try using an Area2D instead.

1 Like

thanks for your advise ! i think i found a solution for my problem by using _integrate_forces instead of _physics_process wrote in the documentation:

extends RigidBody2D
@export var SPEED : float = 200
var _last_rotation:float = 0.0
signal spear_impact(body:Node2D,orthogonal_impact:Vector2)



@onready var vector: Vector2 = Vector2(SPEED,0).rotated(rotation)
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	if not body_entered.is_connected(_on_body_entered):
		body_entered.connect(_on_body_entered)
	apply_central_impulse(vector)



func _integrate_forces(_state: PhysicsDirectBodyState2D) -> void:
	_last_rotation = global_rotation
	global_rotation = _state.linear_velocity.angle()

func _on_body_entered(body:Node2D):
	if body.is_in_group("tilemap"):
		_freeze_object.call_deferred(body)

func _freeze_object(body:Node2D):
	set_physics_process(false)
	global_rotation = _last_rotation
	linear_velocity = Vector2.ZERO
	angular_velocity = 0
	freeze = true
	handle_impact(body,Vector2.from_angle(_last_rotation).orthogonal().floor())

func handle_impact(_body:Node2D,_orthogonal_impact:Vector2):
	pass

func _on_visible_on_screen_notifier_2d_screen_exited() -> void:
	queue_free()

do you still think the area2D is a better idea ,or i can keep mine ?

If it works for you, go for it.

Ok thanks for your help :slight_smile: just for share my latest changes as the solution

extends RigidBody2D
@export var SPEED : float = 200
var _last_rotation:float = 0.0
@onready var wind_zone: Area2D = $wind_zone
@onready var wind_zone_shape: CollisionShape2D = $wind_zone/wind_zone_shape
var is_floor = false
var is_wall = false

@onready var vector: Vector2 = Vector2(SPEED,0).rotated(rotation)
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	#body_entered.connect(_on_body_entered)
	wind_zone.body_entered.connect(_on_wind_zone_entered)
	wind_zone.body_exited.connect(_on_wind_zone_exited)
	apply_central_impulse(vector)



func _integrate_forces(_state: PhysicsDirectBodyState2D) -> void:
	_last_rotation = global_rotation
	global_rotation = _state.linear_velocity.angle()
	if _state.get_contact_count() > 0:
		handle_impact(_state)


func handle_impact(_state:PhysicsDirectBodyState2D):
	if _state.get_contact_collider(0).is_valid():
		var normal = _state.get_contact_local_normal(0)
		var body = _state.get_contact_collider_object(0)
		is_floor = normal.dot(Vector2.UP) > 0.5
		is_wall = abs(normal.dot(Vector2.RIGHT)) > 0.5
		_freeze_object.call_deferred(body)

func _freeze_object(_body:Node2D):
	set_physics_process(false)
	global_rotation = _last_rotation
	linear_velocity = Vector2.ZERO
	angular_velocity = 0
	freeze = true

func _on_visible_on_screen_notifier_2d_screen_exited() -> void:
	queue_free()
1 Like