11.1 跨引擎对照:Cocos / Godot / Phaser / Defold
前 10 章都在 Cocos 内部往下挖:分层、管线、Effect、物理、动画、内存、原生、定制、架构。这一节把镜头拉远——把前两卷讲过的 Cocos 机制放进一个横向坐标系,看看它在同类引擎里处于什么位置。
先说清楚边界。站内已有 Godot 259 篇、Phaser 127 篇、Defold 47 篇专题,本节不重复它们的教程内容,只做一件事:拿它们当参照系,反推 Cocos 的设计取向。读完你应该能回答「为什么 Cocos 是组件挂载而不是纯节点树」「为什么换引擎时某些概念能直接平移、某些必须重学」。
11.1.1 为什么把对照放在卷三末尾
一个反直觉的现象:刚学引擎的人总在问「哪个引擎最好」,而做过两三个项目的人知道——引擎之间没有绝对优劣,只有设计取舍与你的项目特征是否匹配。要做出这种判断,前提是你得先真正吃透一款引擎的内部机制。否则你只是在比较功能勾选表,而勾选表恰恰是最不可靠的选型依据。
这就是对照放在第 11 章、而不是第 1 章的原因。前面十章的每一节都在给你「Cocos 为什么这么设计」的实证:
- 第 1 章的引擎分层与
cc.d.ts映射,让你知道 API 背后有哪些子系统。 - 第 2–3 章的渲染管线与 Effect 体系,让你理解「一次绘制」是怎么被组织的。
- 第 5、6 章的物理与动画内部,让你看到子系统与主循环的耦合方式。
- 第 7–9 章的内存、原生与定制,让你知道哪些部分「动了就要付代价」。
有了这套内部视角,再看别的引擎,你看到的就不再是「它有什么功能」,而是「它做了哪些和我熟悉的引擎不一样的选择,以及那些选择会带来什么后果」。
还有一层更实际的理由:迁移成本几乎总是被低估。团队换引擎时最常见的翻车,不是「新引擎功能不够」,而是「以为能直接平移、结果每个子系统都要重写」。对照表能帮你把「哪些能平移、哪些要重写」提前标出来——这正是本节和下一节要交付的东西。
11.1.2 四种核心设计取向
四款引擎最本质的差异,不在功能多少,而在对象模型和脚本语言这两件事上。它们决定了你写代码的手感,也决定了迁移时的摩擦大小。
| 维度 | Cocos Creator | Godot | Phaser | Defold |
|---|---|---|---|---|
| 对象模型 | Node + Component,功能通过组件挂到节点上 | 一切皆 Node,节点自身即功能,节点组成 Scene 树 | Scene + GameObject,游戏对象是普通 JS 对象 | GameObject + Component,组件类型由引擎固定提供 |
| 脚本语言 | TypeScript / JavaScript | GDScript(Python 风格),另有 C# 与 C++ 扩展 | JavaScript / TypeScript | Lua,另有 C / C++ 原生扩展 |
| 2D / 3D 侧重 | 同一引擎兼顾 2D 与 3D | 同一引擎兼顾 2D 与 3D,2D 走独立管线 | 专注 2D(WebGL / Canvas) | 兼顾 2D 与 3D,2D 是主战场 |
| 导出目标 | H5、微信/字节等小游戏、Android、iOS、桌面 | 桌面、移动、Web(官方导出模板) | 浏览器,可被 WebView 壳承载 | 桌面、移动、Web、主机(需申请) |
| 授权与生态 | 商业公司维护,中文社区强 | MIT 开源,社区驱动 | MIT 开源,Web 生态成熟 | 免费,King 系背景,原生扩展生态 |
这张表里最值得琢磨的是第一行。Godot 走的是「节点即功能」:一个 Sprite2D 节点本身就负责绘制,你要给它加碰撞就再加一个碰撞节点挂在下面。Cocos 走的是「节点是容器、组件是功能」:一个空的 Node 本身什么都不做,你给它挂 Sprite 才绘制、挂 RigidBody2D 才有物理。Defold 更收敛:组件类型是引擎预先定义好的固定集合(sprite、collision object、script 等),你不能像 Cocos 那样自由写一个自定义 Component 类再挂上去,扩展要靠原生扩展。
理解这条差异,你就理解了为什么 Cocos 的脚本里到处是 @ccclass 和 @property——那正是「自定义组件」这套模型的语法入口。
脚本语言这一行同样影响深远。它决定了你能复用多少现有的工程能力:类型检查、静态分析、包管理、单元测试。
| 能力 | Cocos(TS/JS) | Godot(GDScript 为主) | Phaser(JS/TS) | Defold(Lua) |
|---|---|---|---|---|
| 静态类型 | 有(TypeScript 完整类型系统) | GDScript 有可选类型标注,弱于 TS | 有(选 TS 时) | 无(Lua 动态类型) |
| 生态包管理 | npm 生态可直接用 | 需走 Godot 自己的插件体系 | npm 生态可直接用 | LuaRocks 与原生扩展 |
| 单元测试 | vitest / jest 成熟 | GUT 等第三方框架 | vitest / jest 成熟 | 社区方案,较弱 |
| IDE 支持 | VS Code + 官方类型声明 | 官方编辑器内置 | VS Code 良好 | 官方编辑器内置 |
对第 10 章讲过的工程化话题(规范、静态检查、依赖管理)来说,这条差异是决定性的:Cocos 与 Phaser 直接继承了前端工程体系,你在卷三前 10 章学到的 ESLint、tsc、模块化拆分几乎零成本复用;而 GDScript 与 Lua 则需要另起一套工具链。
11.1.3 同一件事,四种写法:生命周期
无论哪款引擎,一个游戏对象都要经历「创建 → 进入场景 → 每帧更新 → 销毁」。四款引擎对这条链的切分方式不同,但都能一一对应。
这张对照表看起来平淡,却是迁移时最先要背下来的一张——因为生命周期回调写错,症状往往是「对象偶尔不更新」这种最难查的间歇性 bug。
| 阶段 | Cocos Creator | Godot | Phaser | Defold |
|---|---|---|---|---|
| 对象创建 | onLoad | _init / _ready | constructor | init |
| 进入场景 | start | _ready | create | init 之后交棒给 update |
| 每帧更新 | update(dt) | _process(delta) | update(time, delta) | update(dt) |
| 物理步进 | 物理系统固定步长驱动 | _physics_process(delta) | Arcade / Matter 的物理更新 | fixed_update |
| 销毁 | onDestroy | _exit_tree / queue_free | destroy 与 SHUTDOWN 事件 | final |
注意一个容易踩的点:Cocos 的 update(dt) 参数是秒,Godot 的 _process(delta) 也是秒,而 Phaser 的 update 回调给的是「累计毫秒 + 本帧毫秒」两个值。迁移时如果直接把帧间隔当秒用,速度会差三个数量级——这类坑在横向对照里才看得清。
另一个差异在初始化与就绪的拆分。Cocos 明确分成 onLoad(对象已构造、父子关系已建立)与 start(首次激活前、同帧内所有组件都已 onLoad 之后),给你一个「先收集、再统一初始化」的窗口。Godot 用 _ready 一次性覆盖,但保证子节点先于父节点就绪;Defold 的 init 之后立刻进入 update,没有额外的「就绪帧」。当你写依赖顺序的初始化逻辑时,这套拆分方式直接决定了代码能不能写对。
11.1.4 把卷二讲过的 Cocos 机制放进坐标系
下面四项都在卷二出现过具体做法,这里只补「别的引擎怎么处理同一件事」,帮你建立映射感。之所以挑这四项,是因为它们恰好覆盖了「资源、绘制、模拟、分发」四条主干——任何一款引擎都绕不开,而各自的处理方式又最能反映它的设计哲学。
读这四项时,请带着一个固定问题:「这件事在 Cocos 里我是怎么做的,换个引擎要付多少重写成本?」 这个问题的答案,就是下一节迁移成本估算的原始数据。
资源加载。 Cocos 用 resources 与 assetManager 配合 Bundle 做分包(见卷二 4.2、4.3)。Godot 用 Resource 配合 preload / load,资源路径本身就是标识;Phaser 习惯在 Scene 的 preload 阶段集中加载;Defold 把资源预先打进 collection 与 atlas,运行时靠工厂(factory)实例化。Cocos 的 Bundle 更接近 Godot 的 PCK 分包,而不是 Phaser 的按需 HTTP 拉取。
合批。 第 2.3 节讲过 Cocos 的合批为什么会被打断。在 3.8.8 的源码树里,2D 合批由 cocos/2d/renderer/batcher-2d.ts 承担(tag 3.8.8)。换到别的引擎你会发现打断条件是相通的:Godot 2D 有自身的批处理,但切换材质或纹理同样会打断;Phaser 的 WebGL 渲染器按纹理组织批次;Defold 干脆用图集把同图纹理合并,本质还是「同纹理才合批」。所以你在 Cocos 里学到的合批直觉,几乎可以原样迁移。
物理。 Cocos 2D 可切换内置物理后端;Godot 内置 2D 与 3D 物理;Phaser 提供 Arcade 与 Matter 两套(Arcade 轻、Matter 真);Defold 内置的 2D 物理属 box2d 系。选哪套引擎,很多时候是在选它的物理表现——这点在第 5 章讲后端差异时已经埋了伏笔。
分包与热更。 Cocos 用 Bundle 组织可独立加载的资源组。Godot 用资源包,Phaser 靠网络层自己管,Defold 用 collection proxy 做动态加载。Cocos 在这件事上给得比较「重」——Bundle 是一等公民,代价是你要理解它的加载与释放时机(这正是第 7 章内存话题的一部分)。
把上面四项压成一张映射表,方便你换引擎时逐条对照:
| 你熟悉的概念(Cocos) | Godot 里的对应物 | Phaser 里的对应物 | Defold 里的对应物 |
|---|---|---|---|
Node 场景树 | Node / Node2D | Scene 与 Container | GameObject 与 collection |
Component | 节点自身 + 子节点 | GameObject 上的方法 | 引擎预定义的组件类型 |
resources / Bundle | Resource / PCK | Scene 的 preload 阶段 | collection 与 atlas |
| 自动合批 | 2D 批处理 | WebGL 渲染器按纹理批次 | atlas 合并 |
RigidBody2D / Collider2D | RigidBody2D / CollisionShape2D | Arcade / Matter 物理体 | 内置 2D 物理组件 |
这张表最重要的作用是暴露「没有对应物」的情况。比如 Cocos 的 Bundle 在 Phaser 里就没有直接等价物,你得自己在网络层实现;Godot 的「节点即功能」在 Cocos 里也没有——你只能靠组件组合去逼近。凡是对照表里某一格是空的,那里就是迁移时最需要重新设计的地方。
11.1.5 L2:Cocos 里的组件与宿主信息(可类型检查)
下面这段是标准 Cocos 组件:@ccclass 声明自定义组件、@property 暴露给编辑器、生命周期回调按上表顺序执行。它同时读取了 sys 上的平台信息——这正是「同一份代码跑在 H5 / 微信 / 原生」时判断分支的入口。所有符号都可在 cc.d.ts(3.8.8)中核到。
import { _decorator, Component, Node, Vec3, sys, director } from 'cc';
const { ccclass, property, menu } = _decorator;
@ccclass('CrossEngineProbe')
@menu('BlockDash/Diagnostics/CrossEngineProbe')
export class CrossEngineProbe extends Component {
@property(Node)
public target: Node | null = null;
@property
public speed = 120;
private _elapsed = 0;
onLoad(): void {
// 宿主信息:平台、是否原生、是否浏览器
console.log('[probe] platform=', sys.platform, 'native=', sys.isNative, 'browser=', sys.isBrowser);
}
start(): void {
if (this.target) {
this.target.setPosition(new Vec3(0, 0, 0));
}
// 与 Defold 的 update、Godot 的 _process 等价:按固定间隔驱动
this.schedule(this.tick, 1 / 60, 60);
}
update(dt: number): void {
// Cocos 的 dt 单位是秒,和 Godot 的 delta 一致;Phaser 给的是毫秒
this._elapsed += dt;
}
private tick(): void {
const scene = director.getScene();
const canvas = scene ? scene.getChildByName('Canvas') : null;
void canvas;
}
onDestroy(): void {
this.unschedule(this.tick);
}
}
这段代码里没有任何「跨引擎抽象」——它就是纯 Cocos 写法。放在这里的意义是:当你换到 Godot 时,onLoad 要改成 _ready、schedule 要改成 Timer 或累加计时、director.getScene() 要改成 get_tree().current_scene。每一处映射你都能指出来,因为你已经知道 Cocos 这一侧在做什么。
还要注意 sys.platform 这类宿主信息在别的引擎里的形态:Godot 用 OS.get_name() 与 OS.has_feature(),Phaser 用 navigator.userAgent 之类的浏览器能力探测,Defold 用 sys.get_sys_info()。它们解决的是同一个问题——「这份代码现在跑在哪里」,但 API 形态完全不同。 这也是为什么「平台分支」通常要包一层自己的适配层,而不是散落在业务代码里。
11.1.6 L1:引擎无关的生命周期状态机(可直接单测)
横向对照里最容易讲错的一点,是把四款引擎的生命周期说成「差不多」。其实它们的顺序约束是可以被形式化的。下面这段是纯 TypeScript,不依赖任何引擎,可以直接用 vitest 断言——它把「创建 → 就绪 → 活跃 → 销毁」这条链写成状态机,任何引擎只要回调顺序与之不符,单测就会失败。
// 引擎无关:把生命周期回调抽象成一条可断言的状态机
type Phase = 'created' | 'ready' | 'active' | 'destroyed';
const ORDER: readonly Phase[] = ['created', 'ready', 'active', 'destroyed'];
const NEXT: Record<Phase, Phase | null> = {
created: 'ready',
ready: 'active',
active: 'destroyed',
destroyed: null,
};
export class LifecycleTrace {
private phase: Phase = 'created';
private readonly log: Phase[] = ['created'];
// 每次 step 前进一个阶段;到达 destroyed 后不再变化
step(): Phase {
const next = NEXT[this.phase];
if (next !== null) {
this.phase = next;
this.log.push(next);
}
return this.phase;
}
advance(frames: number): readonly Phase[] {
for (let i = 0; i < frames; i += 1) {
this.step();
}
return this.log;
}
}
export function assertLifecycleOrder(): void {
const trace = new LifecycleTrace();
trace.advance(4);
if (JSON.stringify(trace.advance(0)) !== JSON.stringify(ORDER)) {
throw new Error('lifecycle order mismatch');
}
}
这段代码的价值不在它多复杂,而在于它示范了一种方法:把「引擎行为」抽成「可断言的不变量」,再用单测钉住。当你在 Cocos 里怀疑「start 会不会在 onLoad 之前跑」时,与其翻文档,不如写这样一个状态机把预期固定下来。
这类「引擎无关」的代码,在迁移时也是资产。状态机的顺序约束不随引擎改变,换到 Godot 或 Phaser 后你仍然可以复用它做回归验证——能被抽出来的逻辑越多,迁移时要重写的部分就越少。这也是下一节估算迁移成本时的一条基本准则。
11.1.7 别把对照表当功能清单
横向对照最常见的误用,是拿它当「功能勾选表」:哪款引擎功能多就选哪款。这几乎必然选错。原因是功能多不等于适合你——一个只需要 H5 2D 的项目,Godot 的 3D 能力是负担而非优势;一个要发微信小游戏的项目,Phaser 再成熟也过不了小游戏引擎的适配关。
正确的用法是反过来:先列出你的项目约束(目标平台、包体上限、联机需求、团队语言栈),再看每款引擎在这条约束上的代价。 这张表只负责帮你定位「差异在哪」,不负责替你下结论。下一节 11.2 会把「按约束做决策」这套方法展开成决策树,并给出迁移成本的估算方式。
作为直觉锚点,下面是四款引擎各自的「甜点区」——不是绝对结论,而是社区里最常见的匹配形态:
| 引擎 | 常见甜点区 | 典型不匹配场景 |
|---|---|---|
| Cocos Creator | 需要同时发 H5、微信小游戏与原生的 2D 项目,团队有前端背景 | 重度 3D、需要极深自定义渲染管线 |
| Godot | 桌面 / 独立游戏、想要开源可控、愿意学 GDScript | 必须发微信小游戏、团队完全无 3D 需求 |
| Phaser | 纯 Web 2D 小游戏、原型验证、Web 生态集成 | 需要原生打包、需要重度 3D |
| Defold | 2D 移动游戏、对包体与性能极敏感、接受 Lua | 重度 3D、需要丰富的第三方 UI 组件 |
看出规律了吗?每一款引擎的「不匹配场景」,几乎都对应它在 11.1.2 那张表里的某个取舍。 Cocos 与 Phaser 的取舍在「多端 vs 纯 Web」,Godot 与 Defold 的取舍在「开源通用 vs 轻量专精」。把取舍记牢,比记功能清单有用得多。
一句话总结这一节:Cocos 的设计取向是「节点当容器、组件当功能、Bundle 当资源一等公民、多端发布当默认能力」。记住这四个词,再看别的引擎,差异就一目了然了。
对照的价值从来不是「证明 Cocos 更好」,而是「让你知道自己为什么选了它」。当你能说清「我选 Cocos 是因为要一发三端、团队是前端背景、且 2D 够用」,你就不会因为别人说某引擎更酷而动摇;反过来,当你发现某个项目其实完全不需要多端、也不需要 Bundle,你也会知道这时候 Cocos 的「重」是纯负担。
把这份判断写成表格存档,下次团队争论选型时,它就是最省事的证据。
下一节就把这套判断变成可操作的方法:按项目特征走决策树,再逐项估算迁移工程量。
阅读导航:上一节:目录 · 下一节:11.2 选型方法与迁移成本 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。