From Godot Skills
Inject a one-shot screenshot timer into a running Godot 4 project, launch the project headfully from CLI, and read the resulting PNG yourself. Use this whenever the user complains the UI "looks wrong / 一塌糊涂 / panels invisible / proportions off", whenever you've just changed a theme/layout/scene and need to verify visually before claiming done, or whenever you're stuck on a visual bug and would otherwise be guessing what's on screen. Strongly prefer this over asking the user to take a screenshot — close the verification loop yourself.
How this skill is triggered — by the user, by Claude, or both
Slash command
/godot:godot-automatic-ui-qaThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
The structural smoke test ("scene instantiates, parser clean, 0 errors") doesn't see whether panels have backgrounds, whether splits give the right proportions, whether icons resolved, or whether text overflowed. The user does. Don't ship UI work blind — see it first.
The structural smoke test ("scene instantiates, parser clean, 0 errors") doesn't see whether panels have backgrounds, whether splits give the right proportions, whether icons resolved, or whether text overflowed. The user does. Don't ship UI work blind — see it first.
This skill gives you a fast loop: inject a temporary screenshot timer into the project, launch it headfully, the project saves a PNG and quits, then you Read the PNG. Edit, repeat. No .capture/ scaffolding, no xvfb, no ffmpeg — that's godot-capture's job for final deliverables. This is for the inner debug loop.
Use this when:
Don't use this for:
godot-capturePick the injection site — the main scene's _ready(), after anything load-bearing has run (theme apply, autoload wiring, bootstrap). A safe spot is right at the end of _ready().
Inject a one-shot timer — one function fires a timer, then on tick saves the viewport image and quits. Mark the code clearly so you can rip it out later.
Launch headfully — godot --path <project_dir> (NO -e, NO bare project.godot path) so Godot runs the game, not the editor. Pin a known resolution with --resolution WxH.
Read the PNG — write to /tmp/<name>.png so the Read tool can open it as an image.
Remove the injection when the visual loop is done.
# DEBUG SCREENSHOT — remove when visual QA pass is finished
func _schedule_screenshot_for_debug() -> void :
var t: SceneTreeTimer = get_tree().create_timer(2.5)
t.timeout.connect(func() -> void :
var img: Image = get_viewport().get_texture().get_image()
if img != null :
img.save_png("/tmp/app_qa.png")
print("[DEBUG] Screenshot saved to /tmp/app_qa.png size=%s" % img.get_size())
var t2: SceneTreeTimer = get_tree().create_timer(0.3)
t2.timeout.connect(func() -> void : get_tree().quit())
)
Call _schedule_screenshot_for_debug() from _ready() after all important setup.
// DEBUG SCREENSHOT — remove when visual QA pass is finished
private void ScheduleScreenshotForDebug()
{
var t = GetTree().CreateTimer(2.5);
t.Timeout += () =>
{
var img = GetViewport().GetTexture().GetImage();
img?.SavePng("/tmp/app_qa.png");
var t2 = GetTree().CreateTimer(0.3);
t2.Timeout += () => GetTree().Quit();
};
}
# Linux / macOS — adjust the binary path for the host OS
rm -f /tmp/app_qa.png
godot --path /path/to/project --resolution 1600x900 2>&1 | tail -10
On macOS the binary typically lives at /Applications/Godot.app/Contents/MacOS/Godot; on Linux it's godot (or godot4) on PATH. Use whichever invocation works in the current shell.
Then use the Read tool on /tmp/app_qa.png. It renders inline as an image — you can see it directly.
The timer wait controls when the screenshot fires relative to scene ready. Tune it:
_ready()func _schedule_screenshot_for_debug() -> void :
var t_setup: SceneTreeTimer = get_tree().create_timer(1.2)
t_setup.timeout.connect(func() -> void :
# do the in-app navigation first (open project, switch screen, drill in, etc.)
_drive_app_to_target_screen()
var t_shot: SceneTreeTimer = get_tree().create_timer(2.0)
t_shot.timeout.connect(func() -> void :
get_viewport().get_texture().get_image().save_png("/tmp/app_qa.png")
var t2: SceneTreeTimer = get_tree().create_timer(0.3)
t2.timeout.connect(func() -> void : get_tree().quit())
)
)
This is how you avoid screenshotting a project picker / login / splash when the bug lives deeper inside.
A single viewport snapshot only shows what's above the fold. If the page exceeds the window height, you'll miss whatever's at the bottom — often the broken section. Two cheap fixes:
ScrollContainer, set sc.scroll_vertical = 99999, wait one timer tick, take a second PNG. Save as /tmp/<task>_top.png and /tmp/<task>_bottom.png. Read both.--resolution 1600x2400 makes the window tall enough that the page fits in one shot. Useful when there is no scroll container to drive.Method 1 is what you usually want — it matches what the user actually sees at standard resolution and surfaces both above- and below-the-fold issues.
Apps with a "pick a project" or "main menu" gate screenshot the gate, not the feature. Drive past it programmatically — but use the project's real API, don't invent names:
project.godot (under [autoload]) and grep for their public methods. Common shapes: a project manager exposing something like open_project(...) or load_document(...); a router/state-machine exposing something like set_screen(...) / goto(...).has_method(...) defensively if you're unsure — the same name across projects often has different signatures.call_deferred(...) for screen swaps so they happen after the current frame settles.godot project.godot and godot --path <dir> both run the game if no -e flag is present. With -e (or --editor) you get the editor. Don't accidentally screenshot the editor.godot --script foo.gd project.godot runs in --script mode, which uses the editor's autoload context — autoload singletons may fail to load with Identifier not found. For real visual QA, run the project headfully, not via --script.--headless disables rendering. Headless screenshots return blank/uninitialized images. Don't pair --headless with this skill.--resolution WxH. Otherwise the OS picks one based on screen and screenshots have inconsistent aspect ratio between iterations — comparisons become harder./tmp/ (or another absolute path you can read back) — the Read tool can read any path it has access to and renders PNGs inline.In Claude Code / Claude.ai, the Read tool natively renders PNG/JPG as inline images. After your bash launch completes:
Read /tmp/<task>_qa.png — you'll see the image directly.Typical loop time per iteration is 10–20 seconds total. Faster than asking the user.
When the active scene embeds another .tscn and you need to manipulate a node inside the sub-scene at runtime, %UniqueName lookups don't reach across — unique names are scoped to their owning scene. Use:
var inner: Node = find_child("<NodeName>", true, false)
# recursive=true to walk descendants; owned=false to NOT filter by ownership
owned=false is the key — without it, find_child only returns nodes owned by the current scene's root, which excludes everything inside an instanced child scene.
When the embedded sub-scene's layout is wrong for your composition (e.g. an embedded library widget has its own HSplit with a useless detail pane in your enclosing layout), the cleanest fix is to surgically lift the useful child out and free the wrapping container:
var split: HSplitContainer = find_child("<HSplitName>", true, false) as HSplitContainer
var parent: Node = split.get_parent()
var keep: Node = split.get_node_or_null("<ChildToKeep>")
if keep != null and parent != null:
var idx: int = split.get_index()
split.remove_child(keep)
parent.add_child(keep)
parent.move_child(keep, idx)
split.queue_free()
This is preferred over split_offset = 99999 + dragger_visibility = DRAGGER_HIDDEN. Those hide the handle but the second pane still claims layout space, leaving a phantom empty column. Re-parenting is unambiguous.
Note: queue_free() on the container frees its remaining children too. If the sub-scene's script keeps @onready references to those children and tries to write to them later (visibility toggles, etc.), you'll get Invalid assignment ... previously freed. Either hide the children instead of freeing them, or guard the sub-scene's call sites with is_instance_valid(...).
If a button looks blank after a theme change, the icon file probably doesn't exist at the path the theme builder looks up. Grep for the file first. If the project uses a stroke-icon set (e.g. lucide-style SVGs under some res://.../icons/<name>.svg), you can write the missing SVG yourself — icon sets like lucide are MIT-licensed and the path data is public.
Template (24×24 viewBox, stroke 2, round caps — the lucide convention):
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="…"/></svg>
stroke="#ffffff" (white) is intentional — it lets the theme re-tint the icon via add_theme_color_override("icon_normal_color", ...) per button state. After writing the SVG, run import once so Godot generates the .import and .ctex files (without that step load("res://...svg") still returns null):
godot --headless --import /path/to/project.godot 2>&1 | tail -5
Then re-launch. Don't fall back to unicode glyphs unless you've confirmed the project's font actually renders them — many UI fonts ship without symbol coverage.
When the screenshot shows something unexpected:
PanelContainer panel stylebox either has zero border width and a background color too close to the parent, or has no stylebox at all. Fix the theme's contrast, don't restructure the layout.split_offset semantics: positive HSplit makes the first (left) child wider; positive VSplit makes the first (top) child taller. If a child lacks size_flags_* set to SIZE_EXPAND_FILL (= 3) it won't grow. Set custom_minimum_size on both children to give the layout something to honor.autowrap_mode, or a fixed-width input forcing the row beyond its container. Don't add custom_minimum_size blindly — check parent constraints first.Container subclass (PanelContainer, VBoxContainer, etc.) and you added absolute-positioned children (anchor dots, badges, overlays) directly to it. Godot Containers auto-stretch every direct child to their content rect, so a small element with a high corner_radius becomes a huge ellipse. Fix: wrap the card root as a plain Control (non-Container), put the visual PanelContainer background as ONE child filling the rect via PRESET_FULL_RECT, and add absolute-position siblings (anchors, badges) directly to the outer Control. Container layout only applies to direct Container children.The injected code is debug instrumentation, not feature code. When the visual loop is done:
_ready() call into it./tmp/*.png cleans itself.Leaving the debug screenshot active will make every future windowed launch auto-quit after a few seconds, which is confusing for the next person (or future-you).
If you anticipate needing this loop again soon, leave the function body in place but un-wire the _ready() call. Then re-wiring is one line, not a rebuild.
A QA pass with this skill is real if:
Don't claim "UI verified" off a structural smoke test or off a screenshot you didn't open.
Symptom: user says the UI looks wrong; you've never seen the rendered app.
application/run/main_scene in project.godot). Append a one-line call to a new _schedule_screenshot_for_debug() at the end of _ready()./tmp/<task>_qa.png as the output path.rm -f /tmp/<task>_qa.png && godot --path /path/to/project --resolution 1600x900Read /tmp/<task>_qa.png. See actual problems (panel borders missing, contrast too low, icon miss, split collapsed, ballooning child).Total: a few minutes per iteration, no user round-trips needed.
npx claudepluginhub cnqdztp/godot-skills --plugin godotProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.