《Cocos 游戏开发高级编程》11.1 跨引擎对照:Cocos / Godot / Phaser / Defold

把前 10 章讲的 Cocos 机制放进横向坐标系:对照 Cocos Creator、Godot、Phaser、Defold 四款引擎在对象模型、脚本语言、2D/3D 侧重、导出目标与生态上的核心差异,给出生命周期、资源加载、合批与物理的逐项对照表,并用一段可类型检查的组件脚本和一段可直接单测的引擎无关状态机,说明横向对照该用在哪、不该用在哪。

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 CreatorGodotPhaserDefold
对象模型Node + Component,功能通过组件挂到节点上一切皆 Node,节点自身即功能,节点组成 Scene 树Scene + GameObject,游戏对象是普通 JS 对象GameObject + Component,组件类型由引擎固定提供
脚本语言TypeScript / JavaScriptGDScript(Python 风格),另有 C# 与 C++ 扩展JavaScript / TypeScriptLua,另有 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 CreatorGodotPhaserDefold
对象创建onLoad_init / _readyconstructorinit
进入场景start_readycreateinit 之后交棒给 update
每帧更新update(dt)_process(delta)update(time, delta)update(dt)
物理步进物理系统固定步长驱动_physics_process(delta)Arcade / Matter 的物理更新fixed_update
销毁onDestroy_exit_tree / queue_freedestroy 与 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 / Node2DScene 与 ContainerGameObject 与 collection
Component节点自身 + 子节点GameObject 上的方法引擎预定义的组件类型
resources / BundleResource / PCKScene 的 preload 阶段collection 与 atlas
自动合批2D 批处理WebGL 渲染器按纹理批次atlas 合并
RigidBody2D / Collider2DRigidBody2D / CollisionShape2DArcade / 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
Defold2D 移动游戏、对包体与性能极敏感、接受 Lua重度 3D、需要丰富的第三方 UI 组件

看出规律了吗?每一款引擎的「不匹配场景」,几乎都对应它在 11.1.2 那张表里的某个取舍。 Cocos 与 Phaser 的取舍在「多端 vs 纯 Web」,Godot 与 Defold 的取舍在「开源通用 vs 轻量专精」。把取舍记牢,比记功能清单有用得多。

一句话总结这一节:Cocos 的设计取向是「节点当容器、组件当功能、Bundle 当资源一等公民、多端发布当默认能力」。记住这四个词,再看别的引擎,差异就一目了然了。

对照的价值从来不是「证明 Cocos 更好」,而是「让你知道自己为什么选了它」。当你能说清「我选 Cocos 是因为要一发三端、团队是前端背景、且 2D 够用」,你就不会因为别人说某引擎更酷而动摇;反过来,当你发现某个项目其实完全不需要多端、也不需要 Bundle,你也会知道这时候 Cocos 的「重」是纯负担。

把这份判断写成表格存档,下次团队争论选型时,它就是最省事的证据。

下一节就把这套判断变成可操作的方法:按项目特征走决策树,再逐项估算迁移工程量。

阅读导航:上一节:目录 · 下一节:11.2 选型方法与迁移成本 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「cocos」更多文章

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