《Cocos 游戏开发实战》1.3 版本管理与协作流程

Cocos 工程该提交什么、永远不提交什么:给出完整 .gitignore 与 Git LFS 配置,讲清 .scene/.prefab 为什么难合并、多人协作怎么切场景与预制体,定下提交信息与分支约定,写一个可单测的语义化版本比较函数,并把版本号打进运行时的 BuildStamp 组件。

1.3 版本管理与协作流程

Cocos 工程进版本库这件事,比纯代码工程麻烦。原因有三:一是工程根目录里有一堆编辑器生成的缓存目录(library/、temp/),几百 MB,提交进去毫无意义;二是 .scene / .prefab 是 JSON,但它们的合并冲突几乎无法手工解决;三是贴图、音频这类二进制资源会迅速把仓库撑爆。这一节的目标是:让「clone 下来、打开编辑器、能跑」成为常态,而不是惊喜。

本节给出四样东西:一份能直接用的 .gitignore、一套 Git LFS 与协作分工约定、一条 CI 类型检查流水线、一个把版本号带进运行时的组件。做完之后,团队里任何人都能用同一条命令拿到一致的工程状态。这也是 16.1「类型校验」与 17.1「灰度」的前置条件——没有干净的版本管理,后面所有自动化都无从谈起。

1.3.1 判断标准:能重建的不提交

上一节已经给出了判断标准,这里把它落成规则:凡是能由 assets/ + settings/ 重新生成的,都不进版本库。

目录能否重建提交
assets/不能(源)✅
settings/不能(源)✅
package.json / tsconfig.json不能(源)✅
library/能(导入缓存)❌
temp/能❌
local/能(个人设置)❌
profiles/能❌
build/能(构建产物)❌

把 library/ 提交进去的代价不是「多占空间」这么简单:它会让每次 git status 出现上百个变动文件,真正的代码改动淹没在噪声里;而且不同机器生成的缓存不一致,合并时全是冲突。

1.3.2 一份完整的 .gitignore

放在工程根目录,逐条都有理由:

# Cocos Creator 生成,可重建
/library/
/temp/
/local/
/build/
/profiles/

# 依赖
/node_modules/

# 系统与编辑器
.DS_Store
Thumbs.db
*.log

前缀的 / 很关键:它把匹配锚定在工程根目录,避免误伤 assets/ 下同名的子目录(比如某个美术资源恰好叫 temp)。写完 .gitignore,用 git status 确认:应该只剩 assets/、settings/、package.json、tsconfig.json、.gitignore 这几项是「未跟踪」或「已修改」。

1.3.3 大文件走 Git LFS

贴图和音频是二进制,Git 对二进制不做增量存储,改十次就存十份。.gitattributes 里把常见资源类型交给 Git LFS:

*.png  filter=lfs diff=lfs merge=lfs -text
*.jpg  filter=lfs diff=lfs merge=lfs -text
*.mp3  filter=lfs diff=lfs merge=lfs -text
*.ogg  filter=lfs diff=lfs merge=lfs -text
*.psd  filter=lfs diff=lfs merge=lfs -text

-text 是关键:它告诉 Git 这些文件不是文本,不要尝试做行尾转换。注意 .meta 文件不要进 LFS——它们是纯文本 JSON,需要正常 diff(2.3 会讲为什么 .meta 必须提交、且必须能 diff)。

团队里每个人 clone 之后要先跑一次 git lfs install,否则拉下来的是 LFS 指针而不是真文件,编辑器导入时会报「资源损坏」。

另一个常被忽略的点:.gitattributes 必须在第一次提交之前就位。 如果先提交了 PNG 再补配置,历史里的二进制已经进库了,后续只能靠 git lfs migrate 重写历史——那是一件让所有人重新 clone 的痛苦事。

1.3.4 初始提交与克隆流程

规矩定好之后,第一次提交按下面的顺序来,别用「一键把整个目录 add 进去」:

cd block-dash
git init
git lfs install
git add .gitignore .gitattributes package.json tsconfig.json
git add assets settings
git commit -m "chore(project): 初始化 block-dash 工程骨架"
git remote add origin git@example.com:team/block-dash.git
git push -u origin main

注意是显式 git add assets settings,而不是 git add .。后者会把 library/、temp/ 也扫进去——即使有 .gitignore,也容易在 .gitignore 写错时翻车。显式列目录能让你在 git status 里亲眼确认「进库的就是这几个」。

新同事 clone 下来之后,只做三件事:

  1. git lfs install(拉真资源);
  2. 用 Cocos Creator 3.8.8 打开工程,等编辑器重建 library/;
  3. 打开 assets/scenes/main.scene,点运行,能跑就对了。

1.3.5 为什么 .scene 冲突这么难解决

.scene 和 .prefab 是 JSON,看起来应该能 merge,实际上几乎不能。原因是它们靠 __id__ 编号互相引用:

以下片段需 Cocos Creator 3.8 编辑器打开,本节不可命令行复现:

{
  "__type__": "cc.SceneAsset",
  "_name": "main",
  "scene": {
    "__id__": 1
  }
}

__id__: 1 指向文件里第二个对象,那个对象又用 __id__ 指向子节点、组件、资源。两个人同时往场景里加节点,各自的编号体系都会整体偏移——A 的 __id__: 7 和 B 的 __id__: 7 很可能不是同一个东西。git 按行 merge 出来的结果,编号对不上,编辑器一打开就是满屏报错。

在编辑器里打开一个冲突后的 main.scene,你会看到:层级管理器里节点树是错的(子节点挂到了别的父节点下),控制台刷出一堆「引用丢失」。这时手工改 JSON 基本救不回来。

对策有三条,按优先级:

  1. 一个场景一个人负责。 在团队里明确 main.scene 归谁,别人要改就走预制体。
  2. 把可复用的东西做成预制体。 玩家、金币、障碍都是独立 .prefab,改动落在预制体上,冲突面小得多。
  3. 场景拆小。 主菜单、关卡、结算各用一个场景,而不是塞进一个巨型场景。

1.3.6 提交信息与分支约定

提交信息用 <type>(<scope>): <subject>,scope 用目录层名,一眼能看出改了哪块:

type用途例子
feat新功能feat(game): 新增二段跳
fix修 bugfix(ui): 修复 HUD 分数溢出
perf性能perf(render): 合并 UI 图集降低 Draw Call
refactor重构refactor(core): 抽出难度曲线函数
chore杂项chore(build): 升级到 3.8.8

分支上,main 永远可构建,功能走 feat/* 分支,发布打 tag(如 v0.1.0)。.scene / .prefab 的改动尽量单独成一个提交,方便冲突时整块回滚,而不是和逻辑改动混在一起。

一个可执行的细则:提交前先跑一次 git status,确认没有 library/ 下的文件被误加。 如果 .gitignore 已生效,这一项应该是空的;一旦出现,说明某条路径规则写错了,趁早修。

1.3.7 美术、策划、程序各自碰什么

三个人在同一棵树里干活,最容易撞车。按目录把「谁碰哪块」写清楚,冲突面立刻小一半:

角色主要目录冲突源约定
美术textures/ audio/同名资源覆盖命名带来源前缀,如 player_ ui_
策划configs/数值被并发修改只改 JSON,源表在工程外
程序scripts/ prefabs/.prefab 冲突一个预制体一个负责人
所有人scenes/.scene 冲突主场景锁定一人维护

关键的一条是策划只碰 configs/:数值调整不进脚本、不进场景,全走 JSON。这样改一次数值不需要动任何 .scene,冲突概率接近零。8.1 会把这条约定落成一张配置表。

1.3.8 CI:把类型检查与单测挡在合并前

版本管理的最后一块拼图是自动化。在 .github/workflows/ci.yml 里放一条最小流水线,每次 push 都跑:

name: ci
on: [push, pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 26
      - run: npm ci
      - run: npx tsc --noEmit
      - run: npx vitest run

这条流水线能挡住两类错误:tsc --noEmit 挡住类型错误(包括 2.2 会讲的 @property 类型不匹配),vitest run 挡住 core/ 里纯逻辑的回归。它挡不住 Cocos 编辑器里的东西——场景搭错、预制体引用丢失,CI 看不出来,那要靠 16.2 的冒烟截图兜底。

1.3.9 版本号与存档迁移

版本号不只是给包管理器看的,它还决定旧存档能不能被新版本读。所以版本比较这段逻辑必须是纯的、可测的——它属于 core/:

export function compareVersion(a: string, b: string): number {
  const pa = a.split('.').map((s) => Number.parseInt(s, 10));
  const pb = b.split('.').map((s) => Number.parseInt(s, 10));
  for (let i = 0; i < 3; i += 1) {
    const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
    if (diff !== 0) return diff > 0 ? 1 : -1;
  }
  return 0;
}

用法是:存档里写一个 schemaVersion,启动时若 compareVersion(存档版本, 当前版本) < 0,就跑迁移链。11.1 会把这条链展开,这里先把「比较」这个最基础的动作做成可单测的函数。

1.3.10 把版本号带进运行时

线上出问题时,第一句话往往是「你装的是哪个版本」。所以构建时要把版本号打进运行时,让崩溃上报和日志都带上它:

import { _decorator, Component, director } from 'cc';

const { ccclass, property } = _decorator;

@ccclass('BuildStamp')
export class BuildStamp extends Component {
  @property
  buildVersion = '0.1.0';

  start(): void {
    const scene = director.getScene();
    const sceneName = scene ? scene.name : 'unknown';
    console.log(`[block-dash] v${this.buildVersion} scene=${sceneName}`);
  }
}

buildVersion 是 @property,所以它可以在编辑器里被赋值(2.2 会讲属性面板与序列化的关系);CI 构建时用一个脚本把它改成 package.json 里的版本。这样一条日志就能回答「哪个版本、在哪个场景」——比让用户截图强得多。

1.3.11 本节自检清单

  • 根目录有 .gitignore,且 git status 里没有 library/ temp/ build/
  • .gitattributes 已把 *.png *.mp3 等交给 LFS,且带 -text
  • .meta 文件没有进 LFS(保持文本可 diff)
  • 团队约定:一个场景一个负责人
  • 提交信息遵循 <type>(<scope>): <subject>
  • CI 里 tsc --noEmit 与 vitest run 都能过
  • compareVersion 是纯函数,能被 vitest 断言
  • BuildStamp 在启动时打印版本与场景名
  • .gitattributes 在第一次提交前就已就位(历史里没有裸二进制)

1.3.12 工程视角的收口

这一节之后,工程里多了 .gitignore、.gitattributes、一条 CI 流水线、一份提交与分支约定、一个 compareVersion 纯函数、一个 BuildStamp 组件。第 1 章到此结束——我们从「产品规格」走到「磁盘结构」再走到「版本纪律」,工程骨架已经完整:有目标、有目录、有协作规则、有自动化。

下一章我们真正进编辑器:场景、节点、预制体到底是怎么被搭出来、又怎么被存成文件的。这是从「配置工程」跨到「使用编辑器」的一步,也是全书第一次大量接触 L3 工程文件——你会看到 .scene 与 .prefab 里那些 __type__ 和 __id__ 到底意味着什么。

阅读导航:上一节:1.2 工程结构与目录约定 · 下一节:2.1 场景、节点与预制体 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「cocos」更多文章

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