乐于分享
好东西不私藏

Godot 编辑器插件开发 2

Godot 编辑器插件开发 2

在上一篇插件教程中,我们掌握了Godot编辑器插件的核心基础,成功实现了自定义停靠面板插件属性检查器拓展插件,了解了插件的运行逻辑与基础开发规范。

本篇将进阶学习编辑器UI拓展开发,重点实现两大核心案例:

  1. 基础插件:在2D场景工具栏自定义按钮,实现选中节点一键显隐切换;

  2. 高级插件:制作自定义侧边菜单,实现编辑器内指定节点的独立2D物理模拟。

通过本篇学习,你将掌握Godot编辑器工具栏、侧边面板的自定义开发,以及物理服务器独立空间隔离的核心技术。

回顾

所有可正常运行的Godot编辑器插件,必须满足固定结构规范,缺一不可:

  1. 统一存放目录:项目根目录下必须存在 addons 文件夹,所有插件均存放于此;

  2. 元信息配置文件:插件目录下必须包含 plugin.cfg,用于存储插件名称、作者、描述、版本等核心配置;

  3. 核心逻辑脚本:至少拥有一个继承自 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(visiblebool) -> 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  # 取消聚焦,适配编辑器UI    visibility_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

插件测试与功能验证

  1. 打开 项目设置 → 插件,启用 Visibility Button 插件;

  2. 打开2D场景,任意选中一个场景节点;

  3. 2D工具栏将出现 Toggle visibility 按钮,点击即可切换节点显隐;

  4. 取消节点选中,按钮自动消失,功能正常。

当前插件局限

当前版本仅支持单个节点选中操作,多选节点时编辑器无法确定操作对象,插件UI会自动消失。后续高级拓展可实现多选批量显隐功能。

自定义2D物理模拟菜单插件

本次开发高阶编辑器插件,实现编辑器内指定节点独立物理模拟,核心解决默认物理引擎全局生效、无法单独模拟选中节点物理效果的问题。

新建物理菜单插件

通过官方工具新建插件,参数配置如下:

  • 插件名称:Physics Menu

  • 子文件夹:physics_menu

  • 描述:在编辑器中对选中对象运行独立2D物理模拟

  • 作者:自定义昵称/姓名

  • 脚本名称:physics_menu.gd

制作插件UI菜单场景

在插件目录 addons/physics_menu/ 下新建独立UI场景,分离UI与逻辑,提升插件可维护性。

  1. 新建场景,根节点设置为 VBoxContainer,命名为 Menu

  2. 设置根节点锚点为全屏矩形,铺满右侧面板可用空间;

  3. 在根节点下依次添加5个Button按钮,节点名与功能对应:

    1. ShowSelectionButton:显示选中节点信息

    2. StartPhysicsButton:启动物理模拟

    3. StopPhysicsButton:停止物理模拟

    4. TenFramesButton:模拟10帧瞬时物理

    5. ResetVelocityButton:重置节点物理速度

  4. 保存场景为 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    # 从编辑器容器移除UI    remove_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 底层调用与独立物理空间隔离技术。