目录规范与版本管理
1.1 assets 目录分层,以及每层的准入门槛
目录规范这件事,我改过三次。前两次失败的原因是"只规定了名字,没规定什么该放进去"。第三次我把每层目录配了一条准入门槛,规则才立住。
我现在 assets/ 下面是这六层:
| 目录 | 放什么 | 准入门槛 | 进不进 bundle |
|---|---|---|---|
scenes/ |
所有场景文件 | 一个场景只放一个 .scene |
不进,场景本身是构建入口 |
prefabs/ |
预制体 | 玩家、障碍、订单条目等可复用单位 | 进 gameplay bundle |
scripts/ |
TypeScript 脚本 | 按模块分子目录,不超过两层 | 不进,脚本编译进 js |
resources/ |
运行时动态加载的资源 | 只有 resources.load 能取到的才放 |
单独打 |
bundles/ |
Asset Bundle | 有独立加载时机的内容 | 本身就是 bundle 单元 |
framework/ |
通用框架代码和公共组件 | 三章之后都用得上的才进 | 不进 |
resources 这层我要特别说明一下,因为它是新手最爱踩的地方。
resources 目录里的东西,在构建时会全部打进一个资源包,运行时用 resources.load 按路径取。方便,但代价是这些资源没法被构建流程裁剪,也没法做分包。我的项目里 resources 只放两类东西:一份字体,和一份语言配置表。其他一律进 bundle。
我一开始把几十张美术图丢进了 resources,结果主包直接到了 3.8 MB。删掉之后主包掉到 1.1 MB。就这一个目录,差了 2.7 MB。
framework 这层是我的个人偏好,不是 Creator 的要求。放事件总线、对象池基类、资源加载封装、错误上报。判断标准只有一条:三个以上模块会用到的,才挪进 framework。只被一个模块用的代码就待在原地,别过早抽象。我为这条规矩付出过代价,是把一个只被订单模块用的分数格式化函数硬塞进 framework,后来那个函数改了三次,framework 那边每次都要跟着 review,纯属添乱。
目录层级我定到两层就停。assets/scripts/gameplay/player/PlayerController.ts 是三层,再深我就不用文件夹而是靠文件名区分了,因为翻目录树本身就在消耗注意力。
1.2 命名规范
命名规范的作用不是好看,是让搜索能用。
我这套项目的命名规则:
- 目录名全小写,用短横线分隔。比如
gameplay、ui-panel、order-system。 - 脚本文件名用大驼峰,一个文件一个类,文件名和类名完全一致。
PlayerController.ts里就写class PlayerController。 - 预制体文件用大驼峰加后缀,
Player.prefab、ObstacleTower.prefab、OrderItem.prefab。 - 场景用
Main、Level_01、Level_02这种格式,编号补零,方便排序。 - 节点名用大驼峰,但层级里带位置语义。
HUD、World、Player、Camera。
第 5 条我踩过坑。早期我在层级里写 node1、node2、node3,因为那是我随手建的。结果三个月后想找摄像机,找了十分钟,最后靠猜。现在层级里的名字都是能读的,因为这些名字会出现在代码和日志里。
命名上有一条我坚持的:不要用 temp、test、new 这类名字进主干。临时的东西我叫 _scratch,带下划线前缀,搜 _scratch 能一次找出所有还没清理的实验品。这个约定救过我一次,有三个 _scratch 里的逻辑后来被误当成正式实现用了两天。
1.3 Git 与 .gitignore:什么必须进,什么必须出
这一节是我被坑得最多的地方。
必须提交的文件有这些:
assets/下面所有内容,包括.meta文件settings/目录,项目设置、纹理压缩预设都在这package.json和package-lock.jsontsconfig.json- 根目录的
.gitignore、README - 自定义构建模板(如果用了)
必须忽略的文件主要是这些:
| 路径 | 是什么 | 能不能删 |
|---|---|---|
library/ |
编辑器缓存,导入后的资源数据 | 能,删了会重新导入,代价是几分钟 |
temp/ |
临时文件,编译中间产物 | 能,删了重新编译 |
local/ |
本地设置,比如编辑器偏好 | 能 |
build/ |
构建输出 | 能,重新构建 |
profiles/ |
编辑器配置,含各平台构建选项 | 能,但里面的构建参数另有备份,见下 |
node_modules/ |
npm 依赖 | 能,npm install 装回来 |
.meta 文件必须提交
这一条我要单独强调,因为它是 Cocos 项目里最容易出事的地方。
.meta 文件是 Creator 为每个资源生成的元数据,里面存着这个资源的 uuid,以及它的导入设置。场景和预制体里引用其他资源,存的就是 uuid。
.meta 不提交会怎样:
- 同事拉下来项目,所有资源重新生成新的 uuid。
- 场景里的引用全是空的,打开是一片粉。
- 更糟的情况是 uuid 冲突,因为重新生成的 uuid 有可能撞上别人仓库里已有的 uuid。
反过来,.meta 被改写也会出事。两个人各自在本地导入了同一张图的不同的压缩配置,产生的 .meta 内容不一样,合并时冲突。这一行的冲突很难看懂,因为里面是一堆十六进制的 uuid 和压缩参数。
.meta 还有一条铁律:绝对不要手工编辑。哪怕你觉得某个字段该改,也要在编辑器里改,让编辑器生成。我试过手改一次,把一张图的 filterMode 从 bilinear 改成 nearest,改完编辑器重新导入,又给我改回去了。
我用的 .gitignore
我的 gitignore 大致是这样,逐条列出来:
| 规则 | 忽略的东西 |
|---|---|
/library/ |
编辑器资源缓存 |
/temp/ |
编译临时目录 |
/local/ |
本地编辑器设置 |
/build/ |
构建输出目录 |
/profiles/ |
编辑器用户配置 |
/node_modules/ |
npm 依赖 |
.DS_Store |
macOS 目录标记 |
*.log |
本地日志 |
有几个决策说明一下。
settings/ 我是提交进去的,虽然它不在忽略列表里,但我特意在 README 里写了"settings 目录要进版本库"。因为项目设置、纹理压缩预设都在这,这些东西不同步,构建结果就不一致。这跟别的框架项目不一样,很多前端项目把配置文件也忽略了,那是因为它们的配置可以随时改。我这个不行。
补一句容易搞混的:构建发布面板里配好的各平台构建选项不在 settings/ 下,它落在 profiles/v2/packages/builder.json。这个目录按官方说法是可再生的,但团队要共用同一套构建参数时,光靠它不够。我额外把构建面板导出的那份 json 提交进仓库,命令行构建时用 configPath 指过去,这样 CI 用的参数和我在面板里配的是同一份。
build/ 我忽略掉,但另外做了件事:在打 tag 的时候把构建产物归档到 CI 的制品存储里。这样出问题时能拿到和线上完全一致的那份包,而不是本地重新构建一份可能不一样的。
.gitignore 里我没写 *.meta,也没写任何针对 assets 的规则。宁可少写一条,也不要多写一条错误规则把该提交的东西挡掉。我见过有人写 assets/**/*.meta 想忽略编辑器噪音,结果整个项目所有资源引用全丢。
1.4 分支策略和冲突高发区
我用主干开发加短分支。规则很简单:
main分支永远是可以构建出包的版本,不接受直接提交。- 每个功能开一个
feature/分支,做完合并,合并完就删。 - 分支存活时间不超过三天。三天合不完的功能,说明它被切成了更大的东西,需要重新拆。
- 每次合并前先 pull 一下主干,让冲突在本地解决,而不是在合并的时候。
冲突高发区我有统计,按次数排:
| 区域 | 冲突原因 | 我的处理 |
|---|---|---|
.meta 文件 |
同一资源的导入配置被两边改 | 合并时选一边,然后重新导入一次,检查引用有没有断 |
构建选项配置(profiles/v2/packages/builder.json) |
两边改了同一个平台的参数 | 约定构建配置只有一个人改,其他人找我要 |
| 同一批脚本 | 分支开发时间重叠 | 分支开发前先在群里说一句,撞车就换个模块先做 |
| 预制体文件 | 预制体本质是一个大 JSON,两边都改就冲突 | 尽量把改动拆在不同预制体上 |
预制体冲突是最烦的。因为 .prefab 是个大 JSON,两个分支都改了同一个节点,合并出来的文件里那一段会变成一堆重复的 key,Creator 读的时候可能直接报错或者丢节点。
我的对策是改结构的时候尽量避开别人正在动的预制体。这个靠约定,靠群里说一声。听起来像老办法,但预制体这东西目前没有更好的合并策略。
还有一个实操建议:合并冲突不要在合并的那一刻解。我的流程是先把主干的最新代码合进我的分支,在我的分支上解决冲突、跑一遍、确认没问题,再合回主干。这样主干上永远不会出现一个半成品状态。
1.5 一个把构建产物挡在版本库外面的具体做法
最后说个我自己的做法,可能有点土,但对独立开发者有用。
我在项目根目录放了一个 build.sh(Windows 上是 build.bat),里面固定三件事:清空 build 目录、跑命令行构建、统计产物体积并打印最大的十个文件。
那个"打印最大的十个文件"是我加的,用一行命令按大小排序输出目录内容。这个输出救了我至少三次。它让我在每次构建后立刻知道包体涨在哪。我有一次发现 audio 目录里多了一份 2 MB 的 wav,是导出时忘了转成压缩格式。包体报告直接把它顶到第一位,一眼就看见了。
这个脚本还有个副作用:build/ 目录每次都是干净的,不会有上次构建的残留文件混在里面。我见过有人构建出问题,去 build 里翻半天找文件,其实那个文件是三天前留下的。
版本库这边还有一条:我给 tag 加了说明。每次打 tag 写清楚这个版本对应的引擎版本、构建命令、以及主要改动。因为线上出问题时,我需要快速确认"当时那个包是用什么构建出来的",编辑器版本、Node 版本、有没有开 md5Cache,这些信息在 tag 说明里全写着。
