← 返回部落格

Godot 4:用信號規範提示詞,讓 MCP 把血條接到玩家血量

在 Cursor 裡向 Godot MCP 下指令前先寫清信號契約:用 health_changed 連 HUD,避免深層 get_node 硬路徑;含提示詞模板、GDScript 範例、反例與 Play 驗收。

發布於
  • Godot MCP
  • Godot 4
  • 信號
  • HUD
  • GDScript
  • Cursor
  • AI game development
  • tutorial

Godot 裡最常見的「AI 改完就斷」不是語法錯誤,而是血條腳本裡寫死了:

$"/root/Game/Player/HitPoints".hp  # 或 get_node("../../Player/...")

場景一重組、Player 一做成子場景實例,這條路徑就失效。Godot MCP 會忠實地依你的提示接線——提示裡若不禁止硬路徑,模型就會繼續造硬路徑。

本文只解決一件事:在 Cursor(或 Claude Code)裡先給出信號契約,再讓 MCP 把玩家血量接到 HUD 血條。不重複安裝步驟,也不展開整關平台跳躍。

步驟做什麼
1定信號名、參數、誰發射 / 誰接收
2用帶約束的提示詞讓 MCP 改腳本與連接
3Play 驗收;對照反例修提示詞

相關閱讀:

官方背景:Scene organization 強調子場景對外用信號 / 匯出,而不是深層路徑。

為什麼「信號規範」要寫進提示詞

做法短期重組場景後
get_node("../Player/Health")快常斷
@onready var bar = %HealthBar(唯一名)同場景可用跨場景仍脆
signal health_changed(current, max) + 連接多寫兩行HUD 與 Player 可各自改樹

MCP 沒有「專案架構記憶」,除非你每次提示都寫約束。把規範寫成可貼上的契約塊,比事後在 diff 裡清硬路徑便宜。

先定最小節點骨架

不必一次做完整 UI。第一版只要:

Game (Node2D / Node)
├── Player (CharacterBody2D)  ← 掛 player_health.gd
│   └── ...
└── HUD (CanvasLayer)
    └── HealthBar (ProgressBar 或 TextureProgressBar)  ← 掛 hud_health.gd

命名建議:

節點建議名說明
玩家根Player掛生命腳本
HUD 層HUDCanvasLayer
血條HealthBarProgressBar;max_value = 1 用比例,或用絕對血量

HUD 若來自 AI Studio 匯出,進工程後盡快改成穩定英文名,再接線。

信號契約(複製到每次提示開頭)

把下面整段當作專案約定,下指令時貼在最前面:

【信號契約 — 血量】
1. 僅由 Player 上的生命腳本發射信號:
   signal health_changed(current: float, max_health: float)
   命名用過去式/已發生語意;參數固定為 (current, max_health)。
2. HUD 血條腳本只連接該信號,更新 ProgressBar;禁止在 HUD 裡 get_node 到 Player。
3. 禁止:跨場景深層 NodePath、Autoload 單例當血量總線(本任務不需要)、在 _process 裡輪詢找 Player。
4. 連接方式優先:編輯器裡信號連接,或在 HUD 腳本用 @export NodePath / 匯出 Node 引用指向 Player 後 connect;不要寫死 "/root/..."。
5. 受傷、治療只改 Player 側數值並 emit;UI 不改血量。

這一段就是本文的核心:規範先於實作。

步驟 1:唯讀冒煙(確認 MCP 看見場景)

列出目前編輯場景的根節點子級名稱。不要修改任何檔案或節點。

確認能看到 Player 與 HUD(或你的等價名)後再寫入。

步驟 2:讓 MCP 在 Player 上實作「只發射」

提示詞範例(接在契約後面):

【信號契約 — 血量】(同上,完整貼上)

在 res://scripts/player_health.gd(若無則建立)並掛到 Player:
- @export var max_health: float = 100.0
- var current: float
- signal health_changed(current: float, max_health: float)
- _ready 時 current = max_health,並 emit 一次 health_changed
- take_damage(amount)、heal(amount);夾緊到 0..max_health;每次變更後 emit
- 不要引用 HUD,不要 get_node 到 UI
儲存後說明信號參數類型。

參考實作(可讓模型產生後你再審):

extends Node
class_name PlayerHealth

signal health_changed(current: float, max_health: float)

@export var max_health: float = 100.0
var current: float

func _ready() -> void:
	current = max_health
	health_changed.emit(current, max_health)

func take_damage(amount: float) -> void:
	if amount <= 0.0 or current <= 0.0:
		return
	current = maxf(0.0, current - amount)
	health_changed.emit(current, max_health)

func heal(amount: float) -> void:
	if amount <= 0.0 or current <= 0.0:
		return
	current = minf(max_health, current + amount)
	health_changed.emit(current, max_health)

臨時驗收傷害(正式專案可刪):

在 Player 上增加臨時 debug_damage.gd:按下 H 鍵呼叫同節點(或父節點)上 PlayerHealth.take_damage(10)。僅用於接線驗收。不要動 HUD。

若生命腳本直接掛在 Player 根上,把 extends Node 改成與根類型一致(例如 CharacterBody2D),或把 PlayerHealth 做成子節點 Health——二選一寫進提示,避免 MCP 猜錯掛載點。

步驟 3:HUD 只訂閱,並用匯出引用找 Player

【信號契約 — 血量】(完整貼上)

建立 res://scripts/hud_health.gd 掛到 HUD 或 HealthBar:
- @export var player_health: PlayerHealth   # 或 Node,再 as PlayerHealth
- @onready var bar: ProgressBar = $HealthBar  # 若腳本掛在 HUD 上;路徑僅限 HUD 子樹內部
- _ready:斷言 player_health != null;player_health.health_changed.connect(_on_health_changed)
- _on_health_changed(current, max_health):bar.max_value = max_health;bar.value = current
  (若只用 0..1 比例:bar.max_value = 1;bar.value = current / max_health)
- 禁止 get_node 到 Player;禁止 /root 絕對路徑
然後:在編輯器把匯出的 player_health 指到 Player 上的 PlayerHealth(或 Health 子節點)。儲存場景。

參考實作:

extends CanvasLayer

@export var player_health: PlayerHealth
@onready var bar: ProgressBar = $HealthBar

func _ready() -> void:
	assert(player_health != null, "Assign PlayerHealth in the inspector")
	player_health.health_changed.connect(_on_health_changed)

func _on_health_changed(current: float, max_health: float) -> void:
	bar.max_value = max_health
	bar.value = current

HUD 內部用 $HealthBar 可以接受——那是同場景子樹,重組 HUD 時一起改;禁止的是跨到 Player 的深層路徑。

也可用編輯器「節點 → 連接信號」由 MCP 完成;提示裡寫清:

用編輯器信號連接:從 Player(PlayerHealth)的 health_changed 連到 HUD 腳本的 _on_health_changed。不要手寫 NodePath 字串。

步驟 4:Play 驗收清單

  1. 進場景瞬間:血條為滿(或等於目前比例)
  2. 按 H:bar.value 下降
  3. 扣到 0:條空;若有死亡邏輯,不因 UI 再改 current
  4. heal 後條回升
  5. 把 Player 拖成更深一層子節點(例如放進 Entities)再 Play:只要匯出引用還在,血條仍應更新——這是相對硬路徑的核心收益

若重組後斷了,先查匯出引用是否變成空,而不是讓 MCP「再寫一條新 get_node」。

驗收提示:

Play 模式下根據報錯診斷:若信號未觸發,檢查是否 connect;若 player_health 為空,只修匯出引用。不要改用 get_node 絕對路徑作為「修復」。先診斷,等我確認再改。

反例:看到這些就改提示詞,不要合併進主幹

反例程式碼 / 行為問題提示詞怎麼加約束
get_node("/root/Game/Player")根路徑一變就斷「禁止 /root 與絕對 NodePath」
get_parent().get_node("Player")依賴兄弟關係「HUD 不得假設與 Player 的相對層級」
Autoload GlobalHealth.hp全域狀態難測、易成垃圾桶「本任務禁止新建 Autoload」
HUD 的 _process 裡每幀找 Player慢且脆「禁止輪詢;只用信號」
信號叫 update / set_hp語意不清、易撞名「信號名必須是 health_changed」

決策文裡「何時用手寫」仍然成立:熱路徑移動、複雜戰鬥結算可以手寫;血條接線這種樣板,用帶契約的 MCP 提示通常更划算。詳見 Godot MCP vs 手寫腳本。

可複用的「契約 + 任務」提示骨架

以後換藍條、Boss 條,只改信號名與節點名:

【信號契約】
- 發射方:<節點/腳本>
- signal <name>(...)
- 接收方:<HUD 腳本方法>
- 禁止:絕對 NodePath、Autoload 總線、_process 輪詢

【任務】
1. …
2. …
3. 儲存場景;列出新增信號與連接方式(編輯器連接 / .connect)

小結

  1. 硬路徑是提示詞漏洞,不是 MCP「不會 Godot」
  2. 先貼 health_changed(current, max_health) 契約,再讓模型改腳本
  3. Player 只 emit;HUD 只 connect + 改 ProgressBar
  4. 跨物件用 @export 引用或編輯器信號連接,不用 /root/...
  5. 用「重組 Player 父節點後仍能掉血」做驗收,而不是「腳本產生成功」

做完這一段,你就有一份可貼上的 Godot MCP 提示規範;後面背包、任務日誌、擊殺計數都可以同一套路擴展。

你可能還會喜歡這些文章