iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
Vibe Coding

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

Day 09:【程式碼品質】讓 AI 自己檢查程式碼,結果發現檢查也要驗證

  • 分享至 

  • xImage
  •  

Day 08,我讓 Claude Code 可以自己執行 Godot。
但實際開發之後,我很快發現一件事:
如果只是改了一行程式碼,真的每次都要把整個遊戲跑起來嗎?
如果只是想確認 GDScript 有沒有寫錯,有沒有更快的方法?
我開始找 Godot 的指令列工具,最後找到 --check-only


一行指令,只檢查語法

我先問 Claude Code:

「有沒有辦法不開編輯器,直接用命令列檢查 GDScript 有沒有語法錯誤?」

它給我的指令是:

godot --headless --script scripts/CardDatabase.gd --check-only

這裡有兩個比較重要的參數:

參數 作用
--headless 不開視窗,直接在 Terminal 執行
--check-only 只檢查語法,不執行程式

--check-only 有一個限制:

它必須搭配 --script 使用,而且一次只檢查指定的那一支腳本。

也就是說,它不是把整個 Godot 專案掃過一遍。


先故意把程式寫壞

光看指令說明還不算數,我直接實測。

我在 CardDatabase.gd 的最後面補了一行故意寫錯的程式碼:

func broken(:

再執行:

godot --headless --script scripts/CardDatabase.gd --check-only

Terminal 出現:

Godot Engine v4.7.1.stable.official.a13da4feb - https://godotengine.org
SCRIPT ERROR: Parse Error: Expected parameter name.
          at: GDScript::reload (res://scripts/CardDatabase.gd:112)
ERROR: Failed to load script "res://scripts/CardDatabase.gd" with error "Parse error".

Godot 很明確地告訴我:第 112 行的函式參數寫錯了,原本應該要有參數名稱。
也就是說,這次 --check-only 確實抓到了我故意製造的錯誤。

檔名、行號、錯誤原因,全都有。
這正是 AI 需要的資訊。

Claude Code 看到這段輸出,就能知道:

  • 哪一個檔案出問題
  • 第幾行出問題
  • 大概是哪一種錯誤

然後自己修改,再重新檢查。
看起來很完美。
但接下來,我踩到了第一個坑。


坑一:exit code 竟然還是 0

命令列工具有一個常見的慣例:
成功通常回傳 exit code 0,失敗則回傳非 0。
Shell 腳本就可以利用這個結果判斷指令到底成功還是失敗。

所以我先用正常的程式碼測一次,再故意把程式碼寫錯,看看 --check-only 的 exit code 會不會跟著改變。

godot --headless --script scripts/CardDatabase.gd --check-only >/dev/null 2>&1

echo "exit code = $?"

結果:

情況 exit code
程式碼正常版本 0
程式碼有語法錯誤版本 0

兩種情況都是 0。
這就麻煩了。

如果我直接用 $? 寫自動化腳本,讓 exit code = 0 時顯示「檢查通過」,就會出現問題。
例如下面這個檢查腳本:

# 範例:這是錯的方式

godot --headless --script "$1" --check-only

if [ $? -eq 0 ]; then
    echo "檢查通過"
else
    echo "有錯誤"
fi

它會把語法錯誤也當成成功。

最後不管程式碼有沒有寫錯,都可能得到:

檢查通過

這種問題特別麻煩。
因為你以為自己已經加了一層安全檢查,實際上這個安全檢查根本沒有發揮作用。

所以我不能直接相信 exit code。
那就改看錯誤訊息。
結果第二個坑就出現了。


坑二:正常的程式碼,也被判定有錯

既然 exit code 不能直接使用,我改成檢查輸出裡面有沒有 "SCRIPT ERROR"。
例如出現:

SCRIPT ERROR: Parse Error: Expected parameter name.

於是我寫了一個批次檢查,跑完整個 scripts/ 目錄:

# 範例:這也是錯的

for f in scripts/*.gd; do

  if godot --headless --script "$f" --check-only 2>&1 | grep -q "SCRIPT ERROR"; then

    echo "❌ $f"

  fi

done

結果出現:

❌ scripts/BattleScene.gd
❌ scripts/RewardScene.gd

奇怪了。

這兩個檔案明明都可以正常執行,為什麼檢查會說它們有錯?

我把其中一個檔案單獨拿出來看,發現:

SCRIPT ERROR: Compile Error: Identifier not found: Global
          at: GDScript::reload (res://scripts/BattleScene.gd:74)

這裡的 Global 是我的 Autoload 單例

我在 project.godot 裡註冊了它,所以正常執行遊戲時,其他腳本可以直接使用 Global

但現在使用的是:

godot --headless --script scripts/BattleScene.gd --check-only

這是一種單檔檢查模式

Godot 只載入我指定的那一支腳本,不會把專案裡的 Autoload 一起載入。

所以:

正常執行遊戲
BattleScene.gd
    ↓
可以找到 Global
    ↓
正常

但單檔檢查:

只載入 BattleScene.gd
    ↓
找不到 Global
    ↓
出現 Compile Error

所以問題不是我的程式碼真的寫錯。

是我使用的檢查方式,讓它產生了假警報。

整理起來就很清楚:

檔案 有沒有引用 Global 單檔檢查結果
CardDatabase.gd 沒有 ✅ 通過
Card.gd 沒有 ✅ 通過
Global.gd 它自己就是 ✅ 通過
BattleScene.gd ❌ 誤報
RewardScene.gd ❌ 誤報

如此一來 一個檢查工具如果一直誤報,久了人就會開始不相信它。
等真的出錯時,反而可能被忽略。
所以這個檢查方法還需要再調整。

找到真正要抓的錯誤

前面測試時,真正的語法錯誤長這樣:

SCRIPT ERROR: Parse Error: Expected parameter name.

而 Autoload 造成的問題是:

SCRIPT ERROR: Compile Error: Identifier not found: Global

這兩種錯誤其實不一樣。

訊息 意思 這次要不要當成錯誤
Parse Error 語法寫錯 ✅ 要
Compile Error: Identifier not found 單檔模式找不到 Autoload ❌ 不要

所以我把檢查條件從:

SCRIPT ERROR

縮小成:

Parse Error

改成:

# ✅ 正確

godot --headless --script scripts/CardDatabase.gd --check-only 2>&1 \
  | grep -q "Parse Error" \
  && echo "❌ 有語法錯誤" \
  || echo "✅ 通過"

這裡的 2>&1 不能省略。

因為 Godot 的錯誤訊息會輸出到另一個訊息管道,如果直接用 grep 去找 Parse Error,可能抓不到。

2>&1 的作用,就是把這些錯誤訊息也交給後面的 grep 檢查。

簡單來說:

Godot 輸出錯誤訊息
        ↓
     2>&1
        ↓
   交給 grep 搜尋

這樣 grep 才能找到 Parse Error。


再跑一次整個目錄

接著把修正後的檢查套到整個 scripts/

for f in scripts/*.gd; do

  if godot --headless --script "$f" --check-only 2>&1 | grep -q "Parse Error"; then

    echo "❌ $f"

  fi

done

這次正常的程式碼不再出現:

❌ scripts/BattleScene.gd
❌ scripts/RewardScene.gd

誤報歸零。

但我還有一個問題:

如果一支腳本同時有真正的語法錯誤,又引用了 Autoload,會不會反而漏掉真正的錯誤?

所以我又把 BattleScene.gd 故意寫壞。
它原本就引用 Global,這次再加上一個真正的語法錯誤。
結果輸出是:

SCRIPT ERROR: Parse Error: Expected parameter name.

還是 Parse Error
不是前面的 Compile Error

原因是:
解析(parse)發生在編譯(compile)之前。

語法都還沒通過,根本還沒走到查找 Global 的階段。
所以這次實測確認:真的有語法錯誤時,Parse Error 會先出現。
這讓我比較放心把它放進自動化檢查。


這次我真的自己測了一遍

這次我沒有只看 Claude Code 給我的答案。

我自己在 Terminal 做了一輪完整測試:

  1. 備份檔案
  2. 故意寫壞程式碼
  3. --check-only
  4. 看錯誤訊息
  5. 看 exit code
  6. 批次檢查整個 scripts/ 目錄
  7. 測試 Autoload 誤報
  8. 再把檔案還原

大約十分鐘。

最後確認了四件事:

  • 錯誤訊息真的有檔名和行號
  • 有語法錯誤時,exit code 還是 0
  • 引用 Autoload 的腳本,在單檔模式下會出現誤報
  • 真正的 Parse Error 會先於 Compile Error 出現

這些都是我自己跑過之後才確定的。

所以現在我不會只問 AI:「這個指令可以嗎?」
我還會繼續追問:

「它成功時怎麼回應?」
「故意弄錯時又怎麼回應?」
「如果一次跑很多檔案呢?」

這些問題,才真正決定這個指令能不能拿來做自動化。


三層驗證,各有用途

到 Day 09 為止,我手上已經有三種不同層次的檢查:

方法 成本 可以檢查什麼
--check-only 最輕,單一檔案 語法
--headless --import 中,整個專案 場景、資源、設定等問題
godot -e + F5 最重,需要人看 遊戲實際執行與畫面

所以現在的使用方式變成:

改一行程式碼,可以先跑 --check-only
改完一個功能,再跑整個專案的 headless 檢查。
完成一段功能後,再實際開遊戲確認。

三種方式各自負責不同的事情。


靜態檢查還是有它的限制

命令列檢查很好用,但它有明確的範圍。
我之前就遇過一個例子。

AI 幫我建立專案骨架之後,我在編輯器裡打開專案。
開得起來,畫面正常,也沒有任何紅字。
但按 F5 執行之後,卻直接報錯:
找不到場景。

後來才發現,project.godot 裡設定的主場景指向 RewardScene.tscn,但那個檔案根本還沒有建立。

所以有時:「語法沒錯」不等於「遊戲跑得起來」。

--check-only 檢查的是單一檔案的語法。

它不知道:

  • 場景檔案存不存在
  • 主場景設定是否正確
  • Node 路徑是否正確
  • 程式實際執行時會不會出錯

所以驗證還是要分層:

語法用 --check-only,實際行為則要真的執行。

Day 10 就會來做這件事:讓 AI 寫一支跑完就刪掉的測試腳本,直接確認遊戲裡的功能真的會動。


今天的筆記

今天的發現,其實不是多學了一個指令。
而是我把一個「看起來可以自動化」的檢查,從頭測了一遍。

結果發現:

  1. 不能只看 exit code。
    這次有語法錯誤時,exit code 還是 0。

  2. 不能看到 SCRIPT ERROR 就全部判定為錯誤。
    單檔模式下,Autoload 會造成誤報。

  3. 要先知道自己到底想檢查什麼。
    這次真正要抓的是 Parse Error

  4. 靜態檢查不能取代實際執行。
    它能抓語法問題,但不知道遊戲實際跑起來會發生什麼。

這兩個實驗,讓我學到寶貴的經驗:

不要只相信工具「看起來應該會這樣工作」,成功和失敗都自己測一次。

尤其當這個工具要成為你的自動化安全網時,更值得先花幾分鐘確認它到底在做什麼。


明天 Day 10,我們把驗證再往前推一步:

不只檢查語法,而是讓 AI 寫一支跑完就刪掉的測試腳本,自己證明遊戲真的會動。


上一篇
Day 08:【CLI 驗證】讓 Claude Code 自己跑 Godot,不再問我「跑起來了嗎?」
系列文
從 Vibe Coding 到系統架構:Gemini x Claude Code 雙 AI 協同開發 Godot 2D Roguelike 卡牌遊戲實戰9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言