优化NPC2D,增加 hook_speaking&unhook_speaking;孤儿院第一次与大胖对话使用该功能

This commit is contained in:
cakipaul 2025-07-16 15:23:00 +08:00
parent 9c3c470f3e
commit 70f9d35d07
4 changed files with 275 additions and 90 deletions

View File

@ -2,10 +2,15 @@
class_name Npc2D extends AnimatedSprite2D class_name Npc2D extends AnimatedSprite2D
signal interacted signal interacted
# 在 unlock player 之前发射 signal talk_finished # 在 unlock player 之前发射
signal talk_finished
# <0 means no walk to edge # 常量定义
const SPEAKING_SIGN_FADE_DURATION := 0.3
const SPEAKING_SCALE_MULTIPLIER := 1.3
enum SpeakingSignMode { HIDDEN = 0, SILENT = 1, SPEAKING = 2 }
# 导出变量
@export var snap_to_edge := true @export var snap_to_edge := true
@export var walk_to_edge_width := 25.0 @export var walk_to_edge_width := 25.0
@export var action_key := 4 @export var action_key := 4
@ -14,41 +19,47 @@ signal talk_finished
enabled = val enabled = val
if is_node_ready(): if is_node_ready():
_align_signs_status() _align_signs_status()
@export var sign_mark_height := 10.0: @export var sign_mark_height := 10.0:
set(val): set(val):
sign_mark_height = val sign_mark_height = val
if is_node_ready(): if is_node_ready() and sign_mark:
sign_mark.sign_mark_offset.y = -sign_mark_height sign_mark.sign_mark_offset.y = -sign_mark_height
@export var speaking_sign_height := 60.0: @export var speaking_sign_height := 60.0:
set(val): set(val):
speaking_sign_height = val speaking_sign_height = val
if is_node_ready(): if is_node_ready() and speaking_sign:
speaking_sign.position.y = -speaking_sign_height speaking_sign.position.y = -speaking_sign_height
@export var sign_x_offset := 0.0: @export var sign_x_offset := 0.0:
set(val): set(val):
sign_x_offset = val sign_x_offset = val
if is_node_ready(): if is_node_ready():
speaking_sign.position.x = sign_x_offset _update_sign_x_positions()
sign_mark.position.x = sign_x_offset
@export var collision_width_and_x := Vector2(20.0, 0): @export var collision_width_and_x := Vector2(20.0, 0):
set(val): set(val):
collision_width_and_x = val collision_width_and_x = val
if is_node_ready(): if is_node_ready():
var shape = area2d.get_node("CollisionShape2D").shape _update_collision_shape()
shape.size.x = collision_width_and_x.x
area2d.position.x = collision_width_and_x.y # 节点引用
@onready var speaking_animation = %SpeakingAnimationPlayer @onready var speaking_animation = %SpeakingAnimationPlayer
@onready var speaking_sign = %SpeakingSign2D as Node2D @onready var speaking_sign = %SpeakingSign2D as Node2D
@onready var sign_mark = %Sign as Sign @onready var sign_mark = %Sign as Sign
@onready var sign_snapper = %SignSnapper as SignSnapper @onready var sign_snapper = %SignSnapper as SignSnapper
@onready var area2d = %Area2D as Area2D @onready var area2d = %Area2D as Area2D
# 内部变量
var ground_archive: GroundArchive var ground_archive: GroundArchive
# 尝试互动的次数
var icount: int: var icount: int:
set(val): set(val):
if icount == val:
return
icount = val icount = val
ground_archive.set_pair(name, "icount", val) if ground_archive:
ground_archive.set_pair(name, "icount", val)
_align_signs_status() _align_signs_status()
var dialogue_title := "" var dialogue_title := ""
@ -56,105 +67,246 @@ var dialogue_res = preload("res://asset/dialogue/npc.dialogue")
var base_scale := Vector2.ONE var base_scale := Vector2.ONE
var base_mod := Color.WHITE_SMOKE var base_mod := Color.WHITE_SMOKE
var speaking_sign_tween: Tween var speaking_sign_tween: Tween
var speaking_sign_mode := SpeakingSignMode.HIDDEN:
# 0 hide; 1 silent; 2 speaking
var speaking_sign_mode := 0:
set(val): set(val):
if speaking_sign_mode != val: if speaking_sign_mode == val:
speaking_sign_mode = val return
if speaking_sign_tween and speaking_sign_tween.is_valid(): speaking_sign_mode = val
speaking_sign_tween.kill() _update_speaking_sign_mode()
speaking_sign_tween = create_tween()
if val == 0: # 强制播放状态管理
speaking_sign_tween.tween_property(speaking_sign, "modulate:a", 0.0, 0.3) var is_hooked := false
speaking_animation.stop() var hook_id := 0 # 用于追踪hook会话避免异步问题
elif val == 1:
speaking_sign_tween.tween_property(speaking_sign, "modulate", base_mod, 0.3)
speaking_sign_tween.parallel().tween_property(
speaking_sign, "scale", base_scale, 0.3
)
speaking_animation.play("speaking")
elif val == 2:
speaking_sign_tween.tween_property(speaking_sign, "modulate", Color.WHITE, 0.3)
speaking_sign_tween.parallel().tween_property(
speaking_sign, "scale", base_scale * 1.3, 0.3
)
speaking_animation.play("speaking")
func _ready() -> void: func _ready() -> void:
# sign position _initialize_components()
sign_mark.sign_mark_offset.y = -sign_mark_height
speaking_sign.position.y = -speaking_sign_height
sign_mark.position.x = sign_x_offset
speaking_sign.position.x = sign_x_offset
# collisiong shape
var shape = area2d.get_node("CollisionShape2D").shape
shape.size.x = collision_width_and_x.x
area2d.position.x = collision_width_and_x.y
sign_snapper.action_on_arrived = action_key
sign_snapper.radius = walk_to_edge_width
sign_snapper.enabled = snap_to_edge
# 设置 speaking_sign 默认值
base_scale = speaking_sign.scale
base_mod = speaking_sign.modulate
speaking_sign.modulate.a = 0.0
if Engine.is_editor_hint(): if Engine.is_editor_hint():
# editor 下都显示 _setup_editor_preview()
return
_setup_game_mode()
_align_signs_status()
func _initialize_components() -> void:
# 设置标记位置
if sign_mark:
sign_mark.sign_mark_offset.y = -sign_mark_height
sign_mark.position.x = sign_x_offset
if speaking_sign:
speaking_sign.position.y = -speaking_sign_height
speaking_sign.position.x = sign_x_offset
base_scale = speaking_sign.scale
base_mod = speaking_sign.modulate
speaking_sign.modulate.a = 0.0
# 设置碰撞形状
_update_collision_shape()
# 配置 sign_snapper
if sign_snapper:
sign_snapper.action_on_arrived = action_key
sign_snapper.radius = walk_to_edge_width
sign_snapper.enabled = snap_to_edge
func _setup_editor_preview() -> void:
if speaking_sign:
speaking_sign.visible = true speaking_sign.visible = true
speaking_sign.modulate.a = 1.0 speaking_sign.modulate.a = 1.0
speaking_sign.get_node("Sprite2D").position.x = -60.0 var sprite = speaking_sign.get_node_or_null("Sprite2D")
speaking_sign.get_node("Sprite2D").frame = 2 if sprite:
sprite.position.x = -60.0
sprite.frame = 2
if sign_mark:
sign_mark.display_sign = true sign_mark.display_sign = true
return
# setup default value
func _setup_game_mode() -> void:
# 获取存档数据
ground_archive = ArchiveManager.archive.ground_archive() ground_archive = ArchiveManager.archive.ground_archive()
icount = ground_archive.get_value(name, "icount", 0) icount = ground_archive.get_value(name, "icount", 0)
if snap_to_edge:
# 连接信号
if snap_to_edge and sign_snapper:
sign_snapper.arrived.connect(_on_interacted) sign_snapper.arrived.connect(_on_interacted)
else: elif sign_mark:
sign_mark.interacted.connect(_on_interacted) sign_mark.interacted.connect(_on_interacted)
# sign_mark.cancel.connect(_stop_speaking)
sign_mark.toggle_active.connect(_on_toggle_active) if sign_mark:
sign_mark.toggle_active.connect(_on_toggle_active)
visibility_changed.connect(_on_visibility_changed) visibility_changed.connect(_on_visibility_changed)
# 开始动画
if sprite_frames and animation: if sprite_frames and animation:
play() play()
func _update_sign_x_positions() -> void:
if speaking_sign:
speaking_sign.position.x = sign_x_offset
if sign_mark:
sign_mark.position.x = sign_x_offset
func _update_collision_shape() -> void:
if not area2d:
return
var collision_shape = area2d.get_node_or_null("CollisionShape2D")
if collision_shape and collision_shape.shape:
collision_shape.shape.size.x = collision_width_and_x.x
area2d.position.x = collision_width_and_x.y
func _update_speaking_sign_mode() -> void:
if not speaking_sign:
return
# 清理之前的补间动画
if speaking_sign_tween and speaking_sign_tween.is_valid():
speaking_sign_tween.kill()
speaking_sign_tween = create_tween()
match speaking_sign_mode:
SpeakingSignMode.HIDDEN:
speaking_sign_tween.tween_property(
speaking_sign, "modulate:a", 0.0, SPEAKING_SIGN_FADE_DURATION
)
if speaking_animation:
speaking_animation.stop()
SpeakingSignMode.SILENT:
speaking_sign_tween.tween_property(
speaking_sign, "modulate", base_mod, SPEAKING_SIGN_FADE_DURATION
)
speaking_sign_tween.parallel().tween_property(
speaking_sign, "scale", base_scale, SPEAKING_SIGN_FADE_DURATION
)
if speaking_animation:
speaking_animation.play("speaking")
SpeakingSignMode.SPEAKING:
speaking_sign_tween.tween_property(
speaking_sign, "modulate", Color.WHITE, SPEAKING_SIGN_FADE_DURATION
)
speaking_sign_tween.parallel().tween_property(
speaking_sign,
"scale",
base_scale * SPEAKING_SCALE_MULTIPLIER,
SPEAKING_SIGN_FADE_DURATION
)
if speaking_animation:
speaking_animation.play("speaking")
func _on_visibility_changed() -> void: func _on_visibility_changed() -> void:
_align_signs_status() _align_signs_status()
func _align_signs_status(): func _align_signs_status() -> void:
sign_mark.enabled = enabled and is_visible_in_tree() if not is_node_ready():
sign_mark.display_sign = icount == 0 return
speaking_sign.visible = enabled and icount > 0 var is_active = enabled and is_visible_in_tree()
if sign_mark:
sign_mark.enabled = is_active
sign_mark.display_sign = icount == 0
if speaking_sign:
speaking_sign.visible = enabled and (icount > 0 or is_hooked)
func _on_toggle_active(activated: bool) -> void: func _on_toggle_active(activated: bool) -> void:
# 如果处于hook状态不响应正常的toggle
if is_hooked:
return
if not activated: if not activated:
speaking_sign_mode = 0 speaking_sign_mode = SpeakingSignMode.HIDDEN
elif speaking_sign_mode == 0: elif speaking_sign_mode == SpeakingSignMode.HIDDEN:
speaking_sign_mode = 1 speaking_sign_mode = SpeakingSignMode.SILENT
func _on_interacted() -> void: func _on_interacted() -> void:
# play dialogue if not dialogue_title:
if dialogue_title: return
SceneManager.lock_player(0, action_key)
icount += 1 # 如果正在hook播放先取消hook
if is_hooked:
_cancel_hook()
SceneManager.lock_player(0, action_key)
icount += 1
if ground_archive:
ground_archive.set_pair(name, "icount", icount) ground_archive.set_pair(name, "icount", icount)
DialogueManager.show_dialogue_balloon(dialogue_res, dialogue_title)
interacted.emit() DialogueManager.show_dialogue_balloon(dialogue_res, dialogue_title)
var out_of_range = speaking_sign_mode == 0 interacted.emit()
speaking_sign_mode = 2
await DialogueManager.dialogue_ended var was_out_of_range = speaking_sign_mode == SpeakingSignMode.HIDDEN
speaking_sign_mode = 0 if out_of_range else 1 speaking_sign_mode = SpeakingSignMode.SPEAKING
# 在 unlock 之前发射
talk_finished.emit() await DialogueManager.dialogue_ended
SceneManager.unlock_player()
speaking_sign_mode = SpeakingSignMode.HIDDEN if was_out_of_range else SpeakingSignMode.SILENT
talk_finished.emit()
SceneManager.unlock_player()
# 新增:强制播放说话动画
func hook_speaking() -> int:
# 强制显示说话气泡动画
# 返回值hook会话ID用于验证unhook的有效性
is_hooked = true
hook_id += 1
var current_hook_id = hook_id
# 确保speaking_sign可见
if speaking_sign and not speaking_sign.visible:
speaking_sign.visible = true
# 保存当前状态并切换到说话模式
speaking_sign_mode = SpeakingSignMode.SPEAKING
return current_hook_id
# 新增:退出强制播放
func unhook_speaking(session_id: int = -1) -> void:
# 退出强制播放模式
# 参数:
# session_id: hook会话ID如果不匹配则忽略此次unhook
# 如果已经不在hook状态或session_id不匹配则忽略
if not is_hooked or (session_id != -1 and session_id != hook_id):
return
_cancel_hook()
# 新增内部取消hook的方法
func _cancel_hook() -> void:
# 内部使用的取消hook方法
is_hooked = false
# 恢复到适当的状态
if sign_mark and sign_mark.activated:
speaking_sign_mode = SpeakingSignMode.SILENT
else:
speaking_sign_mode = SpeakingSignMode.HIDDEN
# 重新对齐显示状态
_align_signs_status()
# 新增检查是否正在hook播放
func is_hook_speaking() -> bool:
"""返回当前是否处于强制播放状态"""
return is_hooked
func _get(property: StringName) -> Variant: func _get(property: StringName) -> Variant:
@ -172,8 +324,9 @@ func _set(property: StringName, value: Variant) -> bool:
func _get_property_list() -> Array[Dictionary]: func _get_property_list() -> Array[Dictionary]:
var hint_str = "" var hint_str = ""
if Engine.is_editor_hint(): if Engine.is_editor_hint() and dialogue_res:
hint_str = ",".join(dialogue_res.get_ordered_titles()) hint_str = ",".join(dialogue_res.get_ordered_titles())
return [ return [
{ {
"name": "dialogue_title", "name": "dialogue_title",

View File

@ -94,8 +94,10 @@ func pre_game_intro():
var p = $"../DeployLayer/四小孩画鬼差的对话ambush/FocusPoint" var p = $"../DeployLayer/四小孩画鬼差的对话ambush/FocusPoint"
camera.focus_node(p, 3.0) camera.focus_node(p, 3.0)
await Util.wait(2.0) await Util.wait(2.0)
_hook_npc3_speaking()
DialogueManager.show_dialogue_balloon(dialogue_c01, "c01_s06_四个小孩画鬼差的对话") DialogueManager.show_dialogue_balloon(dialogue_c01, "c01_s06_四个小孩画鬼差的对话")
await DialogueManager.dialogue_ended await DialogueManager.dialogue_ended
_unhook_npc3_speaking()
# 重置镜头 # 重置镜头
SceneManager.focus_player_and_reset_zoom(2.5) SceneManager.focus_player_and_reset_zoom(2.5)
await Util.wait(2.5) await Util.wait(2.5)
@ -123,14 +125,17 @@ func game_intro() -> void:
# DialogueManager.show_dialogue_balloon( # DialogueManager.show_dialogue_balloon(
# dialogue_c01, "c01_s06_谈论鬼差与猫鼠游戏", [GlobalConfig.DIALOG_IGNORE_INPUT] # dialogue_c01, "c01_s06_谈论鬼差与猫鼠游戏", [GlobalConfig.DIALOG_IGNORE_INPUT]
# ) # )
_hook_npc3_speaking()
DialogueManager.show_dialogue_balloon(dialogue_c01, "c01_s06_谈论鬼差与猫鼠游戏") DialogueManager.show_dialogue_balloon(dialogue_c01, "c01_s06_谈论鬼差与猫鼠游戏")
DialogueManager.dialogue_ended.connect(_game_counting_down, CONNECT_ONE_SHOT) await DialogueManager.dialogue_ended
_game_counting_down()
func _game_counting_down(_res = null): func _game_counting_down(_res = null):
$"Sfx猫鼠游戏".play() $"Sfx猫鼠游戏".play()
DialogueManager.show_dialogue_balloon(dialogue_c01, "c01_s06_猫鼠游戏BGM开始") DialogueManager.show_dialogue_balloon(dialogue_c01, "c01_s06_猫鼠游戏BGM开始")
await DialogueManager.dialogue_ended await DialogueManager.dialogue_ended
_unhook_npc3_speaking()
# 重置镜头 # 重置镜头
SceneManager.focus_player_and_reset_zoom(2.5) SceneManager.focus_player_and_reset_zoom(2.5)
SceneManager.release_player() SceneManager.release_player()
@ -145,6 +150,14 @@ func _game_counting_down(_res = null):
cat.get_node("猫咪嘶吼音效").play() cat.get_node("猫咪嘶吼音效").play()
func _hook_npc3_speaking(_res = null) -> void:
game_kid.get_node("Npc对话3").hook_speaking()
func _unhook_npc3_speaking(_res = null) -> void:
game_kid.get_node("Npc对话3").unhook_speaking()
# 玩家与三个小孩的互动计数 # 玩家与三个小孩的互动计数
func _on_talked(id: int): func _on_talked(id: int):
#talk count #talk count

View File

@ -71,7 +71,7 @@ func _knock_door():
await Util.wait(2.2) await Util.wait(2.2)
$"敲门音效".play() $"敲门音效".play()
await Util.wait(1.2) await Util.wait(1.2)
var stream = preload("res://asset/audio/sfx/交互/序章/03_书店外黄昏_开门.ogg") var stream = preload("uid://ehgd455wq8to")
AudioManager.play_sfx(stream) AudioManager.play_sfx(stream)
@ -95,3 +95,14 @@ func seller_interacted():
# 播放获得动画 # 播放获得动画
SceneManager.enable_prop_item("prop_信碎片2") SceneManager.enable_prop_item("prop_信碎片2")
SceneManager.release_player() SceneManager.release_player()
func jiandu_dialog_triggered() -> void:
var jiandu = $"../DeployLayer/举碗小孩/Npc监督小孩"
jiandu.hook_speaking()
DialogueManager.dialogue_ended.connect(_on_jiandu_dialog_ended, CONNECT_ONE_SHOT)
func _on_jiandu_dialog_ended(_res) -> void:
var jiandu = $"../DeployLayer/举碗小孩/Npc监督小孩"
jiandu.unhook_speaking()

View File

@ -203,12 +203,6 @@ sprite_frames = ExtResource("6_thm8f")
animation = &"杂戏团黄昏-其余小孩" animation = &"杂戏团黄昏-其余小孩"
autoplay = "杂戏团黄昏-其余小孩" autoplay = "杂戏团黄昏-其余小孩"
[node name="Ambush监督小孩" parent="Ground/DeployLayer/其余小孩" instance=ExtResource("9_f61dl")]
position = Vector2(-688, 53)
cooldown_time = 0.1
lock_player_on_playing_dialogue = false
hook_dialogue_title = "c01_s07_监督小孩吉祥话"
[node name="Npc吉祥话1" parent="Ground/DeployLayer/其余小孩" instance=ExtResource("6_fw22n")] [node name="Npc吉祥话1" parent="Ground/DeployLayer/其余小孩" instance=ExtResource("6_fw22n")]
position = Vector2(-44, 78) position = Vector2(-44, 78)
sign_mark_height = 23.0 sign_mark_height = 23.0
@ -237,6 +231,20 @@ autoplay = "杂戏团黄昏_举碗小孩"
position = Vector2(6, 57) position = Vector2(6, 57)
note_key = "c01_s07_钱碗" note_key = "c01_s07_钱碗"
[node name="Npc监督小孩" parent="Ground/DeployLayer/举碗小孩" instance=ExtResource("6_fw22n")]
position = Vector2(6, 72)
snap_to_edge = false
enabled = false
sign_mark_height = 11.0
speaking_sign_height = 54.0
[node name="Ambush监督小孩" parent="Ground/DeployLayer/举碗小孩" instance=ExtResource("9_f61dl")]
position = Vector2(-825, 53)
cooldown_time = 0.1
lock_player_on_playing_dialogue = false
hook_dialogue_title = "c01_s07_监督小孩吉祥话"
hook_method = "jiandu_dialog_triggered"
[node name="报童" parent="Ground/DeployLayer" index="10" instance=ExtResource("9_slaub")] [node name="报童" parent="Ground/DeployLayer" index="10" instance=ExtResource("9_slaub")]
position = Vector2(2080, 6) position = Vector2(2080, 6)
sprite_frames = ExtResource("6_thm8f") sprite_frames = ExtResource("6_thm8f")