技术博客

ScriptServer 与脚本类注册:`class_name` 是怎么被全局看见的

第 88 / 149 篇 · 2026-06-17 · godot · GDScript源码分析编辑器

在 GDScript 里写一个 class_name HealthBar extends ProgressBar,保存之后,工程里任何脚本能直接 HealthBar.new(),编辑器的新建节点对话框里也会出现 HealthBar。这个"全局可见"不是编译期符号表,而是运行时一张注册表查出来的——这张表由 ScriptServer 管理。

和 ClassDB 划清边界

引擎里有两个"类注册中心",职责经常被混。ClassDB(一-1 那篇的主角)管的是原生 C++ 类:注册发生在引擎启动时的 register_module_types,内容是 C++ 类的 MethodBind 表、属性表、信号表。ScriptServer 管的是脚本类:GDScript 的 class_name、C# 的 [GlobalClass]、GDExtension 注册进来的脚本类,全部登记在 ScriptServer 这里。

判断标准很简单:类的实现在引擎二进制里 → ClassDB;实现是一个资源文件(.gd、.cs)→ ScriptServer。Create New Node 对话框里两类混在一起显示,底下是分两路查的。

注册链:从保存文件到进表

以 GDScript 为例,按下保存后:

  1. 编辑器导入管线发现文件变化,触发脚本重编译(热重载那篇讲过这条链)。
  2. GDScriptAnalyzer 在语义分析阶段发现 class_name Foo extends Bar 声明,向 ScriptServer::add_global_class("Foo", 路径, "GDScript", "Bar") 发起注册。
  3. ScriptServer 查全局表:名字已存在 → 返回冲突,编辑器在脚本编辑器里标红 "Class 'Foo' already exists";不存在 → 写入。

核心数据结构就是一张哈希表,value 只有三个字段:

// core/object/script_language.h,结构简写
class GlobalClassCache {
    HashMap<StringName, GlobalScriptClass> classes;
};
struct GlobalScriptClass {
    StringName base;      // 父类名,用于图标继承和兼容性检查
    StringName language;  // "GDScript" / "C#"
    String path;          // 脚本资源路径
};

注意表里没有字段顺序、没有命名空间、没有泛型参数——全局类就是个平铺的名字空间。两个不同目录下的脚本写同名 class_name 一定冲突,因为这张表不感知目录结构。大型工程里给类名加前缀(UIHealthBar 而不是 HealthBar)不是洁癖,是在这张平表里买唯一性。

持久化与冷启动

注册表不是每次启动全量重扫重建的。编辑器首次扫描工程时把结果写进 .godot/global_script_class_cache.cfg,之后启动直接读这个缓存,只有文件变化才增量更新。一万个脚本文件的工程冷启动能撑住,靠的就是这个缓存加 FileSystem 的 mtime 对比,而不是每次开机把全工程 .gd 文件都 parse 一遍。

代价是缓存可能过期:手动从外面拷文件进工程、git 合并冲突解决不当,都可能让 cfg 里的条目和磁盘对不上。排查编辑器里"明明有这个类却标红"的第一步永远是删掉 .godot/global_script_class_cache.cfg 让编辑器重建——这和资源 UID 缓存的故障模式是同一类。

谁在用这张表

消费方有三类,全在编辑器侧:新建节点对话框按名字列脚本类、脚本编辑器的代码补全、检查器里给导出的脚本类型属性画下拉框。运行时不查这张表——运行时拿的是具体的 Script 资源引用,实例化走 Script::instance,和全局名字无关。这意味着全局类数量再多也只拖编辑器,不影响发布版的运行时性能;但每个 class_name 都在编辑器里买了一个全局名字,名字本身会变成需要维护的 API 面。

class_name 的全局注册链 保存 .gd 文件 class_name Foo GDScriptAnalyzer 语义分析发现声明 ScriptServer add_global_class 全局类哈希表 名字→路径/基类 表结构:HashMap<StringName, {base, language, path}> 平铺名字空间:不看目录结构,同名 class_name 必然冲突 持久化在 .godot/global_script_class_cache.cfg,启动读缓存不重新 parse 编辑器侧消费(3 个) 新建节点对话框 / 代码补全 / 导出演示下拉框 运行时:不查这张表 实例化走 Script 资源引用,与全局名字无关