@tool # A draggable item in the game that can be picked up and displayed with an outline effect. class_name Draggable2D extends Area2D # pass self signal picked(node: Draggable2D) signal dropped(node: Draggable2D) # 表现为按钮,即点击发送 picked 信号 @export var act_as_button := false # z +1 when picked (-1 when dropped) # @export var z_up_on_picked := true @export var item_name = "" @export var sprite_offset := Vector2(0, 0): set(val): sprite_offset = val if is_node_ready(): sprite.offset = sprite_offset @export var freezing := false: set(val): freezing = val 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): if holding != val: holding = val var touching := false # 静态变量优化:使用对象引用而非字符串 static var current_focusing_node: Draggable2D = null static var pending_enter_callables: Array[Callable] = [] # 缓存常量 const OUTLINE_THICKNESS := 1.0 const OUTLINE_TWEEN_DURATION := 0.2 const BUTTON_ALPHA_DURATION := 0.15 # 缓存变量 var _outline_tween: Tween var _button_tween: Tween func _ready() -> void: sprite.texture = texture sprite.offset = sprite_offset if Engine.is_editor_hint(): return # 初始化隐藏白边 sprite.material.set_shader_parameter("thickness", 0.0) # 安全检查 if has_signal("mouse_entered"): mouse_entered.connect(_on_mouse_entered) mouse_exited.connect(_on_mouse_exited) else: printerr("Draggable2D: mouse_entered or mouse_exited signal not found.") 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_node 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 pending_enter_callables.size() > 0: var c = pending_enter_callables.pop_front() if c.call(): break _toggle_outline(false) func _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: # update global position var target_pos = get_global_mouse_position() global_position.x = clamp(target_pos.x, limit_rect.position.x, limit_rect.end.x) global_position.y = clamp(target_pos.y, limit_rect.position.y, limit_rect.end.y) func _try_pick() -> void: if act_as_button: # 作为按钮,发送 picked 信号 picked.emit(self) if _button_tween and _button_tween.is_running(): _button_tween.kill() _button_tween = create_tween() _button_tween.tween_property(sprite.material, "shader_parameter/alpha_ratio", 1.0, BUTTON_ALPHA_DURATION) return if not is_focused(): return # reset rotation rotation = 0 _toggle_outline(false) holding = true # z_index += 1 picked.emit(self) _update_pos_to_mouse() func _drop() -> void: if touching: _toggle_outline(true) if holding: holding = false if not touching: current_focusing_node = null # 清空数组前创建副本,避免在迭代时修改数组 var callables_copy = pending_enter_callables.duplicate() for c in callables_copy: if c.call(): break # z_index -= 1 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() # 清理静态引用,避免内存泄漏 if current_focusing_node == self: current_focusing_node = null 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 picked.emit(self)