Defold 团队协作与版本控制

系统讲解 Defold 项目的多人协作工程实践:项目结构与该纳入版本控制的文件、Git 工作流与忽略规则、文本资源与二进制资源的差异、场景文件冲突与合并策略、分支策略与提交规范、依赖库复用、Code Review 要点,以及构建产物与发布管理。

引言

Defold 项目在版本控制上有个天然优势:场景、图集、材质、粒子都是纯文本格式,Git 能 diff 也能合并。但优势不等于没问题——.collection 的自动合并常常产生语法正确却语义错乱的结果,二进制贴图和音频又是另一套规则,再加上多人在同一个关卡上同时改动,冲突几乎必然发生。本文系统讲 Defold 的协作规范:从项目结构讲起,覆盖 Git 工作流、资源类型差异、冲突合并策略、分支策略、依赖库、Code Review 与发布管理。

前置阅读:项目结构与资源管理、关卡编辑与场景组织。


目录


1. Defold 项目结构回顾

1. 典型目录布局

my-game/
  game.project          ← 项目配置,必须入库
  game.projectc         ← 编译产物,不要入库
  input.gamepads        ← 手柄映射,必须入库
  main/
    main.collection     ← 启动集合
    player.go
    player.script
  assets/
    sprites.atlas
    sprites.png         ← 二进制,入库但需注意
    tileset.tilesource
  fonts/
  sounds/
  gui/
  modules/
  libraries/            ← 依赖库缓存,通常不入库
  build/                ← 构建产物,不入库
  .internal/            ← 编辑器内部状态,不入库

2. 该纳入版本控制的文件

文件类型入库说明
game.project是项目配置核心
.collection / .go是纯文本场景
.script / .lua是代码
.atlas / .tilesource是图集定义(文本)
.material / .vp / .fp是材质与着色器
.particle / .gui是粒子与界面定义
.png / .ttf / .wav是源资源(二进制)
game.projectc否编译产物
build/否构建输出
.internal/否编辑器缓存
libraries/否依赖缓存,可重新拉取

3. 团队结构建议

按资源域划分所有权,避免多人同时改同一文件:
  main/           → 客户端程序员
  gui/            → UI 美术
  assets/sprites  → 美术
  sounds/         → 音频
  modules/        → 客户端程序员

踩坑:game.project 是所有人都会改的文件(加依赖、改配置、调渲染)。它是最容易冲突的文件,建议约定:只允许一人(通常是主程)在集成分支上修改,其他人通过 PR 提出需求。


2. Git 基础工作流与忽略规则

1. .gitignore 模板

# Defold 构建产物
/build/
game.projectc

# 编辑器内部状态
.internal/
*.tmp

# 依赖缓存(由 resolve 重新下载)
/libraries/

# 系统文件
.DS_Store
Thumbs.db

# 编辑器
.vscode/
.idea/
*.swp

# 日志
*.log

2. 为什么 libraries 不入库

Defold 的库依赖在 game.project 的 [project] dependencies 中声明,构建时由 bob.jar resolve 自动下载:

[project]
dependencies#0 = https://github.com/defold/extension-camera/archive/refs/tags/2.1.0.zip
dependencies#1 = https://github.com/defold/extension-websocket/archive/refs/tags/3.0.0.zip

把 libraries/ 排除后,克隆仓库只需拉取源码,依赖在首次构建时自动恢复,仓库体积能小一个数量级。

3. 提交信息规范

feat(combat): 增加暴击伤害计算
fix(player): 修复二段跳后穿墙
refactor(ui): 抽出通用的按钮组件
docs(readme): 补充构建步骤
chore(deps): 升级 camera 扩展到 2.1.0

格式:<类型>(<范围>): <描述>
类型:feat / fix / refactor / docs / chore / test / perf

4. 大文件处理

贴图和音频会随时间累积。如果仓库超过几百 MB,用 Git LFS:

git lfs install
git lfs track "*.png"
git lfs track "*.ttf"
git lfs track "*.wav"
git lfs track "*.ogg"
git add .gitattributes
# .gitattributes
*.png filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text

踩坑:LFS 必须在第一次提交大文件之前配置。如果已经提交过,历史记录里的文件不会自动迁移,需要 git lfs migrate import 重写历史,代价很大。


3. 文本资源与二进制资源的差异

1. 两类资源的对比

维度文本资源二进制资源
例子.collection .go .script .atlas.png .ttf .wav .ogg
Git diff可读,能看到改了什么不可读
自动合并支持,但可能语义错乱不支持,只能二选一
冲突解决手工编辑或重做选一边,另一边重做
LFS不需要建议启用

2. 文本资源可以 diff

 {
   "name": "player",
   "type": "go",
   "position": [0, 0, 0],
-  "rotation": [0, 0, 0, 1],
+  "rotation": [0, 0, 0.707, 0.707],
   "components": [...]
 }

git diff 能直接看出「玩家旋转了 90 度」,Code Review 时非常有价值。

3. 二进制资源无法合并

两个人同时改了 player.png,Git 只能说「冲突」,你要么保留 A 要么保留 B,没有中间选项。

规避策略:

1. 一个资源一个负责人,避免并行修改
2. 大图拆小图,减少单文件被同时触碰的概率
3. 用图集(atlas)把多张小图合并,但图集本身又成了热点 → 权衡
4. 修改前在群里喊一声「我要改 player.png」

4. 二进制冲突的解决流程

# 1. 看冲突文件
git status

# 2. 选择保留哪一边
git checkout --ours  assets/player.png    # 保留当前分支
git checkout --theirs assets/player.png   # 保留对方分支

# 3. 或者从共同祖先拿回原版,重新导出
git checkout --merge assets/player.png

# 4. 标记已解决
git add assets/player.png

记忆:文本资源「能合并但可能合错」,二进制资源「合不了只能选」。前者靠 Review 兜底,后者靠流程避免。


4. 场景文件冲突与合并策略

1. 为什么自动合并会出错

.collection 是 JSON,Git 按行合并。当两人在同一个节点的同一个数组里各加一个元素时,Git 会「聪明地」把两个元素都保留,产生语法正确但语义错误的结果:

"children": [
    "player",
    "enemy_a",     ← A 加的
    "enemy_b",     ← B 加的(本不该同时存在)
    "ui"
]

更糟的是节点 ID 引用:A 删了 enemy_a,B 又引用了 enemy_a,合并后引用悬空,编辑器能打开但运行时报错。

2. 减少冲突的组织方式

做法效果
一个 Collection 一个负责人根本性避免
关卡拆分成多个子 Collection各自独立文件
用 Factory 动态生成对象减少场景里的静态节点
频繁同步主干冲突窗口变小
用锁文件机制(如 Git LFS lock)显式独占

3. 用子 Collection 隔离

main.collection
  ├── level_1.collection      ← 关卡美术 A 负责
  ├── level_2.collection      ← 关卡美术 B 负责
  └── ui.collection           ← UI 美术负责

每个人的改动落在自己的子 Collection 里,主 Collection 只在新增关卡时改动一次。

4. LFS 文件锁

# 锁定一个文件,其他人无法推送修改
git lfs lock assets/sprites.png

# 查看已锁定的文件
git lfs locks

# 解锁
git lfs unlock assets/sprites.png

适用场景:关卡文件、大图集这类「绝对不能并行改」的资源。

5. 冲突后的验证清单

1. 编辑器能正常打开项目吗?
2. 所有节点引用都有效吗(无红色警告)?
3. 启动集合能正常运行吗?
4. 关键流程跑一遍(进入关卡、切换场景)?
5. 用 bob 命令行构建一次,确认无解析错误?

踩坑:JSON 自动合并后可能产生重复的节点 ID。Defold 编辑器不一定会立刻报错,但运行时会取到错误的对象。合并后务必在编辑器里搜索一遍可疑的 id。

6. 强制文本冲突标记

# .gitattributes —— 让 JSON 不做自动合并,强制产生冲突标记
*.collection merge=binary
*.go         merge=binary

设置 merge=binary 后,Git 不会尝试自动合并,而是直接标记冲突,逼你手工处理——这比「看起来合并成功但实际错了」安全得多。


5. 分支策略与协作规范

1. 两种主流策略

策略适用团队特点
Trunk-Based小团队(3~8 人)直接在主干开发,短分支
Git Flow大团队 / 有发布周期develop / release / hotfix 分支

2. Trunk-Based 实践

main ──●──●──●──●──●──●──●──→  永远可发布
        \    /
         ●──●   feature 分支,存活不超过 2 天
git switch -c feat/double-jump
# ... 开发 ...
git fetch origin
git rebase origin/main
git push -u origin feat/double-jump
# 开 PR,CI 通过后 squash 合并

要点:分支存活时间短(1~2 天)、频繁 rebase、PR 小而聚焦。

3. Git Flow 实践

main     ──●──────────────●──────────●──→  只放发布版本
            \            /            /
release      ●──●──●──●─●            /      发布准备
            /                        /
develop  ──●──●──●──●──●──●──●──●──●──→    集成分支
              \    /
feature        ●──●                        功能开发
# 新功能
git switch -c feature/inventory develop
# 发布准备
git switch -c release/1.4.0 develop
# 紧急修复
git switch -c hotfix/1.4.1 main

4. 美术资源的分支问题

美术通常不熟悉 Git,让他们在分支间切换容易丢文件。常见折中:

方案 A:美术直接提交到主干(配合资源锁)
方案 B:美术用独立的资源仓库,通过子模块引入
方案 C:美术用 Dropbox / 网盘同步,程序定期导入

5. 保护分支与检查

main / develop 开启保护:
  - 禁止直接推送,必须走 PR
  - 至少 1 人 Approve
  - CI 必须通过(luacheck + busted + bob 构建)
  - 禁止强推

6. 依赖库与资源复用

1. 声明依赖

在 game.project 中声明,URL 指向具体的 tag 或 commit:

[project]
dependencies#0 = https://github.com/defold/extension-camera/archive/refs/tags/2.1.0.zip
dependencies#1 = https://github.com/defold/extension-spine/archive/refs/tags/4.2.3.zip

踩坑:不要指向分支名(如 main.zip)。分支会移动,今天能构建明天可能就失败。永远指向 tag 或 commit SHA。

2. 内部共享库

团队内部复用的代码(通用 UI 组件、工具函数)可以做成私有库:

结构:
my-team-libs/
  ui_widgets/
    widget.gui
    widget.gui_script
  utils/
    math_utils.lua
  game.project       ← 库也要有 game.project
[project]
dependencies#2 = https://git.internal/team/my-team-libs/archive/refs/tags/1.2.0.zip

7. Code Review 与命名约定

1. 审查重点

类型关注点
代码逻辑正确性、性能隐患、错误处理
场景文件节点命名、引用有效性、是否有调试残留
资源命名规范、图集布局、是否误提交临时文件
配置game.project 的改动是否有必要

2. 命名约定

Game Object:  snake_case,语义明确
  player, enemy_grunt, ui_hud_root, spawn_point_01

组件 id:      角色_功能
  player#sprite, player#collision, player#script

脚本文件:     功能名.script
  player.script, enemy_ai.script

Lua 模块:     snake_case.lua
  math_utils.lua, inventory.lua

常量:         UPPER_SNAKE
  MAX_ENEMIES, GRAVITY, DEFAULT_HP

3. 场景文件的检查脚本

CI 里加一段,扫描场景文件中不合规的节点名:

# 找出不符合 snake_case 的节点名
grep -rEn '"name":\s*"[A-Za-z0-9]*[A-Z]' main/ --include=*.collection --include=*.go
-- 或者写个 Lua 检查脚本,扫描调试残留
-- 常见残留:命名为 "test"、"temp"、"copy" 的节点
local BAD_PATTERNS = { "test", "temp", "copy", "debug", "aaa" }

4. PR 模板

【改了什么】简述本次改动
【为什么改】关联 issue 或需求编号
【怎么验证】1. 打开 main.collection 运行;2. 触发目标场景确认行为;3. 用 bob 构建一次
【检查清单】luacheck 通过 / busted 通过 / 场景文件无调试残留节点 / 未提交 build 与 libraries

8. 构建产物与发布管理

1. 产物不入库

build/           ← 完全排除
game.projectc    ← 排除
*.apk / *.ipa / *.zip  ← 排除

产物通过 CI 生成并上传到 Release 或制品库,而不是提交到仓库。

2. 版本号管理

# game.project
[project]
version = 1.4.2
# CI 中按 tag 自动写入
VERSION="${GITHUB_REF_NAME#v}"
sed -i "s/^version = .*/version = $VERSION/" game.project

3. 发布检查清单

发布前:
  [ ] 所有 PR 已合并,develop 与 main 同步
  [ ] CI 全绿(luacheck + busted + 全平台构建)
  [ ] 版本号已更新并打 tag
  [ ] 变更日志已整理

发布后:
  [ ] 产物已上传到 Release
  [ ] 热更新清单已生成并验证
  [ ] 回滚方案已确认

9. 速查表

需求做法备注
忽略构建产物.gitignore 加 build/ game.projectc .internal/依赖缓存也排除
大文件git lfs track "*.png"必须在首次提交前配置
依赖声明game.project 的 dependencies指向 tag 或 SHA,不要指向分支
依赖恢复java -jar bob.jar resolvelibraries 不入库
场景冲突拆分子 Collection + 文件锁一个文件一个负责人
强制手工合并.gitattributes 设 merge=binary避免自动合并出错
二进制冲突git checkout --ours/--theirs只能选一边
文件锁git lfs lock <file>大图集、关卡文件
提交规范feat(scope): 描述Conventional Commits
分支策略小团队 trunk-based,大团队 Git Flow分支存活 1~2 天
版本号game.project 的 [project] versionCI 按 tag 写入
回滚git switch --detach <tag> 重建tag 是锚点

一句话记忆:文本资源(.collection/.go/.script)能 diff 能合并但会合错,二进制资源只能二选一——前者靠拆分子 Collection 和 Code Review 兜底,后者靠 Git LFS 文件锁避免并行;game.project 和 libraries/ 是最容易出问题的两个点,前者约定单人维护,后者不入库;依赖永远指向 tag 而不是分支。


相关阅读

延伸阅读

#!/bin/bash
# ======================================
# 完整示例:项目初始化脚本 setup.sh
# 新成员克隆仓库后执行一次
# ======================================

set -euo pipefail

echo "== 1. 检查 Git LFS =="
if ! command -v git-lfs >/dev/null 2>&1; then
  echo "请先安装 Git LFS: brew install git-lfs"
  exit 1
fi
git lfs install

echo "== 2. 拉取 LFS 资源 =="
git lfs pull

echo "== 3. 检查 Java(bob 需要) =="
if ! command -v java >/dev/null 2>&1; then
  echo "请先安装 JDK 17+"
  exit 1
fi
java -version

echo "== 4. 下载 bob.jar =="
if [ ! -f bob.jar ]; then
  curl -fO https://d.defold.com/stable/bob/bob.jar
fi

echo "== 5. 解析依赖 =="
java -jar bob.jar --root . resolve

echo "== 环境就绪 =="
echo "接下来:用 Defold 编辑器打开 game.project 开始开发"

继续阅读

探索更多技术文章

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

全部文章 返回首页

「defold」更多文章

  1. Defold 测试与持续集成
  2. Defold 本地化与多语言
  3. Defold 分析与崩溃上报