在上一篇插件教程中,我们掌握了Godot编辑器插件的核心基础,成功实现了自定义停靠面板插件和属性检查器拓展插件,了解了插件的运行逻辑与基础开发规范。
本篇将进阶学习编辑器UI拓展开发,重点实现两大核心案例:
基础插件:在2D场景工具栏自定义按钮,实现选中节点一键显隐切换;
高级插件:制作自定义侧边菜单,实现编辑器内指定节点的独立2D物理模拟。
通过本篇学习,你将掌握Godot编辑器工具栏、侧边面板的自定义开发,以及物理服务器独立空间隔离的核心技术。
回顾
所有可正常运行的Godot编辑器插件,必须满足固定结构规范,缺一不可:
统一存放目录:项目根目录下必须存在
addons文件夹,所有插件均存放于此;元信息配置文件:插件目录下必须包含
plugin.cfg,用于存储插件名称、作者、描述、版本等核心配置;核心逻辑脚本:至少拥有一个继承自
EditorPlugin的GD脚本,承载插件所有功能逻辑。
官方插件快速创建工具
Godot内置可视化插件生成工具,无需手动创建文件和配置,大幅降低开发门槛,操作路径:
顶部菜单栏 → 项目 → 项目设置 → 插件标签页
默认插件列表为空,点击创建新插件,填写插件基础信息后,引擎将自动生成完整插件目录、配置文件和基础脚本模板。
2D工具栏节点显隐按钮插件
本次开发一键切换节点显隐的工具栏插件,填写插件创建参数如下:
插件名称:Visibility Button
子文件夹:visibility_button
描述:一键显示/隐藏选中节点
作者:自定义昵称/姓名
脚本名称:visibility_button.gd
点击创建后,引擎自动生成目录结构:项目根目录/addons/visibility_button/,包含两个核心文件:
plugin.cfg:插件元信息配置文件(引擎识别插件的核心);visibility_button.gd:插件功能逻辑脚本。
脚本模板
引擎自动生成的初始脚本模板是所有编辑器插件的通用框架:
@toolextends EditorPluginfunc _enter_tree() -> void:# 插件初始化代码passfunc _exit_tree() -> void:# 插件清理代码pass
@tool 注解:工具脚本标记,让脚本在编辑器运行时生效(普通脚本仅在游戏运行时执行),编辑器UI拓展、场景编辑操作必须依赖该注解;
extends EditorPlugin:继承编辑器插件基类,该类提供所有编辑器交互、UI创建、资源操作的官方API;
_enter_tree():插件启用/加载完成时自动执行,用于初始化UI、变量、信号绑定;
_exit_tree():插件禁用/卸载时自动执行,用于销毁UI、释放内存、清理引用,防止内存泄漏。
监听节点选中状态
本插件无需通用的初始化、销毁生命周期函数,删除原有模板中的 _enter_tree 和 _exit_tree,替换为编辑器专属的节点监听回调函数。
可处理对象判定 _handles()
该函数为编辑器内置回调,当用户选中场景中的任意对象时自动触发,用于判定当前插件是否可以处理该对象。
# 判定插件可处理的对象类型:仅识别场景节点func _handles(object) -> bool:return object is Node
Object 是Godot所有类的基类,通过类型判断,限定插件只响应Node节点及其子类的选中操作。
存储选中节点引用 _edit()
当 _handles() 返回 true 时,该函数立即执行,用于存储当前选中的节点对象。
先定义全局变量存储选中节点,再编写回调逻辑:
# 全局变量:存储当前选中的场景节点var node_to_edit : Node# 接收并存储选中的合法节点func _edit(object: Object) -> void:# 做空值判断,避免空对象报错if !object:return# 赋值保存选中节点引用node_to_edit = object# 测试代码:打印选中节点,验证逻辑(上线可删除/注释)print("当前选中节点:", node_to_edit.name)
工具栏按钮动态显隐控制
编辑器内置_make_visible() 回调函数,用于控制插件UI的显示与隐藏:选中合法节点时显示按钮,取消选中时隐藏按钮。
# 控制插件UI显隐func _make_visible(visible: bool) -> void:if visible:_add_button() # 选中节点:创建并显示工具栏按钮else:_remove_button() # 取消选中:移除并销毁按钮func _add_button() -> void:passfunc _remove_button() -> void:pass
工具栏按钮创建与功能实现
创建按钮并挂载到工具栏
定义全局按钮变量,完善 _add_button() 函数,创建标准化编辑器按钮并添加到2D画布工具栏。
# 全局变量:存储工具栏按钮实例var visibility_button : Button# 创建并添加按钮到2D编辑器工具栏func _add_button() -> void:# 防止重复创建按钮if visibility_button:return# 实例化按钮并设置样式visibility_button = Button.new()visibility_button.text = "Toggle visibility" # 按钮显示文本visibility_button.focus_mode = Control.FOCUS_NONE # 取消聚焦,适配编辑器UIvisibility_button.flat = true # 扁平样式,贴合原生编辑器风格# 绑定按钮点击信号visibility_button.pressed.connect(_on_button_pressed)# 将按钮添加到2D画布顶部工具栏add_control_to_container(EditorPlugin.CONTAINER_CANVAS_EDITOR_MENU, visibility_button)
按钮点击
实现节点显隐切换功能,一键反转节点的可见状态:
# 按钮点击回调:切换节点显隐func _on_button_pressed() -> void:# 反转节点visible属性:显示变隐藏、隐藏变显示node_to_edit.visible = !node_to_edit.visible
资源清理与防内存泄漏优化
未优化的插件会出现重复创建按钮、内存堆积问题,完善 _remove_button() 函数,彻底销毁UI并释放资源:
# 移除并销毁工具栏按钮func _remove_button() -> void:if visibility_button:# 从编辑器容器移除控件remove_control_from_container(EditorPlugin.CONTAINER_CANVAS_EDITOR_MENU, visibility_button)# 销毁节点,释放内存visibility_button.queue_free()# 清空变量引用,彻底杜绝内存泄漏visibility_button = null
插件测试与功能验证
打开 项目设置 → 插件,启用 Visibility Button 插件;
打开2D场景,任意选中一个场景节点;
2D工具栏将出现
Toggle visibility按钮,点击即可切换节点显隐;取消节点选中,按钮自动消失,功能正常。
当前插件局限
当前版本仅支持单个节点选中操作,多选节点时编辑器无法确定操作对象,插件UI会自动消失。后续高级拓展可实现多选批量显隐功能。
自定义2D物理模拟菜单插件
本次开发高阶编辑器插件,实现编辑器内指定节点独立物理模拟,核心解决默认物理引擎全局生效、无法单独模拟选中节点物理效果的问题。
新建物理菜单插件
通过官方工具新建插件,参数配置如下:
插件名称:Physics Menu
子文件夹:physics_menu
描述:在编辑器中对选中对象运行独立2D物理模拟
作者:自定义昵称/姓名
脚本名称:physics_menu.gd
制作插件UI菜单场景
在插件目录 addons/physics_menu/ 下新建独立UI场景,分离UI与逻辑,提升插件可维护性。
新建场景,根节点设置为 VBoxContainer,命名为
Menu;设置根节点锚点为全屏矩形,铺满右侧面板可用空间;
在根节点下依次添加5个Button按钮,节点名与功能对应:
ShowSelectionButton:显示选中节点信息
StartPhysicsButton:启动物理模拟
StopPhysicsButton:停止物理模拟
TenFramesButton:模拟10帧瞬时物理
ResetVelocityButton:重置节点物理速度
保存场景为
menu_ui.tscn,无需挂载脚本,所有逻辑由插件脚本实现。
插件UI加载与卸载
通过插件生命周期函数,实现UI场景的自动加载、挂载与销毁。
@toolextends EditorPlugin# 全局变量:存储菜单UI实例var menu : Node# 插件启用:加载并挂载UI到编辑器右侧面板func _enter_tree() -> void:if menu:return# 预加载并实例化UI场景menu = preload("res://addons/physics_menu/menu_ui.tscn").instantiate()# 绑定所有按钮的点击信号menu.get_node("ShowSelectionButton").pressed.connect(_on_show_selection_pressed)menu.get_node("StartPhysicsButton").pressed.connect(_on_start_physics_pressed)menu.get_node("StopPhysicsButton").pressed.connect(_on_stop_physics_pressed)menu.get_node("TenFramesButton").pressed.connect(_on_ten_frames_pressed)menu.get_node("ResetVelocityButton").pressed.connect(_on_reset_velocity_pressed)# 将UI挂载到2D编辑器右侧面板add_control_to_container(EditorPlugin.CONTAINER_CANVAS_EDITOR_SIDE_RIGHT, menu)# 插件卸载:销毁UI、释放资源func _exit_tree() -> void:if !menu:return# 从编辑器容器移除UIremove_control_from_container(EditorPlugin.CONTAINER_CANVAS_EDITOR_SIDE_RIGHT, menu)# 销毁实例、清空引用menu.free()menu = null
获取编辑器选中节点
封装通用方法,获取用户在场景树中选中的所有节点,支持多选操作,实现「显示选中项」功能:
# 显示所有选中节点的名称和类型func _on_show_selection_pressed() -> void:var selected_nodes = _get_selected_nodes()print("===== 当前选中节点列表 =====")for selected_node in selected_nodes:print("节点名称:", selected_node.name, " | 节点类型:", selected_node.get_class())# 通用工具方法:获取所有选中的场景节点func _get_selected_nodes() -> Array[Node]:return EditorInterface.get_selection().get_selected_nodes()
Godot 2D物理引擎原理
Godot的2D物理系统由PhysicsServer2D(物理服务器)统一管理,所有物理运算均由服务器后台执行,核心核心物理对象:
space(物理空间):独立的物理世界,拥有独立重力、碰撞规则,不同空间的物理效果相互隔离;
shape(碰撞形状):定义物体的碰撞范围与几何形态;
body(物理实体):拥有位置、速度、受力属性的可运动物理节点;
area(区域):用于检测碰撞进入、离开事件,无实体运动;
joint(约束关节):用于连接两个物理物体,限制相对运动。
默认情况下,所有场景节点共享全局默认物理空间,直接启动物理会影响所有节点,因此需要创建自定义独立物理空间实现局部模拟。
自定义独立物理空间开发
定义物理空间资源变量
RID(资源标识)是Godot所有引擎资源的唯一标识,用于操作物理空间、纹理、模型等底层资源:
# 默认物理空间IDvar default_space_rid : RID# 自定义独立物理空间IDvar custom_space_rid : RID
创建自定义物理空间
创建独立物理空间,自定义重力参数,与全局物理空间完全隔离:
# 创建自定义2D物理空间并返回资源IDfunc _create_physics_space() -> RID:# 新建空物理空间var rid = PhysicsServer2D.space_create()# 设置空间重力大小:980像素/秒²(Godot标准重力参数)PhysicsServer2D.area_set_param(rid, PhysicsServer2D.AREA_PARAM_GRAVITY, 980)# 设置重力方向:垂直向下PhysicsServer2D.area_set_param(rid, PhysicsServer2D.AREA_PARAM_GRAVITY_VECTOR, Vector2.DOWN)return rid
将选中节点移入自定义空间
筛选选中的物理节点,将其从默认空间转移到自定义独立空间,实现物理效果隔离:
# 将选中的物理节点移入自定义物理空间func _add_selected_nodes_to_custom_space() -> void:var selected_nodes = _get_selected_nodes()# 无选中节点则直接返回iflen(selected_nodes) == 0:return# 未创建自定义空间则先创建if !custom_space_rid:custom_space_rid = _create_physics_space()# 遍历节点,转移物理空间for selected_node in selected_nodes:# 仅处理2D碰撞物理节点if selected_node is CollisionObject2D:# 修改节点所属物理空间PhysicsServer2D.body_set_space(selected_node.get_rid(), custom_space_rid)# 记录原始默认空间ID(仅首次记录)if !default_space_rid:default_space_rid = selected_node.get_world_2d().space
切换物理空间激活状态
实现默认空间与自定义空间的开关切换,保证仅自定义空间生效:
# 激活自定义物理空间func _activate_custom_physics_space() -> void:PhysicsServer2D.space_set_active(default_space_rid, false) # 关闭默认空间PhysicsServer2D.space_set_active(custom_space_rid, true) # 开启自定义空间PhysicsServer2D.set_active(true) # 启动物理引擎# 停用自定义物理空间,恢复全局默认空间func _deactivate_custom_physics_space() -> void:PhysicsServer2D.space_set_active(default_space_rid, true) # 恢复默认空间PhysicsServer2D.space_set_active(custom_space_rid, false) # 关闭自定义空间PhysicsServer2D.set_active(false) # 停止物理引擎# 整合:启动自定义物理模拟func _start_custom_physics() -> void:_add_selected_nodes_to_custom_space()_activate_custom_physics_space()
物理功能按钮逻辑实现
启动/停止物理模拟
# 开始自定义物理模拟func _on_start_physics_pressed() -> void:_start_custom_physics()# 停止物理模拟,恢复默认状态func _on_stop_physics_pressed() -> void:_deactivate_custom_physics_space()
模拟10帧瞬时物理效果
利用异步等待,实现短时物理模拟,常用于预览节点下落、碰撞效果:
# 模拟10帧物理后自动停止func _on_ten_frames_pressed() -> void:_start_custom_physics()# 等待10次物理帧更新for i inrange(10):await get_tree().physics_frame# 自动关闭物理模拟_deactivate_custom_physics_space()
重置节点物理速度
清除刚体节点的线速度和角速度,防止模拟结束后节点持续运动:
# 重置选中刚体节点的所有速度func _on_reset_velocity_pressed() -> void:var selected_nodes = _get_selected_nodes()for selected_node in selected_nodes:# 仅对2D刚体节点生效if selected_node is RigidBody2D:selected_node.linear_velocity = Vector2.ZERO # 清空线速度selected_node.angular_velocity = 0 # 清空角速度
拓展
优化显隐按钮插件,支持多选节点批量显隐;
为物理插件添加物理参数自定义面板(重力大小、碰撞层级);
增加物理模拟快照功能,保存模拟后的节点位置;
拓展3D物理空间模拟插件,复用本篇底层逻辑。
总结
掌握Godot编辑器插件的UI创建、挂载、销毁全流程;
理解 _handles、_edit、_make_visible 编辑器专属回调逻辑;
掌握编辑器工具栏、右侧面板的自定义控件挂载方法;
精通 PhysicsServer2D 底层调用与独立物理空间隔离技术。
夜雨聆风