xiandie/scene/little_game/general/draggable_rigid.gd

240 lines
6.0 KiB
GDScript3
Raw Normal View History

2025-07-22 11:59:08 +00:00
@tool
# A draggable item in the game that can be picked up and displayed with an outline effect.
class_name DraggableRigid extends RigidBody2D
# pass self
signal picked(node: DraggableRigid)
signal dropped(node: DraggableRigid)
# 如果设置 replaced_area_monitor, 则鼠标事件由 replaced_area_monitor 替代监听
@export var replaced_area_monitor: Area2D
# z +1 when picked (-1 when dropped)
# @export var z_up_on_picked := true
@export var sprite_offset := Vector2.ZERO:
set(val):
sprite_offset = val
if is_node_ready():
sprite.offset = sprite_offset
@export var freezing := false:
set(val):
freezing = val
refresh_gravity_and_collision()
if freezing:
if holding:
_drop()
elif touching:
_on_mouse_exited()
@export var texture: Texture2D:
# The texture to display for the item
set(val):
texture = val
if is_node_ready():
sprite.texture = texture
@export var limit_rect := Rect2(Vector2.ZERO, Vector2(564, 316))
@onready var sprite: Sprite2D = $Sprite2D
# Whether the item is currently being held by the player
var holding := false:
set(val):
holding = val
refresh_gravity_and_collision()
var touching := false
# 静态变量优化:避免频繁的字符串比较
static var current_focusing_node: DraggableRigid = null
static var pending_enter_callables: Array[Callable] = []
# 缓存常量
const OUTLINE_THICKNESS := 1.0
const OUTLINE_TWEEN_DURATION := 0.2
const SHAKE_FPS := 15.0
const SHAKE_DURATION := 0.8
const SHAKE_DELTA := 12.0
const HUD_FADE_DURATION := 0.3
# 缓存变量
var _mouse_event_source: Node
var _outline_tween: Tween
var _shake_tween: Tween
var default_gravity_scale: float
var default_collision_layer: int
func _ready() -> void:
sprite.texture = texture
sprite.offset = sprite_offset
if Engine.is_editor_hint():
return
default_gravity_scale = gravity_scale
default_collision_layer = collision_layer
# 初始化隐藏白边
sprite.material.set_shader_parameter("thickness", 0.0)
# 缓存事件源
if replaced_area_monitor:
_mouse_event_source = replaced_area_monitor
else:
_mouse_event_source = self
_mouse_event_source.mouse_entered.connect(_on_mouse_entered)
_mouse_event_source.mouse_exited.connect(_on_mouse_exited)
func refresh_gravity_and_collision() -> void:
if holding or freezing:
gravity_scale = 0.0
default_collision_layer = 0
else:
gravity_scale = default_gravity_scale
collision_layer = default_collision_layer
func is_focused() -> bool:
return current_focusing_node == self
func _on_mouse_entered() -> bool:
touching = true
if freezing or not is_visible_in_tree():
return false
if holding or is_focused():
return true
# 尝试获得 current_focusing_item
if current_focusing_node:
if not pending_enter_callables.has(_on_mouse_entered):
pending_enter_callables.append(_on_mouse_entered)
return false
current_focusing_node = self
_toggle_outline(true)
return true
func _on_mouse_exited() -> void:
touching = false
pending_enter_callables.erase(_on_mouse_entered)
# freezing 不影响 mouse exited
if is_focused() and not holding:
current_focusing_node = null
# 优化:使用 while 替代 for 循环,找到第一个成功的就停止
while pending_enter_callables.size() > 0:
var c = pending_enter_callables.pop_front()
if c.call():
break
_toggle_outline(false)
func _notification(what: int) -> void:
match what:
NOTIFICATION_PREDELETE:
if holding:
_drop()
elif touching:
_on_mouse_exited()
func _unhandled_input(event: InputEvent) -> void:
if freezing or Engine.is_editor_hint() or not is_visible_in_tree():
return
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
if is_focused() and not holding:
get_viewport().set_input_as_handled()
_try_pick.call_deferred()
elif holding:
get_viewport().set_input_as_handled()
_drop()
elif holding and event is InputEventMouseMotion:
_update_pos_to_mouse()
func _update_pos_to_mouse() -> void:
var mouse_pos := get_global_mouse_position()
mouse_pos = mouse_pos.clamp(limit_rect.position, limit_rect.end)
update_position(mouse_pos)
func update_position(pos: Vector2) -> void:
PhysicsServer2D.body_set_state(
get_rid(), PhysicsServer2D.BODY_STATE_TRANSFORM, Transform2D.IDENTITY.translated(pos)
)
func _try_pick() -> void:
if not is_focused():
return
_toggle_outline(false)
holding = true
_update_pos_to_mouse()
picked.emit(self)
func _drop() -> void:
if touching:
_toggle_outline(true)
if holding:
holding = false
if not touching:
current_focusing_node = null
for c in pending_enter_callables:
if c.call():
break
dropped.emit(self)
func _toggle_outline(display: bool) -> void:
# 避免重复创建 tween
if _outline_tween and _outline_tween.is_running():
_outline_tween.kill()
_outline_tween = create_tween()
var target_thickness := OUTLINE_THICKNESS if display else 0.0
_outline_tween.tween_property(
sprite.material, "shader_parameter/thickness", target_thickness, OUTLINE_TWEEN_DURATION
)
func _exit_tree() -> void:
if touching:
_on_mouse_exited()
func force_hold() -> void:
if holding:
return
if not is_focused() and current_focusing_node:
current_focusing_node._drop()
_toggle_outline(false)
current_focusing_node = self
holding = true
_update_pos_to_mouse()
picked.emit(self)
func invalid_shake() -> void:
if _shake_tween and _shake_tween.is_running():
_shake_tween.kill()
# 抖动效果
_shake_tween = create_tween()
var count := int(SHAKE_DURATION * SHAKE_FPS)
var delta_t := 1.0 / SHAKE_FPS
sprite.modulate = Color.INDIAN_RED
# 预计算随机值,避免在循环中多次调用 randf_range
for i in count:
var decay := exp(-float(i))
var offset := Vector2(
randf_range(-SHAKE_DELTA, SHAKE_DELTA) * decay,
randf_range(-SHAKE_DELTA, SHAKE_DELTA) * decay
)
_shake_tween.tween_property(sprite, "offset", sprite_offset + offset, delta_t).set_trans(
Tween.TRANS_CUBIC
)
_shake_tween.tween_callback(_reset_after_shake)
func _reset_after_shake() -> void:
sprite.modulate = Color.WHITE
sprite.offset = sprite_offset