《Cocos 游戏开发入门》18.1 同一款游戏的两套实现对照

把《方块冲刺》的 mini2d 版与 Cocos Creator 3.8 版并排放在一起,逐项对照帧循环、渲染、输入、物理、动画、资源、UI、存档与构建九组实现,用两张总表回答哪些活是引擎替你干的、哪些必须自己干,作为全卷方法论收口,并衔接迁移清单与实战卷。

18.1 同一款游戏的两套实现对照

第 17 章我们把 mini2d 版的《方块冲刺》打成了可发布的产物。从第 1 章的第一行 requestAnimationFrame 到现在,你已经亲手写出了一个完整的 2D 运行时。这一节做一件别处很难做的事:把同一款游戏的 mini2d 版和 Cocos Creator 3.8 版并排放在一起,逐项对照。

本节是全卷的方法论收口。前面 17 章每章都在做「mini2d 怎么写 + Cocos 怎么做」的对照,这一节把九组对照汇总成两张总表,并回答那个贯穿全书的问题:哪些活是引擎替你干的,哪些活是你必须自己干的。

18.1.1 两套实现的骨架并排

mini2d 版每个模块都对应第 2–17 章的某一节:

mini2d-game/                     # 纯 TS,零 cc 依赖
├── src/core/       loop.ts mat2d.ts node.ts component.ts   # 第 1、3、6 章
├── src/render/     canvas2d.ts atlas.ts render-tree.ts     # 第 2、3 章
├── src/input/      pointer.ts gesture.ts buffer.ts         # 第 5 章
├── src/physics/    aabb.ts broadphase.ts response.ts       # 第 7 章
├── src/anim/       flipbook.ts tween.ts                    # 第 8 章
├── src/assets/     manifest.ts loader.ts refcount.ts       # 第 10 章
├── src/ui/         layout.ts vlist.ts popup.ts             # 第 11 章
├── src/save/       codec.ts migrate.ts                     # 第 12 章
├── src/game/       player.ts coin.ts level.ts              # 《方块冲刺》玩法
└── test/           *.test.ts                               # 第 16 章

Cocos 版是引擎工程,assets/ 里是场景、预制体与挂在节点上的脚本:

cocos-dash/                      # Cocos Creator 3.8 工程
├── assets/scenes/     main.scene level.scene              # 场景(编辑器创建)
├── assets/prefabs/    player.prefab coin.prefab hud.prefab
├── assets/scripts/    player.ts game-root.ts hud.ts       # 组件脚本
├── assets/textures/   atlas.plist atlas.png               # 图集
├── settings/          v2/packages/*.json                  # 构建与物理配置
└── package.json       project.json

mini2d 版每个 .ts 都是你写的;Cocos 版里 .scene / .prefab / .meta 由编辑器生成,你只写 scripts/ 那几个组件。这两棵树的差异,就是下面九组对照。

18.1.2 对照一:帧循环

mini2d 的帧循环自己驱动(第 1、4 章):requestAnimationFrame 拿真实帧间隔,累加到固定步长,再按剩余比例做渲染插值:

// mini2d/src/core/loop.ts(节选)
export class Loop {
  private acc = 0;
  private last = 0;
  private readonly step = 1 / 60;
  private readonly onFixed: (dt: number) => void;
  private readonly onRender: (alpha: number) => void;
  constructor(onFixed: (dt: number) => void, onRender: (alpha: number) => void) {
    this.onFixed = onFixed;
    this.onRender = onRender;
  }
  tick(now: number): void {
    let frame = (now - this.last) / 1000;
    this.last = now;
    if (frame > 0.25) frame = 0.25;      // 切后台回来别让物理爆步
    this.acc += frame;
    while (this.acc >= this.step) { this.onFixed(this.step); this.acc -= this.step; }
    this.onRender(this.acc / this.step); // 插值系数 0~1
  }
}

Cocos 版里循环是引擎的:director 每帧调度场景树,你只写 update(dt)。引擎给的 dt 是真实帧间隔,固定步长要自己累加:

import { _decorator, Component } from 'cc';
const { ccclass } = _decorator;

@ccclass('GameRoot')
export class GameRoot extends Component {
  private acc = 0;
  private readonly step = 1 / 60;

  update(dt: number): void {
    this.acc += Math.min(dt, 0.25);
    while (this.acc >= this.step) { this.fixedStep(this.step); this.acc -= this.step; }
    this.render(this.acc / this.step);
  }

  private fixedStep(dt: number): void { void dt; }
  private render(alpha: number): void { void alpha; }
}

引擎替你做的:rAF 注册、切后台暂停、帧率上限、回调分发顺序。你必须自己做的:固定步长累加器与渲染插值——引擎不替你保证确定性(第 16 章的确定性回放,在 Cocos 里同样靠自己那套累加器)。

18.1.3 对照二:渲染

mini2d 自己遍历节点树、自己调 Canvas2D 的 setTransform 与 drawImage(第 2、3 章):

// mini2d/src/render/render-tree.ts
export function drawTree(root: Node, ctx: CanvasRenderingContext2D): void {
  root.visit((node) => {
    if (!node.active || !node.sprite) return;
    const m = node.worldMatrix;
    ctx.setTransform(m.a, m.b, m.c, m.d, m.tx, m.ty);
    const f = node.sprite.frame;
    ctx.drawImage(f.image, f.x, f.y, f.w, f.h, 0, 0, f.w, f.h);
  });
}

Cocos 版里渲染是引擎的:你往节点上挂 Sprite,引擎负责合批、上传、绘制,你只声明「显示哪一帧」:

import { _decorator, Component, Sprite, SpriteFrame, UITransform } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('Coin')
export class Coin extends Component {
  @property(SpriteFrame)
  goldFrame: SpriteFrame | null = null;

  start(): void {
    const sprite = this.node.getComponent(Sprite);
    if (sprite && this.goldFrame) sprite.spriteFrame = this.goldFrame;
    const ui = this.node.getComponent(UITransform);
    if (ui) ui.setContentSize(32, 32);   // 尺寸/锚点在 UITransform 上
  }
}

引擎替你做的:合批与 Draw Call 排序(第 3 章我们自己写的那套批处理,引擎做得更狠)、图集管理、后端切换。你必须自己做的:绘制顺序仍要靠节点层级与 siblingIndex 表达——引擎只保证「按顺序画」,不保证「按你想要的顺序画」。

18.1.4 对照三:输入

mini2d 自己把 DOM 事件换算成游戏坐标、自己做手势识别与输入缓冲(第 5 章)。Cocos 版里输入分发是引擎的:input 收全局事件,节点上收 TOUCH_START 等冒泡事件,坐标已换算到节点空间:

import { _decorator, Component, NodeEventType, input, Input, EventTouch } from 'cc';
const { ccclass } = _decorator;

@ccclass('Player')
export class Player extends Component {
  onEnable(): void {
    this.node.on(NodeEventType.TOUCH_START, this.onPress, this);
    input.on(Input.EventType.TOUCH_START, this.onAnyPress, this);
  }
  onDisable(): void {
    this.node.off(NodeEventType.TOUCH_START, this.onPress, this);
    input.off(Input.EventType.TOUCH_START, this.onAnyPress, this);
  }
  private onPress(_e: EventTouch): void { /* 命中测试已由引擎完成 */ }
  private onAnyPress(_e: EventTouch): void { /* 全局事件 */ }
}

引擎替你做的:坐标换算、命中测试(节点级 on 只在点中该节点时触发)、多点触控管理。你必须自己做的:手势识别(滑动/长按判定)与输入缓冲——这些是手感层,引擎不代劳。

18.1.5 对照四:物理与碰撞

mini2d 的 AABB、宽相与响应全是手写的(第 7 章):

// mini2d/src/physics/aabb.ts
export interface AABB { x: number; y: number; w: number; h: number; }
export function overlaps(a: AABB, b: AABB): boolean {
  return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
}

Cocos 版里物理是引擎的:挂 RigidBody2D 与 Collider2D,引擎按 PhysicsSystem2D 的固定步长求解:

import { _decorator, Component, RigidBody2D, Collider2D } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('Coin')
export class Coin extends Component {
  @property(RigidBody2D)
  body: RigidBody2D | null = null;

  onLoad(): void {
    const collider = this.node.getComponent(Collider2D);
    if (collider) collider.sensor = true;   // 金币是触发器,不产生反作用力
  }
}

引擎替你做的:碰撞检测、宽相加速、响应求解、固定步长积分。你必须自己做的:分层与掩码规划(第 7.3 节那套 layer / mask,Cocos 里对应碰撞体分组)与「什么算命中」的业务判定。

18.1.6 对照五:动画

mini2d 的帧动画与补间是自研的(第 8 章)。Cocos 版里补间是引擎的:tween 链式调用,Animation 组件播放 AnimationClip:

import { _decorator, Component, tween, Vec3, Animation } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('Coin')
export class Coin extends Component {
  @property(Animation)
  anim: Animation | null = null;

  collect(): void {
    tween(this.node)
      .to(0.2, { scale: new Vec3(0.1, 0.1, 1) })
      .call(() => this.node.destroy())
      .start();
    this.anim?.play('coin_pick');
  }
}

引擎替你做的:缓动函数库、补间生命周期(销毁节点自动停)、剪辑的编辑与播放。你必须自己做的:状态驱动(第 8.3 节「状态变了该播哪段」的映射)——引擎只负责播,不负责决定播什么。

18.1.7 对照六:资源加载

mini2d 有清单、异步加载、引用计数三件套(第 10 章)。Cocos 版里资源管理是引擎的:resources / assetManager 负责加载与缓存,引用计数由引擎维护:

import { _decorator, Component, resources, SpriteFrame, Sprite } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('Loader')
export class Loader extends Component {
  @property(Sprite)
  target: Sprite | null = null;

  loadCoin(): void {
    resources.load('textures/coin/spriteFrame', SpriteFrame, (err, frame) => {
      if (err || !this.target) return;
      this.target.spriteFrame = frame;
    });
  }
}

引擎替你做的:缓存、引用计数、依赖图、释放时机。你必须自己做的:什么资源该预加载、什么时候该主动释放——内存策略永远是业务的。

18.1.8 对照七:UI

mini2d 的布局、虚拟列表、弹窗栈都是自研的(第 11 章)。Cocos 版里布局是引擎的:Widget 做对齐,Layout / ScrollView 做排列与滚动,弹窗栈仍要自己管:

import { _decorator, Component, Widget } from 'cc';
const { ccclass } = _decorator;

@ccclass('Hud')
export class Hud extends Component {
  onLoad(): void {
    const w = this.node.getComponent(Widget);
    if (w) w.updateAlignment();   // 按锚点重新对齐
  }
}

引擎替你做的:锚点对齐、滚动容器、渲染层级。你必须自己做的:弹窗栈的压栈/出栈顺序、返回键语义、界面状态机——这些是产品逻辑。

18.1.9 对照八:存档

mini2d 自己定义序列化格式、版本号与迁移(第 12 章)。Cocos 版里格式仍是你的,引擎只提供存储介质 sys.localStorage:

import { _decorator, Component, sys } from 'cc';
const { ccclass } = _decorator;

@ccclass('SaveSlot')
export class SaveSlot extends Component {
  save(payload: unknown): void {
    sys.localStorage.setItem('dash.save', JSON.stringify({ v: 3, data: payload }));
  }
  load(): unknown {
    const raw = sys.localStorage.getItem('dash.save');
    return raw ? JSON.parse(raw) : null;
  }
}

引擎替你做的:跨平台存储读写(Web 是 localStorage,原生是本地文件)。你必须自己做的:版本号、迁移、冲突解决——第 12 章那套 codec.ts / migrate.ts 原样搬过来,一行都不用改。

18.1.10 对照九:构建

mini2d 版用 Vite 打包(第 17 章):vite build 产出 H5 产物,微信小游戏与原生要自己接适配层。Cocos 版里构建是编辑器的:打开「构建发布」面板,选平台、点构建。这一节属于 L3——需 Cocos Creator 3.8 编辑器打开,本节不可命令行复现。你在编辑器里会看到:面板上有 H5、微信小游戏、Android、iOS 等平台选项卡;每个平台有「主包压缩类型」「资源服务器地址」等选项;点「构建」后产物落在 build/,微信小游戏是一份可直接导入微信开发者工具的工程。引擎替你做的:多端适配、资源打包、SDK 注入。你必须自己做的:产物体积预算、首包与远程包切分——体积永远是项目的,引擎只提供工具。

18.1.11 两张总表

先看引擎替你做的:

领域引擎替你做的
帧循环rAF、切后台暂停、帧率上限、回调分发顺序
渲染合批、Draw Call 排序、图集管理、后端切换
输入坐标换算、命中测试、多点触控
物理碰撞检测、宽相、响应求解、固定步长
动画缓动库、补间生命周期、剪辑播放
资源缓存、引用计数、依赖图、释放时机
UI锚点对齐、滚动容器、渲染层级
存档跨平台存储介质
构建多端适配、资源打包、SDK 注入

再看你必须自己做的:

领域你必须自己做的
帧循环固定步长累加器、渲染插值、确定性保证
渲染绘制顺序的表达(层级与 siblingIndex)
输入手势识别、输入缓冲、手感调校
物理分层与掩码规划、命中业务判定
动画状态到剪辑的映射
资源预加载策略、主动释放时机
UI弹窗栈、界面状态机、返回键语义
存档版本号、迁移、冲突解决
构建体积预算、分包策略

两套实现的对照到这里就完整了。 你会发现一个规律:引擎接管的是机械性的、每个游戏都一样的底层;留给你的永远是决策性的、跟产品强相关的上层。卷一前 17 章让你亲手实现前者,正是为了让你在后者面前不心虚——你不再把引擎当黑盒,而是知道每一层「它替我做了什么」。

下一节把这些对照拧成一份可执行的迁移清单:如果你手里已经有一个 mini2d 这样的自研运行时,怎么一步步把它搬到 Cocos Creator 上。

阅读导航:上一节:目录 · 下一节:18.2 迁移清单:mini2d → Cocos Creator 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「cocos」更多文章

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