《Cocos 游戏开发入门》1.1 环境与仓库骨架

本节把《方块冲刺》的环境搭起来:锁定 Node 26.5 + TypeScript 7 + vitest 5 + vite 8 + Playwright 1.59,讲清三层可运行性口径,给出仓库骨架与 package.json,重点说明 tsconfig 如何通过 types 接入 @cocos/creator-types@3.8.8,让 Cocos 脚本也过 tsc --noEmit 检查。

1.1 环境与仓库骨架

这一卷要做的事有点反常:先不用引擎,从零手写一个能跑的 2D 运行时 mini2d,再用 Cocos Creator 3.8 把同一款游戏做一遍。 所以我们要同时准备两套东西——一套是 mini2d 自己的 TypeScript 工程(Node + vitest + vite + Playwright),另一套是把 Cocos 引擎的类型声明接进来,好让书里的 Cocos 脚本片段也能被类型检查器验一遍。

本节把《方块冲刺》推进到「环境就绪」这一步:装好 Node 26.5 与全套工具链,跑通 vitest 与 vite,理解仓库目录的划分,并把 @cocos/creator-types@3.8.8 通过 tsconfig 的 types 字段接通。本节不含游戏逻辑,但它是后面 17 章的地基。

与站内 game/ 下那 1153 篇的关系:那些文章讲的是「某个引擎怎么做」,本节以及后面每一节讲的,是一行一行写出来的可运行代码 + 真跑过的测试。

1.1.1 三层可运行性口径

在动手之前,先把这套书的验证边界说清楚。否则你很容易误以为「书上所有代码都能一键跑起来」——事实并非如此,而且这个差异本身是本书最诚实的地方。

层内容能否真跑怎么验证
L1mini2d 从零实现(纯 TypeScript,零 cc 依赖)能真跑Node + vitest 单测、Playwright 无头截图
L2Cocos 组件脚本(import { X } from 'cc')能真做类型检查tsc --noEmit 对 @cocos/creator-types@3.8.8 检查
L3Cocos 工程文件(.scene / .prefab / .meta)不能命令行复现结构校验;正文会显式标注「需编辑器」

为什么这么分?因为 Cocos Creator 是一个 GUI 编辑器,它的引擎运行时不在 npm 上,命令行里既装不了也跑不起来。于是本书采取的策略是:

  • 能跑的部分做到极致:L1 全部真跑、真测、真截图。
  • 不能跑的部分如实标注:L3 一律写明「本节需 Cocos Creator 3.8 编辑器,无法命令行复现」。
  • 夹在中间的 Cocos 脚本:用真实引擎类型声明做 tsc --noEmit,保证每个 API 名、每个参数类型都对得上 3.8.8。

你看到的每一段 mini2d 代码,都是在 Node 里跑过、在浏览器里画过、被单测断言过的。

1.1.2 版本口径

版本不是随便选的,全部锁定,且要能互相兼容:

项版本用途
Node.js26.5.0直接运行 TypeScript(只做类型剥离)
TypeScript7.0.2类型检查与编辑器支持
vitest5.0.3L1 逻辑单测与假时钟
Vite8.3.4打包成单文件 bundle
Playwright1.59.1无头浏览器截图回归
@cocos/creator-types3.8.8仅类型声明,供 L2 检查

Node 26 有一个关键特性:它能直接运行 .ts 文件,但只做「类型剥离」,不做语法转换。 这意味着 enum、namespace、构造函数参数属性这类「需要生成运行时代码」的写法不能用——它们会被 Node 直接报错。本书一律用 const 对象 + as const 代替 enum,用模块导出代替 namespace。这不是限制,而是一种更贴近现代 TS 的写法。

1.1.3 仓库骨架

先建目录。mini2d 的源码全部放在 src/ 下,按职责分文件夹:

mini2d/
├── package.json
├── tsconfig.json
├── vitest.config.ts
├── vite.config.ts
├── index.html
└── src/
    ├── core/          # 帧循环、时钟、事件总线
    │   ├── clock.ts
    │   └── loop.ts
    ├── math/          # Vec2、Mat3 等纯数学
    │   └── mat3.ts
    ├── render/        # Canvas2D 绘制、精灵、图集
    ├── camera/        # 相机与视口
    ├── input/         # 指针、手势、输入缓冲
    ├── game.ts        # 贯穿项目的 Game 类
    └── index.ts       # 对外统一出口

三条约定,后面每一章都会遵守:

  1. src/core/ 不依赖任何浏览器 API。 时钟要能被换成假时钟,循环要能在 Node 里手推,所以这一层只接受注入进来的时间源。
  2. src/math/ 是纯函数。 没有类实例状态,方便单测直接断言数值。
  3. src/render/ 是唯一碰 CanvasRenderingContext2D 的地方。 逻辑层不认识 canvas,这样第 16 章才能用「假渲染器」录下绘制指令做回归。

1.1.4 package.json

{
  "name": "mini2d",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "test": "vitest run",
    "test:watch": "vitest",
    "typecheck": "tsc --noEmit"
  },
  "devDependencies": {
    "@cocos/creator-types": "^3.8.8",
    "@playwright/test": "^1.59.1",
    "typescript": "^7.0.2",
    "vite": "^8.3.4",
    "vitest": "^5.0.3"
  }
}

注意 "type": "module"——整个工程走 ESM。Node 26 运行 .ts 时也按 ESM 解析,import 的路径要带扩展名或由 Vite/vitest 解析。为了让同一份源码既能被 Node 直接跑、又能被 Vite 打包,本书统一用不带扩展名的相对导入(如 from '../core/clock'),由工具链负责解析;只有 tsc 的类型检查不看这个。

1.1.5 接通 Cocos 类型:tsconfig 的 types 字段

这是本节最容易被忽略、却最重要的一步。我们想把 import { Component } from 'cc' 也纳入类型检查,就得让 TypeScript 知道 cc 这个模块存在。

@cocos/creator-types@3.8.8 这个包的 engine.d.ts 只有一行:

/// <reference path="./engine/cc.d.ts"/>

它把 3.3 MB 的 engine/cc.d.ts(真实的 Cocos Creator 3.8.8 引擎 API)引了进来。我们只要在 tsconfig.json 里把它列进 types 数组,TypeScript 就会自动加载它:

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true,
    "experimentalDecorators": true,
    "types": ["@cocos/creator-types/engine"]
  },
  "include": ["src", "*.ts"]
}

逐条说明:

  • types: ["@cocos/creator-types/engine"] 是关键。它的语义是「加载 node_modules/@cocos/creator-types/engine.d.ts 这个类型入口」,于是 cc.d.ts 里的 declare module "cc" 生效,from 'cc' 不再是「找不到模块」。
  • skipLibCheck: true 必须开。cc.d.ts 有 3.3 MB,内部大量使用交叉类型与条件类型,逐行检查既慢又容易报无关噪声;我们只关心「自己写的代码是否对得上 API 签名」。
  • experimentalDecorators: true 是 Cocos 组件脚本的前提——@ccclass 是实验性装饰器语法。
  • noEmit: true 因为类型剥离由 Node/Vite 负责,tsc 只当检查器。

装好之后,验证一下。下面这段是 L2 片段,它不参与 mini2d 的运行,只用来确认类型通道是通的:

import { _decorator, Component } from 'cc';

const { ccclass } = _decorator;

@ccclass('HelloTypeCheck')
export class HelloTypeCheck extends Component {
  onLoad(): void {
    console.log('cc 类型已接通,节点名:', this.node.name);
  }
}

跑 npm run typecheck,如果没有报错,就说明 _decorator、Component、this.node 这些符号都真实存在于 3.8.8 的声明里。本书每一个 cc 符号落笔前都做过这一步核对——凭记忆写 API 名是这类书稿最高频的错误来源。

1.1.6 第一个冒烟测试:vitest

mini2d 的核心价值是「可测」。第 1.3 节的固定步长循环、第 4 章的时钟,全都靠假时钟在 Node 里复现。先把测试跑起来。

vitest.config.ts:

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    include: ['src/**/*.test.ts'],
    environment: 'node',
  },
});

environment: 'node' 是刻意的:src/core/ 与 src/math/ 不碰 DOM,测试就该在纯 Node 环境里跑,快且没有浏览器噪声。需要 DOM 的渲染测试会单独用一个 Playwright 工程,那是第 16 章的事。

写一个最小冒烟测试,先证明「测试能跑、断言能过」:

import { describe, expect, it } from 'vitest';

describe('冒烟', () => {
  it('数学是可信的', () => {
    expect(1 + 1).toBe(2);
  });
});
npm run test

看到一行 1 passed 就够了。这个测试很傻,但它是后面所有断言的地基——先确认工具有输出,再往里填真东西。

1.1.7 在浏览器里跑起来:vite

逻辑测试走 Node,画面要进浏览器。vite.config.ts 里把产物配成单文件,方便 Playwright 用 file:// 直接载入:

import { defineConfig } from 'vite';

export default defineConfig({
  base: './',
  build: {
    outDir: 'dist',
    target: 'es2022',
    assetsInlineLimit: 100_000_000,
    cssCodeSplit: false,
    codeSplitting: false,
  },
});

assetsInlineLimit 调到极大 + 关掉代码分割,是为了把 JS、CSS、图片全部内联进一个 index.html——无头浏览器载入一个本地文件即可截图,不用起服务器。base: './' 保证用相对路径引用资源,file:// 下才不会 404。

页面骨架 index.html:

<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <title>方块冲刺 · mini2d</title>
    <style>
      html, body { margin: 0; height: 100%; background: #111; }
      canvas { display: block; width: 100%; height: 100%; }
    </style>
  </head>
  <body>
    <canvas id="stage"></canvas>
    <script type="module" src="/src/index.ts"></script>
  </body>
</html>

npm run dev 会起一个开发服务器;npm run build 产出 dist/index.html。两条路都通,说明环境闭环了。

1.1.8 动手自检清单

环境这东西,最怕「看起来装好了」。逐条打勾再往下走:

  • node --version 输出 v26.5.0
  • npx tsc --version 输出 Version 7.0.2
  • npm run test 输出 1 passed
  • npm run typecheck 不报错(说明 cc 类型已接通)
  • npm run dev 能打开页面,npm run build 能产出 dist/index.html
  • 目录里存在 src/core/、src/math/、src/render/、src/input/、src/game.ts

如果第 4 条报「找不到模块 cc」,九成是 types 数组写错了——检查包名是否是 @cocos/creator-types/engine(带 /engine 后缀),而不是 @cocos/creator-types。

1.1.9 这一节我们没做什么

特意说明一下本节没有涉及的东西,免得你误以为漏了:没写任何游戏逻辑,没碰 canvas 绘制,没引入物理或碰撞。本节唯一的目标是让工具链闭环——能跑测试、能打包、能类型检查。真正的第一行游戏代码在 1.3 节,那时我们会在 src/core/loop.ts 里写下 mini2d 的第一个 requestAnimationFrame 主循环。

环境就绪之后,下一个问题不是「怎么写循环」,而是「循环里那个 dt 到底是什么、坐标从哪里量、y 轴朝哪边」。这些约定如果一开始就想错,后面每章都要还债。下一节我们把「一帧」这件事的心智模型彻底讲清楚:离散时间、坐标原点、y 轴方向,以及屏幕坐标与世界坐标为什么是两套数。

阅读导航:上一节:目录 · 下一节:1.2 游戏的心智模型:帧、时间、坐标系 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「cocos」更多文章

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