《Cocos 游戏开发实战》1.2 工程结构与目录约定

把 1.1 的规格落到磁盘:讲清 Cocos 工程在文件系统里长什么样(assets 进版本库、library/temp/local 不进),定下 assets/ 的六层资源分层与 scripts/ 的 core/game/ui/platform 四层职责,给出可单测的路径常量,并用 resources 演示加载 configs/levels.json。

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

三条纪律:

  1. configs/ 只放 JSON,不放 Excel。 策划在表里改数值,导出脚本转成 JSON 落到这里;源表留在工程外。
  2. textures/ 里的图必须能被合批。 同一套 UI 的图放同一张图集,不然 13.1 讲的 Draw Call 优化无从谈起。
  3. 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/✅需 mockHUD、弹窗栈
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-caseplayer-controller.ts
类名PascalCasePlayerController
资源文件全小写 + 下划线player_idle.png
目录全小写prefabs
Bundlekebab-casebundle-levels

这里有个真实会咬人的坑:macOS 与 Windows 的文件系统默认大小写不敏感,Player.png 和 player.png 在本地是同一个文件;但构建产物跑在 Android / iOS 上是大小写敏感的。于是「本地好好的,打包后图片全丢」就这么发生了。对策只有一个:从第一天起就全用小写 + 下划线命名资源,别等到上线前才排查。这条规则和 15.1 的分包一起,构成了「构建产物在真机上表现一致」的前提。

1.2.9 本节自检清单

  • 磁盘上有 assets/ 与 settings/,且它们是唯一进版本库的两棵目录
  • assets/ 下六个类型目录齐全:scenes prefabs textures audio configs scripts
  • assets/scripts/ 下四层齐全:core game ui platform
  • 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 版本管理与协作流程 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「cocos」更多文章

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