@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