@tool class_name Sign extends Control signal interacted signal cancel signal toggle_active(activated: bool) @export var sign_mark_offset := Vector2.ZERO: set(val): sign_mark_offset = val if is_node_ready(): texture_container.position = val # @export var lock_on_player_freezed := false @export var enabled := true: set(val): if enabled != val: enabled = val align_activation() @export var display_sign := true: set(val): if display_sign != val: display_sign = val if is_node_ready(): _check_sign_display() # @export var draw_shadow := false @onready var texture_container = %TextureContainer as Container @onready var sprite2d = %Sprite2D as Sprite2D # 跳过输入事件 var pass_unhandled_input = false # var shadow_texture = preload("res://asset/art/ui/action_mark/探索空心ui.png") as Texture2D # 同时只能有一个物品被激活交互态,其他物品进入等待队列 static var occupied: NodePath # touching 状态改变时就要擦除 static var _touching_sign_arr: Array[NodePath] = [] # 使用互斥锁保证线程安全。在操作 occupied 或 _touching_sign_arr 时需要先锁定,操作完成后解锁 static var mutex = Mutex.new() var tween: Tween var player_touching := false: set(val): if player_touching != val: player_touching = val var path = get_path() if player_touching and not _touching_sign_arr.has(path): _touching_sign_arr.append(path) if not player_touching: _touching_sign_arr.erase(path) align_activation() var activated = false: set(val): if activated != val: activated = val _check_sign_display() toggle_active.emit(activated) var base_scale = Vector2.ONE func _ready() -> void: texture_container.position = sign_mark_offset base_scale = sprite2d.scale # 读取 parent 的 sign_mark_offset_updated 信号与 sign_mark_offset var parent = get_parent() if parent and parent.has_signal("sign_mark_offset_updated"): sign_mark_offset = parent.sign_mark_offset parent.sign_mark_offset_updated.connect(func(o): sign_mark_offset = o) _check_sign_display() if Engine.is_editor_hint(): return # 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(_on_body_entered) area2d.body_exited.connect(_on_body_exited) visibility_changed.connect(_on_visibility_changed) # 关注 lock 的状态变化 SceneManager.get_lock().hold_changed.connect(_on_lock_hold_changed) func is_hold() -> bool: return SceneManager.get_lock().is_held() func _on_lock_hold_changed(count: int, is_add: bool): if count == 0 or (count == 1 and is_add): align_activation() func _on_visibility_changed() -> void: align_activation() # activition/display mode 变化时 _check_sign_display func _check_sign_display(): if Engine.is_editor_hint(): # 在编辑器中与 display_sign & enabled 状态保持一致 if display_sign and enabled: sprite2d.modulate.a = 1.0 else: sprite2d.modulate.a = 0.0 return if not display_sign or not activated: sprite2d.modulate.a = 0.0 elif sprite2d.modulate.a == 0.0: sprite2d.modulate.a = 1.0 func _on_body_entered(_body: Node2D) -> void: player_touching = true func _on_body_exited(_body: Node2D) -> void: player_touching = false # 四种参数变化时调用 align: 1 enable 2 hold 3 visibility 4 touching func align_activation() -> bool: if Engine.is_editor_hint(): return activated var ideal_status := enabled and player_touching and not is_hold() and is_visible_in_tree() if activated != ideal_status: if ideal_status: _try_activate() else: _try_disactivate() if GlobalConfig.DEBUG: var activated_color = "cyan" if activated else "brown" print_rich( "[b]", get_parent().name, "[/b] activated=[color=", activated_color, "]", activated, "[/color]", " occupied:[color=khaki]", _get_name_from_path(occupied), "[/color] touching:[color=coral]", _get_name_from_paths(_touching_sign_arr), "[/color]" ) return activated func _try_disactivate() -> void: mutex.lock() if activated: activated = false occupied = NodePath(&"") # 转移 active 状态给下一个节点 var self_path = get_path() for p in _touching_sign_arr: # skip self if p == self_path: continue var s = get_node_or_null(p) as Sign if s and s.align_activation(): break mutex.unlock() # point_light.energy = 0.0 if tween and tween.is_valid(): tween.kill() if sprite2d.modulate.a > 0.0: tween = create_tween() tween.tween_property(sprite2d, "modulate:a", 0.0, 0.2) func _try_activate() -> bool: # point_light.energy = 1.0 var path := get_path() mutex.lock() # 若 lock 为 hold 状态则直接跳过 if not occupied or occupied == path: occupied = path activated = true mutex.unlock() if activated and display_sign: if tween and tween.is_valid(): tween.kill() tween = create_tween() tween.tween_property(sprite2d, "modulate:a", 1.0, 0.2) var p_tween = tween.parallel() p_tween.tween_property(sprite2d, "scale", base_scale * Vector2(1.2, 1.2), 0.3) p_tween.tween_property(sprite2d, "scale", base_scale, 0.1) return activated func _get_name_from_path(path: NodePath) -> String: if not path: return "" # return -2 idx var parts = path.get_concatenated_names().split("/") if parts.size() >= 2: return parts[parts.size() - 2] elif parts.size() >= 1: return parts[parts.size() - 1] else: return "" func _get_name_from_paths(paths: Array[NodePath]) -> String: if len(paths) == 0: return "" var names = [] for path in paths: names.append(_get_name_from_path(path)) return ", ".join(names) func _unhandled_input(event: InputEvent) -> void: if Engine.is_editor_hint() or not enabled or pass_unhandled_input: return # if lock_on_player_freezed and SceneManager.is_palyer_freezed_or_locked(): # return if SceneManager.get_lock().is_held(): return if activated: if event.is_action_pressed("interact"): get_viewport().set_input_as_handled() 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() elif event.is_action_pressed("cancel"): get_viewport().set_input_as_handled() if GlobalConfig.DEBUG: print("Sign cancel:", get_parent().name) cancel.emit() release_focus() var shake_tween # 使用无效道具,抖动提示;抖动时使用 texture2d 并且变红为 mod func invalid_shake( normal_texture: Texture2D = null, invalid_texture: Texture2D = null, mod := Color(1.0, 0.6, 0.6, 1.0) ): 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 if invalid_texture: sprite2d.texture = invalid_texture sprite2d.self_modulate = mod 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_sprite2d.bind(normal_texture)) func _reset_sprite2d(texture2d): sprite2d.self_modulate = Color.WHITE if texture2d: sprite2d.texture = texture2d