9.1 拉取与编译引擎源码
前面八章都在「用」引擎:读它的类型声明、猜它的行为、量它的开销。到这一章,视角换一次——把引擎源码拉到本地,亲手编译一份属于自己的引擎。 这是卷三与卷一、卷二最本质的分界:卷一、卷二你始终是引擎的调用者,从这一节开始,你成为引擎的维护者之一。
先划清分工。站内 saas/game-engine/ 那批文章讲的是「自研引擎的商业计划与图形学选型」,本卷不重讲那套;本节要回答的是一个很具体的问题——当你确实需要改动引擎时,怎么从官方仓库拉一份 3.8.8 的源码、把它编译出来、并让编辑器用上它。
与站内
graphics/那 45 篇的分工:那些文章讲的是渲染管线、着色器的通用理论;本节讲的是 Cocos 引擎仓库的具体目录、构建脚本与自定义引擎的接入步骤,所有路径都能在cocos/cocos-engine的v3.8.8tag 里核到。
9.1.1 什么情况下才需要从源码构建
从源码构建是有成本的:多出几百兆的 external、多一套 Node 构建链、多一个「编辑器重启后才生效」的环节,而且每次升级 Creator 都要跟着重新拉一遍。所以第一件事是判断你到底需不需要。
| 你的诉求 | 内置引擎能不能满足 | 要不要源码 |
|---|---|---|
| 调参数、换材质、改 Effect | 能 | 不需要 |
| 想减小包体、裁掉用不到的模块 | 项目设置里的功能裁剪面板就能做 | 通常不需要,但自定义引擎更激进 |
| 改一段引擎实现(比如给某个组件加约束) | 不能 | 需要 |
| 跟进上游尚未进稳定版的安全/性能补丁 | 不能 | 需要 |
| 只想换引擎版本 | 换 Creator 版本即可 | 不需要 |
结论是:大多数项目一辈子都不需要编译引擎。 本节面向的是「确实要动源码」的那部分需求——也正因为少,动手前更要把版本、路径、产物目录这些确定下来,避免团队里只有一个人知道这份引擎是怎么来的。
还有一层隐性成本要算进去:自定义引擎会改变「可复现性」。 官方内置引擎是 Creator 的一部分,任何人装上同一个版本就能得到完全一致的结果;而自定义引擎多了一份「你们自己维护的源码 + 补丁」,一旦某个成员本地引擎路径没接对,就会出现「同一个工程、两台机器表现不一致」的问题。所以真正决定走这条路之前,先问一句:这个问题能不能用面板、用脚本、用 Effect 绕过去?绕不过去,再动源码。
9.1.2 动手前的三条硬约束
在动手前把下面三条钉死,后面每一步都围绕它们展开:
- 版本三对齐:Creator 版本、引擎 tag、
externalcheckout 三者必须指向同一个版本号(这里是 3.8.8)。 - 路径可移植:引擎路径尽量写进项目配置而非全局配置,保证团队拉代码后行为一致。
- 产物可复现:明确「编辑器编译引擎」与「命令行
npm run build」是两条不同的路径,日常只走前者。
9.1.3 仓库长什么样:四层目录
Cocos 引擎仓库有两个内核:C++ 内核用于原生平台,TypeScript 内核用于 Web 与小游戏平台;在内核之上还有一层用 TypeScript 写的框架层,用来抹平两个内核的差异。对应到目录,就是下面这几块(路径均以 cocos/cocos-engine tag v3.8.8 为准):
| 目录 / 文件 | 内容 | 说明 |
|---|---|---|
cocos/ | TypeScript 框架与子系统源码 | 下分 2d/ 3d/ animation/ physics/ physics-2d/ rendering/ core/ game/ ui/ tween/ 等 |
native/ | C++ 内核与 CMake 工程 | 含 CMakeLists.txt、native/external-config.json,external 依赖单独拉取 |
scripts/ | 构建脚本 | build-h5-minified.js、build-h5-source.js、build-declarations.js 等 |
pal/ | 平台抽象层(Platform Abstraction Layer) | 把不同平台的差异收口 |
cc.config.json | 功能与模块清单 | 定义 features 与模块依赖,功能裁剪读的就是它 |
package.json | 版本与构建脚本入口 | version 字段就是 3.8.8,engines.node 要求 >=18 |
值得先记住的是 cc.config.json——它把「功能(feature)」映射到「模块(module)」,并声明 dependentModules 依赖。9.2 讲裁剪时会回到它,这里只需要知道:裁剪不是编辑器拍脑袋,而是按这份清单做的闭包计算。
9.1.4 拉取源码并锁定版本
从 GitHub 拉取,然后切到与 Creator 版本匹配的 tag。官方文档强调「不同的 Cocos Creator 版本需要对应不同的引擎分支」,所以一定要切 tag,不要停在 develop。
git clone https://github.com/cocos/cocos-engine.git
cd cocos-engine
git fetch --tags
# Creator 用的是 3.8.8,就切到对应的 tag
git checkout v3.8.8
切完之后第一件事是核对版本,避免拉错分支这种低级错误。仓库根 package.json 的 version 字段应当与你的 Creator 一致:
node -e "console.log(require('./package.json').version)"
# 3.8.8
这一步值得写进团队的接入文档,因为「引擎源码版本与 Creator 版本不一致」是自定义引擎最常见的翻车点之一。它属于纯逻辑校验,可以抽成一段与引擎无关的代码,直接用 vitest 覆盖:
// src/core/engine-version.ts —— 与引擎无关,可直接单测
export interface VersionMismatch {
readonly item: string;
readonly expected: string;
readonly actual: string;
}
export function checkVersionConsistency(input: {
readonly creator: string;
readonly enginePackage: string;
readonly externalCheckout: string;
}): VersionMismatch[] {
const out: VersionMismatch[] = [];
const want = input.creator;
if (input.enginePackage !== want) {
out.push({ item: 'engine/package.json', expected: want, actual: input.enginePackage });
}
// external 的 checkout 形如 v3.8.8-2,去掉前缀与后缀后主版本段必须与 Creator 对齐
const externalMain = input.externalCheckout.replace(/^v/, '').split('-')[0];
if (externalMain !== want) {
out.push({ item: 'native/external', expected: want, actual: input.externalCheckout });
}
return out;
}
9.1.5 native external:源码包不带的那几百兆
如果你只改 TypeScript 层(UI、动画、tween 这类),可以跳过本节;但只要涉及原生平台,就必须补上 external。原因是 C++ 内核依赖的一批第三方库有几百兆,默认不在源码包里,需要单独获取。
要拿哪个版本,看 native/external-config.json 的 checkout 字段——3.8.8 对应的是:
{
"from": {
"type": "github",
"owner": "cocos",
"name": "cocos-engine-external",
"checkout": "v3.8.8-2"
}
}
官方文档给了三种获取方式,按你的网络环境挑一种:
# 方式一:git 命令行(tag 取自 external-config.json 的 checkout 字段)
cd cocos-engine/native
git clone https://github.com/cocos/cocos-engine-external external
cd external
git checkout -b 3.8.8-2 v3.8.8-2
# 方式二:用引擎自带脚本下载(只拉当前 tag 对应的第三方库)
cd cocos-engine/native
npm install
gulp init
第三种是直接在 cocos-engine-external 仓库页面下载 ZIP,解压后重命名为 external 放进 cocos-engine/native/。无论哪种方式,external 的版本必须与 cocos-engine 一致,否则编译阶段就会失败。
9.1.6 安装依赖与编译 TypeScript 引擎
依赖只需要 Node 与 gulp。在引擎根目录执行:
# 全局装 gulp 构建工具
npm install -g gulp
# 安装引擎自身的构建依赖
npm install
package.json 里的构建脚本是理解编译产物的一把钥匙:
| 脚本 | 作用 |
|---|---|
build:min | 先产出调试信息,再走 build-h5-minified 生成压缩后的引擎包 |
build:declaration | 先 build:const,再生成类型声明 |
build | build:min + build:declaration,是完整的一次构建 |
build:dev | 产出调试信息后走 build-h5-source,生成未压缩、便于断点的引擎包 |
但日常改完 TS 源码,不需要手动跑这些脚本。在编辑器里点菜单栏的 开发者 -> 编译引擎,编辑器会把源码编译到引擎目录下的 bin 文件夹:
cocos-engine/
└── bin/ # 编译产物,编辑器加载的就是这里
需 Cocos Creator 3.8 编辑器打开,本节不可命令行复现:「开发者 -> 编译引擎」是编辑器菜单项,它读取当前自定义引擎路径并调用内置构建链,产物落在
bin/。命令行里npm run build产出的是另一套发布用的包,两者不要混淆。
一个容易混的点:引擎目录下的 bin/ 是引擎自己的编译产物,工程目录下的 build/ 才是《方块冲刺》的构建产物。 前者是「引擎」,后者是「游戏」,两者层级完全不同:
cocos-engine/ # 引擎仓库
├── bin/ # 编译引擎的产物(编辑器加载这里)
├── cocos/ # TS 源码
└── native/ # C++ 内核
block-dash/ # 游戏工程
├── assets/ # 游戏资源与脚本
├── settings/v2/packages # 工程设置(含引擎相关配置)
└── build/ # 构建游戏的产物(H5 / 微信 / 原生)
记住这条:改引擎改的是 cocos-engine/,改游戏改的是 block-dash/。 把两者混在一起,是排查问题时最容易走偏的一步。
9.1.7 让编辑器用上自定义引擎
编译只是「造出来了」,还要让编辑器「用上它」。这一步在 Cocos Creator -> 偏好设置 -> 引擎管理器 里完成:
- 默认是
Use Built-in(内置引擎)。切到Use Custom,把路径指向引擎源码根目录。 - 如果还要定制原生(C++)部分,勾上
Native Module下的Use Custom,编辑器会自动识别引擎根目录下的native/。 - 修改引擎路径后必须重启编辑器才能生效。
需 Cocos Creator 3.8 编辑器打开,本节不可命令行复现:以上全部是偏好设置面板里的操作,没有对应的命令行入口。重启后,连场景编辑器也会改用这份自定义引擎渲染——这意味着你的改动会影响编辑时的所见即所得,改坏了编辑器里就会立刻表现异常。
还有个默认行为要留意:自定义引擎的路径默认保存为全局配置,会影响所有使用同一版本 Creator 的项目。如果你只想让 block-dash 用这份引擎,记得在「引擎管理器」里把存储位置改成项目级,否则同事拉下代码后会莫名共享到你的实验引擎。
9.1.8 常见失败点与排查
自定义引擎的报错往往不在编译阶段,而在「接上之后」才暴露。下面几种最典型:
| 现象 | 大概率原因 | 处理 |
|---|---|---|
| 编辑器打开工程就报错、场景空白 | 自定义引擎路径接上了,但 bin/ 没编译或编译失败 | 回到「开发者 -> 编译引擎」,看构建输出里的第一条错误 |
| 原生构建时 CMake 报找不到第三方库 | native/external 缺失或版本与 external-config.json 不一致 | 按 9.1.4 重新拉取 external,核对 checkout |
| 改了源码,编辑器里毫无变化 | 改完没重新「编译引擎」,或改了编辑器没重启 | 重新编译 + 重启编辑器 |
| 同事拉下代码后行为不一致 | 引擎路径存的是全局配置,各自指向不同源码 | 改成项目级存储,把路径纳入版本控制 |
命令行 npm run build 成功但编辑器仍用旧引擎 | 两套产物不是一回事 | 日常认准「编译引擎」这条路径 |
一个实用习惯:每次改完引擎,先跑一次 tsc --noEmit(或让编辑器编译)确认类型通过,再打开工程看表现。 类型错误往往在「编译引擎」时就会拦住,比运行时报错好定位得多。
9.1.9 运行时核对:一段能过类型检查的脚本
源码编译成功、编辑器也接上了,最后补一道运行时自检:在游戏启动早期打一行引擎与平台信息,方便真机上排查「到底加载的是哪份引擎」。下面这段是 L2 组件脚本,用到的 settings、SettingsCategory、sys 都查过 cc.d.ts:
import { _decorator, Component, settings, SettingsCategory, sys } from 'cc';
const { ccclass } = _decorator;
@ccclass('EngineInfoProbe')
export class EngineInfoProbe extends Component {
start(): void {
// 引擎分组里的 debug 标记,能区分发布版与调试版引擎
const debug = settings.querySettings<boolean>(SettingsCategory.ENGINE, 'debug');
console.log('[engine] debug =', debug);
console.log('[engine] platform =', sys.platform, 'native =', sys.isNative);
console.log('[engine] os =', sys.os, sys.osVersion);
}
}
它的价值不在信息本身,而在于把「引擎身份」变成可观测的:自定义引擎一旦没接上(比如同事没重启编辑器),这行日志会立刻暴露差异。注意 sys.platform 比较一律用 sys.Platform.* 枚举成员,不要用 sys.isMobile 这类近似值——原因在卷二 5.1 已经讲过。
9.1.10 工程决策
把这一节收成几条能直接写进团队规范的话:
- 默认不编译引擎。 能用功能裁剪面板解决的(减包体),优先用面板;只有真要改实现或跟补丁时才走源码。
- 版本必须锁死。 引擎 tag、
external的 checkout、Creator 版本三者对齐,写进 README,并用 9.1.3 那段校验函数在 CI 里跑一遍。 - 接入路径项目级优先。 避免全局自定义引擎「污染」同版本的其他项目。
- 改动要留痕。 自定义引擎一旦偏离官方 tag,就必须维护一份补丁清单,否则 9.3 讲升级时会无从下手。
下一节把这份源码真正用起来:怎么在功能裁剪面板里删掉用不到的模块、怎么在源码里改一段实现并让编辑器加载编译后的引擎。
阅读导航:上一节:目录 · 下一节:9.2 自定义引擎与模块裁剪 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。