ccgf-game-tool
v1.0.25
Published
i18n:ccgf-game-tool.description
Downloads
1,349
Readme
ccgf-game-tool
cocos_gameframework 的编辑器扩展:把框架的 MVC 约定自动化,并在约定被破坏时提前报出来。
背景:框架的约定
约定写在 docs/create-view-design.md:
- Module = Controller:
@RegisterModule({ order, model }),model必填;一个模块目录 = 一对XxxModule.ts+XxxModel.ts - View = 脚本 + 预制体:
@RegisterView({ name, path, layer });脚本放在模块的views/下,预制体放在resources/views/<模块>/下,脚本挂在预制体根节点 .meta(含 UUID)由编辑器自动生成,不手写
这套约定原本只在「创建那一刻」靠工具帮忙,之后就全靠自觉。而约定被破坏后通常是运行期静默出错(加载错预制体、启动顺序不对、脚本丢失),定位成本很高。所以本扩展分两边:生成器(从零到有)+ 体检(从有到对)。
面板
菜单 开发者 → 工作台(也可通过 面板 → ccgf-game-tool)。面板分两个切页。
模块切页
| 区域 | 内容 | | --- | --- | | 左栏 | 模块列表(order 徽标、缺 Model 时显示告警点)、新建模块表单 | | 右栏 | 选中模块的 order 编辑、模块的三个脚本(点击打开)、View 列表(层级标签、预制体缺失告警) | | 顶部 | 「概览」启动顺序 + 层级占用;「体检」一键检查 |
创建 View 时可选择层级(框架 ELayer)与预制体模板。
创建模块会生成三个脚本:
| 文件 | 用途 |
| --- | --- |
| <前缀>Module.ts | Module 子类,用 @RegisterModule 注册 |
| <前缀>Model.ts | Model 子类 |
| <Prefix>Events.ts | 事件声明 —— 一段 declare global { interface IGameEvents { ... } } 增强 |
事件声明文件是纯声明:它让本模块能派发事件、别的模块能监听,而两边不需要互相 import。里面的说明注释是刻意保留的 —— 尤其末尾的 export { }; 不能删(没有 import/export 的文件会被当成全局脚本,declare global 会直接编译报错)。有一条测试专门编译这个模板,并断言删掉那一行会被编译器拒绝。设计与规则见 docs/event.md。
目标包(Asset Bundle)
面板顶部有「目标包」下拉:主包 = assets/scripts + assets/resources/views;选中子包后,create-module / create-view 落到 assets/bundles/<包名>/scripts/ 与 assets/bundles/<包名>/views/,生成的 @RegisterView 会带上 bundle: '<包名>'。
「新建 Bundle」会建好目录、把目录 .meta 的 userData.isBundle 写上(字段照真实 bundle 的 meta 格式),并把目标包切过去。.meta 没写成时会如实提示去资源管理器手动勾选「配置为 Bundle」,不会假装成功。scripts/ 与 views/ 里各有一个 README.md 占位——空目录不会被资源库导入,没有文件的话资源管理器里看不到这两个目录(别删)。
右侧的「删除 Bundle」删除当前子包(连同 scripts/、views/ 下的全部内容,删除前会确认);处于主包时按钮为禁用状态,不会误删主包。删完自动切回主包。
模块列表、View 列表与体检都按当前目标包扫描:选中子包就只看子包里的模块 / View,切回主包就回到主包的数据。
子包模式生成的代码会多两处标记,配合框架的脚本分包:
- 模块:
@RegisterModule({ order, model, bundle: '<包名>' })—— 子包脚本loadBundle时才执行,框架靠这个字段在SubGame.enter('<包名>')时按包启动 - View:
@ccclass('<包名>_<类名>')—— Cocos 的 ccclass 名全局唯一,不加包名前缀两个子游戏生成同名 View 会冲突;注册名与类名不变,大厅用ViewManager.showInBundle('<包名>', 'XxxView')打开
完整约定见框架文档 docs/subgame.md。
数据表工具(独立面板)
菜单「DevTools → 数据表工具」打开,和本工作台完全分开:
- 扫描工程根
tables/**/*.csv,每张表显示 CSV 物理路径 - 筛选:全部表 / 主包 / 指定 Bundle 下的表;全选和导出只作用于当前筛选结果
- 新建表:只生成
ID一列的三行骨架(类型/字段名/注释),不覆盖、不自动导出;编辑交给 Excel/WPS - 全部导出 / 指定导出:CSV →
.bin,按tables/镜像到assets/,并自动 refresh asset-db - 导出成功后同时生成
TableTypes.ts(主包scripts/table/,子包bundles/<bundle>/scripts/table/) - 刷新:外部改名/新增 CSV 后重新扫描;新建/删除 Bundle 会自动通知刷新(手动按钮兜底)
- 不做单元格编辑、不做预览;表名全局唯一,失败会显示表名/行号/字段
格式与 TableManager 用法见框架文档 docs/table-format.md。
Manager 切页
列出 assets/scripts/manager/ 下已有的全局单例,并可直接新建 / 删除。
约定的理由:manager 是全局服务,不属于某个业务模块,所以放在与 framework/、gameplay/ 平级的 assets/scripts/manager/,并且继承框架的 ASingleton,用 XxxManager.getInstance() 取实例。生成的模板:
import { ASingleton } from '../framework/common/Singleton';
export class ConfigManager extends ASingleton {
public init() {
}
public update(dt: number) {
}
}列表里没继承 ASingleton 的 manager 会带告警点,对应体检的 manager-not-singleton。
公共模块切页
列出框架的公共模块(可选、可独立发布的包),分成已安装与未安装两组,并直接代跑 npm 完成安装 / 卸载。
- 安装在工程根目录执行
npm install <说明符>…;卸载执行npm uninstall <包名>。两者都带--no-audit --no-fund,让输出聚焦在真正的问题上。 - 模块的本地依赖会一起装。只装它一个的话,npm 会去 registry 找依赖 —— 而未发布的本地包在 registry 上当然不存在,实测会直接
E404失败。面板上显示的命令与实际执行的是同一个函数产出的,所以你复制去手动跑,行为完全一致。 - 失败会把原始信息整个给你。 不是一句"安装失败",而是弹窗展示退出码、完整命令(方便你自己重跑)、以及 npm 的原始 stdout/stderr;认出故障特征时再补一句提示(找不到 npm / 网络不可达 / 超时 / 证书 / 权限 /
ERESOLVE/ 404 /EBADENGINE……)。实现见source/shared.ts。 - npm 命令常驻在界面上(详情面板里),显示的是与按钮即将执行的动作一致的那条(未安装显示
install,已安装显示uninstall),并提示自动执行失败时可以把命令复制到工程根目录手动跑。命令由主进程拼好下发,界面上是原样展示 —— 你复制到的就是按钮本来会执行的那条。 - 区分三种状态:
installed(依赖里声明了且node_modules里存在)、declared-not-installed(声明了但磁盘上没有 —— 上次安装大概失败了)、not-installed。 - 依赖关系双向显示:它依赖谁(每个依赖都带实时安装状态,所以缺哪个在真正用到之前就能看见),以及谁依赖它。后一个方向也进了卸载确认 —— 只检查脚本引用的话,"另一个公共模块依赖它"会被静默漏掉。
- 可更新状态分五种,而不是"能更新 / 不能更新"两种。远程最新版本号直接取自列表用的那次 registry 搜索(实测搜索结果的
version就等于 registry 的dist-tags.latest,抽样 6/6 一致),所以不增加任何网络开销。关键是 "无法确认"和"已是最新"是两回事 —— 离线绝不会被显示成"已是最新"。本地file:来源显示为不适用,因为对它"更新"只会把同一个本地来源再装一遍。有新版时安装按钮直接变成「更新到 x.y.z」。 - 卸载前会把仍在 import 它的脚本列出来让你确认,因为卸载后这些 import 会失败。
- Windows 注意:npm 是通过
cmd.exe /d /s /c启动并且自己加引号的 —— Node 在修了 CVE-2024-27980 之后不允许直接spawn一个.cmd文件;而且 spawn 必须带上windowsVerbatimArguments: true,否则 Node 会把引号再转义一遍,cmd 把整行当成一个命令名,npm 根本不会被执行(这个标志由启动说明符一起带出,忘不掉;tests/npm-spawn.test.js会真的起一个进程来守它)。子进程输出不能一律按 UTF-8 解:先严解码 UTF-8,失败再回退到控制台代码页(中文机器上是 GBK) ——cmd.exe自己的消息在中文 Windows 上不是 UTF-8,按 UTF-8 解正好就是乱码的来源。同理,故障特征中英文签名都匹配。实现见source/shared.ts的spawnSpecFor/decodeProcessOutput。
「未安装」列表的来源:
| 来源 | 状态 |
| --- | --- |
| 框架自己的工作区(packages/shared/*) | 已实现 —— 用 file: 说明符安装,装的是工作区里那份源码。只在开发时成立:只有外层仓库确实是本框架的(按根 package.json 的 name 判定)才认,所以这个概念永远不会出现在用户工程里 —— 即使那个工程恰好有个 packages/shared 目录 |
| 远程 npm registry | 已实现 —— 用 npm search keywords:ccgf-shared --json 发现,因此与包 scope 无关(搜索 API 的 scope: 限定符实测无效,keywords: 才是精确匹配)。结果缓存 5 分钟,「刷新」按钮强制重拉;拉取失败只显示提示,不影响面板其余功能。走 npm 而不是自己发 HTTPS,是为了自动带上工程的 .npmrc(registry / 代理 / 认证 / cafile)—— 私有 registry 靠的正是这些 |
公共模块是靠 package.json 里的标记(cocosGameFramework.sharedModule,由 tools/build-shared.js 写入)认出来的,不是靠包名匹配 —— 那样 scope 一改就认不出来了。
装 / 卸之后脚本编译器可能需要一点时间;如果 import 仍提示找不到模块,让编辑器重新编译一次或重启。
预制体模板
生成器的默认产物是一个 100×100 的空根节点。模板只改根节点的尺寸与 cc.Widget 对齐 —— 这正是每个 View 都要手设一遍的重复劳动:
| 模板 | 尺寸 | 对齐(_alignFlags) |
| --- | --- | --- |
| empty | 100×100 | 不加 Widget |
| fullscreen | 1280×720 | 45 = TOP|BOT|LEFT|RIGHT(四边拉伸) |
| popup | 600×400 | 192 = HORIZONTAL|VERTICAL(居中) |
| listitem | 600×80 | 41 = TOP|LEFT|RIGHT(顶部横向拉伸) |
体检的检查项
| 规则 | 级别 | 挡住的问题 |
| --- | --- | --- |
| layers-drift | error | 框架 ELayer 与扩展 LAYERS 不同步(加层漏改一处) |
| view-duplicate-name | error | 跨模块同名 View —— ViewManager 按类名全局查找,会加载错预制体 |
| view-layer-invalid | error | layer 不在框架层级里 |
| view-path-not-found | error | @RegisterView.path 对不上真实预制体 |
| view-script-not-mounted | error | 预制体里没挂该 View 脚本 |
| import-broken | error | 相对导入上溯层数不对 / 目标文件不存在 |
| module-order-collision | warn | order 撞号(实际启动顺序取决于脚本加载顺序,不确定) |
| module-order-missing | warn | 解析不到 order |
| module-model-missing | warn | 有 Module 没 Model |
| view-name-mismatch | warn | 文件名 / @RegisterView.name / @ccclass / 类名 四者不一致 |
| view-without-module | warn | views/ 目录存在但模块不存在 |
| manager-not-singleton | warn | assets/scripts/manager 下的 manager 没继承 ASingleton |
| dangling-script-ref | warn | 场景/预制体引用了已删除的脚本(悬空组件,控制台会持续报 missing) |
体检结果里带文件路径的条目可以点击,直接在编辑器里打开对应资源。
生成器的其它保障
- 失败回滚:建模块是 Module/Model/Events 三个文件的原子操作 —— 任一步失败都会删掉已建的部分;预制体建失败会回滚脚本,不留半成品
- 同名提示明确:先查存在性再动手,报「模块已存在:BagModule.ts(如需重建,请先删除该模块)」,而不是含糊的"可能已存在"
- 跨模块重名拦截:创建 View 前扫全工程,重名直接拒绝并指出被哪个模块占用
- 删除前查引用:删除模块时列出引用了该模块脚本的资源,避免留下悬空引用
- 空目录清理:删除 View/模块后收掉
resources/views/<模块>这类空壳
命令
npm install
npm run build # npx tsc -> dist/
npm test # node --test(94 项)
# 不开编辑器,直接对工程跑一遍体检(开发/排查用)
node tests/manual-doctor.js [工程根目录].ts 改动后必须重新编译并重载扩展才会生效;只改 panel.html 的话重开面板即可,但 index.css 是在模块加载时读取的,同样需要重载扩展。
源码结构
source/
main.ts 主进程:方法注册 + 编辑器 API 调用(唯一的副作用层)
panels/workbench/index.ts Vue3 面板
constants.ts 层级/目录/模板/正则的唯一定义
types.ts 快照与发现的类型
naming.ts 命名归一化(纯函数)
project.ts 工程快照读取(唯一的 I/O 层)
doctor.ts 体检的 12 项检查(纯函数)
templates.ts 脚本与预制体模板(纯函数)
tests/ node --test:naming / templates / doctor / 契约 / i18n
i18n/ zh.js + en.js,key 集合由测试保证一致doctor / naming / templates 都是不碰 fs、不碰 Editor 的纯函数,因此可以被单测完全覆盖 —— 这也是 project.ts 之所以要把 I/O 单独拆出来的原因。
已知限制
- 主进程抛出的错误文案目前只有中文(面板 UI 已完整支持中英切换)
- 预制体模板只生成根节点,不生成子节点骨架(标题/关闭按钮等)
queryModuleRefs的引用检查靠扫描序列化资源文本,没有用asset-db的query-asset-users(后者参数与返回结构未核实)
