【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/玩法/系统间通信会更清晰。
相关推荐
Mars-xq1 天前
Android godot 交互数据监听
android·godot·交互
特立独行的猫a1 天前
Docker 管理利器:docker-compose-ui 与 Portainer 详解
运维·ui·docker·容器·portainer
技术小甜甜1 天前
【Godot】【入门】节点生命周期怎么用(避免帧循环乱写导致卡顿的范式)
游戏引擎·godot
tealcwu1 天前
【Unity踩坑】Simulate Touch Input From Mouse or Pen 导致检测不到鼠标点击和滚轮
unity·计算机外设·游戏引擎
ThreePointsHeat1 天前
Unity WebGL打包后启动方法,部署本地服务器
unity·游戏引擎·webgl
erxij1 天前
【游戏引擎之路】《古今东西4》正式立项——新的一年,开始长征
游戏引擎
迪普阳光开朗很健康1 天前
UnityScrcpy 可以让你在unity面板里玩手机的插件
unity·游戏引擎
web小白成长日记1 天前
Vue3+ElementUI树形菜单:构建层次化用户界面
前端·microsoft·ui·面试·elementui
草莓熊Lotso1 天前
Qt 进阶核心:UI 开发 + 项目解析 + 内存管理实战(从 Hello World 到对象树)
运维·开发语言·c++·人工智能·qt·ui·智能手机