iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0
Vibe Coding

從 Vibe Coding 到系統架構:Gemini x Claude Code 雙 AI 協同開發 Godot 2D Roguelike 卡牌遊戲實戰系列 第 19 篇

Day 19:【部署驗證】Godot 跨平台實機驗證:Web、Android、iOS 部署流程與六個踩坑紀錄

  • 分享至 

  • xImage
  •  

線上實測與發布產物: 專案已成功部署至 itch.io,可在瀏覽器中直接運行實測:
https://jasonhung.itch.io/obsidiantrial

在瀏覽器裡遊玩的《黑曜石之試煉》,畫面是第 10 關的魔王戰:上方是魔王血條與下一步意圖,中間五張手牌,下方是玩家血條與狀態列

除了 Web 版本外,同一個 Godot 專案也順利產出了 Android (.apk) 與 iOS (.ipa) 的發布檔,並完成實機安裝與運行驗證。

這次跨平台發布最值得紀錄的一點:
遊戲程式碼並未針對這三個平台進行任何調整。

整體開發時間主要集中在平台專屬設定、環境建置,以及各平台的規範配置上。


一、跨平台發布目標與驗證結果

到昨天為止,《異世界救援》主要還是在我的電腦裡跑。
雖然到目前為止,畫面還很陽春。
不過今天目標是驗證同一個 Godot 專案發布至三個不同平台的可用性。
相關發布產物與驗證情境如下:

平台 產物 這次的用途
Web index.html + .wasm + .pck 瀏覽器直接玩
Android .apk 實際裝進 Android 手機
iOS .ipa 實際裝進 iPhone, iPad

測試結果: 三個平台皆順利完成發布與實機驗證。

發布觀察與結論

這次跨平台發布的經驗顯示:
AI 適合執行自動化匯出流程,但「目標部署路徑與交付物內容」,仍須人工審核與確認。

當專案從單機開發進入對外部署階段,作業範疇將擴展至發布憑證、數位簽章、帳號授權與公開網域託管:

  • 程式碼錯誤:屬於開發層面,修正後即可重新編譯發布。
  • 部署配置錯誤:涉及資安權限、隱私與公開發布範疇,處理成本與影響範圍完全不同。

二、Godot 匯出機制的兩項核心概念差異:樣板(Template)與預設(Preset)

在進行 Godot 跨平台發布時,必須先釐清兩項功能相似但權責不同的核心概念:

是什麼 從哪裡來 存在哪裡
匯出樣板(export template) Godot 官方預先編好的引擎執行檔 官方伺服器下載 系統使用者目錄(所有專案共用)
匯出預設(export preset) 專案專屬的平台發布設定(如 App 名稱、架構、圖示、簽章等) 開發者自行建立 專案裡的 export_presets.cfg

簡單講:

  • 匯出樣板:提供目標平台運行的底層引擎環境。
  • 匯出預設:定義專案在該平台的具體建置參數。

匯出預設設定與關鍵欄位解析

於 Godot 編輯器中點選 「專案 → 匯出」,點擊左上角 「新增...」 即可建立 Web、Android、iOS 等平台的發布預設。

Godot 匯出視窗,新增按鈕展開的平台清單,由上而下是 Android、iOS、Linux、macOS、visionOS、Web、Windows Desktop

新增後,各平台將擁有獨立的配置集:

Godot 匯出視窗,左側預設設定清單有 Web、Android、iOS 三組,右側顯示 Web 這組的名稱、Export Path 與選項分頁

在此面板中,有三項影響 CLI 自動化與部署發布的關鍵設定:

1. 預設名稱(Name)

在 CLI 執行自動化建置時,指令會參照此處定義的名稱:

godot --headless --export-release "Web" build/web/index.html

指令中的 "Web" 即為此組預設的命名(可依專案需求自訂)。

2. 輸出路徑(Export Path)

定義編譯產物的指定放置位置。將 Web 版路徑明確隔離為:

export_path="build/web/index.html"

此設定能確保發布產物獨立存在,避免後續 CLI 自動部署時誤將其他平台的安裝檔一併打包。

3. 執行緒支援(Thread Support)

本專案維持預設關閉。此設定可避免 Web 版發布後需要額外配置伺服器端的 SharedArrayBuffer、COOP(Cross-Origin-Opener-Policy)與 COEP(Cross-Origin-Embedder-Policy)等跨來源隔離標頭。


三、匯出樣板(Export Template)的安裝與版本一致性

除了「匯出預設」外,另一個必須先行配置的元件為「匯出樣板」。

設定位置於:「編輯器 → 管理匯出樣板...」:

Godot 的編輯器選單展開,管理匯出樣板那一項被反白

透過點擊 「下載並安裝(Download & Install)」 或 「Install All Templates」,即可一次性下載各平台所需的編譯預設環境(容量約 1GB)。

匯出樣板管理員,上半部是可用範本的樹狀清單,下半部的已安裝範本有 Android、iOS、Web 與常見底下的 ICU Data

安裝完成後,可至系統使用者目錄下檢查相關檔案(以 macOS 為例):

ls ~/Library/Application\ Support/Godot/export_templates/4.7.1.stable/

會看到目錄內容將包含各平台的二進位檔與壓縮包像:

android_debug.apk
android_release.apk
android_source.zip

ios.zip

web_debug.zip
web_release.zip
web_nothreads_debug.zip
web_nothreads_release.zip

關鍵注意事項:版本匹配與完整性

在配置匯出樣板時,有兩項影響自動化建置的重要規則:

版本字串必須完全一致
樣板目錄名稱(如 4.7.1.stable)必須與執行的 Godot CLI 版本(godot --version)完全匹配,否則 CLI 建置時將無法正確載入對應樣板。

跨平台樣板需預先完整安裝
若系統僅安裝 Web 平台的樣板,後續進行 Android 或 iOS 匯出時,建置流程將因「找不到目標平台樣板」而直接中斷。因此在進行多平台 CLI 自動化發布前,須確保所有目標平台的樣板皆已安裝完畢。


四、Web:先把最簡單的送出去

Web 是三個平台裡最直接的一個。

mkdir -p build/web
godot --headless --export-release "Web" build/web/index.html
python3 -m http.server 8060 --directory build/web

然後瀏覽器打開:

http://localhost:8060

先在本機確認。

mkdir -p 不能省,Godot 不會幫你建立不存在的輸出目錄。

這次產出的主要檔案有:

檔案 大小 用途
index.wasm 38MB Godot 引擎本體
index.pck 9.5MB 遊戲內容
index.js 273KB Web 端的支援程式

這裡有一個跟 Day 09 很像的細節:
--export-release 的 exit code 是有意義的。

成功是 0,失敗是 1。
所以它可以放心放進自動化腳本。

這跟 Day 09 的 --check-only 不一樣。

同一個 Godot 執行檔,不同功能的 exit code 行為可能不同,不能因為之前踩過一個坑,就把所有 CLI 都當成同一種規則。


五、中文遊戲,匯出後才發現字不見了

這次有兩個設定,編輯器裡看起來都正常,匯出後才會知道有沒有設對。

1. 嵌入中文字型

[gui]

theme/custom_font="res://assets/fonts/NotoSansTC-Regular.otf"

如果沒有設定,匯出版的中文可能全部變成空白方框。

原因很簡單:
在 macOS 編輯器裡,Godot 可以借用作業系統的中文字型。
但 Web 和手機沒有這個環境可以借。
所以本機看起來正常,不代表匯出後也正常。

所有中文都顯示成空白方框:
網頁版畫面,所有中文都顯示成空白方框

中文正常顯示:
同一個畫面,中文正常顯示

這我第一次撞到的時候,沒有立刻處理。
因為當時遊戲還是灰盒,美術和字型都還沒定案。

我只先確認:

知道是字型原因就好,目前不會阻止匯出流程。

等到後面確定視覺風格,再換正式字型。
這樣比較不會在還沒決定的東西上先花時間。


2. 打包中文斷行資料

另一個設定是:

[internationalization]

locale/include_text_server_data=true

預設是 false。

少了它,中文可能會出現奇怪的斷行,例如:

失去 3

點生命,本場戰
鬥 +3 攻擊力。

第一個反應很容易是去改 autowrap_mode。

我當時也這樣想。
所以我把同一段文字用四種設定並排測試,包括:

  • WORD_SMART
  • ARBITRARY
  • 搭配 language="zh"

結果四種換行都一樣,而且桌面版本來就是對的。
接著又懷疑是不是手機把卡片壓窄了。
量尺寸:

Card 實際大小   = (200.0, 260.0)
DescLabel 大小  = (160.0, 176.0)
字級            = 21
行數            = 4

尺寸也沒有變。
最後才確認:

問題在匯出版沒有帶完整的 ICU 斷行資料。

這次我就直接記下來:

如果匯出後中文斷行怪怪的,先查 include_text_server_data,不要先去改 autowrap_mode。

代價是包體多約 4.4MB。

另外,匯出樣板裡還有一個容易忽略的:

常見 → ICU Data

我把它也裝起來。

匯出時原本看到:

使用編輯器內嵌文字伺服器資料,若匯出範本使用不同 ICU 版本建置,
匯出專案中的文字顯示可能會異常

安裝後變成:

Using text server data from export templates.

這裡重新匯出後做差分比對,檔案位元組完全沒有改變。
因為目前 Godot 版本相同,編輯器內嵌的 ICU 資料和匯出樣板裡的本來就是同一份。
所以這不是「裝完就修好一個 bug」。
它比較像是把未來升級 Godot 時可能出現的版本差異固定住。


六、Android:程式不用改,環境要準備好

Android 前置需要:

需要 怎麼確認
JDK 17 以上 java -version
Android SDK ls ~/Library/Android/sdk/build-tools
debug keystore ls ~/.android/debug.keystore

Android SDK 和 Java 的路徑要填在 Godot 的編輯器設定裡,不是專案設定。

確認後建立 Android 匯出預設,至少調整:

欄位 值
package/unique_name 例如 com.你的名字.遊戲名
version/name 0.1.0
architectures/arm64-v8a 開
screen/immersive_mode 開

然後:

godot --headless --export-debug "Android" build/ObsidianTrial.apk
adb install -r build/ObsidianTrial.apk
adb shell monkey -p com.你的名字.遊戲名 -c android.intent.category.LAUNCHER 1
adb exec-out screencap -p > shot.png

最後一行很實用。它可以直接把手機畫面抓回電腦,之後要檢查或交給 AI 看都很方便。


不要只確認 APK 能不能安裝

我另外檢查了 APK 裡到底裝了什麼:

aapt2 dump badging build/ObsidianTrial.apk \
  | grep -E "^package|application-label|native-code"

實際輸出:

package: name='com.jasonhung.obsidiantrial' versionCode='1' versionName='0.1.0'
application-label:'黑曜石之試煉'
native-code: 'arm64-v8a'

再確認簽章:

apksigner verify --print-certs build/ObsidianTrial.apk

這次還順手確認了一件事:

aapt2 dump permissions build/ObsidianTrial.apk

結果是空的。這是一個單機離線遊戲,確實沒有需要額外權限。


觸控也不用另外寫

我原本以為手機版還要另外處理一套觸控程式碼。
結果不用。

Godot 的:

emulate_mouse_from_touch = true

預設就是開啟的,所以 Button 的 pressed 可以直接收到手指點擊。
這次唯一跟滑鼠有關、到了手機失效的,是 tooltip_text。
因為手機沒有 hover。程式不會壞,只是那個功能自然不會觸發。


七、iOS 平台發布:簽章配置、規範與實機部署

iOS 平台的發布環境須具備以下前置條件:

  • macOS
  • Xcode
  • Apple Developer 帳號
  1. 簽章配置與 Team ID 提取
    在 Godot 的 iOS 匯出預設中,application/app_store_team_id 為關鍵必填欄位。為確保 Team ID 的正確性,可透過 macOS 的 security CLI 工具從本機簽章憑證中提取:

首先列出本機可用的簽章身分:

security find-identity -v -p codesigning

接著解析目標憑證的詳細資訊:

security find-certificate \
  -c "Apple Development: 你的名字 (ABCDE12345)" \
  -p \
  | openssl x509 -noout -subject

解析輸出範例:

subject= /C=TW/O=.../OU=XXXXXXXXXX/CN=...

其中 OU= 後方的 10 位字串(如 XXXXXXXXXX)即為 Team ID。
我第一次就是拿錯括號裡那組去填。
所以這裡不要看錯。


圖示規範(Icon Requirements)

iOS 發布機制對 App 圖示有強制規範:

[application]

config/icon="res://assets/images/icon.png"

需準備一張 1024×1024 像素 的 PNG 圖片,Godot 在編譯時會自動裁切與產生各尺寸所需的圖示檔。

  • Android:若未配置圖示僅輸出警告,仍可順利產出 .apk。
  • iOS:若缺乏圖示配置,建置流程將直接中斷並宣告失敗。

這就是附錄裡第四個坑。


執行 iOS 自動化匯出指令:

godot --headless --export-debug "iOS" build/ios/App.xcodeproj

Godot 除了產生 App.xcodeproj 專案檔外,會自動呼叫後續的 xcodebuild 流程,最終於指定目錄生成以下產物:

build/ios/App.xcodeproj   # Xcode 專案檔
build/ios/App.xcarchive   # 編譯歸檔包
build/ios/App.ipa         # iOS 安裝套件

取得產物後,可使用 Xcode 的 devicectl 工具將應用程式安裝至實機:

# 1. 查詢已連接的實機 UUID
xcrun devicectl list devices

# 2. 將產物安裝至指定的實機
xcrun devicectl device install app --device <UUID> \
  build/ios/App.xcarchive/Products/Applications/App.app

# 3. 啟動應用程式
xcrun devicectl device process launch --device <UUID> <bundle-id>

在測試過程中,Apple Silicon(M 晶片) Mac 上的 iOS 模擬器測試路徑無法運行。

後來確認,Godot 官方提供的 iOS 模擬器樣板僅包含 x86_64 架構,而 Apple Silicon 的模擬器環境要求 arm64 架構。因此在 Apple Silicon 環境下,發布驗證須直接採用 iOS 實機進行。


八、itch.io 命令列工具 butler 的部署設定與路徑隔離

在三個平台的發布過程中,Web 版的上線授權是資安與路徑控制最關鍵的一環。

itch.io 提供了官方 CLI 工具 butler,驗證流程如下:

butler login

執行後會開啟瀏覽器進行 OAuth 授權,憑證將安全保留於本機環境,無須將帳密交由 AI 處理。登入後確認版本:

butler version

接著於 itch.io 後台建立專案並完成基礎配置:

  1. Kind of project 選 HTML
  2. 上傳的檔案勾選「This file will be played in the browser」
  3. Visibility 改成 Public

部署路徑的審核與隔離

AI 最初提供的上傳指令如下:

butler push build 你的帳號/專案代號:html

在執行前,經確認 butler push 的第一個參數屬性為「指定目錄且會進行遞迴上傳(Recursive Upload)」。
檢視當時的 build/ 目錄結構:

build/

├── index.html、index.wasm、index.pck ...   網頁版,48MB
├── ObsidianTrial.apk                       Android 安裝檔
└── ios/                                    Xcode 專案、xcarchive、ipa,578MB

整體目錄大小達 626MB,其中 578MB 為非 Web 相關產物,且包含簽章後的二進位檔。若直接將整個 build/ 推送至公開頁面,將引發以下三個層面的問題:

  • 資安與敏感資料外洩
    ios/ 目錄內包含 Xcode 專案檔、編譯產物與簽章組態。將其暴露於公開網站,可能導致憑證設定或內部測試環境的資安漏洞。

  • 無謂的上傳成本與頻寬浪費
    Web 版實際檔案僅 48MB,若連同 Android/iOS 的產物一併打包,上傳體積將暴增至 626MB(高出 13 倍),大幅延長部署時間。

  • 網頁載入與部署異常
    itch.io 的 HTML5 播放器會直接讀取根目錄檔案,混雜大型二進位檔(.apk、.ipa)容易導致解壓縮超時或檔案讀取異常。

最終處理方案
最終將 Web 版的輸出路徑獨立隔離:

export_path="build/web/index.html"

並僅針對 Web 專屬目錄進行推送:

butler push build/web 你的帳號/專案代號:html

透過發布路徑的獨立隔離,在無需修改遊戲程式碼的前提下,精確規避了潛在的資安與部署風險。


九、Web 版資源建置體積與網路傳輸量分析

Web 版編譯產物的檔案總和約為 47.48MB,但實際在線上環境(如 itch.io CDN)部署時,伺服器會啟用 HTTP 傳輸壓縮(gzip / brotli)。

透過 curl 量測 itch.io CDN 的實際傳輸數據:

curl -s -o /dev/null --compressed \
  -w "傳輸 %{size_download} bytes  時間 %{time_total}s\n" \
  https://你的遊戲網址/index.wasm

Web 部署產物傳輸數據對比表

檔案 磁碟大小 實際傳輸 壓縮後
index.wasm 37.68MB 10.12MB 73%
index.pck 9.53MB 7.75MB 19%
index.js 0.27MB 0.07MB 74%
總計 47.48MB 17.95MB 62%

實測顯示,玩家首次載入遊戲時的實際網路下載量約為 17.95 MB,整體減少了 62% 的傳輸負擔。

資源壓縮效能與 ICU Data 分析
此傳輸數據亦提供了更精準的資源優化方向:

  • 程式碼與 WebAssembly 壓縮效益高
    index.wasm 與 index.js 屬於文字與二進位程式碼結構,經過 CDN 傳輸壓縮後,體積降至原有的 26–27%。

  • 多媒體與套件檔壓縮效益有限
    index.pck 包含遊戲素材與 ICU 語系資料(約占 4.4MB)。由於 .pck 內部資料在打包階段已進行過一次壓縮,在 CDN 網路傳輸時無法再大幅二次壓縮(僅有 19% 壓縮率),幾乎需以原始體積全額傳輸。

在評估 Web 版載入效能時,應以 HTTP 實際傳輸量(Transfer Size) 作為指標,而非單純觀察本機檔案系統的靜態體積。


十、自動化發布中的職責分工

檢視 Web、Android 與 iOS 三個平台的發布過程,專案並未針對個別平台重構遊戲核心程式碼。

在此架構下,自動化工具與人工審核的職責範疇可劃分如下:

  • 可交付 AI 自動化的項目:
  • 填寫各平台匯出配置
  • 撰寫與執行 CLI 部署指令
  • 檢驗輸出產物完整性
  • 依據錯誤 Log 進行初步排查

必須由開發者人工審核的項目:

  • 部署標的與影響範圍:確認指令上傳的目錄層級與檔案範疇。
  • 資安與敏感資訊:檢查產物中是否包含私鑰、發布憑證或簽章設定。
  • 金鑰與授權邊界:確保登入與授權流程不洩漏機密憑證。
  • 公開存取權限:確認平台的發布設定是否符合預期的公開程度。

以 butler push 為例,AI 提供的指令語法本身並無錯誤,關鍵在於開發者必須識別「將整份目錄作為參數傳遞」所隱含的部署行為。

這與 Day 06 審視 git init 的核心邏輯一致:
凡涉及資料導出、本機環境變更或對外部署的指令,皆須進行邊界審核。

釐清這道界線後,即可在大幅提升自動化效率的同時,精確控管系統的營運與資安風險。


十一、跨平台發布的總結與體會

專案成功部署至 Web、Android 與 iOS 三個平台後,開發的重點也從「編輯器內的邏輯實現」轉移至「真實環境的發布細節」。

實際發布時面臨的挑戰,包含:

  • 前端與 UI 呈現:跨平台的字型渲染與文字斷行適應。
  • 資安與權限配置:發布憑證、簽章設定與平台帳號授權。
  • 部署與網路傳輸:公開權限設定與實際檔案傳輸量控管。

這些範疇與遊戲核心邏輯(如 BattleScene.gd)屬於截然不同的維度。這也印證了自動化流程中「監督」的必要性:

AI 能夠高效執行指令,但開發者仍須清晰識別指令的影響範圍——究竟是在「修改本機環境」,還是在「對外發布資料」。

在執行 butler push 前花費十秒檢查參數,成功避免了 578MB 敏感檔案的誤上傳。在自動化發布流程中,這類人工審核的成本極低,卻能有效防範重大部署風險。

下一步:Day 20
發布流程驗證完畢後,下一個階段將回歸使用者體驗。

接下來將進行無盲測引導的真人實機測試:把遊戲交付給完全未接觸過說明的玩家,觀察遊戲機制與引導設計是否符合預期。

附錄:六個坑

上面是完整流程。

下面留下這次實際撞到的六個坑,之後自己或其他人再遇到,可以直接回來查。

坑 1:匯出樣板只裝了 Web

Android 匯出時:

ERROR: 在預期路徑找不到匯出範本:
.../export_templates/4.7.1.stable/android_debug.apk

原因就是第一次安裝樣板時只勾了 Web。
回到「編輯器 → 管理匯出樣板」重新下載完整包即可,不需要先移除舊的。


坑 2:中文全部變成空白方框

網頁版畫面,所有中文都顯示成空白方框

匯出成功、遊戲也能操作,但中文全部變成方框。
數字和 HP 這些英文正常。
原因是編輯器在 macOS 上可以借用系統中文字型,但瀏覽器沒有同樣的環境。
設定專案字型後就正常。

同一個畫面,中文正常顯示

這也是我第一次刻意「先確認原因、晚一點再修」的例子。
當時遊戲還在灰盒階段,字型風格還沒決定。
知道原因就夠了。


坑 3:中文斷在奇怪的位置

修正前的三張卡,賣血狂暴的描述第一行只有「失去 3」,「戰鬥」被拆到兩行

修正前會出現:

失去 3

點生命,本場戰
鬥 +3 攻擊力。

我先懷疑 autowrap_mode。
測了四種設定,結果一樣。
再懷疑卡片是不是被手機壓窄。
量 Card.tscn:

Card 實際大小   = (200.0, 260.0)
DescLabel 大小  = (160.0, 176.0)
字級            = 21
行數            = 4

也沒變。

最後才找到 include_text_server_data。
缺少 ICU 斷行資料,中文被當成一個沒有斷點的長字,才會出現這種結果。

修正後的同三張卡,換行正常

這個坑有一個很容易誤判的地方:
它不是全部都壞。

例如另一張卡前後斷行剛好都落在合法位置,所以看起來完全正常。
只測一張卡,很可能就會得到「沒問題」的結論。


坑 4:同一個缺失,Android 是警告,iOS 是致命錯誤

Android 沒有設定專案圖示時:

ERROR: No project icon specified.

看起來很嚴重。
但 APK 還是成功產出,只是使用 Godot 預設圖示。

iOS 則不同:

ERROR: 匯出圖示: Invalid icon (icons/settings_58x58): ''
ERROR: Project export for preset "iOS" failed.

直接失敗。
所以不能只看到 ERROR 就猜它在所有平台都會有同樣結果。


坑 5:Apple Silicon 上跑不了這份 iOS 模擬器

我一開始準備了兩台 iOS 模擬器,結果一台都沒用上。
檢查 Godot 官方樣板:

lipo -info libgodot.ios.debug.xcframework/ios-arm64_x86_64-simulator/libgodot.a

結果:

Non-fat file: ... is architecture: x86_64

但 Apple Silicon 上的模擬器目標是 arm64。
所以最後連結時會撞到:

ld: symbol(s) not found for architecture arm64

這次直接改用實機。
如果真的需要模擬器,就得自己從原始碼編一份帶 arm64 切片的樣板。


坑 6:上傳成功,別人還是看不到

最後一個很容易誤會。

butler push 一開始甚至會直接拒絕:

itch.io API error (400):
Please verify your account's email address before uploading a build

先驗證 itch.io 帳號 email。
驗證完成後,butler push 成功。
結果網頁還是 404。

原因是新專案預設是草稿。
要再到專案編輯頁把:

Visibility → Public

所以:

butler status 顯示 build 正常,不代表別人已經看得到。

上傳成功和公開成功,是兩件不同的事。


上一篇
Day 18:【除錯實戰】AI 連續三次診斷都錯,靠架設對照組環境才拆穿
系列文
從 Vibe Coding 到系統架構:Gemini x Claude Code 雙 AI 協同開發 Godot 2D Roguelike 卡牌遊戲實戰 共 19 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言