乐于分享
好东西不私藏

Godot 编辑器插件开发 1

Godot 编辑器插件开发 1

Godot 编辑器拥有极强的可扩展能力,官方提供的 EditorPlugin 核心类,允许开发者自定义扩展编辑器功能,无需修改引擎源码,即可实现编辑器功能的个性化拓展。

通过编辑器插件,我们可以为原生编辑器新增各类自定义能力,常见实用场景如下:

  • 新增编辑器快捷按钮、自定义快捷键

  • 搭建专属自定义停靠面板、功能界面

  • 定制节点属性检查器展示样式

  • 实现自定义资源导入器、专属节点类型

  • 开发自动化工具(自动生成碰撞体、批量处理资源等)

  • 搭建对话编辑器、关卡管理器等专属功能模块

几乎所有编辑器个性化需求,都可以通过现有插件或自定义插件实现。但插件运行在编辑器内核中,若未掌握 @tool 注解、插件生命周期核心规则,极易引发编辑器卡顿、崩溃、内存泄漏等问题。

本文将从零起步,从基础核心语法到实战案例,循序渐进讲解插件开发,带你掌握自定义停靠面板、工具栏按钮、专属属性检查器三大核心插件功能。

基础:@tool 注解

@tool 是 Godot 编辑器扩展开发的必备注解。在脚本头部添加该注解后,当前脚本将不再仅运行于游戏运行时,而是可以在编辑器环境中直接执行

所有编辑器插件主脚本、编辑器预览脚本、自定义编辑器UI脚本,都必须添加 @tool 注解,否则无法在编辑器中生效。

编辑器与运行时逻辑分离

添加 @tool 后,脚本逻辑会同时作用于编辑器和游戏运行场景。我们必须通过Engine.is_editor_hint() 做逻辑分支判断,严格区分编辑器编辑逻辑游戏运行逻辑,避免游戏逻辑在编辑器中异常执行。

@toolextends Node2D# 帧更新逻辑func _process(delta):    # 仅在编辑器环境执行预览、绘制等编辑逻辑    if Engine.is_editor_hint():        queue_redraw()    # 仅在游戏运行时执行游戏核心逻辑    else:        move_character(delta)

未做逻辑分支判断,会导致游戏运行代码在编辑器中执行,引发数据异常、编辑器报错甚至崩溃。

@tool 核心应用场景

@tool 广泛应用于各类编辑器自定义场景:

应用场景

详细说明

自定义编辑器绘制

通过 _draw() 函数在编辑器中绘制辅助线、攻击范围、碰撞预览等可视化图形

属性实时预览

监听 @export 导出属性变更,实时刷新编辑器内的预览效果

编辑器插件开发

所有继承 EditorPlugin 的插件主脚本,必须添加该注解(强制要求)

实战:属性联动实时绘制

在属性检查器中修改半径数值时,编辑器内实时刷新圆形范围预览,仅编辑器生效,不影响游戏运行逻辑。

@toolextends Sprite2D# 导出可编辑半径属性,绑定属性修改回调@export var radius: float = 100.0:    set(value):        radius = value        queue_redraw()  # 属性变更后触发重绘# 编辑器绘制逻辑func _draw():    # 仅编辑器环境执行绘制    if Engine.is_editor_hint():        # 绘制半透明绿色圆形范围预览        draw_circle(Vector2.ZERO, radius, Color(0, 1, 0, 0.3))

插件基础结构与配置

掌握 @tool 基础后,我们正式开始搭建标准 Godot 编辑器插件框架。Godot 插件拥有固定的目录和配置规范,编辑器可自动识别、加载和启用插件。

插件标准目录结构

所有自定义插件必须放置在项目根目录的 addons 文件夹中(无该文件夹可手动创建),单插件独立文件夹隔离,结构清晰、便于维护。

addons/└── my_plugin/                # 自定义插件根目录(插件名)    ├── plugin.cfg            # 插件核心配置文件(必须存在)    ├── my_plugin.gd          # 插件主脚本(生命周期入口)    └── dock/                 # 自定义停靠面板资源目录(可选)        └── my_dock.tscn      # 停靠面板场景文件

目录命名为 addons 而非 plugins,是因为 Godot 将所有引擎附加扩展工具统一归类为附加组件,而 编辑器插件 特指继承自 EditorPlugin 的标准化扩展脚本。

插件配置文件 plugin.cfg

plugin.cfg 是插件的唯一识别配置文件,用于定义插件名称、作者、版本、入口脚本等核心信息,编辑器依靠该文件识别插件。

[plugin]name="我的自定义编辑器插件"description="实现自定义停靠面板、工具栏按钮、属性检查器扩展"author="开发者名称"version="1.0.0"script="my_plugin.gd"  # 绑定插件主脚本(相对路径)

插件主脚本基础生命周期

插件主脚本必须继承 EditorPlugin 类,并实现两大核心生命周期函数,这是插件运行的核心规则:

  • _enter_tree():插件启用/编辑器加载时触发,用于初始化UI、注册功能

  • _exit_tree():插件禁用/编辑器关闭时触发,用于销毁UI、释放资源(核心避坑点)

@toolextends EditorPlugin# 插件启用加载时执行func _enter_tree():    print("我的插件:加载成功!")# 插件禁用卸载时执行func _exit_tree():    print("我的插件:卸载成功!")

插件启用步骤

  1. 将完整插件目录放入项目 addons 文件夹,刷新项目文件;

  2. 点击顶部菜单栏 项目 → 项目设置 → 插件,进入插件管理面板;

  3. 在插件列表中找到自己的插件,勾选前方复选框,即可启用插件;

  4. 启用后编辑器实时加载插件功能,禁用后立即卸载。

实战:自定义编辑器停靠面板

自定义停靠面板是最常用的编辑器扩展方式,可实现常驻编辑器的自定义功能界面,常用于调试面板、资源预览、批量工具、参数配置等场景。

实现停靠面板挂载

通过 add_control_to_dock() 函数可将自定义场景挂载到编辑器指定停靠区域,插件卸载时必须手动移除并释放资源。

@toolextends EditorPluginvar dock: Control  # 定义停靠面板根节点变量# 插件加载:初始化并挂载停靠面板func _enter_tree():    # 预加载并实例化自定义面板场景    dock = preload("res://addons/my_plugin/dock/my_dock.tscn").instantiate()    # 挂载到编辑器左上停靠区域    add_control_to_dock(DOCK_SLOT_LEFT_UL, dock)# 插件卸载:销毁面板、释放资源func _exit_tree():    # 空值校验,避免报错    if dock:        remove_control_from_docks(dock)        dock.queue_free()

停靠面板位置常量

Godot 编辑器预设4个停靠区域,可根据需求自由选择挂载位置:

位置常量

对应编辑器位置

DOCK_SLOT_LEFT_UL

左侧面板 – 上方区域

DOCK_SLOT_LEFT_BL

左侧面板 – 下方区域

DOCK_SLOT_RIGHT_UL

右侧面板 – 上方区域

DOCK_SLOT_RIGHT_BL

右侧面板 – 下方区域

制作停靠面板UI场景

停靠面板本质是普通UI场景,必须以 Control 系列节点作为根节点(推荐 VBoxContainer/HBoxContainer),可自由添加按钮、文本、输入框等UI控件。

面板逻辑脚本(my_dock.gd)

@toolextends VBoxContainer# 绑定场景内UI控件@onready var status_label: Label = $StatusLabel@onready var run_btn: Button = $RunButtonfunc _ready():    # 绑定按钮点击信号    run_btn.pressed.connect(_on_run_click)# 自定义按钮点击逻辑func _on_run_click():    status_label.text = "执行中..."    # 自定义工具逻辑(可拓展批量处理校验等功能)    status_label.text = "执行完成!操作成功"

停靠面板独立于主插件脚本,如需和编辑器核心交互,可通过信号传参、插件主脚本调用等方式实现。

实战:自定义编辑器工具栏按钮

对于单次触发、无需常驻界面的工具功能(一键校验、批量导出、快速重置等),可使用工具栏按钮实现,更加轻便简洁。

实现工具栏按钮

@toolextends EditorPluginvar tool_btn: Button# 插件加载:创建并挂载工具栏按钮func _enter_tree():    # 实例化按钮    tool_btn = Button.new()    tool_btn.text = "自定义工具按钮"    # 绑定点击事件    tool_btn.pressed.connect(_on_tool_btn_click)    # 挂载到编辑器顶部主工具栏    add_control_to_container(CONTAINER_TOOLBAR, tool_btn)# 插件卸载:销毁按钮资源func _exit_tree():    if tool_btn:        remove_control_from_container(CONTAINER_TOOLBAR, tool_btn)        tool_btn.queue_free()# 按钮点击回调逻辑func _on_tool_btn_click():    print("自定义工具栏按钮已触发!")    push_warning("工具执行成功"# 编辑器弹窗提示

编辑器容器位置常量

除顶部工具栏外,还可将自定义控件挂载到编辑器多个内置容器:

容器常量

对应编辑器位置

CONTAINER_TOOLBAR

编辑器顶部主工具栏

CONTAINER_SPATIAL_EDITOR_MENU

3D 编辑器菜单栏

CONTAINER_CANVAS_EDITOR_MENU

2D 编辑器菜单栏

CONTAINER_INSPECTOR_BOTTOM

属性检查器底部区域

实战:自定义属性检查器插件

Godot 编辑器默认自动展示节点属性,通过 EditorInspectorPlugin 类,可针对指定节点类型,定制专属的属性检查器UI,实现属性美化、附加信息展示、自定义操作等功能。

功能:选中 CharacterBody2D 节点时,在属性面板新增专属角色信息展示,自定义移动速度属性的显示样式。

主插件注册检查器插件

在主插件脚本中注册自定义属性检查器,插件卸载时同步注销,避免内存泄漏。

@toolextends EditorPluginvar custom_inspector: MyInspectorPluginfunc _enter_tree():    # 实例化并注册自定义属性检查器插件    custom_inspector = MyInspectorPlugin.new()    add_inspector_plugin(custom_inspector)func _exit_tree():    # 注销并释放检查器资源    if custom_inspector:        remove_inspector_plugin(custom_inspector)        custom_inspector.queue_free()

自定义检查器逻辑

新建脚本继承 EditorInspectorPlugin,实现节点匹配、自定义UI绘制、属性重写等核心逻辑。

@toolclass_name MyInspectorPluginextends EditorInspectorPlugin# 判断当前节点是否由本检查器接管func _can_handle(object: Object) -> bool:    # 仅对 CharacterBody2D 节点生效    return object is CharacterBody2D# 在属性面板顶部添加自定义UI(节点选中时触发)func _parse_begin(object: Object):    var title_label = Label.new()    title_label.text = "===== 角色专属信息面板 ====="    title_label.custom_minimum_size.x = 200    add_custom_control(title_label)# 拦截并自定义指定属性的展示样式func _parse_property(object, type, name, hint_type, hint_string, usage_flags, wide):    # 定制 speed 移动速度属性展示    if name == "speed":        var speed_label = Label.new()        speed_label.text = "当前移动速度:" + str(object.speed)        add_custom_control(speed_label)        return true # 拦截原生属性展示,使用自定义样式    return false

插件开发最佳实践

编辑器插件运行在引擎内核环境,资源管理不当会直接导致编辑器崩溃、内存泄漏、卡顿报错,以下是官方推荐开发规范与避坑要点。

注意事项

详细开发建议

空值强制校验

所有UI控件、插件对象在销毁前,必须做空值判断,避免空指针报错

逻辑环境分离

全程使用 Engine.is_editor_hint() 区分编辑器/游戏运行逻辑

资源彻底清理

_exit_tree() 中必须销毁所有自定义UI、注销所有插件实例,执行 queue_free()

统一资源预加载

插件资源、场景、素材统一使用 preload() 预加载,避免运行时加载异常

标准化异常提示

使用 push_warning() / push_error() 抛出插件异常,方便调试

案例对比

错误写法(会导致内存泄漏、编辑器崩溃)

func _exit_tree():    # 未清理任何资源,控件残留内存,插件重复启用会叠加报错    pass

规范写法

func _exit_tree():    # 清理停靠面板    if dock:        remove_control_from_docks(dock)        dock.queue_free()    # 清理属性检查器插件    if custom_inspector:        remove_inspector_plugin(custom_inspector)        custom_inspector.queue_free()    # 清理工具栏按钮    if tool_btn:        remove_control_from_container(CONTAINER_TOOLBAR, tool_btn)        tool_btn.queue_free()

总结

@tool 注解:编辑器扩展开发基础,让脚本在编辑器环境运行,必须通过 Engine.is_editor_hint() 分离双环境逻辑;

插件基础框架:遵循addons 目录规范,通过 plugin.cfg 配置插件信息,依托 EditorPlugin 生命周期管理插件;

三大插件功能:可自定义停靠面板(常驻界面)、工具栏按钮(快捷操作)、节点专属属性检查器;

核心开发原则:插件卸载时必须彻底清理所有自定义资源,做空值校验,杜绝内存泄漏和编辑器异常。