深入解析Godot通知系统:从生命周期到性能优化的核心机制

📅 2026/8/2 19:26:12
深入解析Godot通知系统:从生命周期到性能优化的核心机制
1. 项目概述为什么Godot的通知系统值得深挖如果你在Godot里写过脚本尤其是继承自Node或Control的脚本大概率见过或写过_ready()、_process(delta)这些方法。你可能知道它们是引擎在特定时机自动调用的但你是否想过引擎是如何知道该在什么时候调用你的方法的这背后就是Godot的通知系统在默默工作。它远不止是几个预设的生命周期回调而是一套贯穿整个引擎对象生命周期、驱动游戏逻辑流转的核心通信机制。很多开发者尤其是从Unity或Unreal Engine转过来的朋友初期可能会觉得Godot的这套系统有点“黑盒”或者仅仅把它当作一些固定的“魔法方法”来用。但实际上深入理解通知系统能让你写出更高效、更符合引擎设计哲学、也更容易调试的代码。它能帮你避免在_process里做不必要的轮询能让你精准地在对象状态改变时执行逻辑也是理解Godot信号系统、场景树管理等高级特性的基石。今天我们就抛开表面深入到源码设计的层面把Godot的通知系统彻底讲透让你不仅能“会用”更能“懂它为什么这么设计”。2. 通知系统的核心设计思想与运作原理2.1 什么是“通知”它与“信号”有何不同在Godot中“通知”是一种由引擎核心发往对象实例的内部消息。你可以把它想象成公司内部的系统广播或工作流触发器。当某个重要的内部事件发生时比如一帧开始、一帧结束、物理步进前、节点被加入场景树、属性被编辑器修改等引擎会向所有相关的对象“广播”一个特定的整数编号这个编号就是“通知”。每个对象内部都有一个_notification(what)虚函数当收到广播时如果该对象重写了这个函数对应的逻辑就会被执行。这里必须厘清一个关键概念通知是单向的、内部的、由引擎驱动的而信号是双向的、用户定义的、用于对象间通信的。驱动方通知由引擎核心驱动你无法手动“发射”一个通知给其他对象。信号则由脚本或C代码主动emit。通信方向通知是引擎到对象自上而下。信号是对象到对象可以是任意方向通常用于解耦。用途通知用于响应引擎内部状态变化和生命周期事件。信号用于游戏逻辑中自定义的、松散耦合的事件通信。性能通知的派发是引擎内部循环的一部分直接调用虚函数开销极低。信号的发射需要查找连接列表并调用回调有一定开销但带来了极大的灵活性。一个常见的误解是试图用通知在不同节点间传递数据。这是错误的使用方式。正确的模式是在节点A的_notification中响应引擎事件如NOTIFICATION_PARENTED即被添加为子节点然后在这个响应函数里通过信号或直接调用去与其他节点如它的新父节点通信。2.2 通知的枚举与分类NOTIFICATION_*常量详解Godot中所有的通知都定义在Variant::Type枚举对于GDScript或各个类的文档中但最常用的是Node和Object类提供的常量。这些常量都是整数为了可读性我们使用它们的名字。主要可以分为以下几大类2.2.1 生命周期与场景树通知这是Node及其子类最常用的一组通知直接关系到节点的“生老病死”和在场景树中的状态。NOTIFICATION_ENTER_TREE: 节点被添加到场景树中时调用。注意此时它的_ready()可能还未被调用如果它还没准备好。这里适合做依赖场景树存在但不需要所有子节点都就绪的初始化。NOTIFICATION_EXIT_TREE: 节点被移除出场景树时调用。这是进行清理工作的黄金位置比如断开信号连接、释放动态加载的资源。重要提示节点被queue_free()后也会经历EXIT_TREE然后才是NOTIFICATION_PREDELETE。NOTIFICATION_READY: 在_ready()函数调用之后发送。这意味着节点及其所有子节点都已完成了它们的_ready()调用。如果你有一段逻辑必须在整个子树都初始化完成后执行可以响应这个通知尽管更常见的做法是在父节点的_ready()中处理因为子节点的_ready()会先于父节点调用。NOTIFICATION_PARENTED: 当节点被添加为另一个节点的子节点时调用。参数中不包含父节点信息你需要通过get_parent()获取。NOTIFICATION_UNPARENTED: 当节点从其父节点移除时调用。NOTIFICATION_PREDELETE: 在对象实例被销毁之前的最后时刻调用。这是进行最终清理的最后一个机会通常用于C扩展中释放原生内存。在GDScript中很少需要直接处理它。2.2.2 帧循环与物理步进通知这些通知驱动着游戏的每一帧。NOTIFICATION_PROCESS: 对应_process(delta)函数。当节点的process_mode不为PROCESS_MODE_DISABLED且引擎处于游戏主循环时每帧发送一次。NOTIFICATION_PHYSICS_PROCESS: 对应_physics_process(delta)函数。在物理步进一个固定的时间间隔默认为1/60秒前发送。所有与物理引擎如RigidBody的速度、力相关的更新都应放在这里以保证确定性。NOTIFICATION_INTERNAL_PROCESS和NOTIFICATION_INTERNAL_PHYSICS_PROCESS: 这是给C模块内部使用的脚本开发者通常不直接接触。NOTIFICATION_WM_CLOSE_REQUEST(Window管理类): 当窗口收到关闭请求时发送常用于弹出“确定要退出吗”对话框。2.2.3 绘制与渲染通知主要用于CanvasItem所有2D节点的基类和ControlUI控件。NOTIFICATION_DRAW: 对应_draw()函数。当控件需要重新绘制自身时发送。你可以在这里调用draw_*系列函数。关键技巧为了性能只在需要时重绘。通常通过queue_redraw()来请求发送此通知而不是每帧都画。NOTIFICATION_VISIBILITY_CHANGED: 当节点的visible属性改变时发送。可以用来处理显示/隐藏时的逻辑比如隐藏时停止粒子发射器。NOTIFICATION_THEME_CHANGED: 当控件使用的主题资源发生改变时发送。UI控件可以在这里更新自己的样式以匹配新主题。2.2.4 输入与焦点通知处理用户交互。NOTIFICATION_WM_MOUSE_ENTER/NOTIFICATION_WM_MOUSE_EXIT: 鼠标进入/离开控件的区域时发送。用于实现悬停效果。NOTIFICATION_FOCUS_ENTER/NOTIFICATION_FOCUS_EXIT: 控件获得/失去键盘焦点时发送。2.2.5 属性与编辑器通知NOTIFICATION_PROPERTY_LIST_CHANGED: 当对象的可编辑属性列表需要更新时发送常用于C中动态暴露属性。NOTIFICATION_EDITOR_PRE_SAVE/POST_SAVE: 在编辑器保存场景前后发送供插件进行数据序列化/反序列化。注意不是所有类都响应所有通知。一个Node会收到生命周期和帧循环通知一个CanvasItem会额外收到绘制通知而一个普通的Resource可能只响应很少的通知。具体需要查阅官方类参考。2.3 引擎内部如何派发通知—— 从主循环到虚函数调用理解派发机制有助于写出更符合引擎节奏的代码。Godot的主循环MainLoop是这一切的调度中心。以SceneTree默认的主循环实现为例迭代与筛选每一帧场景树会遍历所有活跃的节点。对于NOTIFICATION_PROCESS它只遍历那些process_mode不为禁用且processing属性为true的节点。消息派发引擎核心C端会直接调用节点的_notification函数并传入对应的通知常量作为参数。在GDScript中你重写的_notification(what)方法就是这个调用的终点。预设回调的映射像_ready(),_process(),_physics_process(),_draw()这些我们熟悉的方法其实是Godot引擎在Object/Node类中预先实现好的_notification处理器。例如Node类的C代码中可能有如下逻辑// 伪代码示意过程 void Node::_notification(int p_what) { switch (p_what) { case NOTIFICATION_READY: if (has_method(_ready)) { // 检查脚本是否定义了 _ready call(_ready); // 调用GDScript的 _ready 函数 } break; case NOTIFICATION_PROCESS: if (has_method(_process)) { float delta get_process_delta_time(); call(_process, delta); } break; // ... 处理其他通知 } // 可能还会调用一些内置的C逻辑 }执行顺序通知的派发有严格的顺序。例如在一帧中通常是NOTIFICATION_PHYSICS_PROCESS- 物理引擎步进-NOTIFICATION_PROCESS- 渲染。NOTIFICATION_READY则在节点及其子节点被完全添加到场景树后在第一个_process帧之前发送。实操心得正因为_ready等方法是引擎通过通知系统“回调”给你的所以你不能直接期望像调用普通函数一样通过node._ready()来手动触发初始化。正确的触发方式是确保节点在场景树中且处于活跃状态引擎自然会处理。如果你想在运行时重新初始化某个部分应该设计一个自定义的reset()函数并在_ready和这个reset函数中调用相同的初始化逻辑。3. 核心细节解析与高级应用模式3.1 重写_notification函数时机、规范与性能考量虽然Godot为我们提供了_ready()这样的语法糖但直接重写_notification函数在某些场景下是必要且更强大的。何时需要直接使用_notification响应没有对应“糖方法”的通知比如NOTIFICATION_PARENTED、NOTIFICATION_EXIT_TREE、NOTIFICATION_THEME_CHANGED。在C GDExtension模块中这是与引擎核心交互的标准方式你需要重写_notification虚函数。需要在一个函数里处理多个相关通知时比如集中处理所有与场景树相关的生命周期事件。性能极度敏感的场景较少见直接重写_notification并省略switch中不必要的call调用理论上可以减少一次脚本函数查找和调用的开销但这在绝大多数情况下微乎其微不值得牺牲代码可读性。重写规范与示例extends Node2D func _notification(what): match what: NOTIFICATION_PARENTED: print(我被添加到父节点了: , get_parent()) # 可以在这里向新父节点注册自己或建立连接 if get_parent().has_method(register_child): get_parent().register_child(self) NOTIFICATION_ENTER_TREE: print(我进入了场景树) # 此时可以安全地获取场景树相关的单例如 get_tree() NOTIFICATION_EXIT_TREE: print(我离开了场景树) # 必须在这里断开所有信号连接防止内存泄漏和错误回调。 # 例如如果连接了全局自动加载单例的信号 # Global.some_signal.disconnect(_on_some_signal) # 更好的模式是使用 Node 的 tree_exiting 信号但原理相通。 NOTIFICATION_PROCESS: # 注意如果你重写了这个_process(delta) 就不会被自动调用了 # 你必须在这里手动计算delta并执行逻辑或者调用super。 var delta get_process_delta_time() # ... 你的每帧逻辑 ... # 通常建议还是用 _process(delta) 函数除非有特殊理由。 NOTIFICATION_DRAW: # 如果你重写这个_draw() 就不会被调用。 draw_circle(Vector2.ZERO, 50.0, Color.RED)重要警告重写_notification并处理了NOTIFICATION_PROCESS、NOTIFICATION_PHYSICS_PROCESS或NOTIFICATION_DRAW后引擎将不再自动调用对应的_process(delta)、_physics_process(delta)或_draw()函数。因为引擎认为你已经接管了该通知的处理。如果你既想添加自定义逻辑又想保留默认行为或者确保脚本中的_process也被调用必须在你的_notification函数末尾调用父类的方法func _notification(what): match what: NOTIFICATION_PROCESS: # 你的自定义前置逻辑 custom_pre_process() # 调用父类实现这会触发 _process(delta) 的调用 super._notification(what) # 你的自定义后置逻辑 custom_post_process()3.2 利用通知优化性能避免在_process中的无效轮询这是理解通知系统后带来的最直接的性能优化思路。很多新手会写出这样的代码extends Node var target_node: Node func _process(delta): if target_node ! null and target_node.is_inside_tree(): # 对 target_node 进行操作 pass这段代码每帧都在检查一个条件而target_node是否在树中的状态变化频率是很低的。利用通知系统我们可以将其优化为事件驱动模式extends Node var target_node: Node null func set_target_node(new_target: Node): if target_node ! null and target_node.is_inside_tree(): # 断开旧节点树状态变化的监听 target_node.tree_entered.disconnect(_on_target_entered_tree) target_node.tree_exiting.disconnect(_on_target_exiting_tree) target_node new_target if target_node ! null: # 连接新节点树状态变化的监听 target_node.tree_entered.connect(_on_target_entered_tree) target_node.tree_exiting.connect(_on_target_exiting_tree) # 立即检查当前状态 if target_node.is_inside_tree(): _on_target_entered_tree() else: _on_target_exiting_tree() func _on_target_entered_tree(): # 当目标节点进入树时执行一次 print(目标节点已就绪开始处理逻辑) # 初始化或启动相关逻辑 func _on_target_exiting_tree(): # 当目标节点即将离开树时执行一次 print(目标节点即将离开清理逻辑) # 停止或清理相关逻辑 func _exit_tree(): # 别忘了在自己离开时也清理连接 set_target_node(null)这里我们用到了tree_entered和tree_exiting信号它们内部正是由NOTIFICATION_ENTER_TREE和NOTIFICATION_EXIT_TREE通知触发的。我们将每帧的条件判断转换为了只在状态真正改变时执行的回调大大减少了不必要的计算。3.3 自定义通知在C模块与GDScript间建立高效通信Godot允许你定义自己的通知常量通常大于NOTIFICATION_EDITOR_BASE即1000用于你的自定义C模块或GDExtension与脚本之间的内部通信。这比通过信号或调用脚本函数在某些情况下更轻量、更直接。在C模块中GDExtension示例:// 在头文件中定义自定义通知 class MyCustomNode : public godot::Node { GDCLASS(MyCustomNode, godot::Node) private: // 自定义通知常量从 USER_BASE (1000) 开始 static const int NOTIFICATION_MY_CUSTOM_EVENT 1001; protected: static void _bind_methods(); // 重写 _notification 来处理自定义通知 void _notification(int p_what); // 一个触发自定义通知的方法 void trigger_custom_event(); }; // 在实现文件中 void MyCustomNode::_notification(int p_what) { // 首先调用父类处理 super::_notification(p_what); if (p_what NOTIFICATION_MY_CUSTOM_EVENT) { // 处理自定义事件 godot::UtilityFunctions::print(收到自定义通知); // 这里可以执行一些内部逻辑然后可以选择性地调用一个GDScript方法 if (has_method(_on_my_custom_event)) { call(_on_my_custom_event); } } } void MyCustomNode::trigger_custom_event() { // 这是关键从C端发起通知 notification(NOTIFICATION_MY_CUSTOM_EVENT); }在GDScript中:extends MyCustomNode # 假设这个C类已注册为 MyCustomNode func _ready(): # 连接信号或直接调用C方法触发 some_button.pressed.connect(_on_button_pressed) func _on_button_pressed(): # 调用C方法触发内部通知 trigger_custom_event() # 这个函数可以被C端的 _notification 调用 func _on_my_custom_event(): print(GDScript收到了来自C自定义通知的调用)为什么这么做自定义通知提供了一种比直接call(method_name)更结构化的内部通信方式。它允许C代码在特定的、定义良好的“内部事件点”执行逻辑并可选地回调脚本。这对于编写高性能的底层模块如网络同步、物理交互、资源管理非常有用。4. 实操构建一个基于通知系统的状态感知管理器让我们通过一个实战案例综合运用通知系统的知识构建一个StateAwareManager节点。这个管理器可以自动追踪一组目标节点并在任何一个目标节点的“在树中”状态发生变化时重新计算并输出当前所有在树中的目标节点列表。4.1 设计目标与类定义目标通过add_target(node)和remove_target(node)方法动态管理目标节点列表。当任何目标节点进入或退出场景树时自动更新一个“活跃目标列表”。提供一个get_active_targets()方法获取当前所有在树中的目标。管理器自身被销毁或离开场景树时自动清理所有信号连接防止内存泄漏。代码实现# state_aware_manager.gd extends Node class_name StateAwareManager # 存储所有被管理的目标节点 var _all_targets: Array[Node] [] # 存储当前在场景树中的目标节点 var _active_targets: Array[Node] [] # 当活跃目标列表变化时发出的信号 signal active_targets_updated(active_list: Array[Node]) # 公共方法添加一个目标节点进行追踪 func add_target(target: Node) - void: if target null or _all_targets.has(target): return _all_targets.append(target) # 连接目标节点的树状态变化信号 _connect_target_signals(target) # 立即更新其状态 _update_target_state(target) # 公共方法移除一个目标节点的追踪 func remove_target(target: Node) - void: if not _all_targets.has(target): return _disconnect_target_signals(target) _all_targets.erase(target) # 如果它在活跃列表中也需要移除 if _active_targets.has(target): _active_targets.erase(target) active_targets_updated.emit(_active_targets.duplicate()) # 获取当前所有活跃在树中的目标节点 func get_active_targets() - Array[Node]: return _active_targets.duplicate() # 返回副本以保护内部数据 # 私有方法连接目标节点的相关信号 func _connect_target_signals(target: Node) - void: if not target.tree_entered.is_connected(_on_target_tree_entered): target.tree_entered.connect(_on_target_tree_entered.bind(target)) if not target.tree_exiting.is_connected(_on_target_tree_exiting): target.tree_exiting.connect(_on_target_tree_exiting.bind(target)) # 私有方法断开目标节点的相关信号 func _disconnect_target_signals(target: Node) - void: if target.tree_entered.is_connected(_on_target_tree_entered): target.tree_entered.disconnect(_on_target_tree_entered) if target.tree_exiting.is_connected(_on_target_tree_exiting): target.tree_exiting.disconnect(_on_target_tree_exiting) # 私有方法更新单个目标节点的状态并可能触发活跃列表更新 func _update_target_state(target: Node) - void: var was_active _active_targets.has(target) var is_active_now target.is_inside_tree() if was_active and not is_active_now: _active_targets.erase(target) active_targets_updated.emit(_active_targets.duplicate()) elif not was_active and is_active_now: _active_targets.append(target) active_targets_updated.emit(_active_targets.duplicate()) # 如果状态未变则什么都不做 # 信号回调目标节点进入树 func _on_target_tree_entered(target: Node) - void: if not _active_targets.has(target): _active_targets.append(target) active_targets_updated.emit(_active_targets.duplicate()) # 信号回调目标节点即将退出树 func _on_target_tree_exiting(target: Node) - void: if _active_targets.has(target): _active_targets.erase(target) active_targets_updated.emit(_active_targets.duplicate()) # 重写 _notification 来处理管理器自身的生命周期 func _notification(what: int) - void: match what: NOTIFICATION_EXIT_TREE: # 当管理器自己离开场景树时清理所有连接 _cleanup_all_connections() NOTIFICATION_PREDELETE: # 最终保险确保资源释放 _cleanup_all_connections() # 清理所有信号连接 func _cleanup_all_connections() - void: for target in _all_targets: _disconnect_target_signals(target) _all_targets.clear() _active_targets.clear()4.2 使用示例与场景测试创建场景新建一个场景添加一个Node作为根然后添加一个StateAwareManager需先设置脚本实例。添加测试节点添加几个Sprite2D或Label作为目标节点。编写测试脚本给根节点附加脚本extends Node onready var manager $StateAwareManager onready var target1 $Target1 onready var target2 $Target2 func _ready(): # 连接管理器的信号 manager.active_targets_updated.connect(_on_active_targets_updated) # 添加目标 manager.add_target(target1) manager.add_target(target2) # 打印初始状态 print(初始活跃目标: , manager.get_active_targets()) # 模拟动态变化2秒后移除Target1 await get_tree().create_timer(2.0).timeout print(移除 Target1) manager.remove_target(target1) # 再2秒后重新添加Target1假设它还在场景中但未被管理 await get_tree().create_timer(2.0).timeout print(重新添加 Target1) manager.add_target(target1) func _on_active_targets_updated(active_list: Array[Node]): print(活跃目标列表更新: , active_list.map(func(node): return node.name))运行观察运行场景你会在输出中看到当目标节点被添加、移除或者你手动在编辑器中移动节点进出场景树时管理器的活跃列表会自动更新并打印日志。4.3 模式总结与扩展思考这个StateAwareManager展示了通知/信号系统的典型应用模式事件驱动取代了在_process中轮询每个节点is_inside_tree()的低效做法。自动清理通过响应自身的NOTIFICATION_EXIT_TREE确保了管理器生命周期结束时资源的正确释放这是避免内存泄漏和错误回调的关键。松耦合管理器只依赖于节点的tree_entered和tree_exiting信号而不关心节点具体是什么类型、有什么业务逻辑。你可以如何扩展它过滤条件在add_target时增加过滤只管理特定类型或具有特定属性的节点。分层管理让管理器可以嵌套形成一个树状的管理网络。持久化结合Resource将目标节点的引用路径保存下来在场景加载时自动重新连接。5. 常见问题、调试技巧与性能陷阱5.1 为什么我的_ready()或_process()没有被调用这是新手最常见的问题之一根本原因都是节点没有收到对应的通知。检查节点是否在场景树中_ready()只在节点及其所有子节点被添加到场景树后的第一帧之前调用一次。如果你用new()或load().instantiate()创建了一个节点但没有用add_child()将其添加到场景树_ready()永远不会被调用。调试方法在_ready()里加打印或检查is_inside_tree()。检查 Processing 状态_process()和_physics_process()的调用要求节点在场景树中并且processing或physics_processing属性为true默认是true。如果你手动将它们设为false或者节点的任何父节点设置了process_mode为PROCESS_MODE_DISABLED那么该节点及其子节点都不会收到处理通知。调试方法检查节点及其各级父节点的processing、physics_processing属性和process_mode。检查是否重写了_notification如前所述如果你重写了_notification函数并处理了NOTIFICATION_PROCESS等但没有调用super._notification(what)那么对应的“糖方法”就不会被执行。编辑器与运行时的区别在编辑器中某些节点如Tool脚本可能只在运行时接收通知。确保你在运行游戏场景而不是在编辑模式下查看。5.2_enter_tree()vs_ready()初始化顺序的陷阱理解它们的调用顺序至关重要错误的初始化顺序是许多空引用错误的根源。_enter_tree()(自上而下): 当节点自己被添加到场景树时立即调用。此时它的子节点可能还没有被添加如果子节点是在代码中后续添加的或者子节点虽然已在场景中但尚未收到_enter_tree()通知。不适合访问子节点因为子节点可能还不存在或未就绪。_ready()(自下而上): 当节点及其所有子节点都已添加到场景树并且每个节点的_enter_tree()都已被调用后才会调用。Godot会从最底层的叶子节点开始向上调用_ready()。这是访问和操作子节点的安全时机。一个典型错误示例# parent.gd extends Node onready var child_value $ChildNode.some_value # 这行在 _ready() 时执行是安全的 func _enter_tree(): # 错误此时 $ChildNode 可能为 null 或未就绪 print($ChildNode.some_value)正确做法所有依赖于子节点、兄弟节点或父节点完整初始化的代码都应放在_ready()中或者使用onready注解它本质上是_ready()时赋值的语法糖。5.3 内存泄漏与信号连接管理Godot使用引用计数进行内存管理。未正确断开的信号连接是导致内存泄漏和“已释放实例”错误的主要原因。黄金法则谁连接谁断开。如果一个节点A连接了节点B的信号那么当A或B任何一个即将失效离开场景树或被释放时A都有责任断开这个连接。_notification(NOTIFICATION_EXIT_TREE)是执行断开操作的最佳位置之一因为它发生在节点即将离开场景树但还未被销毁之时。extends Node var some_external_node: Node func _ready(): some_external_node get_node(/root/SomeGlobalNode) some_external_node.some_signal.connect(_on_signal) func _notification(what): if what NOTIFICATION_EXIT_TREE: # 断开所有外部连接 if some_external_node and some_external_node.some_signal.is_connected(_on_signal): some_external_node.some_signal.disconnect(_on_signal) func _on_signal(): pass对于使用Callable.bind()创建的绑定连接也需要对应地断开。更现代的Godot版本中如果连接时使用了CONNECT_ONE_SHOT标志则可以自动断开但手动管理依然是最清晰可靠的方式。5.4 性能陷阱过度使用NOTIFICATION_PROCESS与queue_redraw()空转的_process即使函数体是空的引擎为每个启用的节点每帧派发NOTIFICATION_PROCESS也有开销。如果一个节点不需要每帧更新务必将其processing属性设为false或在不需要时调用set_process(false)。滥用queue_redraw()在_process中无条件调用queue_redraw()会导致控件每帧重绘即使外观没有变化。这非常消耗性能。正确的做法是只在控件的状态如位置、颜色、文本实际发生变化时调用queue_redraw()。复杂的_notification匹配在_notification函数中使用一个巨大的match或if-else链来处理几十种通知对于极高性能要求的场景可能是个微开销。但除非你正在开发一个需要处理成千上万个节点的核心系统否则这个开销通常可以忽略不计。代码的可读性和可维护性更重要。5.5 调试工具与技巧打印通知在复杂的节点生命周期调试中可以在_notification函数里打印所有收到的通知帮助你理解调用顺序。func _notification(what): print(name, received notification: , what) # 可以配合一个将数字转为常量名的字典方便阅读使用“远程”场景树在运行游戏时打开“场景”面板切换到“远程”选项卡。这里显示的是实际运行中的场景树。你可以查看每个节点的处理状态是否正在处理。性能分析器使用Godot内置的“调试器”面板中的“性能”页签。监控“空闲时间”和“物理时间”。如果你发现空闲时间很低可能是_process逻辑太重或节点太多。监控“对象计数”确保节点在离开场景树后被正确释放防止内存泄漏。理解并善用Godot的通知系统是从“脚本编写者”迈向“引擎使用者”的关键一步。它让你写的代码不再是孤立的功能块而是能优雅地融入引擎生命周期、高效响应内部事件的有机组成部分。