【Godot】【入门】信号系统从 0 到 1(UI/玩法彻底解耦的通用写法)

信号是 Godot 的"事件总线",用好它可以让 UI 与玩法解耦、模块复用。本文讲清概念、图形化连接 vs 代码连接、全局信号封装,并附常见坑与最佳实践(精力有限,附件后面慢慢补)。

信号是什么

  • 类似 C# 事件/UnityEvent:发送者发射,接收者监听,双方无需相互引用具体类型。
  • 任意节点都可以定义/发射信号,参数自定义。

定义与发射

gdscript 复制代码
signal hit(damage: int, from)

func _on_body_entered(body):
    emit_signal("hit", 10, body)
  • 信号定义放在脚本顶部,emit_signal 传参顺序与定义一致。

两种连接方式

图形化(适合固定场景)

  1. 选中信号源节点 → Node 面板 → Signals → 双击信号。
  2. 选择接收节点(通常场景根),确认后 Godot 生成回调函数并连接。
  • 优点:快、可视化;缺点:动态实例化时容易忘记保存连接。

代码连接(推荐通用写法)

gdscript 复制代码
func _ready():
    $Button.pressed.connect(_on_btn_pressed)

func _on_btn_pressed():
    print("clicked")
  • 动态实例:
gdscript 复制代码
var enemy = EnemyScene.instantiate()
enemy.hit.connect(_on_enemy_hit)
add_child(enemy)
  • 优点:连接逻辑集中、可条件化,便于重构。

全局信号总线(解耦模块)

  • 在 Autoload 添加 SignalsBus.gd
gdscript 复制代码
extends Node
signal player_died
signal coin_collected(amount)
  • 发射:SignalsBus.emit_signal("coin_collected", 1)
  • 监听:SignalsBus.coin_collected.connect(_on_coin)
  • 用途:UI/玩法互不引用,依靠总线通信;多人项目可统一管理事件名。

典型场景示例

UI 按钮 → 游戏逻辑

gdscript 复制代码
# Menu.gd
func _ready():
    $PlayButton.pressed.connect(_on_play)
func _on_play():
    SignalsBus.emit_signal("start_game")
gdscript 复制代码
# GameController.gd
func _ready():
    SignalsBus.start_game.connect(_start)

区域触发器 → 计分

gdscript 复制代码
# Coin.gd
signal collected(value)
func _on_body_entered(body):
    if body.is_in_group("player"):
        emit_signal("collected", 1)
        queue_free()
gdscript 复制代码
# Level.gd
func _ready():
    for coin in $Coins.get_children():
        coin.collected.connect(_on_coin)

信号管理的最佳实践

  • 命名 :用动词过去式或事件名:hit, died, collected, finished
  • 参数少而准:只传接收方需要的数据,避免传整个节点引用(或传引用但注明只读)。
  • 连接位置统一 :集中在 _ready 或初始化函数,便于搜索和断开。
  • 断开连接 :长生命周期节点监听短生命周期对象时,记得在对象销毁前 disconnect 或在 _exit_tree 断开。
  • 避免多次连接 :先检查 is_connected 或确保连接代码只执行一次。

常见坑

  • 未保存连接:编辑器连完信号没保存场景,运行时无回调;改用脚本连接或养成保存习惯。
  • 重复回调 :同一信号多次连接同一函数导致执行多次,排查 connect 是否在循环内。
  • 跨场景引用 null :监听的节点被销毁,回调报 null。使用 weakref 或在 disconnect 后再 free。
  • Autoload 信号未注册:忘记在项目设置里勾选 Autoload,导致发射无监听。

调试技巧

  • 临时打印:在回调里 print("signal hit", value) 确认触发。
  • is_connected 检查:
gdscript 复制代码
if not source.hit.is_connected(_on_hit):
    source.hit.connect(_on_hit)
  • 编辑器 Signal 面板有"Go to Function"可快速跳到生成的回调。

进阶:一次性/延迟信号

  • 一次性:
gdscript 复制代码
var conn = button.pressed.connect(func():
    print("once")
    button.pressed.disconnect(conn)
)
  • 延迟发射:call_deferred("emit_signal", "hit", 10) 避免当前帧状态冲突。

总结复盘

  • 信号=解耦:源的一端发射、接收一端监听,互不依赖具体实现。
  • 固定场景可用图形化连接,动态/可复用模块优先代码连接;全局事件用 Autoload 总线。
  • 关注连接时机、重复连接与断开,避免"回调触发两次"和 null 报错。掌握这些,UI/玩法/系统间通信会更清晰。
相关推荐
Mongnewer5 小时前
试写UI界面设计器
ui·界面设计器
我的offer在哪里5 小时前
示例 Unity 项目结构(Playable Game Template)
unity·游戏引擎
淡海水7 小时前
【节点】[Branch节点]原理解析与实际应用
unity·游戏引擎·shadergraph·图形·branch
TT哇9 小时前
【实习】数字营销系统 银行经理端(interact_bank)前端 Vue 移动端页面的 UI 重构与优化
java·前端·vue.js·ui
木斯佳9 小时前
周末杂谈:UI-UX Pro Max Skill:为AI编程助手注入专业设计智能的终极利器
ui·ai编程·ux
手揽回忆怎么睡9 小时前
opencode和TRAE使用Superpowers 和ui-ux-pro-max skillls
ide·ui·ai·ux
Zik----10 小时前
简单的Unity漫游场景搭建
unity·游戏引擎
草莓熊Lotso11 小时前
Qt 主窗口核心组件实战:菜单栏、工具栏、状态栏、浮动窗口全攻略
运维·开发语言·人工智能·python·qt·ui
御承扬1 天前
鸿蒙NDK UI之文本自定义样式
ui·华为·harmonyos·鸿蒙ndk ui
一起养小猫1 天前
Flutter for OpenHarmony 实战_魔方应用UI设计与交互优化
flutter·ui·交互·harmonyos