用 git 管理 Unity 專案有好一陣子了,剛好最近公司的 open source builder mimiron-lite 發布新版,同時公開了新的作為公司內標準的 git 設定檔。想分享一些設定檔背後的思維還有 git 使用的經驗。

TL;DR 的話可以直接參考已經編輯好的 git 設定檔:
https://gitlab.com/rayark/mimiron-lite

如果對 git 操作不熟悉的話,目前中文教學最推薦的還是 為你自己學 Git by 高見龍。除了線上版教的基本操作外,付費版內很難得有 git 資料結構(commit, tree, blob)的介紹,對於更加熟悉 git 非常有幫助。

用戶端

之前為了公司需要滿足技術人員與非技術背景人員操作的需求 survey 了很多 git 用戶端,目前選定的是需要付費的 Fork。在這之前用的是最常見免費的 SourceTree,不過登入上常常會出現問題需要排解,還有有的時候檔案多的時候會變很慢,對圖片與 LFS 檔案也沒有支援。另外有些程式同事會選用 Windows git 附帶的 Git Extensions 但是這個對美術使用上有些不友善,習慣用戶端有一個視窗的也可能會不習慣 shell extension。還有一個免費的 shell extension 選擇是 TortoiseGit,對美術素材支援到顯示 LFS 內的圖片,但是在 blame、rebase 之類比較複雜的操作比較不順手。GitHub Desktop 是功能太陽春,GitKraken 雖然有做很多美術相關的功能但是可能是因為 Electron 先天問題,在大 repo 上還蠻喘的。SmartGit 則是功能雖多但是對美術素材支援算是沒有,而且年費不便宜。

Fork

目前 Fork 提供 Windows 跟 Mac 的原生用戶端,平常速度上算是不錯。付費目前是一次性 50USD 一個序號最多啟動三台電腦,也比需要訂閱的工具負擔低(雖然不知道以後會不會改)。介面上相當簡潔但可以滿足大部分的開發需求,內建的 rebase 跟 merge 介面難得做得算好用。對於遊戲開發算是 killer feature 是內建的圖片預覽跟 diff(diff Mac 版還只有 side-by-side 沒有 swipe 跟 onion skin),而且在 LFS 的配置下也能正常運作。要說缺點的話就 revert 大量檔案時不時會卡很久,不太知道原因,如果一直發生我會從外面 revert 掉再開。

https://git-fork.com/blog/posts/forkwin-1.38/

伺服器端

一般的 git hosting 服務在 LFS 的容量與流量限制較多(GitHub 的 LFS 條款),對遊戲專案比較不友善。同時使用 hosting 服務代表 git 操作要走公司聯外網路,在素材大一些的專案也不太實際。如果情況許可推薦用主機或是 NAS 架設 GitLab 的 instance。GitLab 可以滿足 git、CI runner專案管理code review 的功能,雖然可能專案管理介面沒有像 Trello 或其他工具漂亮、順手。但是這幾個功能在 GitLab 裡可以相互 reference,像是在 issue 上面 reference merge request 的進度或是特定 CI pipeline 的建置結果,這種整合是難以取代的。只是走 self-hosting IT 方面工就會比較多,備份方面也要注意。

https://about.gitlab.com/stages-devops-lifecycle/issueboard/

External Merger

合併工具大家一般可能用內建的 KDiff3,不過我自己從以前第一份工作用 Perforce 的經驗就非常喜歡 Perforce 的合併工具。幸好 Perforce 的合併工具 P4Merge 可以單獨安裝使用且不收費。大部分的 git 用戶端都可以設定要用哪個外部合併工具,也大多可以識別 P4Merge。程式合併挑選改動跟編輯合併結果相當好用,diff 的判讀也比其他工具好。再加上有非常強大的圖片 diff,可以標出有差異的 pixel 在哪裡。要說明顯的缺點就是如果程式裡有非英文字符則顯示行高會亂掉。

P4Merge image diff

P4Merge three way merge

設定

要進行版本控制 Unity 內的 AssetSerialization 要設定成 Force Text 和 Visible Meta Files 算是基本,只有在很久以前 Unity 4.X 的時代因為編輯器是 32 位元不能定址超過 4G 的記憶體而 Force Text 會增加記憶體用量會考慮關掉。現在的 Unity 編輯器都是 64 位元不會有這個問題,Force Text 後來也變成是預設選項。

另外會要求團隊成員設定 git config core.ignorecase true關於檔名的大小寫),讓 git 把大小寫不同的檔案視為同一檔案。因為 Mac 上有可能是 case-sensitive 檔案系統,大小寫不同的檔案視為不同檔案,但是在 Windows 上卻視為相同檔案。如果沒有這樣設定則 Mac 的使用者可以推只有大小寫不同的檔案進到 repo,然後用 Windows 的成員會拉不下來。

因為我們設定了 core.ignorecase true,所以在設定 .gitignore.gitattributes 也就不會特別加入 match 大小寫的 pattern。

gitignore

.gitignore 主要是忽略 “應該留在電腦上,但是不應該共用出去的檔案” 除了 Unity 的 TempLibrary 之外,主要是程式編輯器的設定。使用過後不應該保留的檔案,像是 Xcode build 或是 APK 我是習慣不加 .gitignore,讓它們顯示為 untracked file 方便清除。一個小細節是如果 pattern 以 / 開頭只會 match repo 根目錄下的檔案或目錄,像是 Library 我會寫成 /Library/ 以免忽略到其他目錄裡面叫做 Library 的目錄。設定的範例可以參考:

https://gitlab.com/rayark/mimiron-lite/-/blob/master/.gitignore

gitattributes

.gitattributes 因為牽涉到 LFSCRLF 所以就比較複雜了。

LFS (Large File Storage)

先說 LFS,基本上是把特定副檔名的檔案搬出 git 移動到 LFS server,而原來的 git 路徑只留下 pointer file,pointer file 裡有 oid。當有需要時 LFS 會用 pointer file 裡的 oid 到 LFS server 查找與下載檔案並取代(Smudge)掉 pointer file。

Pointer file 的內容範例如下,容量大約 130 bytes 左右。記得這個容量大小,如果看到檔案變成這個大小要想是不是 LFS 的替換沒有正常執行。

1
2
3
4
version https://git-lfs.github.com/spec/v1
oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
size 12345
(ending \n)

現在 git 用戶端通常都會偵測 LFS 並在 clone 的時候做好 LFS 的設定,不過如果 LFS 替換有問題可以手動執行:

1
2
git lfs install
git lfs pull

使用 LFS 可以降低 git 歷史的大小,因為 git 本身變成只儲存 pointer file。這樣可以降低 clone 在本機硬碟上的大小,並增進效率。降低 clone 大小這件事對 binary 檔案大的 3D 遊戲專案非常有感。但是缺點有:

  • git 操作變成要持續跟 LFS server 溝通,喪失 git 完全分散式與離線工作的優點
  • 有時需要排除 LFS 製造的問題,最常見的是 clone 或是 fetch 的時候 LFS 沒有觸發替換 pointer file 為真正的檔案內容。有些罕見有遇過的是 GitLab 的 LFS server 弄丟檔案導致 fetch 不下來。
  • 有些功能可能在 LFS 環境下會不能使用,包含部分工具可能會無法 diff LFS 檔案,gitlab-runner exec 在有 LFS 的環境下還無法使用

以遊戲 binary 檔案佔 repo 大部分的情況下是建議遊戲專案使用 LFS 的,而且越早用越好,因為已經推進 git 歷史裡的 binary 檔案會一直留在歷史裡,修改 .gitattributes 只會影響到後面增加的檔案。如果 repo 已經大到受不了了,只能做 migration 一途,但是變成所有人都要重新 clone,是超大的工程。

要進行 migration 首先要在本地 track 所有的分支,因為 lfs migrate 只會作用在本地分支:
https://stackoverflow.com/questions/379081/track-all-remote-git-branches-as-local-branches

然後執行(以改寫 .png 與 .psd 歷史成為 LFS 作示範):

1
2
3
4
git lfs migrate import --everything --include=".png,.psd"
git reflog expire --expire=now --all && git gc --prune=now
git remote set-url origin <新 repo 位置>
git push --all origin --force

另外一種常見的狀況是修改了 .gitattributes 把新的附檔名納入 LFS 範圍,但是已經存在的檔案沒有置換成 pointer file,這時候在 clone 或是 reset 的時候都會收到警告:

1
Encountered 1 file(s) that should have been pointers, but weren't

如果沒有要往回整理歷史只是要把目前檔案換成 pointer file,可以用:

1
2
3
# from https://stackoverflow.com/a/51626808
git rm --cached -r .
git reset --hard

或是使用

1
2
# from https://github.com/git-lfs/git-lfs/issues/3421#issuecomment-610489798
git add --renormalize .

然後把檔案變動 commit / push 即可。

只要在 .gitattributes 加入

1
*.png filter=lfs diff=lfs merge=lfs

像這樣就可以把 PNG 圖檔交給 LFS 管理(因為有設定 core.ignorecase true 所以我們不用寫成 *.[pP][nN][gG])。基本上所有不能直接文字編輯的二進位檔案都該加入 LFS,但是 Unity prefab (.prefab) 跟 scene (.unity) 我會選擇不加入 LFS。在 AssetSerialization 設定為 Force Text 後 Unity 的 GameObject 結構 YAML 算是可以判讀與做文字 diff 的,很多時候可以這樣確認 prefab 或是 scene 的修改有沒有意外改到別的東西。

如果你也習慣這樣判讀 scene 跟 prefab 的改動,記得在升級 Unity 的時候要對所有 scene 跟 prefab 下 AssetDatabase.ForceReserializeAssets 把 .prefab 或是 .unity 升級到新的 Unity 的格式。因為 Unity 預設行為會升級後等到有改動才用新格式儲存,但這樣會變成你的改動跟升級的改動混在一起難以判讀 diff,所以需要在剛升級 Unity 還沒有修改的時候強制檔案升級。

Unity 有提供自己的 YAML merger,不過試用之後覺得結果偶而怪怪的。目前還是只有判讀 diff,沒有在對 scene 跟 prefab 使用自動 merge。

另外一個特例是 .DLL,當 DLL 沒有正確被替換時會造成編譯錯誤,如果 Unity 在抱怨找不到應該定義在 DLL 內的 symbol 時記得先檢查專案內的 DLL 是不是只有 pointer file 的 130 bytes 大小。

CRLF

.gitattributes 另一個大坑是 CRLF,因為 Windows (CRLF \r\n)、 Mac (OSX 以前 CR \r,以後 LF \n) 與 Linux (LF \n) 的行尾不同,git 預設會在 checkout 與 commit 時幫你做轉換,也就是 autocrlf。但是如果沒有小心設定會導致兩個問題:

  • 轉換到根本不是文字的 binary 檔案,這樣檔案本地開起來會是壞的
  • 有些檔案行尾不斷改變,最常見的是 .meta,造成很多不是人為編輯造成的改動

很多人可能會用這個很熱門的 Unity .gitattribute 樣本:
https://gist.github.com/nemotoo/b8a1c3a0f1225bb9231979f389fd4f3f

但是它將了 Unity 的 .asset 檔案設定為 LF,而 Unity 有非常多種 .asset 檔案,之前調查過的結果

強制轉換成 LF 會產生問題,底下也有人回報,但是一直沒有更新,所以不建議用這份設定。

我自己最後的設定是開頭:

* -text

預設先將所有檔案的 text 屬性 拔掉關閉行尾轉換,之後再利用 .gitattributes 可以用下方的規則覆寫上方的規則的特性用白名單方式加回純文字程式碼的行尾轉換。

沒有寫成

* binary

是因為 binary 代表 -text -diff,我們希望拔掉行尾轉換但是希望對 scene、prefab 與 meta 留住文字 diff。看 diff 可以抓到 GameObject、Component 的變化,或是 meta 的 GUID 跑掉的情況。

Prefab 在沒有加入 LFS 沒有 -diff 的情況下會在 git 用戶端裡顯示像這樣的 diff。可以看到我們把 RectTransform 大小改成 200 X 200,但同時不小心把 GameObject 關了。

看到歷史裡面有像這樣 .meta 裡的 GUID 變化是非常危險的,代表 reference 會掉。

至於行尾常常亂跳的 .meta 因為實驗出來在任何平台 Unity Editor 都是存成以 LF 換行,所以寫死

*.meta eol=lf

.mat 也應該是 LF 換行,不過它不常亂跳所以沒有像 meta 寫死。

至於文字編輯的原始碼檔案則應該啟用 autocrlf 壓過 * -text 設定,確保這些檔案在 repo 裡的行尾一致,以免像是 git blame 之類的工具受到整個原始碼的行尾跳動干擾難以閱讀。

*.cs text=auto

另外一種方向是把所有文字檔案都設定為 eol=lf,因為 Windows 上大部分的程式編輯器其實遇到 LF 結尾的原始碼還是能正常運作。這個選擇就比較屬於偏好。

整個設定可以參考

https://gitlab.com/rayark/mimiron-lite/-/blob/master/.gitattributes

EditorConfig

https://editorconfig.org/

這個跟 git 本身無關,不過也是可以放在 Repo 裡的設定檔就順便提一下。目前我們使用的設定很陽春:

1
2
3
4
5
6
7
8
9
root = true

[*]
charset = utf-8

indent_style = space
indent_size  = 4

trim_trailing_whitespace = true

gitattributes 可以控制行尾,但是程式碼還可能會有文字編碼或是 tab / space 不一致的問題。而編碼與 tab / space 可以靠在專案內放置 editorconfig 提示程式編輯工具如何處理來完成。目前 Visual Studio、MonoDevelop 與 Rider 都會自動偵測 editorconfig 的設定,而 Visual Studio Code 與 Notepad++ 則是需要安裝 plugin。

更進一步可以把 Visual Studio 或是 ReSharper 的設定也放進 repo,從使用工具讓行尾、tab / space 之類的統一提升到 naming 或是 coding convention 的統一。不過因為公司還沒有統一導入 JetBrains 的編輯器,所以沒有這方面的實務經驗。

Worktree

如果是開發多平台遊戲的人可能會習慣 clone 同一個專案一次以上,然後把平台設定成不同,例如一個 clone 平台設定成 Android 一個平台設定成 iOS。雖然 cache server 或是 AssetDatabase V2(還不穩定,不建議用在 production)可以降低平台切換的成本,但是在硬碟上保留兩份專案還是最快最直觀的。缺點很直接是硬碟用量很大,但是其實可以 clone 一次然後開一個以上的 worktree,這樣雖然還是有多個專案目錄,但是只會有一個 .git 資料夾,即電腦上只有一份 git 歷史。

在 git 根目錄輸入

1
git worktree add ../project-ios

即可在上層目錄再開一個叫 project-ios worktree

額外的 worktree 一般的 git 用戶端都可以識別,操作起來跟操作普通的 clone 沒有太大差別。

列舉 worktree

1
git worktree list

當不需要使用時只要把 worktree 刪除然後執行

1
git worktree prune

可以參考:

Hooks

git 可以在設定裡加入自動觸發的 shell script 來作檢查或清理,常見的需求有清除空目錄與其 .meta 跟檢查檔案 commit 時有沒有加入對應的 .meta。不過這個我沒有推廣到整個團隊的經驗所以就沒有什麼個人經驗分享,可以參考:

如果有機會自己做的話會比較想用 Python 之類的語言實現,這樣邏輯會比較好懂。另外空目錄的 .meta 平常不會造成問題,因此我沒有特別急著要用 hook 檢查(commit 缺 .meta 則是宣導後蠻少發生的,不過當然如果有自動檢查總是好)。但是有個特別要注意的是 iOS 與 Mac 環境的 .framework.bundle 資料夾,這些資料夾裡面裝的是 plugin 而 import settings 放在目錄 .meta 裡,如果刪除時沒有正確清理目錄 .meta 則 Xcode 會試圖 import 空的 plugin 然後 build fail。

Rebase

使用 git 到現在算是適應 fork 後 fetch upstream、push origin、開 merge request 的流程(可以參考 【狀況題】怎麼跟上當初 fork 專案的進度?),甚至是加其他同事的 fork 當作 remote。在我自己的 fork 上面還蠻習慣用 rebase另一種合併方式(使用 rebase))或是 cherrypick【狀況題】如果你只想要某個分支的某幾個 Commit?)改造歷史的,如果有人在我 merge 之前 merge 到主要分支,我會把自己分支 rebase 到別人的 merge 後重開 merge request,然後開 --no-ffno fast-forward)弄出一串小耳多形狀的歷史([狀況題]為什麼我的分支都沒有「小耳朵」?),方便以後回頭查找。另外有一種情況是可能某個 feature 把 X 改成 Y,後來發現做錯了又從 Y 改成 Z。這樣的情況我會試著在發 merge request 之前把歷史改成 X -> Z 而非 X -> Y -> Z 減少 reviewer 的負擔。感覺這些操作可能可以加入 code review 標準,不過目前時程壓力狀況下還是只有我自己會要求自己,還沒有推廣的經驗。

Branch 直接使用 merge 與 rebase 後 merge --no-ff 的歷史差別,右邊 rebase 的歷史比較好懂(歷史僅供示範用,工作的時候應該情況會更複雜)

關於 rebase 有很多很好的中文文章,可以參考:

對非技術人員的話操作 rebase 可能會比較困難,但是如果非程式都用 pull 的話會產生大量的 merge。有一派的作法是替非技術人員設定 pull.rebaserebase.autoStash

1
2
git config pull.rebase true
git config rebase.autoStash true

這樣在 pull 的時候會觸發 stash、rebase 和 apply stash,對於減少 merge 蠻有效的。但是麻煩的是如果有 conflict 會從 merge conflict 變成更難排除的 rebase conflict。我自己試過一陣子後因為要不斷幫忙處裡 rebase conflict 放棄這個做法,要是有方法可以偵測 conflict 來決定要不要觸發 rebase 的話會考慮嘗試。

如果對 pull rebase 有興趣可以參考:

Sparse checkout & Shallow clone

在 checkout 的時候其實不一定要 checkout 出整個專案,可以將 pattern 寫入 .git/info/sparse-checkout 只 checkout 特定的檔案或資料夾。以下是只 clone 之後只 checkout /Assets//ProjectSettings/ 的示範。

1
2
3
4
5
6
git clone --no-checkout <Repo URI>
cd <Repo 路徑>
git config core.sparseCheckout true
(echo /Assets/) > .git/info/sparse-checkout
(echo /ProjectSettings/) >> .git/info/sparse-checkout
git checkout

Git 2.25.0 加入了 git sparse-checkout,可以用 git sparse-checkout init 取代 git config core.sparseCheckout true git sparse-checkout set/add 取代直接編輯 .git/info/sparse-checkout

另外 git clone 可以加入 --depth 參數限制 clone 下來的歷史深度

1
git clone --depth 5 -b <Branch> <Repo URI>

如果要還原的話執行:

1
git fetch --unshallow

Sparse checkout 與 shallow clone 都可以加快 git 操作的速度與減少空間用量,不過平常直接用在要編輯的 clone 機會不多。最實用的應用是在設定 build pipeline 的 git,控制 build server clone 跟 checkout 的行為達到加速建置的效果。

可以參考:

Branching Model

先前流行過的 Git FlowGit Flow 是什麼?為什麼需要這種東西?)在使用過許久之後決定棄用,它帶來的好處沒有大過它的複雜度帶來的困擾(Hacker News 討論),尤其是在不完全是技術背景的遊戲團隊裡更是執行困難。

另外 Git Flow 沒有特別強調 feature branch 生命週期必須短或是必須頻繁地向主要分支合併。如果有長的 feature branch 就是沒有 integration(整合)。開長的 feature branch 並不會帶給你獨立工作的環境,只是獨立工作的假象。它的真正效果是延遲支付整合的成本,但是到整合的時候就是連本帶利的吐出來。如果有遇過在 sprint 或是 milestone 快結束前好幾個人想 merge 主幹分支互相衝突到死,還有硬是 merge 後程式穩定度大爆炸又沒有時間修的情境就知道我的意思。

現在開始往 Trunk-Based Development 過渡,開短的 feature branch 快速往主要分支合併,或是長的 feature branch 但是用 Branch by AbstractionFeature Toggles 讓未完成的功能還是能定期合併進主要分支又不影響其他人,由此把整合的成本分攤開來。如果有在跑 Scrum 有時會遇到功能在 sprint 結束的時候還收不了尾,如果都放在很久沒合併過的 feature branch 上就這樣帶到下一個 sprint 會非常困擾,所以變成在 sprint 尾聲非得整合進去的壓力很大。如果功能是 feature toggles 控制的話就可以有一個小段落就合併,下個 sprint 開始再重新評估規畫即可。然後 feature 完成之後一般是清除掉 toggle,但也有沿用 toggle 的結構做成 A/B test 或是 remote config 的可能。

另外 code review 也是在頻繁合併發 merge request 的環境比較適合做,一般人一次能 review 的程式長度其實有限。開發超過一週的改動份量 review 起來就會非常吃力,更久就是折磨 reviewer 了。

LFS lock

在 git LFS v2.0.0 新增了 鎖定 特定檔案的功能,新版的 Fork 也支援從 UI 執行 LFS lock。在 gitattributes 加入 lockable 即宣告為可以鎖定的檔案:

1
*.jpg filter=lfs diff=lfs merge=lfs -text lockable

之後便可以用 lock 與 unlock

1
2
git lfs lock <檔案路徑>
git lfs unlock <檔案路徑>

雖然這個功能感覺可以實作類似 Perforce 的 checkout 同時鎖定檔案功能,但是目前還是需要靠手動檢查 lock 狀態,不是很方便。所以沒有推廣到專案上,但是覺得是一個有潛力的方向。希望之後的版本能改進,或是有方法靠 hook 兜出像是 Perforce 偵測到檔案編輯就自動上鎖的功能。

Single source of truth

因為我們曾經是用 Mercurial 來管理專案,所以在導入 git 時有一段時間是程式們改用 git 而美術們繼續用慣用的 Mercurial,用腳本定時複製同步兩邊的改動。結果追查改動難度變超高,要不斷找同步的時間點然後跳到另一邊繼續追查歷史。在急著要出 hotfix 的時候特別歡樂。後來受不了強制所有成員都改用 git,也因為這樣所以才會有上面為非技術人員 survey 用戶端的故事。

這個經驗讓我覺得構成一個遊戲的資料應該放在同一個歷史,即同一個 repo 下。所以也打消了 survey 一些跨版本控制系統同步工具像是 Git Fusion 或是 git-svn 的念頭。變成都用 git,然後儘量解決非技術人員的痛點。同時也漸漸少用 git submodule,要同步 submoudle 跟外面的 repo 的歷史太容易出錯。共用的工具庫現在走回最傳統的上 tag 上版號出安裝包讓各專案安裝,各專案把內容直接 commit 進各自的 repo。

構成一個遊戲的資料應該放在同一個歷史的想法繼續推展下去,server 的原始碼與企劃數值表也都該跟 client 專案放同一個 repo。當遊戲資料格式需要改動的時候,一個 commit 包含 server、client、數值表三者的同步更新,徹底解決 server、client 與數值資料 schema 不同步造成的問題。但是苦於數值沒有現成好的編輯工具,企劃還是得在 Google Sheets 上編輯然後下載下來,所以這個想法還是一直處於我自己的空想階段。

關於數值編輯工具的討論,可以參考:

為什麼要用 git?

其實開啟 LFS 就減損了 git 分散式的特性,使用 thunk-based 對於 branch 能力的需求就降低不少,然後現在我們試圖在 LFS 上面開啟 lock。也會有很多人問說為什麼不乾脆用 SVN、Perforce,在這篇文章草稿討論時也有幾位開發者朋友提到他們改用 Plastic。我想了很久想不太到一個好的答案,git 在設計時應該是沒有把遊戲開發常見的大檔案、無法合併的 binary 使用情境考慮進去,現在是用 LFS 外掛在 Smudge / Clean 系統上湊出來的。而這篇文章很大一部分其實也是在處理這種外掛作法衍生的問題。當初公司從 Mercurial 搬到 git,單純是感覺到 Mercurial 的開發動能比 git 少很多,像是 merge request 接到 code review 的工具 Mercurial 當時找不太到適用的,而 git 有 Gerrit、GitHub 跟後來我們在用的 GitLab。git 用戶端也是推陳出新,而 Mercurial 就是那幾種。所以這是 Worse is better 裡面所說 worse 的勝利嗎?也許是(不是說 Mercurial 就是 better,老實說我也不知道這個問題的 better 是什麼)。

我想至少不管是什麼原因你選擇了用 git 版本控制你的 Unity 專案,至少我可以提供一些經驗與協助。我沒有覺得非用 git 不可,如果你有更喜歡的做法也很好,然後歡迎分享你的經驗。如果沒有想法的話可能就使用者多的地方資源多些,踩坑的隊友也多些。

希望這篇文章能讓洗了 git 頭的大家少跟 git 搏鬥一分,開發遊戲多一分。有機會再見。

鳴謝

感謝 review 過這篇文章草稿給過建議的包子、小善學長、小金學長、Hugo、建豪、蒼時、頭皮、Colin、于修、Denny、 JohnSu、Recca、Jonas 與各位朋友們。如果有想要補充討論的也歡迎留言。