Godot Version
v4.6.2.stable.official [71f334935]
Context
Hi
,
So I’ve been working on yet another side project where you play as a mage manipulating furniture to kill enemies. I’ve got very little experience with the Godot physics engine so this is kind of a learning project.
Question
Sometimes (seemingly randomly), while a piece of furniture is colliding with something, it suddenly gains a huge amount of angular velocity and teleports somewhere in the scene, usually off-screen (I assume this is because of high linear velocity). This is pretty rare, I had to record 6 minutes of gameplay to find an occurrence, but I think it’s a sign that I’m incorrectly using the physics system somehow. Here’s an example of the phenomenon:
Does anyone know what might be causing this to happen?
Edit: I took care to never set the RigidBody2D velocity directly and to instead use apply_central_force(), apply_torque(), and apply_impulse()
References
This is the scene of the vanishing object:
The CollisionPolygon2D:
And some of the RigidBody2D settings:
Full scene file
[gd_scene format=3 uid="uid://cadg3ixkgepqv"]
[ext_resource type="PackedScene" uid="uid://csywxag822rb5" path="res://furniture/furniture.tscn" id="1_3sguc"]
[ext_resource type="Texture2D" uid="uid://c26c6d2eyx86e" path="res://assets/sprites/furniture/table.png" id="2_a70le"]
[ext_resource type="Texture2D" uid="uid://ds2cntfkwgi2u" path="res://assets/sprites/furniture/table-tag.png" id="3_uqgoh"]
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_uqgoh"]
polygon = PackedVector2Array(-46, 33, -60, -27, 57, -27, 42, 33, 34, -16, -37, -16)
[node name="Table" unique_id=1119687450 node_paths=PackedStringArray("collision_players") instance=ExtResource("1_3sguc")]
collision_players = {
&"Furniture": NodePath("FurnitureCollision"),
&"Player": NodePath("PlayerCollision"),
&"TileMapLayer": NodePath("MapCollision")
}
killer_name = "Table"
[node name="Furniture" parent="Sprites" parent_id_path=PackedInt32Array(1077350344) index="0" unique_id=996704128]
texture = ExtResource("2_a70le")
[node name="Tag" parent="Sprites" parent_id_path=PackedInt32Array(1077350344) index="1" unique_id=212728429]
texture = ExtResource("3_uqgoh")
[node name="CollisionPolygon2D" parent="." index="1" unique_id=248987615]
polygon = PackedVector2Array(-60, -27, 57, -27, 42, 33, 34, -16, -37, -16, -46, 33)
[node name="LightOccluder2D" parent="." index="2" unique_id=746587377]
occluder = SubResource("OccluderPolygon2D_uqgoh")
[node name="VisibleOnScreenNotifier2D" parent="." index="3" unique_id=2022141694]
rect = Rect2(-63, -37, 126, 74)
The furniture script
class_name Furniture
extends RigidBody2D
signal collided(state: PhysicsDirectBodyState2D, contact_index: int)
@export_group("References")
@export var tag: Sprite2D
@export var visible_on_screen_notifier: VisibleOnScreenNotifier2D
@export var whoosh_player: AudioStreamPlayer2D
@export var collision_players: Dictionary[StringName, AudioStreamPlayer2D]
@export_group("")
@export_group("audio")
@export_range(0.0, 1.0, 0.0001) var whoosh_volume_multiplier: float
@export var whoosh_pitch_offset_multiplier: float
@export var collision_volume_multiplier: float
@export_group("")
@export var kill_velocity: float
@export var max_velocity: float
@export var killer_name: String
@export var actor: Creature
# override
func _ready() -> void:
whoosh_player.play()
func _process(_delta: float) -> void:
tag.modulate = actor.color if actor else Color.WHITE
whoosh_player.volume_linear = whoosh_volume_multiplier * linear_velocity.length()
whoosh_player.pitch_scale = max(1.0 + whoosh_pitch_offset_multiplier * abs(angular_velocity), 0)
func _integrate_forces(state: PhysicsDirectBodyState2D) -> void:
var velocity: Vector2 = state.linear_velocity
if velocity.length() > max_velocity:
state.linear_velocity = velocity.normalized() * max_velocity
for i in range(state.get_contact_count()):
if not state.get_contact_collider_object(i):
continue
collided.emit(state, i)
_collision(state, i)
var creature: Creature = state.get_contact_collider_object(i) as Creature
if not creature:
continue
if not _should_kill(state, i, creature):
continue
creature.kill(killer_name, velocity)
# other
func is_on_screen() -> bool:
return visible_on_screen_notifier.is_on_screen()
func _collision(state: PhysicsDirectBodyState2D, index: int) -> void:
var collider: Object = state.get_contact_collider_object(index)
var script := collider.get_script() as GDScript
var collider_type: StringName = script.get_global_name() if script else StringName(collider.get_class())
var player: AudioStreamPlayer2D = null
for type: StringName in collision_players.keys():
if collider_type != type:
continue
player = collision_players[type]
break
if not player:
return
var relative_velocity: Vector2 = state.get_contact_local_velocity_at_position(index) - state.get_contact_collider_velocity_at_position(index)
player.volume_linear = collision_volume_multiplier * relative_velocity.length()
player.play()
# - virtual
func _should_kill(state: PhysicsDirectBodyState2D, contact_index: int, victim: Creature) -> bool:
if victim == actor:
return false
var actual_kill_velocity: float = 2 * kill_velocity if victim.is_in_group("Player") else kill_velocity
if state.get_contact_local_velocity_at_position(contact_index).length() < 10.0:
return false
var relative_velocity: Vector2 = state.get_contact_local_velocity_at_position(contact_index) - state.get_contact_collider_velocity_at_position(contact_index)
return relative_velocity.dot(-state.get_contact_local_normal(contact_index)) >= actual_kill_velocity
And the player script
@tool
class_name Player
extends Creature
signal killed(killer_name: String)
enum Action {
NONE = 0,
GRAB,
PUSH,
}
@export_group("References")
@export var hand: Marker2D
@export var wand: Wand
@export var body: Sprite2D
@export var hat: Sprite2D
@export var walk_player: AudioStreamPlayer2D
@export var jump_player: AudioStreamPlayer2D
@export var fall_player: SFXPlayer
@export var land_player: AudioStreamPlayer2D
@export_group("")
@export_group("Stats")
@export var max_speed: float
@export var ground_acceleration: float
@export var ground_deceleration: float
@export var air_multiplier: float
@export var jump_strength: float
@export var action_angular_strength: float
@export var push_strength: float
@export var swing_strength: float
@export_group("")
@export_group("audio")
@export var walk_volume_multiplier: float
@export var fall_volume_multiplier: float
@export var fall_fade_out_duration: float
@export_group("")
@export var input_device := InputDevice.new()
@export var automatic_device_type: bool
@export var invincible: bool
## If false, the player can only control the furniture they controlled last.
@export var allow_furniture_change: bool = true
## If false, only furniture with a null actor can be controlled.
@export var allow_furniture_steal: bool = true
var _jump_cancelled: bool = false
var _dead: bool = false
var _current_action := Action.NONE
var _action_furniture: Furniture = null
var _last_controller_mouse_dir := Vector2.LEFT
var _face_right: bool = false:
get:
return _face_right
set(value):
_face_right = value
body.scale.x = -1 if _face_right else 1
wand.global_position = hand.global_position
var _landed: bool = false
# override
func _ready() -> void:
if Engine.is_editor_hint():
return
input_device.setup_actions()
func _process(_delta: float) -> void:
hat.modulate = color
func _physics_process(delta: float) -> void:
if Engine.is_editor_hint():
return
if not _dead:
_actions()
_move(delta)
if not is_on_floor():
velocity += get_gravity() * delta
if velocity.x > 0:
_face_right = true
elif velocity.x < 0:
_face_right = false
move_and_slide()
var on_floor: bool = is_on_floor()
walk_player.volume_linear = walk_volume_multiplier * abs(velocity.x) * int(on_floor)
fall_player.volume_linear = fall_volume_multiplier * Vector2(velocity.x, max(velocity.y, 0)).length()
if on_floor:
if not _landed:
fall_player.fade_out(fall_fade_out_duration)
land_player.play()
elif not fall_player.playing:
fall_player.fade_in(0.0)
_landed = on_floor
func _input(event: InputEvent) -> void:
if Engine.is_editor_hint():
return
if not automatic_device_type:
return
input_device.is_joypad = InputDevice.is_event_joypad(event)
func _exit_tree() -> void:
if input_device:
input_device.cleanup_actions()
# - custom
func kill(killer_name: String, force: Vector2) -> void:
if invincible:
return
_dead = true
velocity += force
killed.emit(killer_name)
func revive() -> void:
_dead = false
# other
func _actions() -> void:
if not _action_furniture:
_current_action = Action.NONE
var mouse_dir: Vector2 = _get_mouse_dir()
if _current_action == Action.PUSH:
mouse_dir = hand.global_position.direction_to(_action_furniture.global_position)
wand.rotation = mouse_dir.angle()
var furniture_filter := func(furniture: Node2D) -> bool:
if not furniture is Furniture:
return false
if not allow_furniture_steal and (furniture as Furniture).actor and (furniture as Furniture).actor != self:
return false
if not allow_furniture_change and _action_furniture and furniture != _action_furniture:
return false
return (furniture as Furniture).is_on_screen()
var target_furniture: Furniture = null
if _current_action == Action.NONE:
var all_furniture: Array[Node2D] = []
all_furniture.assign(get_tree().get_nodes_in_group("Furniture").filter(func(a: Node) -> bool: return is_instance_valid(a)))
target_furniture = AimAssist.pick_target(hand.global_position, all_furniture.filter(furniture_filter), mouse_dir)
var passive: bool = true
if _should_grab(target_furniture):
_current_action = Action.GRAB
passive = false
if _should_push(target_furniture):
_current_action = Action.PUSH
passive = false
if passive:
_current_action = Action.NONE
if not passive and target_furniture:
if _action_furniture:
_action_furniture.actor = null
_action_furniture = target_furniture
_action_furniture.actor = self
match _current_action:
Action.NONE:
wand.state = Wand.State.PASSIVE if target_furniture else Wand.State.DISABLED
Action.GRAB:
wand.state = Wand.State.GRAB
_grab(mouse_dir)
Action.PUSH:
wand.state = Wand.State.PUSH
_push(mouse_dir)
func _move(delta: float) -> void:
var dir: float
if _dead:
dir = 0.0
else:
dir = Input.get_axis(input_device.get_custom_action("move_left"), input_device.get_custom_action("move_right"));
var acceleration: float = ground_acceleration if sign(dir) == sign(velocity.x) else ground_deceleration
if not is_on_floor():
acceleration *= air_multiplier
var velocity_target: float = 0.0 if abs(dir) < 0.1 else dir * max_speed;
velocity.x = move_toward(velocity.x, velocity_target, acceleration * delta);
if is_on_floor() and Input.is_action_just_pressed(input_device.get_custom_action("jump")):
velocity.y = -jump_strength;
jump_player.play()
if velocity.y < 0.0:
if not _jump_cancelled and not Input.is_action_pressed(input_device.get_custom_action("jump")):
velocity.y /= 2.0;
_jump_cancelled = true
else:
_jump_cancelled = false
func _should_grab(target_furniture: Furniture) -> bool:
if not Input.is_action_pressed("grab"):
return false
if _current_action == Action.NONE and target_furniture and Input.is_action_pressed(input_device.get_custom_action("grab")):
return true
return _current_action == Action.GRAB
func _should_push(target_furniture: Furniture) -> bool:
if not Input.is_action_pressed("push"):
return false
if _current_action == Action.NONE and target_furniture and Input.is_action_pressed(input_device.get_custom_action("push")):
return true
return _current_action == Action.PUSH
func _grab(aim: Vector2) -> void:
_action_furniture.apply_torque(action_angular_strength)
var angle_to_furniture: float = hand.global_position.direction_to(_action_furniture.global_position).angle()
var ray_angle: float = aim.angle()
var swing_dir := Vector2.from_angle(angle_to_furniture).direction_to(Vector2.from_angle(ray_angle))
_action_furniture.apply_central_force(swing_dir * abs(angle_difference(ray_angle, angle_to_furniture)) * swing_strength)
func _push(aim: Vector2) -> void:
_action_furniture.apply_central_force(aim * push_strength)
_action_furniture.apply_torque(action_angular_strength)
func _get_mouse_dir() -> Vector2:
if input_device.is_joypad:
var dir: Vector2 = Input.get_vector(
input_device.get_custom_action("look_left"), input_device.get_custom_action("look_right"),
input_device.get_custom_action("look_up"), input_device.get_custom_action("look_down")
)
if dir == Vector2.ZERO:
return _last_controller_mouse_dir
_last_controller_mouse_dir = dir
return dir
return hand.global_position.direction_to(get_global_mouse_position())


