引言
Defold 项目在版本控制上有个天然优势:场景、图集、材质、粒子都是纯文本格式,Git 能 diff 也能合并。但优势不等于没问题——.collection 的自动合并常常产生语法正确却语义错乱的结果,二进制贴图和音频又是另一套规则,再加上多人在同一个关卡上同时改动,冲突几乎必然发生。本文系统讲 Defold 的协作规范:从项目结构讲起,覆盖 Git 工作流、资源类型差异、冲突合并策略、分支策略、依赖库、Code Review 与发布管理。
目录
- 1. Defold 项目结构回顾
- 2. Git 基础工作流与忽略规则
- 3. 文本资源与二进制资源的差异
- 4. 场景文件冲突与合并策略
- 5. 分支策略与协作规范
- 6. 依赖库与资源复用
- 7. Code Review 与命名约定
- 8. 构建产物与发布管理
- 9. 速查表
- 相关阅读
- 延伸阅读
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 resolve | libraries 不入库 |
| 场景冲突 | 拆分子 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] version | CI 按 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 开始开发"
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。