← 返回博客

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 提示规范;后面背包、任务日志、击杀计数都可以同一套路扩展。

你可能还会喜欢这些文章