@tool class_name Sign extends Control signal interacted signal cancel @export var enabled := true: set(val): if enabled == val: return enabled = val if enabled and player_touching and not activated: activate(null) if not enabled and activated: disactivate(null) player_touching = true _check_sign_display() @export var display_sign := true: set(val): display_sign = val _check_sign_display() @export var draw_shadow := false @onready var texture_container = %TextureContainer as Container @onready var sprite2d = %Sprite2D as Sprite2D # var shadow_texture = preload("res://asset/art/ui/action_mark/探索空心ui.png") as Texture2D # 同时只能有一个物品被激活交互态,其他物品进入等待队列 static var occupied: NodePath static var _pending_activate_sign := [] as Array[NodePath] # 使用互斥锁保证线程安全。在操作 occupied 或 _pending_activate_sign 时需要先锁定,操作完成后解锁 static var mutex = Mutex.new() var player_touching := false var activated = false: set(val): activated = val # queue_redraw() # var sprite2d = Sprite2D.new() var base_scale = Vector2.ONE func _ready() -> void: if Engine.is_editor_hint(): return base_scale = scale # layer = GlobalConfig.CANVAS_LAYER_FG # var point_light = get_node_or_null("../PointLight2D") # if point_light: # point_light.energy = 0.0 var area2d = get_node_or_null("../Area2D") if area2d: area2d.body_entered.connect(activate) area2d.body_exited.connect(disactivate) _check_sign_display() visibility_changed.connect(_on_visibility_changed) func _on_visibility_changed() -> void: if is_visible_in_tree(): if player_touching and not activated and enabled: activate(null) else: if activated: disactivate(null) player_touching = true func _check_sign_display(): if not enabled or not display_sign or not activated: modulate.a = 0.0 else: modulate.a = 1.0 func activate(_body: Node2D) -> bool: # point_light.energy = 1.0 if not is_node_ready(): return false var path := get_path() mutex.lock() player_touching = true if not enabled or not is_visible_in_tree(): mutex.unlock() return false if occupied and occupied != path: if not _pending_activate_sign.has(path): _pending_activate_sign.append(path) mutex.unlock() return false else: occupied = path activated = true mutex.unlock() if activated and display_sign: var tween = create_tween() tween.tween_property(self, "modulate:a", 1.0, 0.2) var p_tween = tween.parallel() p_tween.tween_property(self, "scale", base_scale * Vector2(1.2, 1.2), 0.3) p_tween.tween_property(self, "scale", base_scale, 0.1) return true # if activated: # focus_mode = FOCUS_ALL # grab_focus() # 绘制 prop 图标 # func _draw() -> void: # if draw_shadow and sprite2d.texture: # var texture_x = sprite2d.texture.get_size().x # # var radius = max(36.0, min(texture_x * 0.5, 48.0)) # var radius = 46.0 # var rect = Rect2(-radius, -radius, radius * 2, radius * 2) # draw_texture_rect(shadow_texture, rect, false) # # 比 2.0 略小,选择使用 1.6 # var texture_scale = min(radius * 1.6 / texture_x, 1.1) # sprite2d.scale = Vector2(texture_scale, texture_scale) # # var texture_radius = shadow_texture.x * 0.5 # # draw_circle(Vector2.ZERO, radius, Color(0.8, 0.8, 0.8, 0.8), false, 6.0, true) # # radius -= 6.0 # # if radius > 0: # # draw_circle(Vector2.ZERO, radius, Color(0.3, 0.3, 0.3, 0.3)) # else: # sprite2d.scale = Vector2.ONE func disactivate(_body: Node2D) -> void: # release_focus()d mutex.lock() player_touching = false if activated: activated = false occupied = NodePath("") # 转移 active 状态给下一个节点 while _pending_activate_sign.size() > 0: var path = _pending_activate_sign.pop_front() var _sign = get_node_or_null(path) as Sign if _sign and _sign.player_touching and _sign.activate(null): break else: # make sure the sign is not in the pending list _pending_activate_sign.erase(get_path()) # double check. because the sign may be activated by other body if activated: disactivate(_body) mutex.unlock() # point_light.energy = 0.0 if modulate.a: create_tween().tween_property(self, "modulate:a", 0.0, 0.2) func _unhandled_input(event: InputEvent) -> void: if Engine.is_editor_hint() or not enabled: return if activated: if event.is_action_pressed("interact"): if GlobalConfig.DEBUG: print("Sign interacted:", get_parent().name) interacted.emit() if is_inside_tree(): # grab focus 放在 emit 后面,避免在 emit 时 prop hud 失去 focus # 传送时会导致 is_inside_tree 为 false,此时也无需与 prop hud 抢占 focus focus_mode = FOCUS_ALL grab_focus() _set_handled() elif event.is_action_pressed("cancel"): if GlobalConfig.DEBUG: print("Sign cancel:", get_parent().name) cancel.emit() _set_handled() release_focus() func _set_handled(): var viewport = get_viewport() if viewport: viewport.set_input_as_handled() var shake_tween # 使用无效道具,抖动提示 func invalid_shake(): if shake_tween: shake_tween.kill() # 抖动效果,逐渐减弱 shake_tween = create_tween() var fps := 12.0 var duration := 0.7 var delta := 4.0 var origin_pos = Vector2.ZERO var count = int(duration * fps) var delta_t = 1.0 / fps sprite2d.modulate = Color(1.0, 0.6, 0.6, 1.0) for i in range(count): var offset = Vector2(randf_range(-delta, delta), randf_range(-delta, delta)) * exp(-i) shake_tween.tween_property(sprite2d, "position", origin_pos + offset, delta_t).set_trans( Tween.TRANS_CUBIC ) shake_tween.tween_callback(_reset_modulate) func _reset_modulate(): sprite2d.modulate = Color(1.0, 1.0, 1.0, 1.0)