1.2 工程结构与目录约定
上一节定下了「做什么」;这一节定「放在哪」。听起来像琐事,但 Cocos 工程里最贵的返工几乎都来自目录约定:一个美术同事把 300 张图直接丢进 assets/,主包立刻超红线;一个脚本从 core/ 挪到 game/,十处 import 路径全断。目录结构不是风格问题,而是构建结果问题——它直接决定哪些资源进主包、哪些进分包、哪些根本不进版本库。
本节把《方块冲刺》的磁盘结构钉死:哪些目录进 Git、哪些永远不进,
assets/的六层怎么分,scripts/的四层怎么分职责,路径常量怎么写才能被单测覆盖,以及settings/v2/packages/里究竟存了什么。做完这一节,1.1 那张工程设定表就有了物理落点。
1.2.1 一个 Cocos 工程在磁盘上长什么样
Cocos Creator 的项目根目录有一批固定的兄弟目录。它们的名字不能改,用途也各不相同——其中只有少数几个该进版本库。
以下目录树需 Cocos Creator 3.8 编辑器打开,本节不可命令行复现(目录由编辑器在创建项目时生成):
block-dash/
├── assets/ # 唯一需要进版本库的资源目录
│ ├── scenes/
│ ├── prefabs/
│ ├── textures/
│ ├── audio/
│ ├── configs/
│ ├── scripts/
│ └── bundle-levels/ # 分包目录(详见 4.1 / 15.1)
├── settings/ # 工程与构建配置(进版本库)
│ └── v2/packages/
├── package.json # 工程名、uuid、编辑器版本
├── tsconfig.json # 脚本编译配置
├── library/ # 导入缓存(不进版本库)
├── temp/ # 临时文件(不进版本库)
├── local/ # 本地个人设置(不进版本库)
├── profiles/ # 编辑器偏好(不进版本库)
└── build/ # 构建产物(不进版本库)
在编辑器里打开这个工程,资源管理器(Assets 面板)只显示 assets/ 这一棵树——library/、temp/、local/、build/ 都是编辑器自己在后台生成的,你在面板里根本看不到它们。这是新手最容易困惑的一点:磁盘上有九个目录,编辑器里只有一个。
| 目录 | 谁生成 | 进版本库 | 说明 |
|---|---|---|---|
assets/ | 你 | ✅ | 所有源资源与脚本 |
settings/ | 编辑器 | ✅ | 工程设置、构建配置 |
library/ | 编辑器 | ❌ | 导入缓存,可重建,动辄几百 MB |
temp/ | 编辑器 | ❌ | 临时文件 |
local/ | 编辑器 | ❌ | 个人布局、窗口状态 |
profiles/ | 编辑器 | ❌ | 编辑器偏好 |
build/ | 构建 | ❌ | 构建产物,可重建 |
判断标准很简单:能由 assets/ + settings/ 重新生成的,都不进版本库。 这条规则在 1.3 会变成一份具体的 .gitignore。
1.2.2 assets/ 六层资源分层
assets/ 下面按资源类型分六层,而不是按关卡或功能分。原因很实际:构建时按目录切分包、按目录设压缩策略,类型一致的目录才好统一处理。
| 目录 | 内容 | 典型文件 |
|---|---|---|
scenes/ | 场景 | main.scene、level.scene |
prefabs/ | 预制体 | player.prefab、coin.prefab |
textures/ | 贴图与图集 | player.png、ui.atlas |
audio/ | 音效与音乐 | jump.mp3、bgm.ogg |
configs/ | 数据配置 | levels.json、items.json |
scripts/ | TypeScript 脚本 | game/player.ts |
三条纪律:
configs/只放 JSON,不放 Excel。 策划在表里改数值,导出脚本转成 JSON 落到这里;源表留在工程外。textures/里的图必须能被合批。 同一套 UI 的图放同一张图集,不然 13.1 讲的 Draw Call 优化无从谈起。scripts/只放.ts。 不要把.json混进脚本目录,否则打包器会把它当代码处理。
在编辑器里创建这些目录的操作是:资源管理器空白处右键 → 创建 → 文件夹,然后把文件夹重命名成上面六个名字。编辑器会为每个文件夹生成对应的 .meta(2.3 会讲 .meta 到底存了什么)。
1.2.3 scripts/ 四层职责
脚本目录按职责分四层,这是本书最重要的一条架构约定:
assets/scripts/
├── core/ # 纯逻辑:不 import 'cc',可单测
├── game/ # 玩法:角色、关卡、碰撞、道具
├── ui/ # 界面:菜单、HUD、弹窗
└── platform/ # 平台:存档、分享、广告、埋点
四层的依赖方向是单向的:ui 和 game 可以依赖 core 与 platform,但 core 绝不依赖其他任何一层,尤其不能 import { X } from 'cc'。这条纪律的价值在于:core/ 里的代码能直接拿进 Node 跑单测,不需要引擎、不需要浏览器。
| 层 | 能否 import 'cc' | 能否单测 | 例子 |
|---|---|---|---|
core/ | ❌ 绝不 | ✅ 直接测 | 分数计算、难度曲线、存档迁移 |
game/ | ✅ | 需 mock | 角色控制、碰撞检测 |
ui/ | ✅ | 需 mock | HUD、弹窗栈 |
platform/ | ✅ | 需 mock | 三端存档适配 |
为什么把 core/ 单列一层?因为卷一的教训是:把纯逻辑和引擎调用混在一起,测试就没法写。 一个「难度随关卡递增」的公式,如果写在组件里,就得起一个引擎才能测;放进 core/difficulty.ts,它就是一个纯函数,vitest 一行就能断言。
1.2.4 路径常量写成可单测的纯逻辑
路径字符串散落在十来个文件里,改一次目录就得全局搜索替换。正确做法是集中定义一次,让所有引用都指向同一处。这段代码属于 core/,不碰引擎,可以直接单测:
export const ASSET_ROOT = 'assets';
export const SCRIPT_ROOT = 'assets/scripts';
export const ASSET_DIRS = {
scenes: 'assets/scenes',
prefabs: 'assets/prefabs',
textures: 'assets/textures',
audio: 'assets/audio',
configs: 'assets/configs',
scripts: SCRIPT_ROOT,
} as const;
export const SCRIPT_DIRS = {
core: 'assets/scripts/core',
game: 'assets/scripts/game',
ui: 'assets/scripts/ui',
platform: 'assets/scripts/platform',
} as const;
export const BUNDLE_LEVELS = 'bundle-levels';
export function configPath(name: string): string {
return `configs/${name}`;
}
注意 configPath 返回的是 configs/levels 这种不带扩展名的路径——这是 Cocos 资源系统的惯例:加载时只写相对 assets/ 的路径,不带后缀,由引擎按类型解析。resources.load('configs/levels', JsonAsset, ...) 里的那个字符串,正是这个函数产出的。
这份常量表本身就是一份「文档」:新人打开 core/paths.ts 就知道工程有哪几层目录,不用去问人;而且 as const 让每个值都是字面量类型,写错路径时 TypeScript 会在编译期就报错。
1.2.5 用 resources 加载配置
configs/levels.json 放在 assets/configs/ 下,但它默认不在 resources 目录里,所以 resources.load 找不到它。这里有个 Cocos 的关键机制:只有 assets/resources/ 目录下的资源才能用 resources 这个内置 Bundle 动态加载。
两种做法:
- 若关卡配置要动态加载(不改包就能换),把它移到
assets/resources/configs/levels.json; - 若关卡配置跟包走,就把它设成某个脚本的
@property(JsonAsset),在编辑器里拖引用。
下面演示第一种——resources 加载:
import { _decorator, Component, JsonAsset, resources } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('LevelConfigLoader')
export class LevelConfigLoader extends Component {
@property
configPath = 'configs/levels';
onLoad(): void {
resources.load(this.configPath, JsonAsset, (err, asset) => {
if (err) {
console.error('[block-dash] 关卡配置加载失败:', err.message);
return;
}
const data = asset.json ?? {};
console.log('[block-dash] 已加载配置键:', Object.keys(data).join(', '));
});
}
}
resources 是引擎导出的一个 AssetManager.Bundle 常量,resources.load 的签名是「路径 + 资源类型 + 回调」。回调里拿到的 asset 是 JsonAsset,它的 .json 是解析好的对象——别自己去 JSON.parse 字符串,引擎已经替你做完了。
1.2.6 settings/v2/packages 里存了什么
工程设置面板(菜单「项目 → 项目设置」)里的每一项,最终都会写进 settings/v2/packages/ 下的 JSON。设计分辨率就存在这里。
以下片段需 Cocos Creator 3.8 编辑器打开,本节不可命令行复现:
{
"__version__": "1.0.4",
"general": {
"designResolution": {
"width": 960,
"height": 540,
"fitWidth": false,
"fitHeight": true
}
}
}
在编辑器里打开「项目设置 → 通用」面板,你会看到:设计分辨率两栏填着 960 与 540;Fit Width 未勾选、Fit Height 已勾选——正好对应上面 JSON 里的 fitWidth: false 与 fitHeight: true。永远改面板,不要手改这个文件:__version__ 与字段结构会随编辑器小版本变化,手改容易写出编辑器读不懂的配置。
1.2.7 分包目录 bundle-levels
assets/bundle-levels/ 是本书约定的分包目录。它的作用是:让关卡资源不跟首包走。首包只保留启动场景和主菜单,玩家点「开始」时再异步加载这个 Bundle。
要让一个普通目录变成 Bundle,操作步骤是:在资源管理器里选中 bundle-levels 文件夹 → 属性检查器(Inspector)→ 勾选「配置为 Bundle」→ 在展开的选项里把「Bundle 名称」填成 bundle-levels、优先级保持默认、勾选「压缩」。操作后属性面板应显示:Is Bundle 打勾、Bundle Name 为 bundle-levels、Priority 为 1。
这个勾选会在该目录的 .meta 里写下一段配置,运行时才可能用 assetManager.loadBundle('bundle-levels', ...) 加载它。没有这一步,loadBundle 会报「Bundle 不存在」。 分包策略与首包体积的关系,15.1 会展开;这里只需记住:分包是靠目录 + 勾选实现的,不是靠代码。
1.2.8 命名约定与大小写陷阱
目录定好之后,命名规则也得定。否则同一个工程里会出现 Player.ts 与 player.ts 并存,而 Cocos 的资源 URL 是大小写敏感的:
| 对象 | 约定 | 例子 |
|---|---|---|
| 脚本文件 | kebab-case | player-controller.ts |
| 类名 | PascalCase | PlayerController |
| 资源文件 | 全小写 + 下划线 | player_idle.png |
| 目录 | 全小写 | prefabs |
| Bundle | kebab-case | bundle-levels |
这里有个真实会咬人的坑:macOS 与 Windows 的文件系统默认大小写不敏感,Player.png 和 player.png 在本地是同一个文件;但构建产物跑在 Android / iOS 上是大小写敏感的。于是「本地好好的,打包后图片全丢」就这么发生了。对策只有一个:从第一天起就全用小写 + 下划线命名资源,别等到上线前才排查。这条规则和 15.1 的分包一起,构成了「构建产物在真机上表现一致」的前提。
1.2.9 本节自检清单
- 磁盘上有
assets/与settings/,且它们是唯一进版本库的两棵目录 -
assets/下六个类型目录齐全:scenesprefabstexturesaudioconfigsscripts -
assets/scripts/下四层齐全:coregameuiplatform -
core/里没有任何import { ... } from 'cc' - 路径常量集中在一处,
configPath('levels')返回configs/levels -
resources能加载到configs/levels(说明文件在resources/下或已正确引用) -
bundle-levels已勾选为 Bundle,名称为bundle-levels - 资源文件名全小写 + 下划线,没有大小写混用
-
settings/v2/packages/project.json里设计分辨率是 960 × 540 - 路径常量与 1.1 的工程设定表逐字一致
1.2.10 工程视角的收口
这一节之后,工程里多了:六个资源目录、四个脚本目录、一份路径常量、一个 LevelConfigLoader 组件、一个配置好的 bundle-levels 分包。assets/ 从空目录变成了有形状的骨架,settings/v2/packages/project.json 里第一次有了我们主动设定的值(960×540)。
下一章我们进编辑器,把这套目录真正用起来:怎么在层级管理器里搭出 main 场景、节点和预制体分别是什么、instantiate 复制预制体时到底复制了什么。在动编辑器之前,还有一件必须先办的事——版本管理:哪些文件该提交、哪些永远不该,多人协作时 .scene 冲突怎么办。这就是下一节的内容。
阅读导航:上一节:1.1 需求、用例与验收标准 · 下一节:1.3 版本管理与协作流程 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。