《Cocos 游戏开发高级编程》9.1 拉取与编译引擎源码

本节从 GitHub cocos/cocos-engine 的 v3.8.8 tag 拉取源码讲起,覆盖仓库四层目录、native external 第三方库的三种获取方式、Node 与 gulp 依赖安装、编辑器偏好设置里的自定义引擎接入,以及 TypeScript 引擎与原生引擎各自的编译路径和产物 bin 目录,并给出一个可类型检查的运行时版本核对脚本。

9.1 拉取与编译引擎源码

前面八章都在「用」引擎:读它的类型声明、猜它的行为、量它的开销。到这一章,视角换一次——把引擎源码拉到本地,亲手编译一份属于自己的引擎。 这是卷三与卷一、卷二最本质的分界:卷一、卷二你始终是引擎的调用者,从这一节开始,你成为引擎的维护者之一。

先划清分工。站内 saas/game-engine/ 那批文章讲的是「自研引擎的商业计划与图形学选型」,本卷不重讲那套;本节要回答的是一个很具体的问题——当你确实需要改动引擎时,怎么从官方仓库拉一份 3.8.8 的源码、把它编译出来、并让编辑器用上它。

与站内 graphics/ 那 45 篇的分工:那些文章讲的是渲染管线、着色器的通用理论;本节讲的是 Cocos 引擎仓库的具体目录、构建脚本与自定义引擎的接入步骤,所有路径都能在 cocos/cocos-engine 的 v3.8.8 tag 里核到。

9.1.1 什么情况下才需要从源码构建

从源码构建是有成本的:多出几百兆的 external、多一套 Node 构建链、多一个「编辑器重启后才生效」的环节,而且每次升级 Creator 都要跟着重新拉一遍。所以第一件事是判断你到底需不需要。

你的诉求内置引擎能不能满足要不要源码
调参数、换材质、改 Effect能不需要
想减小包体、裁掉用不到的模块项目设置里的功能裁剪面板就能做通常不需要,但自定义引擎更激进
改一段引擎实现(比如给某个组件加约束)不能需要
跟进上游尚未进稳定版的安全/性能补丁不能需要
只想换引擎版本换 Creator 版本即可不需要

结论是:大多数项目一辈子都不需要编译引擎。 本节面向的是「确实要动源码」的那部分需求——也正因为少,动手前更要把版本、路径、产物目录这些确定下来,避免团队里只有一个人知道这份引擎是怎么来的。

还有一层隐性成本要算进去:自定义引擎会改变「可复现性」。 官方内置引擎是 Creator 的一部分,任何人装上同一个版本就能得到完全一致的结果;而自定义引擎多了一份「你们自己维护的源码 + 补丁」,一旦某个成员本地引擎路径没接对,就会出现「同一个工程、两台机器表现不一致」的问题。所以真正决定走这条路之前,先问一句:这个问题能不能用面板、用脚本、用 Effect 绕过去?绕不过去,再动源码。

9.1.2 动手前的三条硬约束

在动手前把下面三条钉死,后面每一步都围绕它们展开:

  1. 版本三对齐:Creator 版本、引擎 tag、external checkout 三者必须指向同一个版本号(这里是 3.8.8)。
  2. 路径可移植:引擎路径尽量写进项目配置而非全局配置,保证团队拉代码后行为一致。
  3. 产物可复现:明确「编辑器编译引擎」与「命令行 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,再生成类型声明
buildbuild: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 自定义引擎与模块裁剪 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「cocos」更多文章

  1. 《Cocos 游戏联机实战》9.3 延迟补偿与体验权衡
  2. 《Cocos 游戏联机实战》9.2 网络状态 UI 与提示
  3. 《Cocos 游戏联机实战》9.1 同步表现与插值动画