<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ziva</title>
    <description>The latest articles on DEV Community by Ziva (@ziva).</description>
    <link>https://dev.to/ziva</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3845272%2F6667c5de-b09a-4a44-b3a4-19819c554824.png</url>
      <title>DEV Community: Ziva</title>
      <link>https://dev.to/ziva</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ziva"/>
    <language>en</language>
    <item>
      <title>AI assistant for a large Godot project: Ziva and Claude Code tested on Pixelorama</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Sat, 07 Nov 2026 15:23:20 +0000</pubDate>
      <link>https://dev.to/ziva/ai-assistant-for-a-large-godot-project-ziva-and-claude-code-tested-on-pixelorama-1pi5</link>
      <guid>https://dev.to/ziva/ai-assistant-for-a-large-godot-project-ziva-and-claude-code-tested-on-pixelorama-1pi5</guid>
      <description>&lt;p&gt;Can an AI assistant work in a large Godot project without losing track? We gave &lt;a href="https://ziva.sh" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt; and &lt;a href="https://claude.ai/code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; the same feature to build in &lt;a href="https://github.com/Orama-Interactive/Pixelorama" rel="noopener noreferrer"&gt;Pixelorama&lt;/a&gt;, an open-source pixel art editor written in Godot 4 with 249 GDScript files (about 60,000 lines) and 120 scenes. Both used Claude Opus 5.5. The feature touched 8 files: a shader, a new dialog scene and script, the menu code, the shortcut list, the layer effects list, project settings and the translation template.&lt;/p&gt;

&lt;p&gt;All four runs, two per tool, passed the same independent check in the running app. Ziva finished in 6 and 13 minutes; Claude Code took about 16 minutes both times. Claude Code tested its own work more thoroughly. Ziva's game runner could not screenshot Pixelorama while it sat idle, and Ziva left stray files in the project root. Details below.&lt;/p&gt;

&lt;p&gt;Ziva.sh makes Ziva and wrote this test. We ran it on 7 November 2026 with Godot 4.7.2 on Linux, on Pixelorama commit &lt;code&gt;5b2ebf5&lt;/code&gt; (MIT license), using a development build of Ziva from that day and Claude Code 2.1.318. Each tool used its default reasoning setting. The first runs of the two tools overlapped in time and the second runs started together on the same machine, so they shared its CPU. Claude Opus 5.5 in Ziva needs the Pro plan ($50/month).&lt;/p&gt;

&lt;h2&gt;
  
  
  The task
&lt;/h2&gt;

&lt;p&gt;Both tools got this prompt, word for word, in a fresh copy of the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add a Sepia color effect to this project (Pixelorama). It should work like the existing color effects:

1. Effects &amp;gt; Color &amp;gt; Sepia opens a dialog with an Intensity slider (0 to 100%, default 100%) and a live preview. Applying it works on the same targets as Desaturation (selected cels, current frame, all frames, all projects, and only the selection when that option is checked), with undo.
2. Sepia gets its own shortcut entry in Preferences &amp;gt; Shortcuts like the other effects (no default key).
3. Sepia is also available as a non-destructive layer effect in the Layer Effects dialog, with the same Intensity parameter.

Follow the project's existing patterns for effects. When you are done, make sure the project still opens and runs without new errors.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the kind of change where an assistant "loses track" in a big project. Nothing in the prompt says where effects are registered. Pixelorama wires each effect in five places, and missing one gives a menu item that does nothing, a shortcut that never appears, or no layer effect at all.&lt;/p&gt;

&lt;p&gt;Ziva ran in the Godot editor with auto-approve on. Claude Code ran in a terminal with no plugins, MCP servers or project instructions, permissions auto-approved, and Godot and a virtual display available on the command line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Ziva run 1&lt;/th&gt;
&lt;th&gt;Ziva run 2&lt;/th&gt;
&lt;th&gt;Claude Code run 1&lt;/th&gt;
&lt;th&gt;Claude Code run 2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Our check in the running app&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time&lt;/td&gt;
&lt;td&gt;6 min 15 s&lt;/td&gt;
&lt;td&gt;12 min 50 s&lt;/td&gt;
&lt;td&gt;16 min 0 s&lt;/td&gt;
&lt;td&gt;15 min 50 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;$1.94 of Ziva plan usage&lt;/td&gt;
&lt;td&gt;$2.93&lt;/td&gt;
&lt;td&gt;$2.70 (Claude Code's API-price estimate)&lt;/td&gt;
&lt;td&gt;$3.58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak context&lt;/td&gt;
&lt;td&gt;137,000 tokens&lt;/td&gt;
&lt;td&gt;157,000 tokens&lt;/td&gt;
&lt;td&gt;135,000 tokens&lt;/td&gt;
&lt;td&gt;164,000 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files left in the project root&lt;/td&gt;
&lt;td&gt;its test scene and script (Ziva said so)&lt;/td&gt;
&lt;td&gt;an untitled.png it could not explain (Ziva said so)&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Its own testing&lt;/td&gt;
&lt;td&gt;Selected cels target only&lt;/td&gt;
&lt;td&gt;Selected cels and selection-only&lt;/td&gt;
&lt;td&gt;All four targets, save and load, 82 checks&lt;/td&gt;
&lt;td&gt;85 checks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Our check was the same script for every run. It opened Effects &amp;gt; Color &amp;gt; Sepia in the running app, applied it to a blue canvas, undid it, applied 50% to a selection only, confirmed the shortcut entry, and added Sepia as a layer effect. All four produced identical pixel values, and none added new errors to the startup log.&lt;/p&gt;

&lt;p&gt;On a Claude subscription you pay a flat fee rather than Claude Code's estimate, and Ziva's cost depends on your plan's included usage. Neither run came close to the model's context limit, so this test does not show how either tool handles a chat long enough to need summarizing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft97qc4x9haxyodyixku7.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft97qc4x9haxyodyixku7.webp" alt="Pixelorama with the Sepia dialog Ziva built: an Intensity slider at 100 %, a sepia-toned preview, the Only affect selection checkbox and the Selected cels target" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How Ziva found its way around 1,383 files
&lt;/h2&gt;

&lt;p&gt;Ziva does not index or embed your code, and it does not load the project into the model. In both runs its first step was one search for "desaturat" across scripts, scenes, shaders, project settings and translation files. That found every place the existing Desaturation effect is wired. It then read 17 files in run 1 and 22 in run 2 and copied the pattern.&lt;/p&gt;

&lt;p&gt;A few things helped on the way, and they come from Ziva running inside the Godot editor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GDScript checked after every script edit.&lt;/strong&gt; Each time Ziva created or edited a &lt;code&gt;.gd&lt;/code&gt; file, Godot's language server checked it and the files that depend on it. Every one came back with "No problems in this file or its callers."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project settings through Godot.&lt;/strong&gt; Ziva added the shortcut's input action with Godot's project settings API instead of editing &lt;code&gt;project.godot&lt;/code&gt; by hand, and in run 1 it read the live input map first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The real app, not a stub.&lt;/strong&gt; Ziva ran Pixelorama's main scene from the editor and read its output and errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A memory for next time.&lt;/strong&gt; In run 2 Ziva saved a project memory describing how to test Pixelorama code, so the next chat starts with a pointer to it. See &lt;a href="https://ziva.sh/docs/agents-md#memories" rel="noopener noreferrer"&gt;memories&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claude Code got there with shell tools instead: &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;sed&lt;/code&gt;, and Godot from the command line on a virtual display. Its output landed in the same files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Ziva struggled
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ziva's game runner timed out on an idle Pixelorama.&lt;/strong&gt; Pixelorama turns on Godot's low-processor mode, so it only redraws when something changes. Ziva's &lt;code&gt;run_scene&lt;/code&gt; waits for a drawn frame to screenshot, and running the main scene timed out after about 35 seconds, twice in each run. We confirmed the cause: with low-processor mode off, the same call returned a screenshot. Ziva worked around it with a temporary test scene that drove the app and printed its results. In run 2 that scene kept the app drawing, so &lt;code&gt;run_scene&lt;/code&gt; returned screenshots too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ziva's script runner could not load Pixelorama's autoloads.&lt;/strong&gt; Its one-off GDScript runner reported "Identifier not found: Global", so Ziva switched to the test scene above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stray files.&lt;/strong&gt; Run 1 left its test scene and script in the project root and asked whether to delete them. Run 2 deleted its test files, but an &lt;code&gt;untitled.png&lt;/code&gt; appeared in the root during testing, and Ziva flagged it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Less testing than Claude Code.&lt;/strong&gt; Ziva tested only the Selected cels target and said so. Claude Code tested all four targets plus saving and loading. In run 1 it also reported a likely bug in Pixelorama's shared effect code, where applying an effect to all projects crops other projects with the current project's size. We read the line and it matches the report, but we did not reproduce it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Full Game Playtest Agent did not run.&lt;/strong&gt; Ziva tried it in both runs, but a credential problem in our local test setup stopped it before it started. This test says nothing about it either way.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this test does not show
&lt;/h2&gt;

&lt;p&gt;It is one project, one feature, two runs per tool, and one model. The task added a feature across many files. It was not a rename or a move of files across many scenes, which we have not tested yet. Neither tool needed to summarize its chat. Results with other models, a bigger project or a vaguer prompt may differ.&lt;/p&gt;

&lt;h2&gt;
  
  
  If your AI loses track as your project grows
&lt;/h2&gt;

&lt;p&gt;These habits helped in this test and work with any assistant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Point it at an existing example.&lt;/strong&gt; "Make it work like Desaturation" told both tools where to look. Name a similar scene, script or system in your project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write your project's rules down.&lt;/strong&gt; Ziva reads an &lt;code&gt;AGENTS.md&lt;/code&gt; file in your project, and Claude Code reads &lt;code&gt;CLAUDE.md&lt;/code&gt;. Put your folder layout, autoloads and conventions there so the assistant does not rediscover them every chat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask it to run the project.&lt;/strong&gt; Both tools ran Pixelorama and read its errors before reporting back. An assistant that only edits text cannot see a scene that fails to load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start a new chat for a new task.&lt;/strong&gt; A long chat full of old file contents leaves less room for the current change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a broader comparison of the two tools, see &lt;a href="https://ziva.sh/blogs/ziva-vs-claude-code-for-godot" rel="noopener noreferrer"&gt;Ziva vs Claude Code for Godot&lt;/a&gt;. If you use Claude Code and want Ziva's Godot tools inside it, Ziva can run as a &lt;a href="https://ziva.sh/docs/ziva-mcp-server" rel="noopener noreferrer"&gt;local MCP server&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can an AI assistant handle a large Godot project?
&lt;/h3&gt;

&lt;p&gt;In our test, yes. We asked Ziva and Claude Code, both running Claude Opus 5.5, to add a Sepia effect to Pixelorama (249 GDScript files, about 60,000 lines, 120 scenes). The change touched 8 files across a shader, a dialog scene and script, the menu code, the shortcut list, the layer effects list, project settings and the translation template. All four runs (two per tool) passed the same independent check in the running app. This is one project and one task, so treat it as an example, not a benchmark.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does the AI lose track as my Godot project grows?
&lt;/h3&gt;

&lt;p&gt;A model can only see what is in its context window, and a large project does not fit. The agent has to search for the right files, read only those, and keep its notes as the chat grows. In our Pixelorama test Ziva read 17 and 22 of the project's 1,383 files, and its context peaked at 137,000 and 157,000 tokens, far below Claude Opus 5.5's limit. Ziva also checks every GDScript edit with Godot's language server, can save project memories that carry over to new chats, reads an AGENTS.md file with your project's rules, and summarizes long chats automatically when they near the model's limit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Ziva refactor across many Godot files?
&lt;/h3&gt;

&lt;p&gt;We tested a multi-file feature, not a rename: Ziva created a shader, a dialog scene and its script, and edited five existing files, including project.godot through Godot's own project settings API. Both Ziva runs passed our check. We have not yet tested a large rename or move across many scenes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Claude Code or Ziva better for a large Godot project?
&lt;/h3&gt;

&lt;p&gt;On this task both produced working code. Ziva finished in 6 and 13 minutes and charged $1.94 and $2.93 of plan usage. Claude Code took about 16 minutes each time and estimated $2.70 and $3.58 at API prices. Claude Code tested more thoroughly: it checked all four apply targets and save and load, and in one run reported a likely bug in Pixelorama's shared effect code. Ziva tested only the default target, and each run left files in the project root that Ziva pointed out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Ziva index my whole project?
&lt;/h3&gt;

&lt;p&gt;No. Ziva has no embedding or semantic index of your code. It searches the project when it needs something, with ripgrep-based file and text search, and reads files in windows. The Indexing project message you may see on a big project's first chat builds the history Ziva uses for Undo, not a code index.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>ai</category>
      <category>gamedev</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>Is Godot Good for Digital Twins? An Honest 2026 Answer</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Sat, 07 Nov 2026 15:04:50 +0000</pubDate>
      <link>https://dev.to/ziva/is-godot-good-for-digital-twins-an-honest-2026-answer-59ho</link>
      <guid>https://dev.to/ziva/is-godot-good-for-digital-twins-an-honest-2026-answer-59ho</guid>
      <description>&lt;p&gt;Originally published on ziva.sh: &lt;a href="https://ziva.sh/blogs/godot-digital-twin" rel="noopener noreferrer"&gt;https://ziva.sh/blogs/godot-digital-twin&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yes, for the visual and interactive half of a digital twin: operator screens, training scenes, layout reviews and PLC test benches, with no license fees. No, if the twin has to be the source of truth for physics or process numbers. Godot's physics is built for games, it has no built-in industrial protocols, and it does not import STEP or JT CAD files. The best-known Godot factory simulator, Open Industry Project, archived its Godot version in October 2026 and moved to a browser app.&lt;/p&gt;

&lt;p&gt;If your company needs a twin built and checked against your drawings, &lt;a href="https://kenalabs.com/" rel="noopener noreferrer"&gt;Kena Labs&lt;/a&gt; does that: it builds 3D simulations from a client's drawings, photos, CAD, reports and sensor data and checks them against those sources. &lt;a href="https://kenalabs.com/#contact" rel="noopener noreferrer"&gt;Book a demo with Kena Labs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Ziva.sh publishes this page. Kena Labs is a sister company of Ziva. Facts about Godot and other projects come from their own pages and repositories as of 7 November 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Godot fits the presentation layer.&lt;/strong&gt; MIT license "for any purpose", strong UI, XR built in, C# and C++ (GDExtension) for your own integrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It does not fit as the physics or process model.&lt;/strong&gt; Godot Jolt "is not able to make such guarantees" about determinism, and CAD has to be converted to glTF or FBX first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PLC connectivity is up to you.&lt;/strong&gt; No Modbus or OPC UA addon on the Asset Library today; a Siemens S7 plugin exists, and Modbus TCP is a short GDScript file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Industrial Godot is real but small.&lt;/strong&gt; eSOL sells a Godot-based engine for industrial twins, W4 Games sells Godot enterprise support, and Open Industry Project left Godot in 2026.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What a digital twin needs, and what Godot covers
&lt;/h2&gt;

&lt;p&gt;A digital twin is a 3D model of a real asset that stays tied to it: live data from the plant, a model you can trust, and a way to act on what you see. Here is how Godot covers each part.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Godot today&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;License and cost&lt;/td&gt;
&lt;td&gt;"free and open source software released under the permissive MIT license", "free to use Godot Engine, for any purpose" (&lt;a href="https://godotengine.org/license/" rel="noopener noreferrer"&gt;license&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operator UI and dashboards&lt;/td&gt;
&lt;td&gt;The UI system was made for games but "is also capable of handling complex UI applications and tools" (&lt;a href="https://godotengine.org/features/" rel="noopener noreferrer"&gt;features&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XR training&lt;/td&gt;
&lt;td&gt;"Both OpenXR and WebXR support is built directly into the engine." (&lt;a href="https://godotengine.org/features/" rel="noopener noreferrer"&gt;features&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your own integrations&lt;/td&gt;
&lt;td&gt;First-class C# on desktop and mobile, and C++ through GDExtension "without having to recompile the engine" (&lt;a href="https://godotengine.org/features/" rel="noopener noreferrer"&gt;features&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3D import&lt;/td&gt;
&lt;td&gt;glTF 2.0 (recommended), Blender &lt;code&gt;.blend&lt;/code&gt;, FBX, OBJ and COLLADA (&lt;a href="https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_3d_scenes/available_formats.html" rel="noopener noreferrer"&gt;formats&lt;/a&gt;). No STEP, IGES, JT or IFC: convert CAD to glTF or FBX first.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Physics&lt;/td&gt;
&lt;td&gt;Jolt is the default 3D engine for new projects, "developed by Jorrit Rouwe with a focus on games and VR applications" (&lt;a href="https://docs.godotengine.org/en/stable/tutorials/physics/using_jolt_physics.html" rel="noopener noreferrer"&gt;docs&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Determinism&lt;/td&gt;
&lt;td&gt;"Simulations in Godot Jolt may look deterministic, and may even happen to be deterministic, but this should not be relied upon if determinism is a hard requirement." (&lt;a href="https://github.com/godot-jolt/godot-jolt#what-about-determinism" rel="noopener noreferrer"&gt;Godot Jolt&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PLC and plant data&lt;/td&gt;
&lt;td&gt;Nothing built in. The Asset Library has a &lt;a href="https://godotengine.org/asset-library/asset/4025" rel="noopener noreferrer"&gt;Siemens S7 PLC Plugin&lt;/a&gt; (MIT, version 0.1.0 for Godot 4.4) and no Modbus or OPC UA addon. Godot's &lt;code&gt;StreamPeerTCP&lt;/code&gt;, &lt;code&gt;WebSocketPeer&lt;/code&gt; and &lt;code&gt;HTTPRequest&lt;/code&gt; carry anything you write yourself.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern that follows: let a PLC, a simulation tool or a historian own the numbers, and let Godot show them and take operator input. The same split is common in Unity and Unreal twins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Godot is a good choice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You deploy to many screens or customers.&lt;/strong&gt; No seats, royalties or runtime fees, and you can ship modified versions "even commercially and under a different license (including proprietary)" (&lt;a href="https://godotengine.org/license/" rel="noopener noreferrer"&gt;license&lt;/a&gt;). Unity Personal, by contrast, is "For gaming and entertainment applications only" (&lt;a href="https://unity.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want your own editor.&lt;/strong&gt; Godot's editor is built from the same UI nodes as its projects, so you can turn it into a layout or scenario tool. Open Industry Project's maintainer said in February 2024, after moving the project to Godot: "the fact that editor is built using the same UI as the games gives this project a lot of power" (&lt;a href="https://github.com/Open-Industry-Project/Open-Industry-Project-Godot/issues/26" rel="noopener noreferrer"&gt;issue #26&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Training and walkthroughs.&lt;/strong&gt; VR and AR training, onboarding and sales demos need a good-looking scene and input handling more than certified physics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PLC test benches and teaching rigs.&lt;/strong&gt; A conveyor cell driven by a real or soft PLC over Modbus TCP is a small, well-understood project.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it falls short
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Physics you can sign off on.&lt;/strong&gt; Game physics trades accuracy for speed, and Godot Jolt makes no determinism promise. If the twin must predict throughput, loads or robot behavior, use a tool built for it and import its results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CAD and BIM.&lt;/strong&gt; Engineering formats need a conversion step (CAD to glTF or FBX, often through Blender), and large plants need mesh reduction. Unity, by comparison, says "70+ CAD and 3D file formats can be imported" (&lt;a href="https://unity.com/solutions/digital-twins" rel="noopener noreferrer"&gt;Unity&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Industrial protocols.&lt;/strong&gt; OPC UA, MQTT and most PLC drivers are libraries you wrap yourself. Open Industry Project's Godot version did it with &lt;a href="https://github.com/open62541/open62541" rel="noopener noreferrer"&gt;open62541&lt;/a&gt; for OPC UA and &lt;a href="https://github.com/libplctag/libplctag" rel="noopener noreferrer"&gt;libplctag&lt;/a&gt; for Modbus TCP and EtherNet/IP, inside a custom Godot build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ecosystem.&lt;/strong&gt; NVIDIA Omniverse and Isaac Sim come with OpenUSD pipelines, robot importers and ROS 2 bridges; Unity and Unreal have industry teams and partners. Godot's industrial ecosystem is a handful of companies and community projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What happened to Open Industry Project
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.openindustryproject.org/" rel="noopener noreferrer"&gt;Open Industry Project&lt;/a&gt; is the project most answers cite for Godot in industry. Its Godot repository described itself as a "Free and open-source warehouse/manufacturing development framework and simulator built on Godot", supported OPC UA, EtherNet/IP, Modbus TCP, Siemens S7, Beckhoff ADS, Universal Robots RTDE and MQTT, and has 859 stars.&lt;/p&gt;

&lt;p&gt;On GitHub that repository is now archived, with this notice: "This is the Godot version of Open Industry Project, and it is no longer maintained. Its successor runs in the browser and as a desktop app" (&lt;a href="https://github.com/Open-Industry-Project/Open-Industry-Project-Godot" rel="noopener noreferrer"&gt;repository&lt;/a&gt;). The successor, created on 19 October 2026, describes itself as a "Browser-based industrial automation simulator: conveyors, sensors, PLCs (TypeScript, three.js, Jolt)" (&lt;a href="https://github.com/Open-Industry-Project/Open-Industry-Project" rel="noopener noreferrer"&gt;repository&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The maintainers have not published a reason in the repositories. The Godot version's repository shows what users ran into: the project shipped its own Godot fork, and a request for Linux support has been open since November 2025 (&lt;a href="https://github.com/Open-Industry-Project/Open-Industry-Project-Godot/issues/228" rel="noopener noreferrer"&gt;issue #228&lt;/a&gt;). The new version's site lists its price as "Free. No account, no licence key." If you were planning to build on the Godot version, start from the new one instead or treat the Godot code as reference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who uses Godot for industrial work today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;eSOL eXRP.&lt;/strong&gt; A "Godot-based Real-time 3D Engine" for "3D simulations, digital twins, HMIs, and more in industrial sectors such as mobility, robotics, and manufacturing". Its listed uses include "Digital twins for production lines" and "Control simulation for conveyance machines", and eSOL says it "Jointly developed a 3D-LiDAR point cloud simulator" with Tadano Infrastructure Solutions using Godot (&lt;a href="https://www.esol.com/embedded/product/exrp.html" rel="noopener noreferrer"&gt;eSOL&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;W4 Games.&lt;/strong&gt; It offers "Direct involvement of Godot founders and core maintainers", calls itself "Your enterprise partner in the Godot Engine ecosystem" and sells professional services and "Enterprise-grade support for teams using Godot in production" (&lt;a href="https://www.w4games.com/" rel="noopener noreferrer"&gt;W4 Games&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Universities.&lt;/strong&gt; At GodotFest 2026, an LSU team presented a multiplayer VR and AR digital twin of the university's distillation columns built with Godot (&lt;a href="https://media.ccc.de/v/godotfest-26-37-building-a-multiplayer-network-for-digital-twins-in-vr-ar" rel="noopener noreferrer"&gt;talk&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community PLC projects.&lt;/strong&gt; The &lt;a href="https://github.com/wepall-robotics/siemens-plugin" rel="noopener noreferrer"&gt;Siemens S7 PLC Plugin&lt;/a&gt; and conveyor-cell demos driven by CODESYS or OpenPLC, many of them built on the archived Open Industry Project.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Godot compared with the usual digital twin platforms
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Non-game licensing, from its own pages&lt;/th&gt;
&lt;th&gt;Strongest at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://godotengine.org/license/" rel="noopener noreferrer"&gt;Godot&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;MIT, "for any purpose"&lt;/td&gt;
&lt;td&gt;Free, customizable visualization and tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://unity.com/products/unity-industry" rel="noopener noreferrer"&gt;Unity Industry&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;"$4,950.00/yr per seat"; Unity Personal is "For gaming and entertainment applications only"&lt;/td&gt;
&lt;td&gt;CAD import, mobile and XR deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.unrealengine.com/faq" rel="noopener noreferrer"&gt;Unreal Engine&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Free for most non-game companies under $1 million in annual gross revenue; above that, $1,850 per seat a year, or a 5% royalty if you license an app that runs the engine&lt;/td&gt;
&lt;td&gt;Photorealistic large scenes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.nvidia.com/en-us/use-cases/industrial-facility-digital-twins/" rel="noopener noreferrer"&gt;NVIDIA Omniverse&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;"As of May 2026, Omniverse is freely available for both development and production use" (&lt;a href="https://docs.omniverse.nvidia.com/dev-overview/latest/common/NVIDIA_Omniverse_License_Agreement.html" rel="noopener noreferrer"&gt;license&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;OpenUSD pipelines and physics-based industrial twins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/isaac-sim/IsaacSim" rel="noopener noreferrer"&gt;NVIDIA Isaac Sim&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Apache 2.0 for the repository code; Omniverse Kit SDK and assets under other terms&lt;/td&gt;
&lt;td&gt;Robots, sensors and ROS 2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For the wider field, including Siemens, Azure Digital Twins, AWS IoT TwinMaker and open-source options, see our &lt;a href="https://ziva.sh/blogs/best-digital-twin-software" rel="noopener noreferrer"&gt;list of the best digital twin software in 2026&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting Godot to a PLC
&lt;/h2&gt;

&lt;p&gt;Modbus TCP is the easiest place to start: a request is a 7-byte header plus a few bytes of function code and addresses, which GDScript can send over &lt;code&gt;StreamPeerTCP&lt;/code&gt;. We wrote and tested one: &lt;a href="https://ziva.sh/blogs/godot-modbus-tcp" rel="noopener noreferrer"&gt;How to connect Godot to a PLC over Modbus TCP&lt;/a&gt;. For Siemens S7-1200 and S7-1500 PLCs, the S7 plugin above reads and writes variables directly. For OPC UA, the realistic routes are a C# OPC UA library in a Godot .NET project or a GDExtension around open62541. Whichever you pick, poll on a timer rather than every frame, and treat a dropped connection as an error the operator sees, not a value that silently freezes.&lt;/p&gt;

&lt;p&gt;If you build the Godot side yourself, &lt;a href="https://ziva.sh/godot-ai" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt;, an AI agent that works inside the Godot editor, can write the scenes, GDScript or C# and the UI for you, and run the scene to check it. It does not come with PLC drivers or validated physics. Ziva also works inside &lt;a href="https://ziva.sh/isaac-sim" rel="noopener noreferrer"&gt;NVIDIA Isaac Sim&lt;/a&gt;, for teams through Ziva Enterprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to have a twin built instead
&lt;/h2&gt;

&lt;p&gt;If the twin has to match drawings and sensor data, the 3D is the smaller part of the work: collecting the drawings, checking dimensions and wiring the data take most of the time. &lt;a href="https://kenalabs.com/" rel="noopener noreferrer"&gt;Kena Labs&lt;/a&gt; builds simulations from that material. In its words, it turns "specs and photos into interactive 3D": "Drawings, photos, CAD, reports and sensor data become one scene", the scene is "Checked against the drawing, the sensor record or the case file", and it can be handed off as "OpenUSD for simulation, IFC for the plant model". Its published demos, on public-domain and sample material, include a &lt;a href="https://kenalabs.com/case-studies/pump-house/" rel="noopener noreferrer"&gt;1950 pump house rebuilt from one drawing&lt;/a&gt; and a &lt;a href="https://kenalabs.com/case-studies/parcel-line/" rel="noopener noreferrer"&gt;parcel line tested in 16 scenarios at once in NVIDIA Isaac Sim&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Kena Labs is a sister company of Ziva. &lt;a href="https://kenalabs.com/#contact" rel="noopener noreferrer"&gt;Send Kena Labs your drawing or spec and book a demo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can Godot be used for a digital twin?
&lt;/h3&gt;

&lt;p&gt;Yes, as the 3D, UI and interaction layer of a digital twin: operator screens, training scenes, layout reviews and PLC test benches. Godot is MIT licensed, so there are no seat or runtime fees, and it imports glTF, Blender, FBX, OBJ and COLLADA. It has no built-in OPC UA, Modbus or MQTT client, no native STEP or JT CAD import, and its physics is a game engine's physics: the Godot Jolt project says simulations should not be relied on to be deterministic. Teams that need an engineering-grade twin usually run the model outside Godot and use Godot to show it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Godot good for industrial simulation?
&lt;/h3&gt;

&lt;p&gt;For visual, interactive simulations such as conveyor cells, warehouse layouts and training, it works, and eSOL sells a Godot-based engine, eXRP, for industrial digital twins and HMIs. For simulations whose numbers must be trusted, it is a weaker base than tools built for that job, such as Siemens Plant Simulation and Process Simulate or NVIDIA Isaac Sim, because physics is not guaranteed to be deterministic and industrial protocols are add-ons. Open Industry Project, the best-known Godot factory simulator, archived its Godot version in October 2026 and moved to a browser app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Godot good for non-game applications?
&lt;/h3&gt;

&lt;p&gt;Often, yes. Godot's license lets you use it for any purpose, its UI system is, in the project's words, capable of handling complex UI applications and tools, and the editor itself is a Godot application. It suits apps that need real-time 2D or 3D, XR or custom editors. For plain forms-and-tables business software, a regular app framework is usually simpler.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who offers enterprise support for Godot in industrial projects?
&lt;/h3&gt;

&lt;p&gt;W4 Games, which offers "Direct involvement of Godot founders and core maintainers", sells professional services and premium support for teams using Godot in production. eSOL sells eXRP, a Godot-based real-time 3D engine for industrial digital twins and HMIs, with engineering, consulting and support services.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>digitaltwin</category>
      <category>iot</category>
    </item>
    <item>
      <title>How to Connect Godot to a PLC over Modbus TCP (Tested GDScript Client)</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Sat, 07 Nov 2026 15:04:42 +0000</pubDate>
      <link>https://dev.to/ziva/how-to-connect-godot-to-a-plc-over-modbus-tcp-tested-gdscript-client-4nk9</link>
      <guid>https://dev.to/ziva/how-to-connect-godot-to-a-plc-over-modbus-tcp-tested-gdscript-client-4nk9</guid>
      <description>&lt;p&gt;Originally published on ziva.sh: &lt;a href="https://ziva.sh/blogs/godot-modbus-tcp" rel="noopener noreferrer"&gt;https://ziva.sh/blogs/godot-modbus-tcp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Godot has no Modbus or OPC UA support built in, and the Godot Asset Library has no Modbus or OPC UA addon. Modbus TCP is simple enough that you do not need one: each request is a 7-byte header and a 5-byte command sent over TCP, which GDScript can do with &lt;code&gt;StreamPeerTCP&lt;/code&gt;. Below is a complete client for reading coils and holding registers and writing single coils and registers, a conveyor that runs while a PLC coil is on, and how we tested both. For Siemens S7 PLCs and OPC UA, see &lt;a href="https://ziva.sh/blogs/godot-modbus-tcp#siemens-s7-and-opc-ua" rel="noopener noreferrer"&gt;the other options&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We tested this on Godot 4.7.2 on Linux, headless, against two independent Modbus TCP servers: &lt;a href="https://github.com/pymodbus-dev/pymodbus" rel="noopener noreferrer"&gt;pymodbus&lt;/a&gt; 3.16.2 and &lt;a href="https://github.com/sourceperl/pyModbusTCP" rel="noopener noreferrer"&gt;pyModbusTCP&lt;/a&gt; 0.3.1. All 15 checks passed on both on 7 November 2026. We did not test it against PLC hardware. Ziva.sh publishes this page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The client
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;modbus_tcp_client.gd&lt;/code&gt;. It supports function codes 1 (read coils), 3 (read holding registers), 5 (write single coil) and 6 (write single register), one request at a time. Every call returns &lt;code&gt;null&lt;/code&gt; on failure and sets &lt;code&gt;last_error&lt;/code&gt;, so a lost PLC shows up as an error instead of a value that silently stops changing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;class_name&lt;/span&gt; &lt;span class="n"&gt;ModbusTcpClient&lt;/span&gt;
&lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;RefCounted&lt;/span&gt;
&lt;span class="c1"&gt;## Minimal Modbus TCP client (FC1, FC3, FC5, FC6), one request at a time.&lt;/span&gt;
&lt;span class="c1"&gt;## Every call returns null on failure and sets last_error; Modbus exception&lt;/span&gt;
&lt;span class="c1"&gt;## replies also set last_exception_code. Successful writes return true.&lt;/span&gt;
&lt;span class="c1"&gt;##&lt;/span&gt;
&lt;span class="c1"&gt;## Calls block the calling thread until the reply arrives or timeout_ms passes.&lt;/span&gt;
&lt;span class="c1"&gt;## A LAN PLC answers in a few ms; run the client on a Thread for slow links.&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION_NAMES&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Illegal Function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Illegal Data Address"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Illegal Data Value"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Server Device Failure"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Server Device Busy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Gateway Path Unavailable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Gateway Target Failed To Respond"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;unit_id&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;timeout_ms&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;last_exception_code&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;_tcp&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;StreamPeerTCP&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;_transaction_id&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;connect_to_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="n"&gt;last_exception_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;_tcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;StreamPeerTCP&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_tcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect_to_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"connect to &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt; failed: &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_ticks_msec&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;timeout_ms&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_ticks_msec&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_tcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;_tcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_status&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;StreamPeerTCP&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;STATUS_CONNECTED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;
            &lt;span class="n"&gt;StreamPeerTCP&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;STATUS_ERROR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StreamPeerTCP&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;STATUS_NONE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"connect to &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt; refused or failed"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;OS&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delay_msec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_fail_and_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"connect to &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt; timed out after &lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt; ms"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout_ms&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;read_coils&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_transact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ceili&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;coils&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;coils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;coils&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;read_holding_registers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_transact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x03&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;registers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;registers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;registers&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;write_single_coil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_transact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0xFF00&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mh"&gt;0x0000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;write_single_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_transact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x06&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_transact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;function_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reply_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="n"&gt;last_exception_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_tcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;StreamPeerTCP&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;STATUS_CONNECTED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"not connected; call connect_to_host() first"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mh"&gt;0xFFFF&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mh"&gt;0xFFFF&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"address &lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt; or value/count &lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt; is outside 0..65535"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;_transaction_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_transaction_id&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFFFF&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pdu&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PackedByteArray&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;function_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="c1"&gt;# MBAP header: transaction id, protocol id 0, byte count of unit id + PDU, unit id.&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mbap&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PackedByteArray&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;_transaction_id&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_transaction_id&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pdu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unit_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_tcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mbap&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pdu&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_fail_and_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"send failed: &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;error_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_ticks_msec&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;timeout_ms&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_read_exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;_transaction_id&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; \
            &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;unit_id&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;254&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_fail_and_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bad MBAP header &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt; for transaction &lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hex_encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;_transaction_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_read_exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;function_code&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mh"&gt;0x80&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;last_exception_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Modbus exception &lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;) for function &lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt; at address &lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION_NAMES&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s2"&gt;"unknown code"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;function_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;pdu&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;function_code&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;reply_size&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;reply_size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;function_code&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_fail_and_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"unexpected reply &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt; to request &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hex_encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;pdu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hex_encode&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_read_exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;byte_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_tcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_tcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_available_bytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;byte_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_tcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;byte_count&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_tcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;StreamPeerTCP&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;STATUS_CONNECTED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"connection closed while waiting for &lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt; bytes"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;byte_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_ticks_msec&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# The late reply would desync the next transaction, so drop the socket.&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_fail_and_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"timeout: no reply within &lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt; ms (connection closed)"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;timeout_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;OS&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delay_msec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_fail_and_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;_tcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;disconnect_from_host&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
    &lt;span class="n"&gt;push_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ModbusTcpClient: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ModbusTcpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect_to_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"192.168.0.10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;push_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_single_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1234&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# holding register 40001&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;regs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_holding_registers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# [1234, ...]&lt;/span&gt;
&lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_single_coil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;# coil 00004&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;coils&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_coils&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;# [false, false, false, true, ...]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Addresses are zero-based offsets: register 40001 in a PLC manual is offset 0, and coil 00001 is offset 0. If your values are off by one register, this is why.&lt;/p&gt;

&lt;h2&gt;
  
  
  A conveyor driven by a PLC coil
&lt;/h2&gt;

&lt;p&gt;This Node3D polls coil 0 every 50 ms and moves a &lt;code&gt;Box&lt;/code&gt; child along the belt while the coil is on. If the connection drops, it stops the belt rather than leaving it running on the last value it saw.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Node3D&lt;/span&gt;
&lt;span class="c1"&gt;## Moves $Box along the belt while PLC coil `motor_coil` is on. Polls every 50 ms.&lt;/span&gt;

&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;plc_host&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;
&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;plc_port&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;
&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;motor_coil&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;belt_speed&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="c1"&gt;# meters per second&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ModbusTcpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;motor_on&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_ready&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect_to_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plc_host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plc_port&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Timer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wait_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;
    &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_poll_plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;add_child&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_poll_plc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Timer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;coils&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_coils&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;motor_coil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;coils&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# Comms lost: stop the belt; the client already logged why.&lt;/span&gt;
        &lt;span class="n"&gt;motor_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;motor_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coils&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_physics_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;motor_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;belt_speed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Polling on a 50 ms &lt;code&gt;Timer&lt;/code&gt; keeps PLC traffic independent of frame rate. Each call blocks until the reply arrives; a PLC on the local network answers in a few milliseconds, but a dead one blocks for up to &lt;code&gt;timeout_ms&lt;/code&gt;, so keep the timeout short or move polling to a &lt;code&gt;Thread&lt;/code&gt; on slow links.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we tested it
&lt;/h2&gt;

&lt;p&gt;We ran a Modbus TCP server with 16 coils and 16 holding registers on 127.0.0.1, then a headless Godot script that writes and reads back registers and coils, reads past the end of the data (which must return Modbus exception 2, Illegal Data Address), checks the connection still works after the exception, checks a 200 ms timeout against a server that never answers, and runs the conveyor scene for a few frames with coil 0 on and then off.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv /tmp/modbus-venv
/tmp/modbus-venv/bin/pip &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;pyModbusTCP&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;0.3.1
/tmp/modbus-venv/bin/python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"from pyModbusTCP.server import DataBank, ModbusServer; ModbusServer(host='127.0.0.1', port=5021, data_bank=DataBank(coils_size=16, d_inputs_size=16, h_regs_size=16, i_regs_size=16)).start()"&lt;/span&gt; &amp;amp;
godot &lt;span class="nt"&gt;--headless&lt;/span&gt; &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--import&lt;/span&gt;
godot &lt;span class="nt"&gt;--headless&lt;/span&gt; &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--script&lt;/span&gt; res://test_modbus.gd &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5021
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project holds a minimal &lt;code&gt;project.godot&lt;/code&gt;, &lt;code&gt;modbus_tcp_client.gd&lt;/code&gt;, &lt;code&gt;conveyor.gd&lt;/code&gt; and &lt;code&gt;conveyor.tscn&lt;/code&gt; (a Node3D with &lt;code&gt;conveyor.gd&lt;/code&gt; attached and a &lt;code&gt;MeshInstance3D&lt;/code&gt; child named &lt;code&gt;Box&lt;/code&gt;), plus this test script:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;test_modbus.gd&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;SceneTree&lt;/span&gt;
&lt;span class="c1"&gt;## Headless test: godot --headless --path . --script res://test_modbus.gd -- --port=5020&lt;/span&gt;
&lt;span class="c1"&gt;## Expects a server with 16 coils and 16 holding registers (addresses 0..15) on 127.0.0.1.&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Variant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"PASS"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;"FAIL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_initialize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5020&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;OS&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_cmdline_user_args&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;begins_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"--port="&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trim_prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"--port="&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Godot &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;, server 127.0.0.1:&lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_version_info&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ModbusTcpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"connect"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect_to_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"FC6 write register 0 = 1234"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_single_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1234&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"FC6 write register 1 = 0xBEEF"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_single_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0xBEEF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;regs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_holding_registers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"FC3 read registers 0..1 == [1234, 48879]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;regs&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1234&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0xBEEF&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;regs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"FC5 write coil 3 = true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_single_coil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;coils&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_coils&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"FC1 read coils 0..7 has only coil 3 on"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coils&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;coils&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_single_coil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;coils&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_coils&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"FC5 coil 3 = false then FC1 read coils 0..15 all off"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coils&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;coils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;coils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;coils&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_holding_registers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"FC3 read register 100 returns exception 2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_exception_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_coils&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"FC1 read coil 16 returns exception 2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_exception_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;regs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_holding_registers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"connection still usable after exception"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;regs&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1234&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;regs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;silent&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TCPServer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;silent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5099&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mute_plc&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ModbusTcpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;mute_plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="n"&gt;mute_plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect_to_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5099&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_ticks_msec&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mute_plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_coils&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_ticks_msec&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt;
    &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"timeout on silent server after ~200 ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;mute_plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;begins_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt; ms: &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mute_plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"call after timeout reports not connected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mute_plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_coils&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;mute_plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;begins_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"not connected"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;mute_plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;silent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"connect refused reports error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ModbusTcpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect_to_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5098&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;conveyor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Node3D&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"res://conveyor.tscn"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instantiate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;conveyor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plc_port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;
    &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_child&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conveyor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_single_coil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;create_timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;moved_x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conveyor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Box"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;
    &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"conveyor box moves while coil 0 is on"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;moved_x&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"x=&lt;/span&gt;&lt;span class="si"&gt;%.3f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;moved_x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;plc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_single_coil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;create_timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stopped_x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conveyor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Box"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;create_timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;final_x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conveyor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Box"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;
    &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"conveyor box stops when coil 0 is off"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_equal_approx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stopped_x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;final_x&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;"x=&lt;/span&gt;&lt;span class="si"&gt;%.3f&lt;/span&gt;&lt;span class="s2"&gt; then &lt;/span&gt;&lt;span class="si"&gt;%.3f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stopped_x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;final_x&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"RESULT: &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt; failures)"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"PASS"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;"FAIL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PASS and FAIL lines against pyModbusTCP 0.3.1 are below. The full log also has &lt;code&gt;ERROR: ModbusTcpClient: ...&lt;/code&gt; lines from the checks that expect an error; those are expected. pymodbus 3.16.2 passed the same 15 checks, with the timeout at 200 ms instead of 201 ms.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Godot 4.7.2-stable (arch_linux), server 127.0.0.1:5021
PASS connect
PASS FC6 write register 0 = 1234
PASS FC6 write register 1 = 0xBEEF
PASS FC3 read registers 0..1 == [1234, 48879] [1234, 48879]
PASS FC5 write coil 3 = true
PASS FC1 read coils 0..7 has only coil 3 on [false, false, false, true, false, false, false, false]
PASS FC5 coil 3 = false then FC1 read coils 0..15 all off [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false]
PASS FC3 read register 100 returns exception 2 Modbus exception 2 (Illegal Data Address) for function 3 at address 100
PASS FC1 read coil 16 returns exception 2 Modbus exception 2 (Illegal Data Address) for function 1 at address 16
PASS connection still usable after exception [1234]
PASS timeout on silent server after ~200 ms 201 ms: timeout: no reply within 200 ms (connection closed)
PASS call after timeout reports not connected not connected; call connect_to_host() first
PASS connect refused reports error
PASS conveyor box moves while coil 0 is on x=0.183
PASS conveyor box stops when coil 0 is off x=0.208 then 0.208
RESULT: PASS (0 failures)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With no server listening, the same script exits with code 1 and FAIL lines, so a broken setup cannot pass silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Function codes 1, 3, 5 and 6 only. Discrete inputs (2), input registers (4) and multiple writes (15, 16) need a few more lines each.&lt;/li&gt;
&lt;li&gt;No automatic reconnect: after a timeout the socket is closed and you call &lt;code&gt;connect_to_host&lt;/code&gt; again.&lt;/li&gt;
&lt;li&gt;No TLS or authentication. Keep plain Modbus TCP on an isolated control network.&lt;/li&gt;
&lt;li&gt;32-bit values split across two registers are not decoded; word order differs between PLC vendors.&lt;/li&gt;
&lt;li&gt;It does not work in web exports. Godot's &lt;a href="https://docs.godotengine.org/en/stable/tutorials/export/exporting_for_web.html" rel="noopener noreferrer"&gt;web export docs&lt;/a&gt; say: "Low-level networking is not implemented due to lacking support in browsers."&lt;/li&gt;
&lt;li&gt;Tested on Linux desktop only, not on PLC hardware.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Siemens S7 and OPC UA
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Siemens S7.&lt;/strong&gt; The &lt;a href="https://godotengine.org/asset-library/asset/4025" rel="noopener noreferrer"&gt;Siemens S7 PLC Plugin&lt;/a&gt; on the Asset Library (MIT, version 0.1.0 for Godot 4.4) says it connects to S7-300, S7-400, S7-1200 and S7-1500 PLCs and reads and writes variables directly. We did not test it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OPC UA in C#.&lt;/strong&gt; The OPC Foundation's &lt;a href="https://github.com/OPCFoundation/UA-.NETStandard" rel="noopener noreferrer"&gt;UA .NET Standard&lt;/a&gt; stack, "The official OPC Foundation reference implementation of OPC UA for .NET", runs on .NET 8, which Godot's .NET build uses, and Godot's docs say "NuGet packages can be installed and used with Godot, as with any C# project." C# projects cannot be exported to the web.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OPC UA in a GDExtension.&lt;/strong&gt; Wrap &lt;a href="https://github.com/open62541/open62541" rel="noopener noreferrer"&gt;open62541&lt;/a&gt;, a C implementation of OPC UA under MPL 2.0. Open Industry Project's &lt;a href="https://github.com/Open-Industry-Project/oip-comms" rel="noopener noreferrer"&gt;oip-comms&lt;/a&gt; did this for its Godot version, which is now archived and no longer maintained.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A bridge process.&lt;/strong&gt; Run an OPC UA client such as Node-RED or Python's asyncua next to Godot and pass values over a WebSocket. This is the only route that also works in a web export.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We tested none of the OPC UA routes. For whether Godot is the right engine for the whole twin, see &lt;a href="https://ziva.sh/blogs/godot-digital-twin" rel="noopener noreferrer"&gt;Is Godot good for digital twins?&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Need the whole twin built?
&lt;/h2&gt;

&lt;p&gt;A PLC link is one piece. If you need a plant, line or cell rebuilt from your drawings and checked against them, &lt;a href="https://kenalabs.com/" rel="noopener noreferrer"&gt;Kena Labs&lt;/a&gt; builds 3D simulations from a client's drawings, photos, CAD, reports and sensor data and checks them against those sources; one of its demos is a &lt;a href="https://kenalabs.com/case-studies/parcel-line/" rel="noopener noreferrer"&gt;parcel line tested in 16 scenarios at once&lt;/a&gt;. Kena Labs is a sister company of Ziva. &lt;a href="https://kenalabs.com/#contact" rel="noopener noreferrer"&gt;Book a demo with Kena Labs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you are building the Godot side yourself, &lt;a href="https://ziva.sh/godot-ai" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt;, an AI agent inside the Godot editor, can write this kind of client and scene code for you and run the scene to check it.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can Godot connect to a PLC over Modbus TCP or OPC UA?
&lt;/h3&gt;

&lt;p&gt;Yes, but neither is built in. Modbus TCP is a small binary protocol over TCP, so a GDScript client on StreamPeerTCP is enough; ours reads coils (function 1) and holding registers (function 3) and writes single coils and registers (functions 5 and 6), and it passed every check against two independent Modbus servers on Godot 4.7.2. For OPC UA you need a library: the OPC Foundation's .NET stack in a Godot .NET project, a GDExtension around open62541, or a separate bridge process that passes values to Godot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there a Modbus or OPC UA addon for Godot?
&lt;/h3&gt;

&lt;p&gt;Not on the Godot Asset Library as of November 2026; searches for Modbus and OPC UA return nothing relevant. The Asset Library does have a Siemens S7 PLC Plugin (MIT, version 0.1.0 for Godot 4.4) that reads and writes S7-300, S7-400, S7-1200 and S7-1500 variables. Open Industry Project's oip-comms GDExtension supported OPC UA and Modbus TCP, but it is archived and no longer maintained.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Modbus TCP work in a Godot web export?
&lt;/h3&gt;

&lt;p&gt;No. Godot's web export docs say low-level networking is not implemented because browsers do not support it; only HTTP, WebSocket client and WebRTC work. For a browser-based HMI or twin, run a bridge that reads the PLC and sends values to Godot over a WebSocket.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>iot</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to simulate keyboard and mouse input in GUT and GdUnit4 (tested on Godot 4.7.2)</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Sat, 07 Nov 2026 06:42:49 +0000</pubDate>
      <link>https://dev.to/ziva/how-to-simulate-keyboard-and-mouse-input-in-gut-and-gdunit4-tested-on-godot-472-4c51</link>
      <guid>https://dev.to/ziva/how-to-simulate-keyboard-and-mouse-input-in-gut-and-gdunit4-tested-on-godot-472-4c51</guid>
      <description>&lt;p&gt;In GUT, queue input on a &lt;code&gt;GutInputSender&lt;/code&gt; aimed at &lt;code&gt;Input&lt;/code&gt; and &lt;code&gt;await&lt;/code&gt; its &lt;code&gt;idle&lt;/code&gt; signal before you assert; in GdUnit4, call the scene runner's &lt;code&gt;simulate_key_*&lt;/code&gt;, &lt;code&gt;simulate_action_*&lt;/code&gt; and &lt;code&gt;simulate_mouse_button_*&lt;/code&gt; methods, wait, and &lt;code&gt;await runner.await_input_processed()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Ziva.sh publishes this page and makes Ziva. We ran every test and command on this page on 7 November 2026 with Godot 4.7.2 on Linux, GUT 9.7.1 and GdUnit4 6.2.3, the latest releases that day, on the public &lt;a href="https://github.com/ziva-sh/ziva-godot-sample" rel="noopener noreferrer"&gt;Star Catcher sample&lt;/a&gt;, a 2D game where the arrow keys move a square into stars. Method names come from the installed addons' source code.&lt;/p&gt;

&lt;h2&gt;
  
  
  GUT
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://gut.readthedocs.io/en/latest/class_ref/class_gutinputsender.html" rel="noopener noreferrer"&gt;GUT's GutInputSender&lt;/a&gt; sends input events to receivers. With &lt;code&gt;Input&lt;/code&gt; as the receiver, GUT 9.7.1 passes each event to &lt;code&gt;Input.parse_input_event&lt;/code&gt;, so the game sees it like hardware input. Chained calls are queued and played back in real time, and &lt;code&gt;await _sender.idle&lt;/code&gt; waits until the queue is empty.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="c1"&gt;# res://test/test_input.gd&lt;/span&gt;
&lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;GutTest&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;_sender&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GutInputSender&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;after_each&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;_sender&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;release_all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;_sender&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;test_holding_right_arrow_collects_a_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;add_child_autofree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"res://main.tscn"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instantiate&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;_sender&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_down&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KEY_RIGHT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hold_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_sender&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;idle&lt;/span&gt;
    &lt;span class="n"&gt;assert_eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"HUD/ScoreLabel"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Score: 1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;test_holding_ui_right_action_collects_a_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;add_child_autofree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"res://main.tscn"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instantiate&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;_sender&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_down&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ui_right"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hold_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_sender&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;idle&lt;/span&gt;
    &lt;span class="n"&gt;assert_eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"HUD/ScoreLabel"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Score: 1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;test_clicking_a_button_emits_pressed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;add_child_autofree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Button&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;watch_signals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_sender&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mouse_left_click_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;220&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_sender&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;idle&lt;/span&gt;
    &lt;span class="n"&gt;assert_signal_emitted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"pressed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;hold_for(1.2)&lt;/code&gt; sends the matching release 1.2 seconds after the press, and &lt;code&gt;wait(0.3)&lt;/code&gt; gives the game time to react; both also take a frame count as a string such as &lt;code&gt;"10f"&lt;/code&gt;. GutInputSender also has &lt;code&gt;key_up&lt;/code&gt;, &lt;code&gt;action_up&lt;/code&gt;, &lt;code&gt;mouse_left_button_down&lt;/code&gt;, &lt;code&gt;mouse_left_button_up&lt;/code&gt;, &lt;code&gt;mouse_right_button_down&lt;/code&gt;, &lt;code&gt;mouse_right_button_up&lt;/code&gt;, &lt;code&gt;mouse_double_click&lt;/code&gt;, &lt;code&gt;mouse_motion&lt;/code&gt;, &lt;code&gt;mouse_relative_motion&lt;/code&gt;, &lt;code&gt;wait_frames&lt;/code&gt; and &lt;code&gt;hold_frames&lt;/code&gt;. GUT's docs say to call &lt;code&gt;release_all()&lt;/code&gt; "between each test when using &lt;code&gt;Input&lt;/code&gt; as a receiver" and &lt;code&gt;clear()&lt;/code&gt; "so that state does not leak between tests", which is what &lt;code&gt;after_each&lt;/code&gt; does here.&lt;/p&gt;

&lt;p&gt;Under &lt;code&gt;xvfb-run&lt;/code&gt; with &lt;code&gt;-gcompact_mode&lt;/code&gt;, all three tests passed. Headless, the two keyboard tests passed and the click test failed; see &lt;a href="https://ziva.sh/blogs/godot-gut-gdunit4-simulate-input#pitfalls" rel="noopener noreferrer"&gt;Pitfalls&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  GdUnit4
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://godot-gdunit-labs.github.io/gdUnit4/latest/advanced_testing/scene_runner/keys/" rel="noopener noreferrer"&gt;GdUnit4's scene runner&lt;/a&gt; loads a scene, or wraps a node you pass it, and feeds simulated input through &lt;code&gt;Input.parse_input_event&lt;/code&gt;. GdUnit4's docs say "you must wait at least one frame cycle after simulating inputs" and point to &lt;code&gt;await runner.await_input_processed()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="c1"&gt;# res://test/test_input.gd&lt;/span&gt;
&lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;GdUnitTestSuite&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;test_holding_right_arrow_collects_a_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scene_runner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"res://main.tscn"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;simulate_key_press&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KEY_RIGHT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;await_millis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;simulate_key_release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KEY_RIGHT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;await_input_processed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;assert_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find_child&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ScoreLabel"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Score: 1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;test_holding_ui_right_action_collects_a_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scene_runner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"res://main.tscn"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;simulate_action_press&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ui_right"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;await_millis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;simulate_action_release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ui_right"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;await_input_processed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;assert_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find_child&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ScoreLabel"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Score: 1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;test_clicking_a_button_emits_pressed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;auto_free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Button&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scene_runner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;monitor_signals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_mouse_position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;220&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;simulate_mouse_button_pressed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MOUSE_BUTTON_LEFT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;await_input_processed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;assert_signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_emitted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"pressed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GdUnit4 6.2.3 has three methods for each kind of input:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Press and release&lt;/th&gt;
&lt;th&gt;Press only&lt;/th&gt;
&lt;th&gt;Release&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Key&lt;/td&gt;
&lt;td&gt;&lt;code&gt;simulate_key_pressed(KEY_ENTER)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;simulate_key_press(KEY_SHIFT)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;simulate_key_release(KEY_SHIFT)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mouse button&lt;/td&gt;
&lt;td&gt;&lt;code&gt;simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;simulate_mouse_button_press(MOUSE_BUTTON_LEFT)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;simulate_mouse_button_release(MOUSE_BUTTON_LEFT)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input action&lt;/td&gt;
&lt;td&gt;&lt;code&gt;simulate_action_pressed("ui_accept")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;simulate_action_press("ui_right")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;simulate_action_release("ui_right")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;GdUnit4 moves the mouse with &lt;code&gt;set_mouse_position(position)&lt;/code&gt; and &lt;code&gt;simulate_mouse_move(position)&lt;/code&gt;, or gradually with &lt;code&gt;simulate_mouse_move_relative&lt;/code&gt; and &lt;code&gt;simulate_mouse_move_absolute&lt;/code&gt;. In GdUnit4 6.2.3, &lt;code&gt;simulate_key_pressed&lt;/code&gt; waits one frame between the press and the release, so &lt;code&gt;await&lt;/code&gt; it, while &lt;code&gt;simulate_action_pressed&lt;/code&gt; and &lt;code&gt;simulate_mouse_button_pressed&lt;/code&gt; press and release within the same call. For a key combination, GdUnit4's &lt;a href="https://godot-gdunit-labs.github.io/gdUnit4/latest/advanced_testing/scene_runner/keys/" rel="noopener noreferrer"&gt;key docs&lt;/a&gt; say to hold each key with &lt;code&gt;simulate_key_press&lt;/code&gt;, such as &lt;code&gt;KEY_CTRL&lt;/code&gt; then &lt;code&gt;KEY_C&lt;/code&gt;, because the &lt;code&gt;shift&lt;/code&gt; and &lt;code&gt;control&lt;/code&gt; arguments are deprecated and "will be removed in v7.0".&lt;/p&gt;

&lt;p&gt;Under &lt;code&gt;xvfb-run&lt;/code&gt;, GdUnit4 6.2.3 passed all three tests. GdUnit4's master branch, the unreleased 7.0.0 (commit 2cc420a), also passed all three with &lt;code&gt;--headless&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outside a test framework
&lt;/h2&gt;

&lt;p&gt;Godot itself has two ways to fake input from code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="c1"&gt;# A key event: _input() sees it, and the ui_right action mapped to that key becomes pressed.&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;InputEventKey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keycode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KEY_RIGHT&lt;/span&gt;
&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pressed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;
&lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse_input_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# An action only: Input.is_action_pressed("ui_right") is true, but _input() is never called.&lt;/span&gt;
&lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_press&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ui_right"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ui_right"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our headless Godot 4.7.2 run, a node's &lt;code&gt;_input&lt;/code&gt; printed the parsed key's press and release and nothing for &lt;code&gt;action_press&lt;/code&gt;, and &lt;code&gt;Input.is_action_pressed("ui_right")&lt;/code&gt; was true after both. Godot's &lt;a href="https://docs.godotengine.org/en/stable/classes/class_input.html" rel="noopener noreferrer"&gt;Input docs&lt;/a&gt; say &lt;code&gt;parse_input_event&lt;/code&gt; "Also generates Node._input() calls" and that &lt;code&gt;action_press&lt;/code&gt; "will not cause any Node._input() calls". To release a parsed key, send another &lt;code&gt;InputEventKey&lt;/code&gt; with &lt;code&gt;pressed = false&lt;/code&gt;. Neither call touches the operating system: Godot's docs say sending Alt/Cmd + Tab as an &lt;code&gt;InputEventKey&lt;/code&gt; "won't toggle between active windows".&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Headless runs don't click Controls.&lt;/strong&gt; With &lt;code&gt;godot --headless&lt;/code&gt;, Godot 4.7.2 delivered our simulated clicks to &lt;code&gt;_input&lt;/code&gt; but never pressed a &lt;code&gt;Button&lt;/code&gt;: the click test failed in GUT 9.7.1, in GdUnit4 6.2.3 and in a plain project using &lt;code&gt;Input.parse_input_event&lt;/code&gt;, and passed in all three under &lt;code&gt;xvfb-run&lt;/code&gt; (GUT with &lt;code&gt;-gcompact_mode&lt;/code&gt;). GdUnit4 6.2.3's own warning says "Godot 'InputEvents' are not transported by the Godot engine in headless mode", although our key and action tests passed headless. GdUnit4's unreleased master runs the scene in its own &lt;code&gt;SubViewport&lt;/code&gt;, and there the click test passed headless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GUT's results panel covers the game.&lt;/strong&gt; Under &lt;code&gt;xvfb-run&lt;/code&gt;, GUT 9.7.1's click test still failed, because GUT's output panel, on a &lt;code&gt;CanvasLayer&lt;/code&gt; at layer 128, was the control under the click. With &lt;code&gt;-gcompact_mode&lt;/code&gt; the click reached the button and the test passed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GdUnit4 6.2.3 refuses &lt;code&gt;--headless&lt;/code&gt;.&lt;/strong&gt; GdUnit4 6.2.3's &lt;code&gt;runtest.sh --headless&lt;/code&gt; stops with "Headless mode is not supported!" and exit code 103 before running any test, unless you add &lt;code&gt;--ignoreHeadlessMode&lt;/code&gt;. GdUnit4's master says headless "is supported by default".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;simulate_frames&lt;/code&gt; counts frames, not seconds.&lt;/strong&gt; GdUnit4's &lt;code&gt;simulate_frames(72)&lt;/code&gt; waits for 72 process frames, so the game time that passes depends on the frame rate. Holding &lt;code&gt;ui_right&lt;/code&gt; for 72 frames passed headless, where the test took about half a second, and failed 3 times out of 3 under &lt;code&gt;xvfb-run&lt;/code&gt;, where it took 110 to 178 ms and the player never reached the star. Wait for time instead: &lt;code&gt;await await_millis(1200)&lt;/code&gt; in GdUnit4, &lt;code&gt;hold_for(1.2)&lt;/code&gt; in GUT.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;simulate_action_pressed&lt;/code&gt; is too short for polling.&lt;/strong&gt; GdUnit4 6.2.3's &lt;code&gt;simulate_action_pressed&lt;/code&gt; presses and releases the action in the same call: right after it, &lt;code&gt;Input.is_action_pressed("ui_right")&lt;/code&gt; was false, and the Star Catcher player, which reads &lt;code&gt;Input.get_vector&lt;/code&gt; in &lt;code&gt;_physics_process&lt;/code&gt;, moved 0 px in the next 500 ms. Use &lt;code&gt;simulate_action_press&lt;/code&gt;, wait, then &lt;code&gt;simulate_action_release&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GdUnit4's Shift and Ctrl arguments are going away.&lt;/strong&gt; GdUnit4 6.2.3 prints "Deprecated! Don't use 'shift_pressed' it will be removed in v7.0" for &lt;code&gt;simulate_key_pressed(KEY_A, true)&lt;/code&gt;, and on GdUnit4's master the same call is a parse error, "Too many arguments for "simulate_key_pressed()" call". Hold &lt;code&gt;KEY_SHIFT&lt;/code&gt; with &lt;code&gt;simulate_key_press&lt;/code&gt; instead.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Running headless in CI
&lt;/h2&gt;

&lt;p&gt;Import the project once, then run the suite. For tests that only use keys and actions, headless is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;godot &lt;span class="nt"&gt;--headless&lt;/span&gt; &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--import&lt;/span&gt;
godot &lt;span class="nt"&gt;--headless&lt;/span&gt; &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; addons/gut/gut_cmdln.gd &lt;span class="nt"&gt;-gdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;res://test &lt;span class="nt"&gt;-gexit&lt;/span&gt;
./addons/gdUnit4/runtest.sh &lt;span class="nt"&gt;--godot_binary&lt;/span&gt; /usr/bin/godot &lt;span class="nt"&gt;--headless&lt;/span&gt; &lt;span class="nt"&gt;--ignoreHeadlessMode&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; res://test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GUT 9.7.1 exited 0 when every test passed and 1 when one failed. GdUnit4 6.2.3's &lt;code&gt;runtest.sh&lt;/code&gt; exited 0 on success, 100 when a test failed, and 103 when we left out &lt;code&gt;--ignoreHeadlessMode&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For tests that click Controls, drop &lt;code&gt;--headless&lt;/code&gt; and run under &lt;code&gt;xvfb-run&lt;/code&gt;, which starts a virtual X display, so the CI machine needs no screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xvfb-run &lt;span class="nt"&gt;-a&lt;/span&gt; godot &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; addons/gut/gut_cmdln.gd &lt;span class="nt"&gt;-gdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;res://test &lt;span class="nt"&gt;-gcompact_mode&lt;/span&gt; &lt;span class="nt"&gt;-gexit&lt;/span&gt;
xvfb-run &lt;span class="nt"&gt;-a&lt;/span&gt; ./addons/gdUnit4/runtest.sh &lt;span class="nt"&gt;--godot_binary&lt;/span&gt; /usr/bin/godot &lt;span class="nt"&gt;-a&lt;/span&gt; res://test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both exited 0 with all three tests passing.&lt;/p&gt;

&lt;p&gt;If you would rather not script inputs in a test framework, &lt;a href="https://ziva.sh/docs/creating#playtest-your-game" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt;, an AI agent inside the Godot editor, has a &lt;code&gt;run_scene&lt;/code&gt; tool that runs a scene in the open editor with timed key, action and mouse inputs and returns the runtime errors, the console output and a screenshot; Ziva's &lt;code&gt;run_scene&lt;/code&gt; doesn't replace GUT or GdUnit4 tests and doesn't run headless in CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://ziva.sh/blogs/godot-automated-playtesting" rel="noopener noreferrer"&gt;How to automate playtesting in Godot&lt;/a&gt;: bots, MCP servers and AI agents&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gut.readthedocs.io/en/latest/Mocking-Input.html" rel="noopener noreferrer"&gt;GUT: Mocking Input&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GdUnit4 scene runner: &lt;a href="https://godot-gdunit-labs.github.io/gdUnit4/latest/advanced_testing/scene_runner/actions/" rel="noopener noreferrer"&gt;actions&lt;/a&gt;, &lt;a href="https://godot-gdunit-labs.github.io/gdUnit4/latest/advanced_testing/scene_runner/keys/" rel="noopener noreferrer"&gt;keys&lt;/a&gt;, &lt;a href="https://godot-gdunit-labs.github.io/gdUnit4/latest/advanced_testing/scene_runner/mouse/" rel="noopener noreferrer"&gt;mouse&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I simulate keyboard and mouse input in Godot tests (GUT or GdUnit4)?
&lt;/h3&gt;

&lt;p&gt;In GUT, create GutInputSender.new(Input), queue input such as key_down(KEY_RIGHT).hold_for(1.2).wait(0.3), action_down("ui_right") or mouse_left_click_at(Vector2(350, 220)), await the sender's idle signal, then assert; call release_all() and clear() in after_each. In GdUnit4, load the scene with scene_runner("res://main.tscn") and call simulate_key_press and simulate_key_release, simulate_action_press and simulate_action_release, or set_mouse_position and simulate_mouse_button_pressed, wait with await await_millis(1200) rather than a frame count, then await runner.await_input_processed() before asserting. In our Godot 4.7.2 tests with GUT 9.7.1 and GdUnit4 6.2.3, key and action input worked headless, but clicks on a Button only worked with a display, such as xvfb-run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why doesn't my simulated mouse click press a Button when I run Godot tests headless?
&lt;/h3&gt;

&lt;p&gt;With godot --headless, Godot 4.7.2 delivered our simulated mouse clicks to _input but never pressed a Button, in GUT 9.7.1, in GdUnit4 6.2.3 and in a plain project using Input.parse_input_event. The same tests passed under xvfb-run, a virtual X display. GdUnit4 6.2.3's own warning says "Godot 'InputEvents' are not transported by the Godot engine in headless mode". In GUT, also pass -gcompact_mode on a display, because GUT's results panel otherwise covers the game and takes the click.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I run GUT or GdUnit4 tests headless in CI, and what exit codes do they return?
&lt;/h3&gt;

&lt;p&gt;Import once with godot --headless --path . --import. GUT 9.7.1: godot --headless --path . -s addons/gut/gut_cmdln.gd -gdir=res://test -gexit, which exited 0 when every test passed and 1 when one failed. GdUnit4 6.2.3: ./addons/gdUnit4/runtest.sh --godot_binary /usr/bin/godot --headless --ignoreHeadlessMode -a res://test, which exits 0 on success and 100 on a failed test; without --ignoreHeadlessMode, GdUnit4 6.2.3 refuses to run headless and exits 103. For tests that click Controls, run either tool without --headless under xvfb-run -a.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between Input.parse_input_event and Input.action_press in Godot?
&lt;/h3&gt;

&lt;p&gt;Input.parse_input_event feeds an InputEvent, such as an InputEventKey, through Godot's input system, so Node._input() receives it and any action mapped to that key becomes pressed. Input.action_press only marks the action as pressed: Input.is_action_pressed returns true, but Godot's docs say it "will not cause any Node._input() calls". In our Godot 4.7.2 run, a node's _input printed the parsed key press and release and nothing for action_press.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>testing</category>
      <category>gamedev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Godot MCP server for Claude Code and Cursor: run the game, send input, take screenshots</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Sat, 07 Nov 2026 06:42:41 +0000</pubDate>
      <link>https://dev.to/ziva/godot-mcp-server-for-claude-code-and-cursor-run-the-game-send-input-take-screenshots-5cm</link>
      <guid>https://dev.to/ziva/godot-mcp-server-for-claude-code-and-cursor-run-the-game-send-input-take-screenshots-5cm</guid>
      <description>&lt;p&gt;Several Godot MCP servers let Claude Code or Cursor run your game, send it input and screenshot the running game: Ziva, Godot MCP Pro, GDAI MCP, Godot AI (hi-godot/godot-ai) and Erodenn/godot-mcp-runtime, plus Summer Engine's MCP for Summer's own editor. In &lt;a href="https://ziva.sh/docs/ziva-mcp-server" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt;, one tool does all three: &lt;code&gt;run_scene&lt;/code&gt; runs a scene in the open Godot editor, sends timed keyboard, mouse, input-action and UI-click input, and returns a screenshot of the running game with that run's console output and runtime errors. Ziva's &lt;code&gt;get_godot_errors&lt;/code&gt; reads Godot's errors and output, and Ziva's &lt;code&gt;get_screenshot&lt;/code&gt; captures the editor, not the game. To connect Claude Code, click &lt;strong&gt;Install for Claude Code&lt;/strong&gt; in Ziva's settings, which runs &lt;code&gt;claude mcp add --scope user ziva-godot -- ...&lt;/code&gt; for you. The Ziva plugin supports Godot 4.2 or later and its MCP tools need no paid plan, but Ziva's &lt;code&gt;run_scene&lt;/code&gt; and &lt;code&gt;get_screenshot&lt;/code&gt; need the Ziva dock signed in (the free Hobby plan works).&lt;/p&gt;

&lt;p&gt;Ziva.sh makes Ziva and publishes this page. Facts about other servers are quoted from each vendor's own page or README as of 7 November 2026, except Godot AI's C# statement, from its README on 4 November 2026. Disclosure: GDAI MCP is made by Delano Lourenco, a Ziva cofounder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Godot MCP servers compared: run, input, screenshot
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;Cost/licence&lt;/th&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Run game&lt;/th&gt;
&lt;th&gt;Input to running game&lt;/th&gt;
&lt;th&gt;Game screenshot&lt;/th&gt;
&lt;th&gt;Errors/output&lt;/th&gt;
&lt;th&gt;Godot version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://ziva.sh/docs/ziva-mcp-server" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt; (local MCP server inside the Ziva plugin)&lt;/td&gt;
&lt;td&gt;Ziva: free Hobby plan works; sign-in needed for screenshots&lt;/td&gt;
&lt;td&gt;Ziva: install the plugin, click &lt;strong&gt;Install for Claude Code&lt;/strong&gt; or &lt;strong&gt;Install for Cursor&lt;/strong&gt;; no Node.js or uv&lt;/td&gt;
&lt;td&gt;Ziva: &lt;code&gt;run_scene&lt;/code&gt;, in the open editor&lt;/td&gt;
&lt;td&gt;Ziva: &lt;code&gt;run_scene&lt;/code&gt; actions, keys, left and right clicks, clicks on a Control by path or text, mouse motion&lt;/td&gt;
&lt;td&gt;Ziva: &lt;code&gt;run_scene&lt;/code&gt; returns the final frame; &lt;code&gt;get_screenshot&lt;/code&gt; is the editor only&lt;/td&gt;
&lt;td&gt;Ziva: &lt;code&gt;run_scene&lt;/code&gt; output and runtime errors; &lt;code&gt;get_godot_errors&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Ziva: plugin 4.2+; we ran &lt;code&gt;run_scene&lt;/code&gt; on 4.7.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/Coding-Solo/godot-mcp" rel="noopener noreferrer"&gt;Coding-Solo/godot-mcp&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Coding-Solo/godot-mcp: free, MIT&lt;/td&gt;
&lt;td&gt;Coding-Solo/godot-mcp: &lt;code&gt;claude mcp add godot -- npx @coding-solo/godot-mcp&lt;/code&gt;; Node.js 18+&lt;/td&gt;
&lt;td&gt;Coding-Solo/godot-mcp: "Execute Godot projects in debug mode"&lt;/td&gt;
&lt;td&gt;Coding-Solo/godot-mcp: not stated&lt;/td&gt;
&lt;td&gt;Coding-Solo/godot-mcp: not stated&lt;/td&gt;
&lt;td&gt;Coding-Solo/godot-mcp: "Retrieve console output and error messages"&lt;/td&gt;
&lt;td&gt;Coding-Solo/godot-mcp: not stated; UID tools need 4.4+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://gdaimcp.com/" rel="noopener noreferrer"&gt;GDAI MCP&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;GDAI MCP: "$19 one-time payment", &lt;a href="https://gdaimcp.com/license" rel="noopener noreferrer"&gt;binary only&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;GDAI MCP: addon in &lt;code&gt;res://addons/&lt;/code&gt;, plus &lt;code&gt;uv&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;GDAI MCP: &lt;code&gt;play_scene&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;GDAI MCP: &lt;code&gt;simulate_input&lt;/code&gt;, "keyboard keys and input actions"; mouse not stated&lt;/td&gt;
&lt;td&gt;GDAI MCP: &lt;code&gt;get_running_scene_screenshot&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;GDAI MCP: &lt;code&gt;get_godot_errors&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;GDAI MCP: "Godot 4.2+" on its site; its GitHub README says 4.1+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://godot-mcp.abyo.net/" rel="noopener noreferrer"&gt;Godot MCP Pro&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Godot MCP Pro: "$15 one-time purchase"; server proprietary&lt;/td&gt;
&lt;td&gt;Godot MCP Pro: addon plus the Node.js server from the paid package; Node.js 18+&lt;/td&gt;
&lt;td&gt;Godot MCP Pro: &lt;code&gt;play_scene&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Godot MCP Pro: &lt;code&gt;simulate_key&lt;/code&gt;, &lt;code&gt;simulate_mouse_click&lt;/code&gt;, &lt;code&gt;simulate_mouse_move&lt;/code&gt;, &lt;code&gt;simulate_action&lt;/code&gt;, &lt;code&gt;simulate_sequence&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Godot MCP Pro: &lt;code&gt;get_game_screenshot&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Godot MCP Pro: &lt;code&gt;get_editor_errors&lt;/code&gt;, &lt;code&gt;get_output_log&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Godot MCP Pro: "Godot 4.4 or later"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/hi-godot/godot-ai" rel="noopener noreferrer"&gt;Godot AI (hi-godot/godot-ai)&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Godot AI: free, MIT&lt;/td&gt;
&lt;td&gt;Godot AI: addon from AssetLib, &lt;code&gt;uv&lt;/code&gt;, then &lt;strong&gt;Configure&lt;/strong&gt; in the Godot AI dock&lt;/td&gt;
&lt;td&gt;Godot AI: &lt;code&gt;project_run&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Godot AI: &lt;code&gt;game_manage&lt;/code&gt; with &lt;code&gt;input_key&lt;/code&gt;, &lt;code&gt;input_mouse&lt;/code&gt;, &lt;code&gt;input_gamepad&lt;/code&gt;, &lt;code&gt;input_action&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Godot AI: &lt;code&gt;editor_screenshot&lt;/code&gt; of the "running game framebuffer"&lt;/td&gt;
&lt;td&gt;Godot AI: &lt;code&gt;logs_read&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Godot AI: "Godot 4.7+"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/Erodenn/godot-mcp-runtime" rel="noopener noreferrer"&gt;Erodenn/godot-mcp-runtime&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;godot-mcp-runtime: free, MIT&lt;/td&gt;
&lt;td&gt;godot-mcp-runtime: &lt;code&gt;npx -y godot-mcp-runtime&lt;/code&gt; with &lt;code&gt;GODOT_PATH&lt;/code&gt;; Node.js 20+; "No Godot addon"&lt;/td&gt;
&lt;td&gt;godot-mcp-runtime: &lt;code&gt;run_project&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;godot-mcp-runtime: &lt;code&gt;simulate_input&lt;/code&gt;: keys, mouse clicks and motion, clicks on UI elements, actions, text&lt;/td&gt;
&lt;td&gt;godot-mcp-runtime: &lt;code&gt;take_screenshot&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;godot-mcp-runtime: &lt;code&gt;get_debug_output&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;godot-mcp-runtime: "Godot 4.x"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.summerengine.com/godot-mcp" rel="noopener noreferrer"&gt;Summer Engine MCP&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Summer Engine: "free, MIT-licensed"; "Cloud generation ... uses credits"&lt;/td&gt;
&lt;td&gt;Summer Engine: &lt;code&gt;npx -y summer-engine@latest setup claude-code --yes&lt;/code&gt;, plus the Summer Engine desktop app&lt;/td&gt;
&lt;td&gt;Summer Engine: &lt;code&gt;summer_play&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Summer Engine: &lt;code&gt;summer_game_input&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Summer Engine: &lt;code&gt;summer_screenshot&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Summer Engine: &lt;code&gt;summer_get_debugger_errors&lt;/code&gt;, &lt;code&gt;summer_get_console&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Summer Engine: its own desktop app, "compatible with Godot 4"; not stock Godot&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notes on the table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Erodenn/godot-mcp-runtime's README calls it "a lightweight &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; server" whose "runtime bridge adds screenshots, input simulation, UI discovery, and live GDScript against the running scene tree". Erodenn/godot-mcp-runtime injects that bridge as an autoload when &lt;code&gt;run_project&lt;/code&gt; is called and says it leaves "No leftover autoloads, no modified project files".&lt;/li&gt;
&lt;li&gt;Summer Engine's FAQ answers "Does Summer MCP work with the stock Godot editor?" with "No. Its engine tools drive the Summer Engine desktop app, which is compatible with Godot 4 and opens compatible Godot 4 projects."&lt;/li&gt;
&lt;li&gt;Godot MCP Pro's site lists "187 Tools, 26 Categories". Godot AI's README lists "46 tools and 120+ operations".&lt;/li&gt;
&lt;li&gt;Coding-Solo/godot-mcp had 6,547 GitHub stars on 7 November 2026. Coding-Solo/godot-mcp's README lists running the project, capturing debug output and editing scenes, and lists no input or screenshot tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Set up Ziva's MCP server in Claude Code or Cursor
&lt;/h2&gt;

&lt;p&gt;Ziva's MCP server ships inside the Ziva plugin and runs only on your computer, with no hosted URL. &lt;a href="https://ziva.sh/docs/installation" rel="noopener noreferrer"&gt;Install Ziva&lt;/a&gt; in your Godot project, open the project in Godot, wait for Ziva to start and sign in to Ziva in its dock. In Ziva, open &lt;strong&gt;Settings → Models &amp;amp; Providers → Ziva as an MCP server&lt;/strong&gt; and check that it shows &lt;strong&gt;Running&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude Code
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Make sure the &lt;code&gt;claude&lt;/code&gt; command works in your terminal.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;AI application&lt;/strong&gt;, choose &lt;strong&gt;Claude Code&lt;/strong&gt; and click &lt;strong&gt;Install for Claude Code&lt;/strong&gt;. Ziva runs &lt;code&gt;claude mcp add --scope user ziva-godot -- ...&lt;/code&gt; with its generated command, so &lt;code&gt;ziva-godot&lt;/code&gt; is available in every project.&lt;/li&gt;
&lt;li&gt;Start a new Claude Code session from your Godot project folder and ask: "Use ziva-godot's run_scene tool to run res://main.tscn for 2500 ms, holding ui_right from 300 ms for 1200 ms, and tell me which errors it printed."&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Cursor
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Under &lt;strong&gt;AI application&lt;/strong&gt;, choose &lt;strong&gt;Cursor&lt;/strong&gt; and click &lt;strong&gt;Install for Cursor&lt;/strong&gt;. Ziva adds &lt;code&gt;ziva-godot&lt;/code&gt; under &lt;code&gt;mcpServers&lt;/code&gt; in your project's &lt;code&gt;.cursor/mcp.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Open that same project folder in Cursor and restart Cursor.&lt;/li&gt;
&lt;li&gt;Ask Cursor to use ziva-godot's &lt;code&gt;run_scene&lt;/code&gt; on a scene, as in the Claude Code example.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If Godot is closed, a command-line client such as Claude Code started from your project folder sees one Ziva tool, &lt;code&gt;start_godot&lt;/code&gt;, which opens the editor; after that, the full Ziva tool list appears. Codex, VS Code, Windsurf, Zed, Gemini CLI and other apps have their own &lt;strong&gt;Install for [app]&lt;/strong&gt; button, and the &lt;a href="https://ziva.sh/docs/ziva-mcp-server" rel="noopener noreferrer"&gt;Ziva MCP server guide&lt;/a&gt; has the manual setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Ziva's MCP tools return
&lt;/h2&gt;

&lt;p&gt;Ziva's &lt;code&gt;run_scene&lt;/code&gt; takes a scene path, a run length in &lt;code&gt;duration_ms&lt;/code&gt; and an optional list of timed inputs. Each input starts at &lt;code&gt;at_ms&lt;/code&gt; and is held for &lt;code&gt;hold_ms&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scene_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"res://main.tscn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ui_right"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"at_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hold_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Space"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"at_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hold_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ziva's input types are &lt;code&gt;action&lt;/code&gt; (a named input action), &lt;code&gt;key&lt;/code&gt; (a raw keyboard key, with Shift, Ctrl, Alt or Meta held as modifiers), &lt;code&gt;mouse&lt;/code&gt; (a left or right click at a pixel position), &lt;code&gt;target&lt;/code&gt; (a click on a Control found by node path or visible text) and &lt;code&gt;motion&lt;/code&gt; (relative mouse movement, for a camera with a captured mouse). Ziva's &lt;code&gt;run_scene&lt;/code&gt; returns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One screenshot: the final frame of the running game, as an image Claude Code or Cursor can look at.&lt;/li&gt;
&lt;li&gt;An input delivery report with the frame and millisecond each press and release landed on, so the client can tell an input the game never got from one it ignored.&lt;/li&gt;
&lt;li&gt;The Godot console output and runtime errors from that run, with file and line.&lt;/li&gt;
&lt;li&gt;With &lt;code&gt;speed: true&lt;/code&gt;, the frame rate, and when the run is slow, the slowest functions with file and line.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ziva's other tools for this loop are &lt;code&gt;get_godot_errors&lt;/code&gt; for script errors, runtime errors, stack traces and &lt;code&gt;print()&lt;/code&gt; output, &lt;code&gt;get_screenshot&lt;/code&gt; for the editor window or its 2D or 3D viewport, &lt;code&gt;get_scene_tree&lt;/code&gt; to inspect the scene, and &lt;code&gt;run_tests&lt;/code&gt; for a test suite. MCP calls skip Ziva's approval prompts, so use your client's permission settings. In our &lt;a href="https://ziva.sh/blogs/godot-automated-playtesting#what-zivas-run_scene-returned-on-star-catcher" rel="noopener noreferrer"&gt;Godot playtesting guide&lt;/a&gt;, Ziva's &lt;code&gt;run_scene&lt;/code&gt; held &lt;code&gt;ui_right&lt;/code&gt; in a sample game and returned the null-instance error from &lt;code&gt;res://main.gd:15&lt;/code&gt; with a screenshot showing the score still at 0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tested: Claude Code running a Godot game through Ziva's MCP server
&lt;/h2&gt;

&lt;p&gt;On 7 November 2026 we connected Claude Code 2.1.318 to Ziva's MCP server with the same stdio command Ziva's &lt;strong&gt;Install for Claude Code&lt;/strong&gt; button registers, opened the &lt;a href="https://github.com/ziva-sh/ziva-godot-sample" rel="noopener noreferrer"&gt;Star Catcher sample&lt;/a&gt; in Godot 4.7.2 with a development build of Ziva from 7 November 2026 and the Ziva dock signed in to a Hobby account, and ran one prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use the ziva-godot MCP tools. Run the main scene of this Godot project for about 4 seconds while holding the right arrow key (ui_right) for 2 seconds, then tell me what the screenshot shows and list any runtime errors exactly as reported. Do not edit any files.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude Code saw 42 &lt;code&gt;ziva-godot&lt;/code&gt; tools, read the main scene path with Ziva's &lt;code&gt;get_project_info&lt;/code&gt;, then called Ziva's &lt;code&gt;run_scene&lt;/code&gt; once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scene_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"res://main.tscn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ui_right"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"at_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hold_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The call took 8.1 seconds. Ziva's input delivery report said the press landed at 0 ms and the release at 2009 ms, and Ziva's &lt;code&gt;run_scene&lt;/code&gt; returned the sample's two real errors word for word:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: main.gd:5 @ @implicit_ready(): Node not found: "%ScoreLabel" (relative to "/root/Main").
ERROR: _on_star_collected: Invalid assignment of property or key 'text' with value of type 'String' on a base object of type 'null instance'.
  Source: res://main.gd:15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpkj9j7inffrq4ckop167.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpkj9j7inffrq4ckop167.png" alt="Final frame returned by Ziva's run_scene to Claude Code after holding ui_right for 2 seconds in the Star Catcher sample: one star has been collected, two yellow stars remain, and the label still reads Score: 0 because the score label reference is null" width="640" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From the screenshot and the errors, Claude Code concluded that a star was collected but the score label never updated, which is the bug. With Godot closed, the same Claude Code setup saw only Ziva's &lt;code&gt;start_godot&lt;/code&gt; tool, called it, and had the editor open with all Ziva tools available 5.2 seconds later; the first &lt;code&gt;run_scene&lt;/code&gt; after that hit a one-off upload error and worked on retry. With the Ziva dock signed out, Ziva's &lt;code&gt;run_scene&lt;/code&gt; and &lt;code&gt;get_screenshot&lt;/code&gt; returned "Not signed in", while Ziva's &lt;code&gt;get_godot_errors&lt;/code&gt; and &lt;code&gt;get_project_info&lt;/code&gt; still worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Ziva's MCP server can't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ziva's &lt;code&gt;run_scene&lt;/code&gt; returns one frame per call, not a video or a stream. To see a later moment, call it again with a longer &lt;code&gt;duration_ms&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Ziva's &lt;code&gt;run_scene&lt;/code&gt; needs the Godot editor open, so Ziva has no headless or CI mode.&lt;/li&gt;
&lt;li&gt;Ziva's &lt;code&gt;run_scene&lt;/code&gt; has no gamepad or touch input.&lt;/li&gt;
&lt;li&gt;Ziva's &lt;code&gt;run_scene&lt;/code&gt; refuses to start while you are playing a game in the editor, because it would close your game.&lt;/li&gt;
&lt;li&gt;Ziva's Full Game Playtest Agent, which plays toward a goal and returns pass, fail or blocked, is not available over MCP.&lt;/li&gt;
&lt;li&gt;Ziva's &lt;code&gt;run_scene&lt;/code&gt; and &lt;code&gt;get_screenshot&lt;/code&gt; need the Ziva dock signed in, because Ziva uploads each screenshot to Ziva's servers before returning it. Signed out, they return "Not signed in".&lt;/li&gt;
&lt;li&gt;Ziva's &lt;code&gt;run_scene&lt;/code&gt; only appears when &lt;strong&gt;Enable playtest tool&lt;/strong&gt; is on in Ziva's &lt;strong&gt;Settings → Agent&lt;/strong&gt;. Ziva's &lt;code&gt;generate_image&lt;/code&gt; tool draws on your plan's included usage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to choose a Godot MCP server
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pick Ziva&lt;/strong&gt; if you also want an AI agent inside the Godot editor, or you don't want to install Node.js or uv. Ziva's &lt;code&gt;run_scene&lt;/code&gt; gives Claude Code or Cursor input, a screenshot and errors in one call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick Godot AI (hi-godot/godot-ai)&lt;/strong&gt; if you are on Godot 4.7 or later and want a free, MIT-licensed server with gamepad input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick Erodenn/godot-mcp-runtime&lt;/strong&gt; if you want a free, MIT server with no addon to install or commit, or the game launched without the editor open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick Coding-Solo/godot-mcp&lt;/strong&gt; if you only need to run the project and read errors, with no editor addon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick Godot MCP Pro or GDAI MCP&lt;/strong&gt; if you prefer a one-time purchase: Godot MCP Pro lists mouse input and needs Godot 4.4 or later, and GDAI MCP lists keys and input actions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For CI&lt;/strong&gt;, use scripted GUT or GdUnit4 input tests rather than an MCP client; our &lt;a href="https://ziva.sh/blogs/godot-automated-playtesting" rel="noopener noreferrer"&gt;playtesting guide&lt;/a&gt; has tested examples.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://ziva.sh/docs/ziva-mcp-server" rel="noopener noreferrer"&gt;Ziva as an MCP server&lt;/a&gt;: full setup for every supported app&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ziva.sh/blogs/godot-automated-playtesting" rel="noopener noreferrer"&gt;How to automate playtesting in Godot&lt;/a&gt;: GUT, GdUnit4, headless bots and AI agents&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ziva.sh/blogs/ziva-vs-godot-mcp" rel="noopener noreferrer"&gt;Ziva vs Godot MCP&lt;/a&gt;: Ziva compared with Coding-Solo and other open-source servers&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ziva.sh/blogs/ziva-vs-gdai-mcp" rel="noopener noreferrer"&gt;Ziva vs GDAI MCP&lt;/a&gt;: Ziva compared with GDAI MCP&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Which Godot MCP server lets Claude Code run the game, send input and take screenshots?
&lt;/h3&gt;

&lt;p&gt;As of 7 November 2026, these Godot MCP servers list all three: Ziva's local MCP server, whose run_scene tool runs a scene in the open Godot editor, sends timed keyboard, mouse, input-action and UI-click input, and returns a screenshot of the running game with its console output and runtime errors; Godot MCP Pro ($15 one-time) with play_scene, simulate_key, simulate_mouse_click and get_game_screenshot; GDAI MCP ($19 one-time) with play_scene, simulate_input for keys and input actions, and get_running_scene_screenshot; Godot AI (hi-godot/godot-ai, free, MIT, Godot 4.7+) with project_run, game_manage input operations and editor_screenshot of the running game; and Erodenn/godot-mcp-runtime (free, MIT, no addon) with run_project, simulate_input and take_screenshot. Summer Engine's MCP does the same in Summer's own desktop editor, not the stock Godot editor. Coding-Solo/godot-mcp runs the project and captures debug output, and its README lists no input or screenshot tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I connect Claude Code or Cursor to Ziva's Godot MCP server?
&lt;/h3&gt;

&lt;p&gt;Open your Godot project with the Ziva plugin installed, then in Ziva open Settings → Models &amp;amp; Providers → Ziva as an MCP server and check that it shows Running. Under AI application, choose Claude Code and click Install for Claude Code, which runs claude mcp add --scope user ziva-godot with Ziva's generated command; or choose Cursor and click Install for Cursor, which adds ziva-godot to the project's .cursor/mcp.json. Ziva's MCP server is bundled in the Ziva plugin, so there is no Node.js or uv to install, and it runs on your computer, not at a hosted URL. Ziva's run_scene and get_screenshot need the Ziva dock signed in; the free Hobby plan works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Ziva's get_screenshot tool show the running game?
&lt;/h3&gt;

&lt;p&gt;No. Ziva's get_screenshot captures the Godot editor: the whole window, the 2D or 3D viewport, or the Ziva panel. A screenshot of the running game comes from Ziva's run_scene, which returns the final frame of each run, so to see a later moment call run_scene again with a longer duration_ms.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there an MCP server that works with Godot C# (.NET) projects?
&lt;/h3&gt;

&lt;p&gt;Yes, with limits. Ziva's local MCP server works in Godot C# (.NET) projects: Claude Code or Cursor edits the .cs files, and Ziva's run_scene and get_godot_errors return Godot's C# build errors with file, line and error code, such as CS0266. Ziva's MCP server has no separate C# build or C# test tool, so a C# change is checked by running the scene or by the client running dotnet build itself, and you need the .NET version of Godot and the .NET SDK. Godot AI (hi-godot/godot-ai) says its C# support is text-only and that it does not build .NET or report compiler errors. Erodenn/godot-mcp-runtime's attach_script attaches a GDScript or C# script to a node.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a Godot MCP server run my game headless in CI?
&lt;/h3&gt;

&lt;p&gt;Ziva's MCP server cannot: Ziva's run_scene needs the Godot editor open on your computer. Coding-Solo/godot-mcp and Erodenn/godot-mcp-runtime launch Godot themselves from the command line with no editor addon, and godot-mcp-runtime adds a background mode that moves the game window off-screen. For pass or fail checks on every commit, scripted GUT or GdUnit4 input tests run headless without any AI client.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>mcp</category>
      <category>claudecode</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>How to export a Godot game to the web and share a playable link</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Thu, 05 Nov 2026 13:59:32 +0000</pubDate>
      <link>https://dev.to/ziva/how-to-export-a-godot-game-to-the-web-and-share-a-playable-link-2g88</link>
      <guid>https://dev.to/ziva/how-to-export-a-godot-game-to-the-web-and-share-a-playable-link-2g88</guid>
      <description>&lt;p&gt;To put a Godot 4 game in a browser, export it with the Web preset (single-threaded, the default since Godot 4.3), serve the folder over HTTP, and upload it to a host that gives you a link: itch.io, a static host such as GitHub Pages, or, if you use &lt;a href="https://ziva.sh/godot-ai" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt;, its &lt;strong&gt;Share Web build&lt;/strong&gt; menu item, which exports, uploads and returns a play.ziva.sh link from inside the Godot editor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; Ziva.sh publishes this page and makes Ziva. Facts about other tools come from their own pages, as of 5 November 2026. We ran every Godot command on this page on 5 November 2026 with Godot 4.7.2 on Linux, on the public &lt;a href="https://github.com/ziva-sh/ziva-godot-sample" rel="noopener noreferrer"&gt;Star Catcher sample&lt;/a&gt; (and the C# test on a one-script C# project), and loaded each build in Chromium. We did not test Safari.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ways to get a playable link
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Host&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Limits&lt;/th&gt;
&lt;th&gt;Private link&lt;/th&gt;
&lt;th&gt;Threaded builds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://itch.io/docs/creators/html5" rel="noopener noreferrer"&gt;itch.io&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;itch.io: free&lt;/td&gt;
&lt;td&gt;itch.io: 500 MB extracted, 1,000 files, 200 MB per file&lt;/td&gt;
&lt;td&gt;itch.io: Draft secret link, or Restricted with a password&lt;/td&gt;
&lt;td&gt;itch.io: SharedArrayBuffer support checkbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/en/pages" rel="noopener noreferrer"&gt;GitHub Pages&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;GitHub Pages: free for public repositories&lt;/td&gt;
&lt;td&gt;GitHub Pages: 1 GB per site, 100 GB a month soft bandwidth&lt;/td&gt;
&lt;td&gt;GitHub Pages: not covered here&lt;/td&gt;
&lt;td&gt;GitHub Pages: no header settings in its docs; PWA workaround&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.netlify.com/routing/headers/" rel="noopener noreferrer"&gt;Netlify&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Netlify: free plan with monthly credits&lt;/td&gt;
&lt;td&gt;Netlify Drop: "individual files over 10MB may cause your deploy to get stuck"&lt;/td&gt;
&lt;td&gt;Netlify Drop: temporary password until you claim the site&lt;/td&gt;
&lt;td&gt;Netlify: &lt;code&gt;_headers&lt;/code&gt; file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://developers.cloudflare.com/pages/platform/limits/" rel="noopener noreferrer"&gt;Cloudflare Pages&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Cloudflare Pages: free plan&lt;/td&gt;
&lt;td&gt;Cloudflare Pages: 25 MiB per file, 20,000 files&lt;/td&gt;
&lt;td&gt;Cloudflare Pages: not covered here&lt;/td&gt;
&lt;td&gt;Cloudflare Pages: &lt;code&gt;_headers&lt;/code&gt; file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://lobsite.com/hosting/html5-games" rel="noopener noreferrer"&gt;Lobsite&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Lobsite: free, or $20 a year for Godot 4&lt;/td&gt;
&lt;td&gt;Lobsite free: 5 MB per file; Standard: 100 MB per file, 2 GB per site&lt;/td&gt;
&lt;td&gt;Lobsite: password on Standard&lt;/td&gt;
&lt;td&gt;Lobsite: &lt;code&gt;lobsite.toml&lt;/code&gt; headers via its CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://ziva.sh/docs/share-web-builds" rel="noopener noreferrer"&gt;Ziva Share Web build&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Ziva: every plan, including free Hobby&lt;/td&gt;
&lt;td&gt;Ziva: 300 MB after compression&lt;/td&gt;
&lt;td&gt;Ziva: unlisted preview only; published games are public&lt;/td&gt;
&lt;td&gt;Ziva: headers set for you; single-threaded on Godot 4.3+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Godot 4.7.2's single-threaded &lt;code&gt;index.wasm&lt;/code&gt; is 39,514,754 bytes before compression. That is above Cloudflare Pages' 25 MiB per-file limit and Lobsite's free 5 MB limit, and over the 10 MB at which Netlify says Drop deploys may stall, so check per-file limits before you pick a free host. Gotm.io, which older answers still list for Godot games, no longer works: its domain registration has expired.&lt;/p&gt;

&lt;h2&gt;
  
  
  Export a Godot 4 game for the web
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Use the &lt;strong&gt;Compatibility&lt;/strong&gt; renderer. Godot's docs say Godot 4 "can only target WebGL 2.0 (using the Compatibility rendering method)".&lt;/li&gt;
&lt;li&gt;Open &lt;strong&gt;Project &amp;gt; Export&lt;/strong&gt;, click &lt;strong&gt;Add...&lt;/strong&gt; and choose &lt;strong&gt;Web&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If Godot says export templates are missing, click &lt;strong&gt;Manage Export Templates&lt;/strong&gt; and download them. They go in &lt;code&gt;~/.local/share/godot/export_templates/4.7.2.stable/&lt;/code&gt; on Linux; the Godot 4.7.2 template bundle covers every platform and is about 1.2 GB.&lt;/li&gt;
&lt;li&gt;Leave &lt;strong&gt;Variant &amp;gt; Thread Support&lt;/strong&gt; off. Godot's docs call single-threaded "the preferred and now default way to export your games on the Web" since Godot 4.3.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Export Project&lt;/strong&gt; and save as &lt;code&gt;index.html&lt;/code&gt; in an empty folder.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From the command line, with a preset named &lt;code&gt;Web&lt;/code&gt; in &lt;code&gt;export_presets.cfg&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;godot &lt;span class="nt"&gt;--headless&lt;/span&gt; &lt;span class="nt"&gt;--path&lt;/span&gt; my-game &lt;span class="nt"&gt;--export-release&lt;/span&gt; &lt;span class="s2"&gt;"Web"&lt;/span&gt; ../build/index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On our machine that took about 3 seconds and produced 9 files, 39.9 MB in total:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Bytes&lt;/th&gt;
&lt;th&gt;gzip -9&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;index.wasm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;39,514,754&lt;/td&gt;
&lt;td&gt;10,084,297&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;index.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;279,815&lt;/td&gt;
&lt;td&gt;68,400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;index.pck&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7,316&lt;/td&gt;
&lt;td&gt;3,487&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;index.html&lt;/code&gt;, two audio worklets, three icons&lt;/td&gt;
&lt;td&gt;55,307&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Almost all of it is the engine. Hosts that gzip on the fly send about a quarter of those bytes; Godot's docs list GitHub Pages as one that does and itch.io as one that doesn't.&lt;/p&gt;

&lt;p&gt;A minimal &lt;code&gt;export_presets.cfg&lt;/code&gt; that exports without warnings in 4.7.2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[preset.0]&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Web"&lt;/span&gt;
&lt;span class="py"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Web"&lt;/span&gt;
&lt;span class="py"&gt;runnable&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;export_filter&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"all_resources"&lt;/span&gt;
&lt;span class="py"&gt;include_filter&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;
&lt;span class="py"&gt;exclude_filter&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;
&lt;span class="py"&gt;export_path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"../build/index.html"&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nn"&gt;[preset.0.options]&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="na"&gt;variant/&lt;/span&gt;&lt;span class="py"&gt;thread_support&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;include_filter&lt;/code&gt;, &lt;code&gt;exclude_filter&lt;/code&gt; and at least one option, the export still succeeds but prints &lt;code&gt;ERROR: Couldn't find the given section "preset.0" and key "include_filter"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the build in your browser
&lt;/h2&gt;

&lt;p&gt;Double-clicking &lt;code&gt;index.html&lt;/code&gt; does not work. Chromium showed &lt;code&gt;Failed to fetch&lt;/code&gt; over the Godot logo, and the console said &lt;code&gt;URL scheme "file" is not supported&lt;/code&gt; for &lt;code&gt;index.wasm&lt;/code&gt; and &lt;code&gt;index.pck&lt;/code&gt;. Serve the folder instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;build
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; http.server 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;a href="http://localhost:8000" rel="noopener noreferrer"&gt;http://localhost:8000&lt;/a&gt;. Our single-threaded build ran from this plain server with no extra headers: the console printed &lt;code&gt;single-threaded&lt;/code&gt;, and holding the right arrow moved the player into a star and the score went to 1. Godot's docs also describe a button between &lt;strong&gt;Stop&lt;/strong&gt; and &lt;strong&gt;Play Edited Scene&lt;/strong&gt;, shown when a runnable web preset and templates exist, that opens the game in your default browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix "Cross Origin Isolation" and "SharedArrayBuffer" missing
&lt;/h2&gt;

&lt;p&gt;With Thread Support on and no special headers, Godot 4.7.2 stopped with this error over the logo (Godot's docs say &lt;strong&gt;Extensions Support&lt;/strong&gt; needs the same headers):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The following features required to run Godot projects on the Web are missing:
Cross-Origin Isolation - Check that the web server configuration sends the correct headers.
SharedArrayBuffer - Check that the web server configuration sends the correct headers.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Threaded builds use &lt;code&gt;SharedArrayBuffer&lt;/code&gt;, which browsers only allow on cross-origin isolated pages. Three fixes, in the order we'd try them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Export single-threaded.&lt;/strong&gt; Turn Thread Support and Extensions Support off and export again. Unless a GDExtension needs them, this is the simplest fix; Godot's docs call single-threaded exports "more compatible overall with stores like itch.io" and web portals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send the two headers.&lt;/strong&gt; Godot's docs list &lt;code&gt;Cross-Origin-Opener-Policy: same-origin&lt;/code&gt; and &lt;code&gt;Cross-Origin-Embedder-Policy: require-corp&lt;/code&gt;. Locally, this tiny server is enough:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;   &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;http.server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

   &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;COIHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SimpleHTTPRequestHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
       &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;end_headers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
           &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cross-Origin-Opener-Policy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;same-origin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cross-Origin-Embedder-Policy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;require-corp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;end_headers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

   &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ThreadingHTTPServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])),&lt;/span&gt; &lt;span class="n"&gt;COIHandler&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;serve_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With it, the threaded build printed &lt;code&gt;multi-threaded&lt;/code&gt; and ran. Netlify and Cloudflare Pages read the same headers from a &lt;code&gt;_headers&lt;/code&gt; file. On itch.io, tick &lt;strong&gt;SharedArrayBuffer support&lt;/strong&gt; under &lt;strong&gt;Embed options &amp;gt; Frame options&lt;/strong&gt;; itch.io's announcement calls it experimental.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use the PWA workaround when you can't set headers.&lt;/strong&gt; In the export preset, enable &lt;strong&gt;Progressive Web App&lt;/strong&gt; and keep &lt;strong&gt;Ensure Cross Origin Isolation Headers&lt;/strong&gt; on (it is on by default). A service worker then adds the headers. It needs HTTPS or localhost. In our tests, 4 out of 4 first visits showed a black screen with no message, and the game ran after one manual reload, so tell playtesters to reload once.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Share the link with friends or playtesters
&lt;/h2&gt;

&lt;h3&gt;
  
  
  itch.io
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Export single-threaded.&lt;/li&gt;
&lt;li&gt;ZIP the &lt;strong&gt;contents&lt;/strong&gt; of the export folder, so &lt;code&gt;index.html&lt;/code&gt; is at the top of the ZIP. Our 9-file build made a 10.2 MB ZIP.&lt;/li&gt;
&lt;li&gt;On itch.io, create a new project, set its kind to &lt;strong&gt;HTML&lt;/strong&gt;, upload the ZIP and tick &lt;strong&gt;This file will be played in the browser&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Set the embed size to your game's viewport, and tick &lt;strong&gt;Mobile friendly&lt;/strong&gt; if it works on phones.&lt;/li&gt;
&lt;li&gt;Leave the project as a &lt;strong&gt;Draft&lt;/strong&gt; and share the secret link, or publish it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;itch.io's docs say HTML5 ZIPs must have at most 1,000 files, 500 MB extracted and 200 MB per file, and that a Draft is "only accessible by people who can edit it, and people with the secret link". For testers you approve, set the page to &lt;strong&gt;Restricted&lt;/strong&gt; and add a password; itch.io says you can "include the password directly in the URL". To upload from inside Godot, the free MIT-licensed plugins &lt;a href="https://github.com/Lighar/ShipItch" rel="noopener noreferrer"&gt;ShipItch&lt;/a&gt;, &lt;a href="https://godotengine.org/asset-library/asset/4492" rel="noopener noreferrer"&gt;Itch Exporter &amp;amp; Uploader&lt;/a&gt; and &lt;a href="https://github.com/impactcores/greenlight" rel="noopener noreferrer"&gt;Greenlight&lt;/a&gt; export and upload with itch.io's &lt;code&gt;butler&lt;/code&gt; tool, and &lt;a href="https://github.com/BenjaTK/Majordomo" rel="noopener noreferrer"&gt;Majordomo&lt;/a&gt; pushes builds to itch.io with &lt;code&gt;butler&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Pages, Netlify or Cloudflare Pages
&lt;/h3&gt;

&lt;p&gt;Commit the export folder to a repository and turn on Pages, or drop the folder on Netlify. GitHub's Pages docs for github.com describe no way to set custom response headers, so on GitHub Pages use the single-threaded export or the PWA workaround above. Watch per-file limits: Cloudflare Pages caps each file at 25 MiB, and Godot 4.7.2's &lt;code&gt;index.wasm&lt;/code&gt; is 39.5 MB.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ziva's Share Web build: a play.ziva.sh link from the editor
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://ziva.sh/godot-ai" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt; is an AI assistant that runs inside the Godot editor. Its &lt;strong&gt;Share Web build&lt;/strong&gt; item, in the &lt;strong&gt;⋮&lt;/strong&gt; menu of the Ziva chat header, does the export and the hosting for you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ziva runs Godot's web export with your own Godot editor, on a copy of your project without Ziva's own addon, and adds a Web export preset if your project has none.&lt;/li&gt;
&lt;li&gt;If the export templates are missing, Ziva downloads Godot's official templates first.&lt;/li&gt;
&lt;li&gt;On Godot 4.3 and later, Ziva exports single-threaded unless a GDExtension ships only threaded web libraries, and then warns that the game won't run on iPhone or Safari. On Godot 4.2, which has no single-threaded templates, builds are threaded.&lt;/li&gt;
&lt;li&gt;Ziva compresses and uploads the build, and you get an unlisted preview link to try it.&lt;/li&gt;
&lt;li&gt;You add a title, description and cover, choose &lt;strong&gt;Approve and publish&lt;/strong&gt;, and get a play.ziva.sh link to share.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We shared the Star Catcher sample this way on 5 November 2026, signed in on a fresh Ziva install with no export templates. Downloading Godot 4.7.2's templates (1,222 MB) took about 21 minutes on our connection; extracting them, exporting and uploading the 9 compressed files (13.5 MB) took under a minute more. The game is at &lt;a href="https://play.ziva.sh/timely-hallway/" rel="noopener noreferrer"&gt;https://play.ziva.sh/timely-hallway/&lt;/a&gt;, and it played in Chromium without signing in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgu2lcnt7wyqm867nr1lz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgu2lcnt7wyqm867nr1lz.webp" alt="The Star Catcher sample running on its play.ziva.sh page in Chromium, signed out: the teal square has collected a star and the label reads Score: 1" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Share Web build works on every Ziva plan, including the free Hobby plan, once you're signed in, for builds up to 300 MB after compression. Rebuilding keeps the same link, and links don't expire until you delete the game. You can also ask Ziva in chat to prepare the game for publishing; Ziva builds the preview and shows the listing, and publishes only after you approve, even with auto-approve on. The &lt;a href="https://ziva.sh/docs/share-web-builds" rel="noopener noreferrer"&gt;Share web builds docs&lt;/a&gt; cover updating, deleting and linking a game from another computer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can you export a Godot 4 C# project to the web?
&lt;/h2&gt;

&lt;p&gt;No. Godot 4.7.2's .NET editor refused our one-script C# project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Exporting to Web is currently not supported in Godot 4 when using C#/.NET. Use Godot 3 to target Web with C#/Mono instead.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Godot's docs say "Projects written in C# using Godot 4 currently cannot be exported to the web." The tracking issue, &lt;a href="https://github.com/godotengine/godot/issues/70796" rel="noopener noreferrer"&gt;godotengine/godot#70796&lt;/a&gt;, was locked on 19 June 2026 with the comment "The foundation is evaluating solutions for C# web support." For a browser build today, use GDScript, or Godot 3 with C#. The same limit applies to Ziva's Share Web build.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Ziva's Share Web build doesn't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ziva doesn't upload to itch.io, GitHub Pages or any host other than play.ziva.sh.&lt;/li&gt;
&lt;li&gt;A published Ziva build is public: it is listed on &lt;a href="https://play.ziva.sh" rel="noopener noreferrer"&gt;play.ziva.sh&lt;/a&gt;, where players can rate and comment, and new games are announced in Ziva's Discord. Only the preview is unlisted.&lt;/li&gt;
&lt;li&gt;Ziva has no custom domains or password-protected builds.&lt;/li&gt;
&lt;li&gt;Ziva can't export C# projects to the web, because Godot 4 can't.&lt;/li&gt;
&lt;li&gt;Share Web build is for Godot only, needs the Godot editor with Ziva installed, and needs you signed in to Ziva.&lt;/li&gt;
&lt;li&gt;Ziva uploads only Godot's web export, not your project folder or chats. Like any web build, the export contains your game's scenes and scripts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://ziva.sh/docs/share-web-builds" rel="noopener noreferrer"&gt;Share web builds&lt;/a&gt;: Ziva's step-by-step docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ziva.sh/blogs/godot-multiplayer-without-server" rel="noopener noreferrer"&gt;Godot online multiplayer without a server&lt;/a&gt;, including web exports&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ziva.sh/blogs/godot-automated-playtesting" rel="noopener noreferrer"&gt;How to automate playtesting in Godot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.godotengine.org/en/stable/tutorials/export/exporting_for_web.html" rel="noopener noreferrer"&gt;Godot's "Exporting for the Web" docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I export my Godot 4 game so it can be played in a web browser?
&lt;/h3&gt;

&lt;p&gt;Use the Compatibility renderer, open Project &amp;gt; Export, add a Web preset, install the export templates if Godot asks, leave Thread Support off (the default since Godot 4.3) and export to index.html. From the command line, godot --headless --path my-game --export-release "Web" ../build/index.html does the same with a preset named Web. Serve the folder over HTTP, for example with python3 -m http.server, because opening index.html as a file fails with "Failed to fetch". In our Godot 4.7.2 test, a single-threaded export ran from a plain local server with no special headers.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the fastest way to share a playable build of my Godot game with friends or playtesters as a link?
&lt;/h3&gt;

&lt;p&gt;Export single-threaded and upload the folder to any host that gives you a URL. itch.io (free) takes a ZIP with index.html at its root; tick This file will be played in the browser, and leave the page as a Draft to share a secret link. Netlify Drop publishes a dropped folder without an account, but Netlify's docs say individual files over 10 MB may cause the deploy to get stuck, and Godot 4.7.2's index.wasm is 39.5 MB. If you use Ziva, an AI assistant inside the Godot editor, its Share Web build menu item exports the game, uploads it and returns a play.ziva.sh link on every Ziva plan, with no hosting setup; publishing makes the game public on play.ziva.sh.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where can I host a Godot HTML5 game for free?
&lt;/h3&gt;

&lt;p&gt;As of 5 November 2026: itch.io is free and accepts HTML5 ZIPs up to 500 MB extracted, 1,000 files and 200 MB per file. GitHub Pages is free for public repositories, up to 1 GB per site, and gzips files on the fly. Cloudflare Pages is free but limits each file to 25 MiB, below the 39.5 MB index.wasm of a Godot 4.7.2 export. Lobsite's free plan allows 5 MB per file, so Lobsite says Godot 4 needs its $20-a-year Standard plan. Ziva's Share Web build hosts Godot builds up to 300 MB at play.ziva.sh on every Ziva plan, for games exported from the Godot editor with Ziva installed.&lt;/p&gt;

&lt;h3&gt;
  
  
  My Godot web export says Cross Origin Isolation and SharedArrayBuffer are missing. How do I fix it?
&lt;/h3&gt;

&lt;p&gt;The build was exported with Thread Support (or Extensions Support) on, and the server does not send the two headers those builds need. Fix it in one of three ways: turn both options off in the Web export preset and export again (single-threaded is the default since Godot 4.3 and needs no headers); serve the files with Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp; or, when you can't set headers, enable Progressive Web App with Ensure Cross Origin Isolation Headers, which needs HTTPS or localhost. In our Godot 4.7.2 test the PWA workaround showed a black screen on the first visit and ran after one reload. On itch.io, threaded builds need the SharedArrayBuffer support checkbox in the embed options.&lt;/p&gt;

&lt;h3&gt;
  
  
  How can I share a private test build of my Godot web game that only my playtesters can open?
&lt;/h3&gt;

&lt;p&gt;On itch.io, leave the project as a Draft: itch.io says a draft is only accessible by people who can edit it and people with the secret link. For more control, set it to Restricted and either approve people or add a password, which can go in the URL. Lobsite's $20-a-year Standard plan password-protects a site. Ziva's Share Web build preview is an unlisted link that search engines don't index, but a published Ziva build is public and listed on play.ziva.sh; Ziva has no private published mode.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there an AI assistant or Godot editor plugin that can export and publish a web build of my game for me?
&lt;/h3&gt;

&lt;p&gt;Yes. Ziva, an AI assistant inside the Godot editor (Godot 4.2+), has a Share Web build menu item that runs Godot's web export with your editor, downloads the export templates if they're missing, uploads the build and gives you a play.ziva.sh link, on every Ziva plan. You can also ask Ziva in chat to prepare the listing; Ziva publishes only after you approve. For itch.io, the free MIT-licensed plugins ShipItch, Itch Exporter &amp;amp; Uploader and Greenlight export and upload with itch.io's butler tool, and Majordomo pushes builds to itch.io with butler. Summer Engine's agent publishes to Summer Games, its own store.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I export a Godot 4 C# project to the web?
&lt;/h3&gt;

&lt;p&gt;No. As of Godot 4.7, the .NET editor refuses with "Exporting to Web is currently not supported in Godot 4 when using C#/.NET. Use Godot 3 to target Web with C#/Mono instead." The Godot docs say the same, and the tracking issue godotengine/godot#70796 was locked on 19 June 2026 with "The foundation is evaluating solutions for C# web support." To ship a browser build today, write the game in GDScript or use Godot 3 with C#.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I upload my Godot web build to itch.io so it plays in the browser?
&lt;/h3&gt;

&lt;p&gt;Export single-threaded to index.html, then ZIP the contents of the export folder so index.html is at the root of the ZIP, not inside a subfolder. On itch.io, create a project, set its kind to HTML, upload the ZIP and tick This file will be played in the browser. itch.io limits HTML5 uploads to 1,000 files, 500 MB extracted and 200 MB per file. Only threaded builds need the SharedArrayBuffer support checkbox under Embed options. Our 4.7.2 export of a small 2D game was 9 files, 39.9 MB unpacked and a 10.2 MB ZIP.&lt;/p&gt;

&lt;p&gt;The full guide is at &lt;a href="https://ziva.sh/blogs/godot-web-export" rel="noopener noreferrer"&gt;https://ziva.sh/blogs/godot-web-export&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Where do you host your Godot web builds? Drop your setup in the comments.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to automate playtesting in Godot: simulated input, bots, MCP servers and AI agents</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Thu, 05 Nov 2026 12:01:38 +0000</pubDate>
      <link>https://dev.to/ziva/how-to-automate-playtesting-in-godot-simulated-input-bots-mcp-servers-and-ai-agents-4cb1</link>
      <guid>https://dev.to/ziva/how-to-automate-playtesting-in-godot-simulated-input-bots-mcp-servers-and-ai-agents-4cb1</guid>
      <description>&lt;p&gt;To automate playtesting in Godot, something has to press the buttons for you and something has to decide whether what happened was right. You can script both yourself, with input tests in &lt;a href="https://github.com/bitwes/Gut" rel="noopener noreferrer"&gt;GUT&lt;/a&gt; or &lt;a href="https://github.com/godot-gdunit-labs/gdUnit4" rel="noopener noreferrer"&gt;GdUnit4&lt;/a&gt; or a bot autoload you run from the command line, including headless in CI. Or you can hand the pressing to an AI: an MCP server that lets Claude Code or Cursor run the game, or an agent inside the editor such as &lt;a href="https://ziva.sh/docs/creating#playtest-your-game" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; Ziva.sh publishes this page and makes Ziva. Facts about other tools come from their own pages, as of 5 November 2026. We ran every Godot command and test on this page on 5 November 2026 with Godot 4.7.2 on Linux, on the public &lt;a href="https://github.com/ziva-sh/ziva-godot-sample" rel="noopener noreferrer"&gt;Star Catcher sample&lt;/a&gt;, a small game with one deliberate bug: collecting a star should add a point, but the score label is never found. Disclosure: GDAI MCP is made by Delano Lourenco, a Ziva cofounder.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ways to automate playtesting in Godot
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Way&lt;/th&gt;
&lt;th&gt;Who decides what to press&lt;/th&gt;
&lt;th&gt;Headless or CI&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://gut.readthedocs.io/en/latest/class_ref/class_gutinputsender.html" rel="noopener noreferrer"&gt;GUT&lt;/a&gt; input tests&lt;/td&gt;
&lt;td&gt;GUT: your test script, through GutInputSender&lt;/td&gt;
&lt;td&gt;GUT: yes; exited 1 on a failing test in our run&lt;/td&gt;
&lt;td&gt;GUT: free, MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://godot-gdunit-labs.github.io/gdUnit4/latest/advanced_testing/scene_runner/actions/" rel="noopener noreferrer"&gt;GdUnit4&lt;/a&gt; scene runner&lt;/td&gt;
&lt;td&gt;GdUnit4: your test script, through the scene runner&lt;/td&gt;
&lt;td&gt;GdUnit4: yes; exited 100 on a failing test in our run&lt;/td&gt;
&lt;td&gt;GdUnit4: free, MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A bot autoload run from the command line&lt;/td&gt;
&lt;td&gt;Bot: your script&lt;/td&gt;
&lt;td&gt;Bot: yes with &lt;code&gt;godot --headless&lt;/code&gt;; screenshots and video need a display&lt;/td&gt;
&lt;td&gt;Godot: free, MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://ziva.sh/blogs/godot-automated-playtesting#let-claude-code-or-cursor-run-the-game-godot-mcp-servers" rel="noopener noreferrer"&gt;Godot MCP servers&lt;/a&gt; with Claude Code or Cursor&lt;/td&gt;
&lt;td&gt;MCP servers: the AI client's model&lt;/td&gt;
&lt;td&gt;MCP servers: drive a running editor or game; Beckett Full adds a headless CI runner&lt;/td&gt;
&lt;td&gt;MCP servers: free to $19 one-time, plus the client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://ziva.sh/blogs/godot-automated-playtesting#let-ziva-run-the-game-and-read-the-errors" rel="noopener noreferrer"&gt;Ziva run_scene&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Ziva run_scene: Ziva's agent, or Claude Code over Ziva's MCP&lt;/td&gt;
&lt;td&gt;Ziva run_scene: no; the Godot editor must be open&lt;/td&gt;
&lt;td&gt;Ziva run_scene: every plan, including the free Hobby plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://ziva.sh/blogs/godot-automated-playtesting#ziva-full-game-playtest-agent" rel="noopener noreferrer"&gt;Ziva Full Game Playtest Agent&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Ziva Full Game Playtest Agent: an AI playing toward your goal&lt;/td&gt;
&lt;td&gt;Ziva Full Game Playtest Agent: no; the Godot editor must be open&lt;/td&gt;
&lt;td&gt;Ziva Full Game Playtest Agent: Pro ($50/month) and Ultra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://modl.ai/" rel="noopener noreferrer"&gt;modl.ai&lt;/a&gt;, &lt;a href="https://nunu.ai/" rel="noopener noreferrer"&gt;nunu.ai&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;modl.ai, nunu.ai: their AI agents, from the screen&lt;/td&gt;
&lt;td&gt;modl.ai: "upload your latest build"; nunu.ai: "PC and mobile (iOS and Android)"&lt;/td&gt;
&lt;td&gt;modl.ai, nunu.ai: no price on their home pages&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Scripted tests and bots are free, repeatable and fast, but they only check what you thought to script. Agents find paths you did not script, but each run costs model time and an agent's choices vary between runs. Most games want both: scripted checks on every commit, an agent pass before a release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simulate keyboard and mouse input in Godot
&lt;/h2&gt;

&lt;p&gt;Godot itself has two ways to fake input from code. Both moved the Star Catcher player 360 px to the right in 1.2 seconds of game time in our run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="c1"&gt;# A raw key, as if the Right arrow were pressed. _input() sees it, and it drives the ui_right action.&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;InputEventKey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keycode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KEY_RIGHT&lt;/span&gt;
&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pressed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;
&lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse_input_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# A named action from the Input Map. No InputEvent is created, so _input() never sees it.&lt;/span&gt;
&lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_press&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ui_right"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ui_right"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Godot's &lt;a href="https://docs.godotengine.org/en/stable/classes/class_input.html" rel="noopener noreferrer"&gt;Input docs&lt;/a&gt; say &lt;code&gt;parse_input_event&lt;/code&gt; "Feeds an InputEvent to the game" and "Also generates Node._input() calls", and that &lt;code&gt;action_press&lt;/code&gt; "will not cause any Node._input() calls". Neither touches the operating system: sending an InputEventKey "won't toggle between active windows". To release a parsed key, send a copy with &lt;code&gt;pressed = false&lt;/code&gt;. An &lt;code&gt;InputEventAction&lt;/code&gt; sent through &lt;code&gt;parse_input_event&lt;/code&gt; works the same way for actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  GUT: GutInputSender
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://gut.readthedocs.io/en/latest/class_ref/class_gutinputsender.html" rel="noopener noreferrer"&gt;GUT's GutInputSender&lt;/a&gt; "can be used to send &lt;code&gt;InputEvent*&lt;/code&gt; events to various objects. It also allows you to script out a series of inputs and play them back in real time." With &lt;code&gt;Input&lt;/code&gt; as the receiver, GUT's &lt;a href="https://gut.readthedocs.io/en/latest/Mocking-Input.html" rel="noopener noreferrer"&gt;Mocking Input page&lt;/a&gt; says Godot treats the events "exactly the same as if the events were triggered from hardware". This test passed on the fixed game and failed on the sample's &lt;code&gt;main&lt;/code&gt; branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="c1"&gt;# res://test/test_star.gd&lt;/span&gt;
&lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;GutTest&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;_sender&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GutInputSender&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;after_each&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;_sender&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;release_all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;_sender&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;test_walking_right_collects_a_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;add_child_autofree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"res://main.tscn"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instantiate&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;_sender&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_down&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ui_right"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hold_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_sender&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;idle&lt;/span&gt;
    &lt;span class="n"&gt;assert_eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"HUD/ScoreLabel"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Score: 1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After importing the project once with &lt;code&gt;godot --headless --path . --import&lt;/code&gt;, GUT 9.6.1 ran it headless with &lt;code&gt;godot --headless --path . -s addons/gut/gut_cmdln.gd -gdir=res://test -gexit&lt;/code&gt;. On the buggy branch GUT reported &lt;code&gt;["Score: 0"] expected to equal ["Score: 1"]&lt;/code&gt;, listed the &lt;code&gt;Invalid assignment of property or key 'text' ... on a base object of type 'null instance'&lt;/code&gt; error under "Unexpected Errors", and exited with code 1. GutInputSender also has &lt;code&gt;key_down&lt;/code&gt;, &lt;code&gt;key_up&lt;/code&gt;, &lt;code&gt;mouse_left_click_at&lt;/code&gt; and other mouse methods.&lt;/p&gt;

&lt;h3&gt;
  
  
  GdUnit4: the scene runner
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://godot-gdunit-labs.github.io/gdUnit4/latest/advanced_testing/scene_runner/actions/" rel="noopener noreferrer"&gt;GdUnit4's scene runner&lt;/a&gt; has functions that "mimic user key inputs as actions for testing purposes", plus &lt;a href="https://godot-gdunit-labs.github.io/gdUnit4/latest/advanced_testing/scene_runner/keys/" rel="noopener noreferrer"&gt;key&lt;/a&gt; and &lt;a href="https://godot-gdunit-labs.github.io/gdUnit4/latest/advanced_testing/scene_runner/mouse/" rel="noopener noreferrer"&gt;mouse&lt;/a&gt; functions such as &lt;code&gt;simulate_key_pressed(KEY_ENTER)&lt;/code&gt; and &lt;code&gt;simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT)&lt;/code&gt;. The same check in GdUnit4 7.0.0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="c1"&gt;# res://test/test_star.gd&lt;/span&gt;
&lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;GdUnitTestSuite&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;test_walking_right_collects_a_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scene_runner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"res://main.tscn"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;simulate_action_press&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ui_right"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;simulate_frames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;simulate_action_release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ui_right"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;await_input_processed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;assert_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find_child&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ScoreLabel"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Score: 1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GdUnit4 ran it with &lt;code&gt;./addons/gdUnit4/runtest.sh --godot_binary /usr/bin/godot --headless -a res://test&lt;/code&gt;, printed "Running in headless mode, Godot does not render anything.", reported a Godot runtime error and "Expecting: 'Score: 1' but was 'Score: 0'" on the buggy branch, and exited with code 100. On the fixed game it exited 0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the game play itself: a bot and the command line
&lt;/h2&gt;

&lt;p&gt;For a run through the real game rather than a test scene, add an autoload that plays only when you ask it to. This one walks right into the first star and checks the score:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="c1"&gt;# res://bot.gd, registered under [autoload] in project.godot as Bot="*res://bot.gd"&lt;/span&gt;
&lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Node&lt;/span&gt;

&lt;span class="c1"&gt;# Plays only when the game is started with "-- --bot", then quits with exit code 0 (pass) or 1 (fail).&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_ready&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;OS&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_cmdline_user_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"--bot"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;get_tree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;
    &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_press&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ui_right"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;get_tree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;
    &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ui_right"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;get_tree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_tree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_scene&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"HUD/ScoreLabel"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"BOT: score label says '&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;get_tree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"Score: 1"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it from the project folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;godot &lt;span class="nt"&gt;--headless&lt;/span&gt; &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--fixed-fps&lt;/span&gt; 60 &lt;span class="nt"&gt;--quit-after&lt;/span&gt; 600 &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--bot&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the sample's buggy &lt;code&gt;main&lt;/code&gt; branch, Godot 4.7.2 printed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Godot Engine v4.7.2.stable.arch_linux.ed1daf0bf (2026-08-17 01:18:38 UTC) - https://godotengine.org

ERROR: Node not found: "%ScoreLabel" (relative to "/root/Main").
   at: get_node (scene/main/node.cpp:1975)
   GDScript backtrace (most recent call first):
       [0] @implicit_ready (res://main.gd:5)
SCRIPT ERROR: Invalid assignment of property or key 'text' with value of type 'String' on a base object of type 'null instance'.
          at: _on_star_collected (res://main.gd:15)
          GDScript backtrace (most recent call first):
              [0] _on_star_collected (res://main.gd:15)
              [1] _on_body_entered (res://star.gd:8)
BOT: score label says 'Score: 0'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exit code was 1. After changing &lt;code&gt;%ScoreLabel&lt;/code&gt; to &lt;code&gt;$HUD/ScoreLabel&lt;/code&gt; in &lt;code&gt;main.gd&lt;/code&gt;, the same command printed &lt;code&gt;BOT: score label says 'Score: 1'&lt;/code&gt; and exited 0. What the flags did:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--headless&lt;/code&gt; runs with no window and no audio, so it works on a CI machine. Nothing is rendered, so there are no screenshots.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--fixed-fps 60&lt;/code&gt; gives every frame exactly 1/60 s of game time; &lt;code&gt;godot --help&lt;/code&gt; says it "disables real-time synchronization". The whole run, startup included, took 0.78 s of wall time for 2 seconds of game time, and the player moved the same 360 px in every run we measured.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--quit-after 600&lt;/code&gt; stops the game after 600 iterations (frames) if the bot never quits, so a stuck run cannot hang CI.&lt;/li&gt;
&lt;li&gt;Arguments after &lt;code&gt;--&lt;/code&gt; reach the game through &lt;code&gt;OS.get_cmdline_user_args()&lt;/code&gt;, so normal launches ignore the bot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bot must decide pass or fail itself. Without the bot, &lt;code&gt;godot --headless --path . --fixed-fps 60 --quit-after 600&lt;/code&gt; printed the same &lt;code&gt;Node not found&lt;/code&gt; error and still exited 0.&lt;/p&gt;

&lt;p&gt;To watch what the bot did, record it with &lt;code&gt;--write-movie run.avi&lt;/code&gt;. That needs rendering: with &lt;code&gt;--headless&lt;/code&gt;, Godot 4.7.2 crashed (signal 11) in our test. On a virtual X display (Xvfb, no GPU), the same run without &lt;code&gt;--headless&lt;/code&gt; wrote a 122-frame AVI at 60 FPS and printed &lt;code&gt;Done recording movie at path&lt;/code&gt;. For longer games, chain several inputs, check signals or node state at each step, and give each level its own bot path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let Claude Code or Cursor run the game: Godot MCP servers
&lt;/h2&gt;

&lt;p&gt;An MCP server gives an AI client such as Claude Code or Cursor tools to control Godot. The client's model decides what to press. These servers list tools for running the game, sending input and seeing the result:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;MCP server&lt;/th&gt;
&lt;th&gt;Run the game&lt;/th&gt;
&lt;th&gt;Send input&lt;/th&gt;
&lt;th&gt;See the running game&lt;/th&gt;
&lt;th&gt;Licence and price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://ziva.sh/docs/ziva-mcp-server" rel="noopener noreferrer"&gt;Ziva's MCP server&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Ziva: &lt;code&gt;run_scene&lt;/code&gt; runs a scene from the open editor&lt;/td&gt;
&lt;td&gt;Ziva: actions, keys, mouse clicks, clicks on a Control by path or text, mouse motion&lt;/td&gt;
&lt;td&gt;Ziva: final screenshot plus console output and runtime errors; &lt;code&gt;get_screenshot&lt;/code&gt; (editor)&lt;/td&gt;
&lt;td&gt;Ziva: &lt;code&gt;run_scene&lt;/code&gt; on every plan, including the free Hobby plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://godot-mcp.abyo.net/" rel="noopener noreferrer"&gt;Godot MCP Pro&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Godot MCP Pro: "Launch the game"&lt;/td&gt;
&lt;td&gt;Godot MCP Pro: &lt;code&gt;simulate_key&lt;/code&gt;, &lt;code&gt;simulate_mouse_click&lt;/code&gt;, &lt;code&gt;simulate_action&lt;/code&gt;, &lt;code&gt;simulate_sequence&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Godot MCP Pro: &lt;code&gt;get_game_screenshot&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Godot MCP Pro: "$15 one-time purchase"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://gdaimcp.com/" rel="noopener noreferrer"&gt;GDAI MCP&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;GDAI MCP: runs the game and checks it with screenshots&lt;/td&gt;
&lt;td&gt;GDAI MCP: "press keyboard keys and input actions"&lt;/td&gt;
&lt;td&gt;GDAI MCP: screenshots of "the editor and running game"&lt;/td&gt;
&lt;td&gt;GDAI MCP: "$19 one-time payment", &lt;a href="https://gdaimcp.com/license" rel="noopener noreferrer"&gt;binary-only licence&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://forum.godotengine.org/t/beckett-zero-sidecar-mcp-server-for-godot-4-2-the-ai-sees-and-optionally-playtests-your-game/141177" rel="noopener noreferrer"&gt;Beckett&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Beckett Lite and Full: "run your game"&lt;/td&gt;
&lt;td&gt;Beckett Full: "simulate input, click 2D and 3D UI, scroll, drag, record and replay"&lt;/td&gt;
&lt;td&gt;Beckett Lite and Full: "screenshot the running game"&lt;/td&gt;
&lt;td&gt;Beckett: Lite free (MIT), Full "$15, one-time"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/satelliteoflove/godot-mcp" rel="noopener noreferrer"&gt;satelliteoflove/godot-mcp&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;satelliteoflove/godot-mcp: run, stop, restart; freeze and step&lt;/td&gt;
&lt;td&gt;satelliteoflove/godot-mcp: "actions, joypad, raw keys, mouse-look, text"&lt;/td&gt;
&lt;td&gt;satelliteoflove/godot-mcp: game screenshots and live state as JSON&lt;/td&gt;
&lt;td&gt;satelliteoflove/godot-mcp: free, MIT; Godot 4.5+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/hi-godot/godot-ai/blob/main/docs/TOOLS.md" rel="noopener noreferrer"&gt;Godot AI (hi-godot/godot-ai)&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Godot AI: &lt;code&gt;project_run&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Godot AI: &lt;code&gt;input_key&lt;/code&gt;, &lt;code&gt;input_mouse&lt;/code&gt;, &lt;code&gt;input_gamepad&lt;/code&gt;, &lt;code&gt;input_action&lt;/code&gt;, &lt;code&gt;input_sequence&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Godot AI: &lt;code&gt;editor_screenshot&lt;/code&gt; with the running game's framebuffer&lt;/td&gt;
&lt;td&gt;Godot AI: free, MIT; Godot 4.7+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/masteryee-labs/Open-Godot-MCP" rel="noopener noreferrer"&gt;Open Godot MCP&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Open Godot MCP: &lt;code&gt;godot_game&lt;/code&gt; play, stop, freeze&lt;/td&gt;
&lt;td&gt;Open Godot MCP: &lt;code&gt;godot_input&lt;/code&gt; for keyboard, mouse, gamepad and text&lt;/td&gt;
&lt;td&gt;Open Godot MCP: &lt;code&gt;godot_screenshot&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Open Godot MCP: free, MIT; Godot 4.5+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/Coding-Solo/godot-mcp" rel="noopener noreferrer"&gt;Coding-Solo/godot-mcp&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Coding-Solo/godot-mcp: "run projects, capture debug output"&lt;/td&gt;
&lt;td&gt;Coding-Solo/godot-mcp: no input tool listed&lt;/td&gt;
&lt;td&gt;Coding-Solo/godot-mcp: no game screenshot listed&lt;/td&gt;
&lt;td&gt;Coding-Solo/godot-mcp: free, MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.summerengine.com/godot-ai" rel="noopener noreferrer"&gt;Summer Engine MCP&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Summer Engine: "play"&lt;/td&gt;
&lt;td&gt;Summer Engine: "input"&lt;/td&gt;
&lt;td&gt;Summer Engine: "screenshots"&lt;/td&gt;
&lt;td&gt;Summer Engine: MIT CLI and MCP tools that "drive Summer's desktop editor, not stock Godot"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two vendors also describe verification beyond pressing keys. Godot MCP Pro's page says it can "assert game state, and run stress tests with random input". Beckett's author wrote on 10 July 2026 about Beckett Full: "A headless runner plays every saved suite in your own CI and exits non-zero on failure."&lt;/p&gt;

&lt;h2&gt;
  
  
  Let Ziva run the game and read the errors
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ziva.sh/docs" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt; is an AI agent that works inside the Godot editor. Since version &lt;a href="https://ziva.sh/changelog" rel="noopener noreferrer"&gt;3.0.0&lt;/a&gt; (June 2026), Ziva's &lt;code&gt;run_scene&lt;/code&gt; tool runs a scene live from the open editor, in the editor's embedded game view, and returns a screenshot of the running game plus the console output and runtime errors from that run. Ziva's &lt;code&gt;run_scene&lt;/code&gt; is on every plan, including the free Hobby plan. Ziva's agent calls it on its own when you ask for a concrete check, for example: "Run my level, jump onto the first platform, and check that the player lands."&lt;/p&gt;

&lt;p&gt;Ziva's &lt;code&gt;run_scene&lt;/code&gt; takes a timed list of inputs, each with a start time and a hold time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;action&lt;/code&gt;: a named input action, pressed with &lt;code&gt;Input.action_press&lt;/code&gt;, so it works without a key mapping.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;key&lt;/code&gt;: a raw &lt;code&gt;InputEventKey&lt;/code&gt;, which &lt;code&gt;_input&lt;/code&gt; sees.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mouse&lt;/code&gt;: a left or right click at a pixel position.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;target&lt;/code&gt;: a click on a Control found by node path or visible text.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;motion&lt;/code&gt;: relative mouse movement, for a first-person camera with a captured mouse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ziva sends raw keys and mouse clicks as InputEvents through &lt;code&gt;Input.parse_input_event&lt;/code&gt;, the channel a keyboard's events take inside Godot, and presses named actions with &lt;code&gt;Input.action_press&lt;/code&gt;. Ziva presses no physical key, and Ziva's &lt;code&gt;run_scene&lt;/code&gt; has no gamepad or touch input. Each Ziva &lt;code&gt;run_scene&lt;/code&gt; result includes a delivery report with the frame and millisecond every press and release landed on, and &lt;code&gt;speed: true&lt;/code&gt; adds average FPS, 1% low and worst frame time.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Ziva's run_scene returned on Star Catcher
&lt;/h3&gt;

&lt;p&gt;We called Ziva's &lt;code&gt;run_scene&lt;/code&gt; directly through Ziva's local MCP endpoint, without a model, on the Star Catcher sample in Godot 4.7.2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A plain 2.5-second run returned &lt;code&gt;Node not found: "%ScoreLabel" (relative to "/root/Main")&lt;/code&gt; with source &lt;code&gt;res://main.gd:5&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Holding &lt;code&gt;ui_right&lt;/code&gt; from 300 ms for 1,200 ms moved the player 360 px and collected the first star. Ziva's result added &lt;code&gt;Invalid assignment of property or key 'text' with value of type 'String' on a base object of type 'null instance'&lt;/code&gt; at &lt;code&gt;res://main.gd:15&lt;/code&gt;, and the screenshot showed the score still at 0.&lt;/li&gt;
&lt;li&gt;For a UI check, we added a Restart button inside a CanvasLayer to our test copy (it is not in the public sample). Ziva's &lt;code&gt;run_scene&lt;/code&gt; clicked it by its visible text "Restart", by its node path &lt;code&gt;RestartLayer/RestartButton&lt;/code&gt; and by a mouse click at its pixel position, and the button's &lt;code&gt;pressed&lt;/code&gt; handler printed each time.&lt;/li&gt;
&lt;li&gt;A target that matched nothing, "Play Again", came back in Ziva's delivery report as &lt;code&gt;delivered: false&lt;/code&gt; with "no Control matched target 'Play Again'". The run itself still reported success, so read the delivery report.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw5u0pp90w5s2lwyc3d7c.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw5u0pp90w5s2lwyc3d7c.webp" alt="Screenshot returned by Ziva's run_scene after holding ui_right for 1.2 seconds in the Star Catcher sample: the teal player square has moved right past the first star, two yellow stars remain, the label still reads Score: 0, and the Restart button we added for testing sits top right" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a separate &lt;a href="https://www.youtube.com/shorts/l7fjl7f2sEg" rel="noopener noreferrer"&gt;unedited recording&lt;/a&gt; on the same sample, one prompt asked Ziva to add stars, play the game and check that the score goes up. Ziva ran the game while holding &lt;code&gt;ui_right&lt;/code&gt;, read the two errors, changed the lookup in &lt;code&gt;main.gd&lt;/code&gt; to &lt;code&gt;$HUD/ScoreLabel&lt;/code&gt;, ran the game again and saw &lt;code&gt;Score: 1&lt;/code&gt;. The sample's &lt;a href="https://github.com/ziva-sh/ziva-godot-sample" rel="noopener noreferrer"&gt;README&lt;/a&gt; has the exact prompt and every change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Ziva's run_scene from Claude Code or Cursor
&lt;/h3&gt;

&lt;p&gt;Ziva runs a local MCP server, so Claude Code, Codex, Cursor and other MCP apps can call Ziva's &lt;code&gt;run_scene&lt;/code&gt; and &lt;code&gt;get_screenshot&lt;/code&gt; tools. Ziva's MCP server is local only, with no hosted URL. To connect Claude Code (&lt;a href="https://ziva.sh/docs/ziva-mcp-server" rel="noopener noreferrer"&gt;full steps&lt;/a&gt;):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your project in Godot with Ziva installed and signed in.&lt;/li&gt;
&lt;li&gt;In Ziva, open &lt;strong&gt;Settings → Models &amp;amp; Providers → Ziva as an MCP server&lt;/strong&gt; and check that it shows &lt;strong&gt;Running&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;AI application&lt;/strong&gt;, choose &lt;strong&gt;Claude Code&lt;/strong&gt; and click &lt;strong&gt;Install for Claude Code&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Start a new Claude Code session from your Godot project folder and ask, for example: "Use ziva-godot's run_scene tool to run res://main.tscn for 2500 ms, holding ui_right from 300 ms for 1200 ms, and tell me which errors it printed."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Claude Code then gets the same screenshot, console output and errors as Ziva's own agent. Ziva's &lt;code&gt;run_scene&lt;/code&gt; only appears when playtesting is enabled in Ziva's settings, and Ziva's Full Game Playtest Agent is not available over MCP.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ziva Full Game Playtest Agent
&lt;/h3&gt;

&lt;p&gt;Ziva's Full Game Playtest Agent, added in &lt;a href="https://ziva.sh/changelog" rel="noopener noreferrer"&gt;3.1.4&lt;/a&gt; (August 2026), plays a whole game toward a goal you set, such as "chop down a tree and build a crafting bench". Ziva's Full Game Playtest Agent is on Ziva's Pro ($50/month) and Ultra plans. Ask Ziva to "use the Full Game Playtest Agent". Ziva's Full Game Playtest Agent starts the game, keeps it paused while the model thinks, sends keyboard and mouse input one action at a time, checks the result with screenshots and values it reads from the running scene, and returns &lt;strong&gt;pass&lt;/strong&gt;, &lt;strong&gt;fail&lt;/strong&gt; or &lt;strong&gt;blocked&lt;/strong&gt; with evidence. With &lt;strong&gt;Record playtests&lt;/strong&gt; on in &lt;strong&gt;Settings → Agent&lt;/strong&gt;, Ziva saves a video of the run with its goal and inputs drawn in. Ziva's Full Game Playtest Agent cannot edit your project: it reports, and you or Ziva's normal chat fix the bug. See &lt;a href="https://ziva.sh/blogs/ai-agent-playtest-godot-game" rel="noopener noreferrer"&gt;Is there an AI agent that can playtest a Godot game?&lt;/a&gt; and the &lt;a href="https://ziva.sh/full-game-tester" rel="noopener noreferrer"&gt;Full Game Tester page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist: find bugs in your Godot game before release
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Make errors fail the run. Godot exited 0 after a &lt;code&gt;SCRIPT ERROR&lt;/code&gt; in our test, so have the bot call &lt;code&gt;get_tree().quit(1)&lt;/code&gt;, or fail the CI step when the log contains &lt;code&gt;SCRIPT ERROR&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Script the main path: start, first level, win, lose, restart. Run it as a bot or as GUT or GdUnit4 tests headless on every commit.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;--fixed-fps 60&lt;/code&gt; so timers and physics advance the same way every run.&lt;/li&gt;
&lt;li&gt;Click through the UI, not only the gameplay: menus, pause, restart and buttons inside CanvasLayers.&lt;/li&gt;
&lt;li&gt;Record a bot run with &lt;code&gt;--write-movie&lt;/code&gt; on a display when a failure is hard to picture.&lt;/li&gt;
&lt;li&gt;Before a release, let an agent play the paths you did not script, such as Ziva's Full Game Playtest Agent or Claude Code with an MCP server.&lt;/li&gt;
&lt;li&gt;Check frame rate at your target FPS. Ziva's &lt;code&gt;run_scene&lt;/code&gt; with &lt;code&gt;speed: true&lt;/code&gt; reports average FPS, 1% low and worst frame time.&lt;/li&gt;
&lt;li&gt;Test the exported build on each platform yourself. Ziva's playtesting runs in the editor, not on an export.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Ziva doesn't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ziva plays games only inside an open Godot editor, so Ziva has no headless or CI mode; use GUT, GdUnit4 or a bot for CI.&lt;/li&gt;
&lt;li&gt;Ziva plays one game at a time, with no multiplayer or multi-window runs.&lt;/li&gt;
&lt;li&gt;Ziva sends keyboard and mouse input only, with no gamepad or touch input, and no exported builds, mobile or web.&lt;/li&gt;
&lt;li&gt;Ziva's Full Game Playtest Agent is Godot only, on Pro and Ultra, uses a hosted model, and cannot edit your project.&lt;/li&gt;
&lt;li&gt;Ziva's playtesting needs you signed in to Ziva, even when Claude Code calls &lt;code&gt;run_scene&lt;/code&gt; over MCP; the free Hobby plan is enough for &lt;code&gt;run_scene&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Ziva doesn't tell you whether your game is fun.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ziva.sh/blogs/ai-agent-playtest-godot-game" rel="noopener noreferrer"&gt;Is there an AI agent that can playtest a Godot game?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ziva.sh/docs/ziva-mcp-server" rel="noopener noreferrer"&gt;Ziva as an MCP server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ziva.sh/blogs/ziva-vs-godot-mcp" rel="noopener noreferrer"&gt;Ziva vs Godot MCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ziva.sh/blogs/ziva-vs-gdai-mcp" rel="noopener noreferrer"&gt;Ziva vs GDAI MCP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I automate playtesting for my Godot game so it plays itself from start to finish?
&lt;/h3&gt;

&lt;p&gt;Write a small bot that presses the inputs a player would and checks the result, then run the game with it from the command line. In Godot the bot can be an autoload that calls Input.action_press and Input.action_release (or Input.parse_input_event with an InputEventKey) on a timer, started with godot --headless --fixed-fps 60 --quit-after 600, and it should call get_tree().quit(1) when a check fails, because Godot's exit code stays 0 even after script errors. GUT's GutInputSender and GdUnit4's scene runner do the same inside a test suite. If you would rather not script the inputs, Ziva, an AI agent inside the Godot editor, runs a scene and sends keyboard and mouse input with its run_scene tool on every plan, and Ziva's Full Game Playtest Agent (Pro and Ultra) plays toward a goal you set and returns pass, fail or blocked.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there an AI agent that can play my Godot game and find bugs?
&lt;/h3&gt;

&lt;p&gt;Yes. Ziva's Full Game Playtest Agent, on Ziva's Pro ($50/month) and Ultra plans, plays a Godot game in the editor toward a goal you describe, sends keyboard and mouse input, and returns a pass, fail or blocked verdict with evidence, plus a video if Record playtests is on. Ziva's Full Game Playtest Agent cannot edit your project; fixes happen in a normal Ziva chat. On every Ziva plan, including the free Hobby plan, Ziva's run_scene tool runs a scene with timed inputs and returns a screenshot, the console output and the runtime errors. MCP servers such as Godot MCP Pro, GDAI MCP, Beckett Full and satelliteoflove/godot-mcp give Claude Code or Cursor input and screenshot tools, and that client's model decides what to press.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I simulate keyboard and mouse input in Godot tests (GUT or GdUnit4)?
&lt;/h3&gt;

&lt;p&gt;In GUT, create GutInputSender.new(Input), chain calls such as action_down("ui_right").hold_for(1.2).wait(0.3), await the sender's idle signal, then assert; call release_all() and clear() in after_each. GUT's GutInputSender also has key_down, key_up and mouse methods such as mouse_left_click_at. In GdUnit4, load the scene with scene_runner("res://main.tscn") and call simulate_action_press, simulate_action_release, simulate_key_pressed or simulate_mouse_button_pressed, then await runner.await_input_processed() or runner.simulate_frames(n). Outside a test framework, Godot's Input.parse_input_event with an InputEventKey or InputEventAction feeds Godot's input system and reaches _input; Input.action_press sets the action without any _input call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there an AI assistant that can run my Godot game and check its own work instead of me copying errors back?
&lt;/h3&gt;

&lt;p&gt;Yes. Ziva, an AI agent inside the Godot editor, has had a run_scene tool since version 3.0.0 (June 2026) on every plan. Ziva's run_scene runs a scene from the open editor, sends timed keyboard and mouse input, and returns a screenshot plus that run's console output and runtime errors, so Ziva can fix the script and run the scene again without you pasting anything. Claude Code, Codex and Cursor can call the same run_scene tool through Ziva's local MCP server. Godot MCP Pro, GDAI MCP, Beckett, satelliteoflove/godot-mcp and Godot AI (hi-godot) give an external AI client similar run, input and screenshot tools, and Summer Engine's MCP tools do it for Summer's own editor rather than stock Godot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which Godot MCP server lets Claude Code run the game, send input and take screenshots?
&lt;/h3&gt;

&lt;p&gt;As of 5 November 2026: Godot MCP Pro ($15 one-time) lists simulate_key, simulate_mouse_click, simulate_action and get_game_screenshot. GDAI MCP ($19 one-time) can press keyboard keys and input actions and screenshot the running game. Beckett Full ($15 one-time) simulates input and clicks UI, and the free Beckett Lite screenshots the running game. The MIT-licensed satelliteoflove/godot-mcp and Godot AI (hi-godot/godot-ai) inject input and capture the running game. Ziva's local MCP server gives Claude Code Ziva's run_scene tool (timed keyboard and mouse input, final screenshot, console output and runtime errors) and get_screenshot for the editor. Coding-Solo/godot-mcp runs the project and captures debug output but lists no input tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are the best AI tools for automated game QA testing that work with Godot?
&lt;/h3&gt;

&lt;p&gt;For Godot specifically: Ziva's Full Game Playtest Agent (Pro and Ultra) plays the game in the editor toward a goal and returns a verdict; Ziva's run_scene (every plan) runs a scene with scripted inputs and returns errors and a screenshot; MCP servers such as Godot MCP Pro, GDAI MCP, Beckett, satelliteoflove/godot-mcp and Godot AI let Claude Code or Cursor drive the game; GUT and GdUnit4 run scripted input tests headless in CI. modl.ai and nunu.ai sell black-box AI QA that plays a build by watching the screen; neither company's home page mentions Godot.&lt;/p&gt;

&lt;h3&gt;
  
  
  How can I find bugs in my Godot game automatically before release?
&lt;/h3&gt;

&lt;p&gt;Use three layers. First, GUT or GdUnit4 tests that simulate input and assert results, run headless in CI; both exit non-zero when a test fails. Second, a bot autoload that plays the main path with godot --headless --fixed-fps 60 --quit-after 600 and calls get_tree().quit(1) when a check fails, because Godot itself exits 0 even after a SCRIPT ERROR. Third, an exploratory pass by an agent that decides what to press, such as Ziva's Full Game Playtest Agent (Pro and Ultra) or Claude Code with a Godot MCP server. Ziva plays the game inside the Godot editor, not an exported build, so test exports on each target platform separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there a Godot editor plugin with an AI agent that can playtest my game inside the editor?
&lt;/h3&gt;

&lt;p&gt;Ziva is a Godot editor plugin (Godot 4.2 or later) with an AI agent in a dock. On every plan, Ziva's run_scene tool runs a scene from the editor, sends keyboard and mouse input through Godot's own input system, and returns a screenshot, console output and runtime errors. On Pro and Ultra, Ziva's Full Game Playtest Agent plays the game one action at a time toward a goal, with the game paused while it thinks, and returns pass, fail or blocked with evidence. The Godot editor has to stay open, Ziva plays one game at a time, and Ziva has no gamepad or touch input. Beckett and Godot MCP Pro are editor plugins too, but their agent is an external MCP client such as Claude Code.&lt;/p&gt;

&lt;p&gt;The full guide is at &lt;a href="https://ziva.sh/blogs/godot-automated-playtesting" rel="noopener noreferrer"&gt;https://ziva.sh/blogs/godot-automated-playtesting&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;How do you playtest your Godot game today? Drop your setup in the comments.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>testing</category>
      <category>ai</category>
    </item>
    <item>
      <title>How to add online multiplayer to a Godot game without running a server</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Thu, 05 Nov 2026 10:23:53 +0000</pubDate>
      <link>https://dev.to/ziva/how-to-add-online-multiplayer-to-a-godot-game-without-running-a-server-5gh3</link>
      <guid>https://dev.to/ziva/how-to-add-online-multiplayer-to-a-godot-game-without-running-a-server-5gh3</guid>
      <description>&lt;p&gt;Godot's multiplayer works on your local network as soon as you set it up, but over the internet the host's router blocks incoming connections unless they forward a port. To let friends join without port forwarding or a server of your own, every player connects out to a relay. If you ship on Steam, &lt;a href="https://godotsteam.com/" rel="noopener noreferrer"&gt;GodotSteam&lt;/a&gt; gives you Valve's free relay and Steam lobbies. Outside Steam, the options are Epic Online Services through the community &lt;a href="https://github.com/3ddelano/epic-online-services-godot" rel="noopener noreferrer"&gt;EOSG plugin&lt;/a&gt;, a hosted Godot service such as &lt;a href="https://www.gd-sync.com/" rel="noopener noreferrer"&gt;GD-Sync&lt;/a&gt;, or, if you can run a small server yourself, punch-through with &lt;a href="https://github.com/foxssake/noray" rel="noopener noreferrer"&gt;noray&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ziva.sh/docs/multiplayer" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt;, an AI agent that works inside the Godot editor, has a hosted relay on its Pro and Ultra plans. Ask it to "add online multiplayer" and it connects your project to the relay and writes the networking code with Godot's own &lt;code&gt;WebSocketMultiplayerPeer&lt;/code&gt;, &lt;code&gt;@rpc&lt;/code&gt;, &lt;code&gt;MultiplayerSpawner&lt;/code&gt; and &lt;code&gt;MultiplayerSynchronizer&lt;/code&gt;. There is no server to run and no port to forward, players don't need a Ziva account, and because it uses WebSockets the same game also works in web exports. It suits co-op, party, card and turn-based games with up to 32 connections per room, not competitive shooters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; Ziva.sh publishes this page and makes Ziva. Facts about other tools come from their own pages and announcements, as of 5 November 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ways to play online without a server of your own, compared
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;How players connect&lt;/th&gt;
&lt;th&gt;Lobbies&lt;/th&gt;
&lt;th&gt;Web (HTML5) export&lt;/th&gt;
&lt;th&gt;What you run&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://ziva.sh/docs/multiplayer" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Ziva: AI agent in the Godot editor with a hosted relay; writes the code too&lt;/td&gt;
&lt;td&gt;Ziva: every message goes through Ziva's relay over secure WebSockets; no port forwarding&lt;/td&gt;
&lt;td&gt;Ziva: none built in; any string is a room, the agent writes a room-code screen&lt;/td&gt;
&lt;td&gt;Ziva: yes&lt;/td&gt;
&lt;td&gt;Ziva: nothing; one player's game is the host&lt;/td&gt;
&lt;td&gt;Ziva: included in Pro ($50/month) and Ultra; no per-player fees; traffic limited to 500 GB a month per account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://godotsteam.com/" rel="noopener noreferrer"&gt;GodotSteam&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;GodotSteam: community Steamworks integration with SteamMultiplayerPeer&lt;/td&gt;
&lt;td&gt;GodotSteam: Valve relays P2P traffic over its network&lt;/td&gt;
&lt;td&gt;GodotSteam: Steam lobbies&lt;/td&gt;
&lt;td&gt;GodotSteam: no (Linux, Mac, Windows)&lt;/td&gt;
&lt;td&gt;GodotSteam: nothing; players need Steam&lt;/td&gt;
&lt;td&gt;GodotSteam: free; Steam Direct costs $100 per game&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/3ddelano/epic-online-services-godot" rel="noopener noreferrer"&gt;EOSG&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;EOSG: community Godot 4.2+ plugin for Epic Online Services&lt;/td&gt;
&lt;td&gt;EOSG: Epic's P2P interface&lt;/td&gt;
&lt;td&gt;EOSG: Epic lobbies and sessions&lt;/td&gt;
&lt;td&gt;EOSG: not in its platform list&lt;/td&gt;
&lt;td&gt;EOSG: nothing&lt;/td&gt;
&lt;td&gt;EOSG: Epic Online Services is free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.gd-sync.com/" rel="noopener noreferrer"&gt;GD-Sync&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;GD-Sync: hosted multiplayer backend with a Godot 4 plugin&lt;/td&gt;
&lt;td&gt;GD-Sync: through GD-Sync's global relay network&lt;/td&gt;
&lt;td&gt;GD-Sync: public and private lobbies, matchmaking&lt;/td&gt;
&lt;td&gt;GD-Sync: yes&lt;/td&gt;
&lt;td&gt;GD-Sync: nothing&lt;/td&gt;
&lt;td&gt;GD-Sync: free plan 4 players per lobby, 200 MB per day; Indie $7/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://github.com/foxssake/noray" rel="noopener noreferrer"&gt;noray&lt;/a&gt; + netfox&lt;/td&gt;
&lt;td&gt;noray: open-source NAT punch-through orchestrator and UDP relay&lt;/td&gt;
&lt;td&gt;noray: punch-through first, UDP relay fallback&lt;/td&gt;
&lt;td&gt;noray: no&lt;/td&gt;
&lt;td&gt;noray: no (UDP)&lt;/td&gt;
&lt;td&gt;noray: your own noray server for a shipped game&lt;/td&gt;
&lt;td&gt;noray: free and open source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://heroiclabs.com/nakama/" rel="noopener noreferrer"&gt;Nakama&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Nakama: open-source game server with an official Godot 4 client&lt;/td&gt;
&lt;td&gt;Nakama: server-relayed or server-authoritative matches&lt;/td&gt;
&lt;td&gt;Nakama: matchmaker, friends, groups&lt;/td&gt;
&lt;td&gt;Nakama: not stated on its Godot page&lt;/td&gt;
&lt;td&gt;Nakama: your own Nakama server, or a paid Heroic Cloud deployment&lt;/td&gt;
&lt;td&gt;Nakama: free to self-host; Heroic Cloud is usage-based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://doc.photonengine.com/fusion-godot/v3-shared-authority/getting-started/sdk-download" rel="noopener noreferrer"&gt;Photon Fusion for Godot&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Photon: cloud netcode; Godot 4.6+ SDK released 26 October 2026&lt;/td&gt;
&lt;td&gt;Photon: through Photon's cloud&lt;/td&gt;
&lt;td&gt;Photon: not checked for the Godot SDK&lt;/td&gt;
&lt;td&gt;Photon: yes, with custom templates&lt;/td&gt;
&lt;td&gt;Photon: nothing&lt;/td&gt;
&lt;td&gt;Photon: price for the Godot SDK not checked; the SDK is a preview "not intended for production use"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Godot built-in ENet&lt;/td&gt;
&lt;td&gt;Godot: ENetMultiplayerPeer over UDP&lt;/td&gt;
&lt;td&gt;Godot: the host forwards a UDP port, or opens it with UPNP&lt;/td&gt;
&lt;td&gt;Godot: none&lt;/td&gt;
&lt;td&gt;Godot: no (ENet is UDP)&lt;/td&gt;
&lt;td&gt;Godot: the host's own PC&lt;/td&gt;
&lt;td&gt;Godot: free&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Talo's WebSocket channels avoid servers too but are, in Talo's words, for "lightweight, asynchronous interactions", not real-time play. Hathora stopped serving game studios in 2026, and Rivet now describes itself as an orchestrator for agent workloads, so older answers that recommend them are out of date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it works on LAN but not online
&lt;/h2&gt;

&lt;p&gt;On a LAN, your friend's game can reach yours directly. Over the internet, your router only lets in replies to connections you started, so a friend connecting to your ENet server is dropped. Godot's docs say you'll "probably have to forward the server port on your router", and it has to be UDP. You can try opening it from the game with Godot's &lt;code&gt;UPNP&lt;/code&gt; class, but it only works if the router has UPnP turned on.&lt;/p&gt;

&lt;p&gt;There are three ways around it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Port forwarding or UPnP.&lt;/strong&gt; Free, but every host has to change router settings, and it fails behind carrier-grade NAT.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NAT punch-through with a relay fallback.&lt;/strong&gt; Both players connect to a small coordinating server that helps them reach each other directly, and relays the traffic when that fails. noray does this for Godot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A relay for everything.&lt;/strong&gt; Every player connects out to a relay that forwards the messages. Nothing has to be opened on anyone's router, at the cost of a little extra delay. Steam (through GodotSteam), GD-Sync and Ziva's relay work this way.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to add multiplayer to a Godot game with Ziva
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://ziva.sh/docs/installation" rel="noopener noreferrer"&gt;Install Ziva&lt;/a&gt; in your Godot project and sign in with a Pro or Ultra plan.&lt;/li&gt;
&lt;li&gt;Ask in the chat, for example: "Add online multiplayer so a friend on another network can join me with a room code. I don't want to run a server."&lt;/li&gt;
&lt;li&gt;Ziva runs its &lt;code&gt;setup_multiplayer&lt;/code&gt; tool, which writes three settings into &lt;code&gt;project.godot&lt;/code&gt;: your account id, a game id and the relay address. They are public identifiers, not secrets, and they ship with your export.&lt;/li&gt;
&lt;li&gt;It then writes ordinary Godot multiplayer code: a &lt;code&gt;WebSocketMultiplayerPeer&lt;/code&gt; that joins a room on the relay, &lt;code&gt;@rpc&lt;/code&gt; functions, a &lt;code&gt;MultiplayerSpawner&lt;/code&gt; for players and &lt;code&gt;MultiplayerSynchronizer&lt;/code&gt; nodes for their movement.&lt;/li&gt;
&lt;li&gt;Run two copies of the game and test, or export it. Players only need the game; they don't sign in to anything.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One player's game acts as the host that spawns players and owns shared state. If the host leaves, another player can take over, but anything that wasn't synced to the others is lost. For card, board and turn-based games, ask Ziva to save room progress on the relay instead, so the game survives everyone leaving; see &lt;a href="https://ziva.sh/docs/multiplayer-state" rel="noopener noreferrer"&gt;Persistent and shared state&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you would rather use Steam, Epic Online Services, ENet or a dedicated server, say so and Ziva writes that code instead, without the relay, on any plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  An example: a co-op arena with room codes
&lt;/h2&gt;

&lt;p&gt;We tested this on 5 November 2026 in Godot 4.7.2, signed in to ziva.sh on a Pro plan, with Ziva's default model and the production relay. The prompt was: "Make a small 2D top-down game in a new scene res://mp/arena.tscn where each player is a colored square they move with the arrow keys. Add online multiplayer so a friend on another network can join me: a start screen with a Host button that shows a short room code, and a Join button with a field to type the code in. I don't want to run or rent a server. Make the start screen the main scene."&lt;/p&gt;

&lt;p&gt;Ziva first ran &lt;code&gt;setup_multiplayer&lt;/code&gt;, read its own multiplayer docs, then wrote a start menu scene, an arena scene and four scripts in &lt;code&gt;res://mp/&lt;/code&gt;, including a small autoload that carries the room code between scenes. The menu makes a random four-character code, joins the relay room with that name through &lt;code&gt;WebSocketMultiplayerPeer&lt;/code&gt; and shows the code; the Join button connects with the code the friend types. In the arena, the lowest player id is the host and spawns everyone through a &lt;code&gt;MultiplayerSpawner&lt;/code&gt;, and each square has a &lt;code&gt;MultiplayerSynchronizer&lt;/code&gt; for its position. Ziva then ran the menu itself, clicked Host and confirmed that it connected and showed a code.&lt;/p&gt;

&lt;p&gt;We ran two desktop copies of the game: the host got a code, the second copy joined with it, and each square's movement showed up in the other window. We then exported the same project to the web with no changes and opened it in two browser tabs: one hosted and the other joined. Finally a desktop copy hosted and a browser tab joined:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdegv2g3gs0l6xkpmggmf.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdegv2g3gs0l6xkpmggmf.webp" alt="A desktop Godot game window and a browser tab in the same room, KLGJ, each showing the same red and blue squares; the blue square is moved from the browser tab and appears at the same place in the desktop window" width="799" height="246"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No port was forwarded and no server was running; every copy connected out to Ziva's relay. In a separate test, two headless Godot clients sending RPCs through the relay measured a round trip of 27 to 34 ms from Japan. Your numbers depend on where the room is and where your players are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lobbies and room codes
&lt;/h2&gt;

&lt;p&gt;None of Godot's networking classes include lobbies. Steam (GodotSteam), Epic Online Services (EOSG), GD-Sync and Nakama each have their own lobby or matchmaking service. Ziva's relay has none: a room is just a name, and everyone who connects with the same name lands in the same room. A room-code screen is therefore a few lines of game code. The host makes a short random code and joins the room with that name, and the friend types the code to join the same room. Use random codes rather than words like "lobby", so strangers don't end up in your room.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multiplayer in web exports
&lt;/h2&gt;

&lt;p&gt;A browser can't open UDP sockets or accept incoming connections, so a Godot web export can't use ENet or run a WebSocket server. Godot's web exports support two kinds of networking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;WebSocketMultiplayerPeer&lt;/code&gt; as a client of a server or relay. When the page is served over HTTPS, as on most hosts, the server must be reachable over &lt;code&gt;wss://&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WebRTCMultiplayerPeer&lt;/code&gt;, which connects browsers to each other but needs a signaling server, and usually a STUN server, plus a TURN server for players behind strict NAT.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GodotSteam doesn't support web builds, the EOSG plugin doesn't list them, and noray relays UDP only. GD-Sync supports HTML5. Ziva's relay uses &lt;code&gt;WebSocketMultiplayerPeer&lt;/code&gt; over &lt;code&gt;wss://&lt;/code&gt;, so the multiplayer code Ziva writes runs in desktop and web exports alike. If you host the web build on your own site and it sends a Content Security Policy, that policy must allow &lt;code&gt;wss:&lt;/code&gt; connections.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Ziva's relay doesn't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No lobbies, matchmaking, server browser or friend invites; the game shares room codes itself.&lt;/li&gt;
&lt;li&gt;No dedicated or authoritative server: the relay forwards messages and never runs your game code, so it doesn't stop cheating.&lt;/li&gt;
&lt;li&gt;No region choice: each room runs in one Cloudflare location near whoever opened it, so players far from it see more delay. Fine for co-op, card and turn-based games; not for competitive shooters or fighting games.&lt;/li&gt;
&lt;li&gt;At most 32 connections per room, about 30 new connections per minute for the whole game, and about 4 MB of saved data per room with the code Ziva writes.&lt;/li&gt;
&lt;li&gt;Each account's relay traffic is limited to 500 GB per calendar month, with a warning email at 50 GB. Past the limit, new connections are refused until the next month; players already connected stay connected.&lt;/li&gt;
&lt;li&gt;Only on Pro and Ultra, and only in Godot. If the developer's subscription ends, the game stops accepting new multiplayer connections.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See &lt;a href="https://ziva.sh/docs/multiplayer-limitations" rel="noopener noreferrer"&gt;Multiplayer limitations&lt;/a&gt; for the details.&lt;/p&gt;

&lt;h2&gt;
  
  
  When another tool is better
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Shipping on Steam: GodotSteam.&lt;/li&gt;
&lt;li&gt;Fast, competitive games: netfox, with noray or your own server.&lt;/li&gt;
&lt;li&gt;Server-side game logic and many players: Nakama on your own server or Heroic Cloud, or Colyseus.&lt;/li&gt;
&lt;li&gt;Free hosting for a small game and you write the code yourself: GD-Sync's free plan or Epic Online Services.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ziva.sh/blogs/godot-multiplayer" rel="noopener noreferrer"&gt;Godot multiplayer in 2026: what actually works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ziva.sh/docs/multiplayer" rel="noopener noreferrer"&gt;Ziva multiplayer docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ziva.sh/docs/multiplayer-limitations" rel="noopener noreferrer"&gt;Multiplayer limitations&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I add online multiplayer to my Godot 4 game?
&lt;/h3&gt;

&lt;p&gt;Godot 4 has multiplayer built in: create an ENetMultiplayerPeer (or a WebSocketMultiplayerPeer if players join from web builds, with the server on a desktop or hosted machine), call create_server on the host and create_client on the others, assign it to multiplayer.multiplayer_peer, then sync the game with @rpc functions, a MultiplayerSpawner and MultiplayerSynchronizer nodes. Over the internet, the host usually has to forward a UDP port on their router, so many games add a relay: GodotSteam for Steam builds, Epic Online Services through the EOSG plugin, a hosted service such as GD-Sync, or Ziva's hosted relay, which Ziva, an AI agent inside the Godot editor, sets up on its Pro and Ultra plans when you ask it to add multiplayer.&lt;/p&gt;

&lt;h3&gt;
  
  
  How can my friends join my Godot game over the internet without port forwarding or running my own server?
&lt;/h3&gt;

&lt;p&gt;Use a relay that every player connects out to, so nobody has to accept incoming connections. GodotSteam gives you Valve's free relay and Steam lobbies, but only for players on Steam. The EOSG plugin wraps Epic Online Services, which is free. GD-Sync is a hosted Godot plugin with lobbies and a free plan for up to 4 players per lobby. On Ziva's Pro and Ultra plans, Ziva connects your game to its hosted relay over secure WebSockets, using Godot's built-in multiplayer API.&lt;/p&gt;

&lt;h3&gt;
  
  
  My Godot multiplayer works on LAN but not online. What's the easiest way to get NAT punchthrough or a relay working?
&lt;/h3&gt;

&lt;p&gt;It works on LAN because nothing blocks a local connection; over the internet the host's router drops the incoming UDP packets unless the port is forwarded. Your options: forward the port (or open it with Godot's UPNP class, which only works if the router allows it), add NAT punch-through with a relay fallback (noray with netfox), or send all traffic through a relay, which is what Steam's networking does by default and what GD-Sync and Ziva's hosted relay do.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I make a lobby system in Godot where players can host and join with a room code?
&lt;/h3&gt;

&lt;p&gt;Make a start screen with a Host button and a Join field. Host generates a short random code, shows it, and connects to a room named after that code; Join connects to the same room with the code the player typed. Where the room lives depends on your networking: Steam lobbies (GodotSteam), Epic sessions (EOSG), GD-Sync lobbies, a Nakama matchmaker, or a room on Ziva's relay, where any string is a room name. Ziva has no lobby service of its own; when you ask it for a room-code screen, it writes that screen and the connection code in your project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a Godot web (HTML5) export do online multiplayer, and what do I need for WebSocket or WebRTC?
&lt;/h3&gt;

&lt;p&gt;Yes, but a browser cannot open UDP sockets or run a WebSocket server, so ENet does not work. Use WebSocketMultiplayerPeer as a client of a server or relay reachable over wss:// (the page is served over https), or WebRTCMultiplayerPeer, which needs a signaling server, usually a STUN server, and a TURN server for players behind strict NAT. GodotSteam does not support web builds, and the EOSG plugin does not list them. GD-Sync supports HTML5, and Ziva's hosted relay uses WebSocketMultiplayerPeer over wss://, so the same multiplayer code runs in desktop and web exports.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use Steam networking for multiplayer in my Godot game, and what can I use if I'm not shipping on Steam?
&lt;/h3&gt;

&lt;p&gt;If you ship on Steam, GodotSteam is the usual choice: Valve relays the traffic for free and Steam handles lobbies, but every player needs Steam and it does not work in web builds. Without Steam, use Epic Online Services through the EOSG plugin (free, desktop and mobile), a hosted Godot service such as GD-Sync, Ziva's hosted relay (Pro and Ultra), a server you run with Nakama or Colyseus, or noray for NAT punch-through on your own server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there an AI assistant or Godot plugin that can set up online multiplayer in my game for me?
&lt;/h3&gt;

&lt;p&gt;Ziva is an AI agent inside the Godot editor that does this on its Pro and Ultra plans. Ask it to add multiplayer and it configures your project for Ziva's hosted relay, then writes the Godot code itself: WebSocketMultiplayerPeer, @rpc functions, MultiplayerSpawner and MultiplayerSynchronizer nodes, and screens such as a host and join menu. There is no server to run and the game needs no Ziva multiplayer add-on. If you would rather use Steam, Epic Online Services, ENet or a dedicated server, Ziva writes that code instead on any plan. GD-Sync and other multiplayer plugins give you a service but you write the game code.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the cheapest or free way to host online multiplayer for a small indie Godot game?
&lt;/h3&gt;

&lt;p&gt;Free options: Godot's built-in ENet with port forwarding, GodotSteam's relay (free, but publishing on Steam costs $100 per game), Epic Online Services through the EOSG plugin, GD-Sync's free plan (4 players per lobby, 200 MB of transfer per day), and noray's free public instance, which its authors don't recommend for shipped games. Ziva's hosted relay is included in Ziva's Pro plan ($50/month) and Ultra plan, with no per-player fees; each account's relay traffic is limited to 500 GB a month.&lt;/p&gt;

&lt;p&gt;The full guide is at &lt;a href="https://ziva.sh/blogs/godot-multiplayer-without-server" rel="noopener noreferrer"&gt;https://ziva.sh/blogs/godot-multiplayer-without-server&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;How do players join each other in your Godot game today? Drop your setup in the comments.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>multiplayer</category>
      <category>networking</category>
    </item>
    <item>
      <title>An unedited test of an AI assistant fixing a Godot runtime error</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Thu, 05 Nov 2026 08:27:08 +0000</pubDate>
      <link>https://dev.to/ziva/an-unedited-test-of-an-ai-assistant-fixing-a-godot-runtime-error-3m6p</link>
      <guid>https://dev.to/ziva/an-unedited-test-of-an-ai-assistant-fixing-a-godot-runtime-error-3m6p</guid>
      <description>&lt;p&gt;AI assistant demos for game engines are usually edited, so it is hard to tell what the tool did on its own. Here is one you can check: an unedited, real-time recording of Ziva, an AI assistant inside the Godot editor, working on a small sample game with one deliberate bug, plus the project itself so you can run the same test.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/l7fjl7f2sEg" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; Ziva.sh makes Ziva. The recording was made with a Ziva development build signed in to a local test account; the &lt;a href="https://github.com/ziva-sh/ziva-godot-sample" rel="noopener noreferrer"&gt;sample repository's README&lt;/a&gt; explains how it was set up and recorded.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sample and the bug
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/ziva-sh/ziva-godot-sample" rel="noopener noreferrer"&gt;Star Catcher&lt;/a&gt; is a tiny top-down Godot 4 game: move a square with the arrow keys and collect stars. Each star is meant to add one point to a score label.&lt;/p&gt;

&lt;p&gt;The bug is the kind you get after moving a node into its own scene. &lt;code&gt;main.gd&lt;/code&gt; looks the label up by its unique name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;onready&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;score_label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;ScoreLabel&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the label now lives in &lt;code&gt;hud.tscn&lt;/code&gt;, which &lt;code&gt;main.tscn&lt;/code&gt; instances. A &lt;code&gt;%&lt;/code&gt; unique name only resolves inside the scene that owns the node, so the game starts with &lt;code&gt;Node not found: "%ScoreLabel" (relative to "/root/Main")&lt;/code&gt;, and collecting a star throws a null-instance error at &lt;code&gt;main.gd:15&lt;/code&gt;. The score label never changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt
&lt;/h2&gt;

&lt;p&gt;This was the only message, and it does not mention the bug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add two more stars to the level and make the stars spin slowly. Then play the game, grab a star with the arrow keys and check that the score goes up.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Ziva read the scene tree and the project's three scripts and three scenes.&lt;/li&gt;
&lt;li&gt;It edited &lt;code&gt;star.gd&lt;/code&gt; to rotate each star and added two stars to &lt;code&gt;main.tscn&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It ran the game in the editor with &lt;code&gt;run_scene&lt;/code&gt;, holding the &lt;code&gt;ui_right&lt;/code&gt; input action (an injected action, not a physical key press). The player touched a star, the star disappeared, and the score stayed at 0.&lt;/li&gt;
&lt;li&gt;The run returned the two errors above, and they also appeared in Godot's Debugger.&lt;/li&gt;
&lt;li&gt;Ziva explained that the label's unique name lives inside &lt;code&gt;hud.tscn&lt;/code&gt;, not &lt;code&gt;main.tscn&lt;/code&gt;, and changed the lookup to &lt;code&gt;$HUD/ScoreLabel&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It ran the game again. The label read &lt;code&gt;Score: 1&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ziva's full diff is one commit on the &lt;a href="https://github.com/ziva-sh/ziva-godot-sample/tree/ziva-run-2026-11-04" rel="noopener noreferrer"&gt;&lt;code&gt;ziva-run-2026-11-04&lt;/code&gt; branch&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How reliable is it?
&lt;/h2&gt;

&lt;p&gt;Four takes were run with the same prompt. In the first, the sample had a more obvious bug (&lt;code&gt;$HUD/Score&lt;/code&gt; for a label in the same scene), and Ziva fixed it from reading the script before running anything, so the sample was changed to the HUD-scene version. In all three takes with this bug, Ziva found it by running the game and reading the error. The published take is the third, the only one where the Debugger panel was readable on screen. Model output varies, so your run will differ in star positions and wording.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it yourself
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Install Godot 4.7 and the &lt;a href="https://ziva.sh/docs/installation" rel="noopener noreferrer"&gt;Ziva plugin&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Clone &lt;a href="https://github.com/ziva-sh/ziva-godot-sample" rel="noopener noreferrer"&gt;https://github.com/ziva-sh/ziva-godot-sample&lt;/a&gt; and open it in Godot (stay on &lt;code&gt;main&lt;/code&gt;, which has the bug).&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;main.tscn&lt;/code&gt; and send the prompt above in a new Ziva chat.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To see the bug without Ziva, press F5 and walk right into the first star. Ziva's tools are also available to Claude Code, Codex and Cursor through its &lt;a href="https://ziva.sh/docs/ziva-mcp-server" rel="noopener noreferrer"&gt;local MCP server&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>ai</category>
      <category>gdscript</category>
    </item>
    <item>
      <title>How to generate sprites, pixel art and tiles for Godot with AI</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Thu, 05 Nov 2026 07:48:29 +0000</pubDate>
      <link>https://dev.to/ziva/how-to-generate-sprites-pixel-art-and-tiles-for-godot-with-ai-2b97</link>
      <guid>https://dev.to/ziva/how-to-generate-sprites-pixel-art-and-tiles-for-godot-with-ai-2b97</guid>
      <description>&lt;p&gt;To make 2D game art without leaving the Godot editor, ask &lt;a href="https://ziva.sh/docs/creating#images-and-sound-effects" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt;, an AI agent that works inside Godot. Type something like "make a 64x64 pixel-art knight facing right". Ziva generates the image, shows it in the chat and saves a PNG in &lt;code&gt;res://assets/generated/&lt;/code&gt;, where Godot imports it. It makes sprites with transparent backgrounds, seamless tiles and full backgrounds, in a pixel-art or smooth painted finish, and it can match a reference image you attach. It can then animate a sprite into a sprite sheet and set it up on an AnimatedSprite2D, or turn a tile into a TileSet on a TileMapLayer and paint a level with it. Image generation works on every Ziva plan, including the free Hobby plan.&lt;/p&gt;

&lt;p&gt;Ziva makes one image per request and one tile at a time. It does not generate full autotile sets, 8-direction character sheets or normal maps. Dedicated generators such as PixelLab and Retro Diffusion do more of that, and you import their files yourself. The table shows which tool does what.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; Ziva.sh publishes this page and makes Ziva. Facts about other tools come from their own pages, as of 4 November 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI sprite and tile tools for Godot compared
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Makes&lt;/th&gt;
&lt;th&gt;Transparent background&lt;/th&gt;
&lt;th&gt;How art gets into Godot&lt;/th&gt;
&lt;th&gt;Account and price&lt;/th&gt;
&lt;th&gt;Commercial use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ziva&lt;/td&gt;
&lt;td&gt;Ziva: AI agent in the Godot editor that also writes code and edits scenes&lt;/td&gt;
&lt;td&gt;Ziva: pixel-art or smooth sprites, seamless tiles, backgrounds; 2-16 frame animations from a sprite&lt;/td&gt;
&lt;td&gt;Ziva: sprites yes; tiles and backgrounds opaque&lt;/td&gt;
&lt;td&gt;Ziva: PNG saved to &lt;code&gt;res://assets/generated/&lt;/code&gt;; can set up AnimatedSprite2D frames and a TileSet on a TileMapLayer&lt;/td&gt;
&lt;td&gt;Ziva: every plan, including free &lt;a href="https://ziva.sh/pricing" rel="noopener noreferrer"&gt;Hobby&lt;/a&gt;; each image uses some of the plan's included usage&lt;/td&gt;
&lt;td&gt;See Ziva's &lt;a href="https://ziva.sh/terms" rel="noopener noreferrer"&gt;terms&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://godotengine.org/asset-library/asset/5332" rel="noopener noreferrer"&gt;Sprite Fusion&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Sprite Fusion: Godot plugin (Asset Library, Godot 4.5) plus web generator&lt;/td&gt;
&lt;td&gt;Sprite Fusion: pixel sprites 16-128 px, animations, 8 directions, normal maps&lt;/td&gt;
&lt;td&gt;Sprite Fusion: yes, when enabled&lt;/td&gt;
&lt;td&gt;Sprite Fusion: PNGs in &lt;code&gt;res://spritefusion/generated&lt;/code&gt; and a SpriteFrames resource&lt;/td&gt;
&lt;td&gt;Sprite Fusion: account; Starter $9/month, Creator $19/month, Pro $49/month&lt;/td&gt;
&lt;td&gt;Sprite Fusion: "Can I use the pixel art in my game? Yes."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://godotengine.org/asset-library/asset/5294" rel="noopener noreferrer"&gt;PixelMaker&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;PixelMaker: Godot plugin (Asset Library, Godot 4.2+, needs Python 3.10+)&lt;/td&gt;
&lt;td&gt;PixelMaker: sprites, backgrounds, walk-cycle frames, isometric tilesets&lt;/td&gt;
&lt;td&gt;PixelMaker: not stated&lt;/td&gt;
&lt;td&gt;PixelMaker: sprite sheet PNG and GIF; isometric tiles with a Godot 4 TileSet &lt;code&gt;.tres&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;PixelMaker: free plugin; your own OpenAI API key for AI features&lt;/td&gt;
&lt;td&gt;PixelMaker: not stated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/scenario-labs/godot-plugin" rel="noopener noreferrer"&gt;Scenario for Godot&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Scenario: Godot plugin (tested on Godot 4.7.2) for Scenario's cloud models&lt;/td&gt;
&lt;td&gt;Scenario: images, PBR materials with normal maps, skyboxes, 3D models&lt;/td&gt;
&lt;td&gt;Scenario: through background-removal models&lt;/td&gt;
&lt;td&gt;Scenario: sets the texture of the selected Sprite2D or TextureRect; materials saved as &lt;code&gt;.tres&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Scenario: API key; 50 free daily credits; Starter $15/month&lt;/td&gt;
&lt;td&gt;Scenario: paid plans commercial; free plan "personal and evaluation use only"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.pixellab.ai/" rel="noopener noreferrer"&gt;PixelLab&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;PixelLab: pixel-art web app with Aseprite and Pixelorama extensions, API and MCP server&lt;/td&gt;
&lt;td&gt;PixelLab: sprites, 4 and 8-direction characters, animations, Wang tilesets, textures&lt;/td&gt;
&lt;td&gt;PixelLab: yes&lt;/td&gt;
&lt;td&gt;PixelLab: export PNGs or sprite sheets, then import&lt;/td&gt;
&lt;td&gt;PixelLab: free trial with limited generations; paid plans&lt;/td&gt;
&lt;td&gt;PixelLab: commercial use allowed on all tiers; no training models on the output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://retrodiffusion.ai/" rel="noopener noreferrer"&gt;Retro Diffusion&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Retro Diffusion: pixel-art generator on the web, API, MCP and an Aseprite extension&lt;/td&gt;
&lt;td&gt;Retro Diffusion: sprites, animations, Wang and blob tilesets, seamless tiles&lt;/td&gt;
&lt;td&gt;Retro Diffusion: yes&lt;/td&gt;
&lt;td&gt;Retro Diffusion: download, then import&lt;/td&gt;
&lt;td&gt;Retro Diffusion: pay per image from $0.03; free to start; Aseprite extension $65&lt;/td&gt;
&lt;td&gt;Retro Diffusion: "All generated art can be used commercially."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://ludo.ai/docs/game-asset-generation" rel="noopener noreferrer"&gt;Ludo.ai&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Ludo.ai: web game-asset generator with API, MCP and a free CLI toolbox&lt;/td&gt;
&lt;td&gt;Ludo.ai: sprites, animated sprite sheets, single map tiles, seamless textures&lt;/td&gt;
&lt;td&gt;Ludo.ai: yes for sprites and map tiles&lt;/td&gt;
&lt;td&gt;Ludo.ai: download; its free CLI writes a Godot 4 SpriteFrames &lt;code&gt;.tres&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Ludo.ai: 30 free trial credits; Indie $20/month; API and MCP from Pro $50/month&lt;/td&gt;
&lt;td&gt;Ludo.ai: commercial use on every plan; free-plan sprite sheets are watermarked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://kenney.nl/assets" rel="noopener noreferrer"&gt;Kenney&lt;/a&gt;, &lt;a href="https://opengameart.org/" rel="noopener noreferrer"&gt;OpenGameArt&lt;/a&gt;, &lt;a href="https://itch.io/game-assets/free" rel="noopener noreferrer"&gt;itch.io&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;Kenney, OpenGameArt and itch.io: free asset libraries&lt;/td&gt;
&lt;td&gt;Libraries: ready-made sprites and tilesets&lt;/td&gt;
&lt;td&gt;Libraries: usually&lt;/td&gt;
&lt;td&gt;Libraries: download, then import&lt;/td&gt;
&lt;td&gt;Libraries: free&lt;/td&gt;
&lt;td&gt;Kenney: CC0; OpenGameArt and itch.io: check each asset's license&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Other generators are mostly web apps that end at a downloaded PNG. Ziva is an agent that edits your project, so after it makes a sprite it can add it to a scene, set up the animation or tileset and write the code that uses it. Two older Godot plugins are worth skipping: Piforge's API domain no longer resolves, and the Sprite Pipeline plugin says in its README that it is archived and unsupported.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to make sprites in Godot with Ziva
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://ziva.sh/docs/installation" rel="noopener noreferrer"&gt;Install Ziva&lt;/a&gt; in your Godot project and sign in. Any plan works, including the free Hobby plan.&lt;/li&gt;
&lt;li&gt;Ask for the art in the chat and say the style, size and what it is, for example "Make a 64x64 pixel-art knight in silver armor, facing right". You can attach a picture and ask for "something in this style".&lt;/li&gt;
&lt;li&gt;Ziva generates it, shows a preview in the chat and saves &lt;code&gt;knight.png&lt;/code&gt; in &lt;code&gt;res://assets/generated/&lt;/code&gt;. If a file with that name exists, Ziva saves a new one next to it instead of overwriting it.&lt;/li&gt;
&lt;li&gt;Ask for the next step in the same chat: "animate it walking, 6 frames, on an AnimatedSprite2D", "add it to my player scene" or "make a matching grass tile and set up my TileMapLayer with it".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each image uses a small amount of your plan's included usage, and the chat shows the cost. Turn the tools off in &lt;strong&gt;Settings → Agent → Enable image and sound generation tools&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  An example: a slime, its hop animation and a ground tile
&lt;/h2&gt;

&lt;p&gt;We tested this on 4 November 2026 in Godot 4.7.2, signed in to ziva.sh, with Ziva's default model. The first prompt was "Make a 64x64 pixel-art sprite of a small round green slime creature with a leaf sprouting from its head, facing right, for my 2D platformer. Then make a new scene res://scenes/sprite_demo.tscn with a Node2D root and add the slime to it as a Sprite2D near the middle so I can see it." The image step took 16 seconds. Ziva saved &lt;code&gt;slime.png&lt;/code&gt; with a transparent background and a 16-color palette, set the Sprite2D's texture filter to Nearest, ran the scene to check it, and pointed out that the face looked more front-facing than facing right.&lt;/p&gt;

&lt;p&gt;The second prompt was "Animate the slime hopping, 6 frames, and replace the Sprite2D in sprite_demo.tscn with an AnimatedSprite2D that plays the hop on loop." The animation step took 19 seconds and returned a 384x64 sheet of six 64x64 frames, which Ziva wired into a looping &lt;code&gt;hop&lt;/code&gt; animation. Here is the sprite and the sheet, enlarged 3 times:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi99g45ndmix5qpc7agxb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi99g45ndmix5qpc7agxb.webp" alt="A 64x64 pixel-art green slime with a leaf on its head, and a six-frame sprite sheet of the same slime squashing, stretching up and landing, generated by Ziva" width="800" height="135"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The frames keep the slime's colors and leaf, though the jumping frames are taller and narrower than the original, and one frame has a stray pixel to clean up.&lt;/p&gt;

&lt;p&gt;The third prompt asked for "a 32x32 pixel-art seamless tile of grass-topped dirt in the same style", set up as the TileSet of a new TileMapLayer with a ground strip 24 tiles wide. Ziva generated the tile in 14 seconds, created the TileSet and painted the strip. A fourth prompt asked for a mossy cobblestone floor tile. On the left is the cobblestone tile repeated 3x3, on the right the grass tile repeated 3 times:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxvqryybgzp9chxiqs7ai.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxvqryybgzp9chxiqs7ai.webp" alt="Left: a 32x32 mossy cobblestone pixel-art tile repeated in a 3x3 grid with no visible seams. Right: a 32x32 grass-topped dirt tile repeated in a row, where each tile contains two grass bands" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The cobblestone repeats with no visible seams. The grass tile joins cleanly side to side, but because Ziva makes tiles that repeat in every direction, it drew two grass bands in one tile. Ziva's tiles suit fills such as stone, dirt, water and floors; for a grass edge over dirt, ask for two separate tiles or use a terrain tileset tool.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpd0tkc32leg6tycm1u1z.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpd0tkc32leg6tycm1u1z.webp" alt="The Godot editor with the Ziva chat showing the generated slime, a scene with a Ground TileMapLayer and a Slime AnimatedSprite2D on the ground strip, and the SpriteFrames panel with the hop frames" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each of the four turns cost about 2 cents of usage, including the chat model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Animated sprite sheets and AnimatedSprite2D
&lt;/h2&gt;

&lt;p&gt;Ziva animates an existing sprite: it sends your sprite to the image model with the motion you describe ("walking right", "idle breathing", "sword swing") and gets back 2 to 16 frames in one row, each the size of the original and at most 1536 px wide in total, with one shared palette for pixel art. It saves the sheet and a small metadata file, then builds the SpriteFrames animation on your AnimatedSprite2D. It does not make 4 or 8-direction sheets in one go; ask for each direction separately.&lt;/p&gt;

&lt;p&gt;For a sheet from another tool:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select your &lt;strong&gt;AnimatedSprite2D&lt;/strong&gt;, and in the Inspector set &lt;strong&gt;Sprite Frames&lt;/strong&gt; to &lt;strong&gt;New SpriteFrames&lt;/strong&gt;, then click it to open the &lt;strong&gt;SpriteFrames&lt;/strong&gt; panel.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add frames from a Sprite Sheet&lt;/strong&gt; (the grid icon), pick the PNG, and set &lt;strong&gt;Horizontal&lt;/strong&gt; and &lt;strong&gt;Vertical&lt;/strong&gt; to the number of columns and rows.&lt;/li&gt;
&lt;li&gt;Click the frames in order, add them, then set the animation's FPS and loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To split a sheet into separate PNGs and a Godot 4 &lt;code&gt;.tres&lt;/code&gt; in the browser, use Ziva's free &lt;a href="https://ziva.sh/tools/sprite-sheet-slicer" rel="noopener noreferrer"&gt;Sprite Sheet Slicer&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tilesets and TileMapLayer
&lt;/h2&gt;

&lt;p&gt;Ziva generates one tile per request, square, 3:2 or 2:3, as a seamlessly repeating opaque texture. Ask it to set the tile up and it creates a TileSet with an atlas on your TileMapLayer, at the tile size you give, with optional full-tile collision, and can paint horizontal and vertical lines, stairs and filled areas. This needs Godot 4.3 or later. Ziva does not generate full autotile, Wang or terrain sets. Its tiles repeat in every direction, which suits fills such as stone, dirt and floors better than edge tiles, and the seam comes from the prompt plus a center crop, so check the joins.&lt;/p&gt;

&lt;p&gt;To set up terrain autotiling by hand in Godot 4:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select the TileMapLayer, open its &lt;strong&gt;TileSet&lt;/strong&gt; in the Inspector and add a &lt;strong&gt;Terrain Set&lt;/strong&gt; (mode &lt;strong&gt;Match Corners and Sides&lt;/strong&gt; for a 47-tile blob set) with one terrain.&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;TileSet&lt;/strong&gt; panel at the bottom, choose &lt;strong&gt;Paint&lt;/strong&gt;, pick the terrain property, and paint the peering bits on each tile.&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;TileMap&lt;/strong&gt; panel, open the &lt;strong&gt;Terrains&lt;/strong&gt; tab, pick the terrain and paint; Godot picks the matching tiles.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ziva's free &lt;a href="https://ziva.sh/tools/autotile-template-generator" rel="noopener noreferrer"&gt;Autotile Template Generator&lt;/a&gt; makes a 47-tile blob, 16-tile or 3x3 template as a PNG plus a TileSet &lt;code&gt;.tres&lt;/code&gt; with the peering bits already set, so you only paint over the template. The &lt;a href="https://ziva.sh/blogs/godot-tilemap" rel="noopener noreferrer"&gt;Godot TileMapLayer tutorial&lt;/a&gt; covers layers, collision and painting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transparent backgrounds and crisp pixel art
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transparency.&lt;/strong&gt; Ziva asks the image model for a real transparent background on every sprite and saves a PNG with an alpha channel. Many chat apps paint a gray-and-white checkerboard into the pixels instead. To check any PNG, put it over a colored ColorRect in Godot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pixel finish.&lt;/strong&gt; Ziva snaps pixel art to its grid with nearest-neighbor scaling, makes every pixel fully opaque or fully transparent and limits the palette to 16 colors, so there are no blurry in-between pixels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blurry in Godot.&lt;/strong&gt; Godot's default filter smooths small sprites. Set &lt;strong&gt;Project Settings → Rendering → Textures → Canvas Textures → Default Texture Filter&lt;/strong&gt; to &lt;strong&gt;Nearest&lt;/strong&gt;, or set &lt;strong&gt;Texture → Filter&lt;/strong&gt; to &lt;strong&gt;Nearest&lt;/strong&gt; on one node. Ziva's own pixel-art results remind the agent to do this, but it does not change import settings for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Seamless textures and normal maps
&lt;/h2&gt;

&lt;p&gt;A Ziva tile is a seamless texture you can also use outside a TileMap. In 2D, set &lt;strong&gt;Texture → Repeat&lt;/strong&gt; to &lt;strong&gt;Enabled&lt;/strong&gt; on a Sprite2D, turn on &lt;strong&gt;Region&lt;/strong&gt; and make the region larger than the texture; in 3D, put the PNG in a StandardMaterial3D's &lt;strong&gt;Albedo → Texture&lt;/strong&gt; and raise &lt;strong&gt;UV1 → Scale&lt;/strong&gt;. Ziva does not make normal maps or PBR materials. For 2D lighting, Ziva's free &lt;a href="https://ziva.sh/tools/normal-map-generator" rel="noopener noreferrer"&gt;Normal Map Generator&lt;/a&gt; makes one from a sprite in your browser; for 3D materials, Scenario's Godot plugin generates PBR materials, and the &lt;a href="https://ziva.sh/tools/seamless-texture-generator" rel="noopener noreferrer"&gt;Seamless Texture Generator&lt;/a&gt; makes your own image tileable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Ziva's image generation doesn't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One image per request; no variations, seeds or style presets. Ask again for another take.&lt;/li&gt;
&lt;li&gt;No full tileset, autotile or terrain set, and no multi-row or 4 and 8-direction sprite sheets.&lt;/li&gt;
&lt;li&gt;No normal maps, PBR materials, background removal, upscaling or editing of an existing image.&lt;/li&gt;
&lt;li&gt;Named characters and brands are rejected by the image model; describe the look instead.&lt;/li&gt;
&lt;li&gt;In Godot it works in Ziva's default (agent) mode, not in Plan or Ask mode.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When another tool is better
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Large, consistent pixel-art sets, 8-direction characters and Wang tilesets: PixelLab or Retro Diffusion, then import the files.&lt;/li&gt;
&lt;li&gt;PBR materials and normal maps for 3D: Scenario's Godot plugin.&lt;/li&gt;
&lt;li&gt;Free, ready-made art with a clear license: Kenney's CC0 packs.&lt;/li&gt;
&lt;li&gt;Hand-drawn polish: generate a base with any tool, then clean it up in Aseprite or Pixelorama. The &lt;a href="https://ziva.sh/blogs/pixel-art-tutorial" rel="noopener noreferrer"&gt;pixel art tutorial&lt;/a&gt; covers editing and sprite-sheet export.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ziva.sh/blogs/godot-ai-3d-models" rel="noopener noreferrer"&gt;How to generate 3D models for Godot with AI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ziva.sh/blogs/godot-ai-sound-effects" rel="noopener noreferrer"&gt;How to generate sound effects for Godot with AI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ziva.sh/docs/creating#images-and-sound-effects" rel="noopener noreferrer"&gt;Create and test: images, sound and 3D models&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How can I use AI to make sprites and other 2D art for my Godot game?
&lt;/h3&gt;

&lt;p&gt;Inside the Godot editor, ask Ziva, an AI agent that works in Godot, for the art in plain words, such as "make a 64x64 pixel-art knight facing right". Ziva generates the image, shows it in the chat and saves a PNG in res://assets/generated/, where Godot imports it. It makes sprites with transparent backgrounds, seamless tiles and backgrounds, can animate a sprite into a sprite sheet and can set it up on an AnimatedSprite2D or a TileMapLayer. With a web generator such as PixelLab, Retro Diffusion or Ludo.ai, you download the PNG and import and set it up in Godot yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there a Godot editor plugin that can generate sprites or textures with AI, so I don't have to leave the editor?
&lt;/h3&gt;

&lt;p&gt;Yes. Ziva generates sprites, seamless tiles, backgrounds and sprite-sheet animations from a prompt inside the editor on every plan, including the free Hobby plan, and is also a general AI agent that writes GDScript and edits scenes. Sprite Fusion's Godot plugin generates pixel-art sprites and animations with a Sprite Fusion account and builds a SpriteFrames resource. PixelMaker generates sprites, backgrounds and isometric tilesets with your own OpenAI API key. Scenario's Godot plugin generates images, PBR materials and 3D models with a Scenario API key.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the best AI pixel art or sprite sheet generator, and can I use what it makes in a commercial game?
&lt;/h3&gt;

&lt;p&gt;Dedicated pixel-art generators such as PixelLab and Retro Diffusion give the most control: PixelLab makes 4 and 8-direction characters and Wang tilesets, and Retro Diffusion sells per image from $0.03. Both say on their own pages that generated art can be used commercially. Ludo.ai licenses output for commercial use on every plan; Scenario's free plan is for personal and evaluation use only. Ziva is the choice when you want the art made, saved and set up inside your Godot project; its terms are at ziva.sh/terms. Steam asks you to disclose AI-generated content that ships in your game.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I make an animated sprite sheet, like a walk cycle, with AI and get it playing in an AnimatedSprite2D in Godot?
&lt;/h3&gt;

&lt;p&gt;With Ziva, first make or attach the character sprite, then ask "animate it walking right, 6 frames, and set it up on my AnimatedSprite2D". Ziva generates the frames in one row from your sprite, saves the sheet and builds the SpriteFrames animation for you. With a sheet from another tool, select the AnimatedSprite2D, create a new SpriteFrames, open the SpriteFrames panel, click "Add frames from a Sprite Sheet", set the horizontal and vertical frame counts, select the frames and set the FPS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I generate a tileset with AI and set it up as a TileMap with terrain autotiling in Godot 4?
&lt;/h3&gt;

&lt;p&gt;Partly. Ziva generates one seamless tile per request and can turn a tile image into a TileSet atlas on a TileMapLayer (Godot 4.3+) and paint lines, stairs and fills with it, but it does not generate a full autotile or terrain set. PixelLab and Retro Diffusion generate Wang tilesets, and Layer's MCP tool exports a Godot 4 TileSet. For terrain autotiling, add a terrain set to the TileSet, paint the peering bits for each tile in the TileSet panel and paint with the Terrains tab of the TileMap panel. Ziva's free Autotile Template Generator gives you a 47-tile blob template with the peering bits already set.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I make seamless, tileable textures with AI for my Godot game, including normal maps?
&lt;/h3&gt;

&lt;p&gt;Ask Ziva for a tile, for example "make a 32x32 pixel-art cobblestone tile", and it generates a seamlessly repeating, opaque texture in res://assets/generated/. Ziva does not generate normal maps or PBR materials; use Ziva's free Normal Map Generator in the browser for 2D sprites, or Scenario's Godot plugin for 3D materials. In Godot 2D, set Texture &amp;gt; Repeat to Enabled on the node; in 3D, put the texture in a StandardMaterial3D and raise UV1 &amp;gt; Scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I get AI to generate game sprites as PNGs with a real transparent background, not a fake checkerboard?
&lt;/h3&gt;

&lt;p&gt;Use a tool that asks the image model for real transparency instead of drawing it. Ziva requests a transparent background from the model for every sprite and saves a PNG with an alpha channel; tiles and backgrounds come back opaque. PixelLab, Retro Diffusion, Ludo.ai and Sprite Fusion also offer transparent backgrounds. Chat apps often draw a gray-and-white checkerboard into the pixels instead; check by placing the PNG over a colored background in Godot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where can I get free 2D sprites and tilesets that I'm allowed to use in a commercial game?
&lt;/h3&gt;

&lt;p&gt;Kenney's asset packs are public domain (CC0) and can be used in commercial games without credit. On OpenGameArt and itch.io, check the license on each asset before you use it. Import them by copying the files into your Godot project folder.&lt;/p&gt;

&lt;p&gt;The full guide is at &lt;a href="https://ziva.sh/blogs/godot-ai-sprites" rel="noopener noreferrer"&gt;https://ziva.sh/blogs/godot-ai-sprites&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Where do the sprites and tiles in your Godot game come from today? Drop your workflow in the comments.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>ai</category>
      <category>pixelart</category>
    </item>
    <item>
      <title>How to generate 3D models for Godot with AI</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Wed, 04 Nov 2026 17:17:52 +0000</pubDate>
      <link>https://dev.to/ziva/how-to-generate-3d-models-for-godot-with-ai-40f5</link>
      <guid>https://dev.to/ziva/how-to-generate-3d-models-for-godot-with-ai-40f5</guid>
      <description>&lt;p&gt;To make 3D models without leaving the Godot editor, ask &lt;a href="https://ziva.sh/docs/creating#3d-models" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt;, an AI agent that works inside Godot. Type something like "make a low-poly wooden treasure chest with metal corner bands and put it in a test scene". On Ziva Pro or Ultra, Ziva writes a Blender Python script, runs it in a copy of Blender it downloads to your computer, shows you a rendered preview in the chat and saves the model in your project as a &lt;code&gt;.blend&lt;/code&gt; file, which Godot imports as a scene. It can then add the model to a scene and change it when you ask.&lt;/p&gt;

&lt;p&gt;Ziva is not a text-to-3D generator like Meshy or Tripo. It builds models from shapes, materials and modifiers in code, which suits editable low-poly props, buildings, furniture and stylized scenery. For organic characters, creatures or realistic sculpted detail, use a text-to-3D or image-to-3D tool and import its GLB file. The table shows which tool does what.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; Ziva.sh publishes this page and makes Ziva. Facts about other tools come from their own pages, as of 4 November 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI 3D model tools for Godot compared
&lt;/h2&gt;

&lt;p&gt;Ziva's 3D output: a &lt;code&gt;.blend&lt;/code&gt; file made by a Blender Python script that Ziva runs in its own downloaded Blender, on Pro and Ultra plans. Ziva's 3D Model Generator does not use Rodin, Meshy, Tripo, Hunyuan 3D or any other text-to-3D or image-to-3D model.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;How models get into Godot&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Account and price&lt;/th&gt;
&lt;th&gt;Commercial use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/scenario-labs/godot-plugin" rel="noopener noreferrer"&gt;Scenario for Godot&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Scenario for Godot: editor dock (its README says tested on Godot 4.7.2) running Rodin, Meshy, Tripo and Hunyuan 3D models&lt;/td&gt;
&lt;td&gt;Scenario: text, image&lt;/td&gt;
&lt;td&gt;Scenario: GLB instanced under the selected Node3D&lt;/td&gt;
&lt;td&gt;Scenario: GLB&lt;/td&gt;
&lt;td&gt;Scenario: API key; 50 free daily credits; Starter $15/month&lt;/td&gt;
&lt;td&gt;Scenario: paid plans commercial; free plan personal and evaluation use only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://rapidassets.ai/blog/godot-ai-plugin" rel="noopener noreferrer"&gt;Rapid Assets&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Rapid Assets: free plugin for Godot 4.0+ on Windows and macOS&lt;/td&gt;
&lt;td&gt;Rapid Assets: text; some workflows take a reference image&lt;/td&gt;
&lt;td&gt;Rapid Assets: GLB saved to &lt;code&gt;res://RapidAssets/Models/&lt;/code&gt;, dropped into the scene as a MeshInstance3D&lt;/td&gt;
&lt;td&gt;Rapid Assets: GLB&lt;/td&gt;
&lt;td&gt;Rapid Assets: free account; generation runs in its cloud; paid limits not stated&lt;/td&gt;
&lt;td&gt;Rapid Assets: "intended for commercial and non-commercial" projects; check its terms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://developers.tripo3d.ai/docs/plugins" rel="noopener noreferrer"&gt;Tripo plugin&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Tripo plugin v0.02, Godot 4.4+&lt;/td&gt;
&lt;td&gt;Tripo: text, image&lt;/td&gt;
&lt;td&gt;Tripo: previewed, saved or added to the current scene as a Node3D&lt;/td&gt;
&lt;td&gt;Tripo: not stated for the plugin&lt;/td&gt;
&lt;td&gt;Tripo: API key; prepaid, $1 = 100 credits, 10-50 credits per model&lt;/td&gt;
&lt;td&gt;Tripo: check its terms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/DeemosTech/Godot-Rodin-Plugin" rel="noopener noreferrer"&gt;Hyper3D Rodin plugin&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Hyper3D Rodin's RodinBridge, Godot 4.4+, needs the Rodin webpage open in Chrome 116+&lt;/td&gt;
&lt;td&gt;Rodin plugin: image&lt;/td&gt;
&lt;td&gt;Rodin plugin: imported into Godot automatically&lt;/td&gt;
&lt;td&gt;Rodin: not stated&lt;/td&gt;
&lt;td&gt;Hyper3D Rodin: free plan, credits $1.50 each; Creator $30/month&lt;/td&gt;
&lt;td&gt;Hyper3D Rodin: paid plans "Unlimited export and any use"; free plan not stated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.meshy.ai/integrations/godot" rel="noopener noreferrer"&gt;Meshy&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Meshy: web generator with a Godot bridge plugin from meshy.ai&lt;/td&gt;
&lt;td&gt;Meshy: text, image&lt;/td&gt;
&lt;td&gt;Meshy: sent from your Meshy workspace to Godot; no generation inside Godot&lt;/td&gt;
&lt;td&gt;Meshy: GLB, FBX, OBJ, USDZ, STL, &lt;code&gt;.blend&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Meshy: the Godot bridge needs Pro ($20/month) or above; free plan 100 credits a month&lt;/td&gt;
&lt;td&gt;Meshy: free plan CC BY 4.0 (credit required); paid plans private&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://godotengine.org/asset-library/asset/5395" rel="noopener noreferrer"&gt;Sloyd&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Sloyd: web generator; Sloyd Godot Bridge on the Asset Library, Godot 4.2+&lt;/td&gt;
&lt;td&gt;Sloyd: text, image, templates&lt;/td&gt;
&lt;td&gt;Sloyd: sent from the Sloyd web app "straight into your active scene"&lt;/td&gt;
&lt;td&gt;Sloyd: GLB, OBJ, FBX&lt;/td&gt;
&lt;td&gt;Sloyd: 1 free generation a day; Plus $15/month&lt;/td&gt;
&lt;td&gt;Sloyd: free use is personal only; Plus and up commercial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/ahujasid/blender-mcp" rel="noopener noreferrer"&gt;MCP for Blender&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;MCP for Blender: Blender add-on that lets Claude, Codex, Cursor and others drive Blender&lt;/td&gt;
&lt;td&gt;MCP for Blender: prompt in your AI client&lt;/td&gt;
&lt;td&gt;MCP for Blender: export from Blender, then import&lt;/td&gt;
&lt;td&gt;MCP for Blender: anything Blender exports&lt;/td&gt;
&lt;td&gt;MCP for Blender: free with your own Meshy, Hunyuan3D or Rodin keys; Premium from $10/month&lt;/td&gt;
&lt;td&gt;MCP for Blender: depends on the model provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://github.com/microsoft/TRELLIS.2" rel="noopener noreferrer"&gt;TRELLIS.2&lt;/a&gt; and &lt;a href="https://github.com/Tencent-Hunyuan/Hunyuan3D-2.1" rel="noopener noreferrer"&gt;Hunyuan3D 2.1&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;TRELLIS.2 and Hunyuan3D 2.1: open image-to-3D models you run yourself&lt;/td&gt;
&lt;td&gt;TRELLIS.2 and Hunyuan3D 2.1: image&lt;/td&gt;
&lt;td&gt;TRELLIS.2 and Hunyuan3D 2.1: import the GLB&lt;/td&gt;
&lt;td&gt;TRELLIS.2: GLB; Hunyuan3D 2.1: mesh (GLB in its examples)&lt;/td&gt;
&lt;td&gt;TRELLIS.2 and Hunyuan3D 2.1: free; TRELLIS.2 needs an NVIDIA GPU with at least 24 GB&lt;/td&gt;
&lt;td&gt;TRELLIS.2: MIT; Hunyuan3D 2.1: license excludes the EU, UK and South Korea&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://kenney.nl/assets" rel="noopener noreferrer"&gt;Kenney&lt;/a&gt;, &lt;a href="https://quaternius.com/" rel="noopener noreferrer"&gt;Quaternius&lt;/a&gt;, &lt;a href="https://kaylousberg.com/" rel="noopener noreferrer"&gt;KayKit&lt;/a&gt;, &lt;a href="https://poly.pizza/" rel="noopener noreferrer"&gt;Poly Pizza&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;Kenney, Quaternius, KayKit and Poly Pizza: free 3D asset libraries&lt;/td&gt;
&lt;td&gt;Libraries: ready-made models, no prompt&lt;/td&gt;
&lt;td&gt;Libraries: download, then import&lt;/td&gt;
&lt;td&gt;Libraries: formats vary per pack&lt;/td&gt;
&lt;td&gt;Kenney, Quaternius, Poly Pizza and KayKit's free packs: free&lt;/td&gt;
&lt;td&gt;Kenney: CC0; Quaternius: free for commercial games, no resale as assets; Poly Pizza: per model, CC0 or CC-BY; KayKit: CC0 (pack checked)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ziva&lt;/td&gt;
&lt;td&gt;Ziva: AI agent in the Godot editor; runs a Blender Python script in a copy of Blender that Ziva downloads to your computer&lt;/td&gt;
&lt;td&gt;Ziva: text, or a reference image the agent looks at&lt;/td&gt;
&lt;td&gt;Ziva: saved to &lt;code&gt;res://&lt;/code&gt;; Godot imports &lt;code&gt;.blend&lt;/code&gt; as a scene; the agent can add it to a scene&lt;/td&gt;
&lt;td&gt;Ziva: &lt;code&gt;.blend&lt;/code&gt;; in our tests the agent also added a &lt;code&gt;.glb&lt;/code&gt; export to its Blender script and used that in the scene&lt;/td&gt;
&lt;td&gt;Ziva: &lt;a href="https://ziva.sh/pricing" rel="noopener noreferrer"&gt;Pro or Ultra plan&lt;/a&gt;; one-time Blender download of about 260-390 MB&lt;/td&gt;
&lt;td&gt;Ziva: see Ziva's &lt;a href="https://ziva.sh/terms" rel="noopener noreferrer"&gt;terms&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cloud generators return a mesh made by a neural model and stop there, or import it into a scene. Ziva's models are built by scripts, so you can ask for changes to proportions, colors or parts in the same chat, and because Ziva is an agent that edits your scenes and scripts, it can also place the model in a scene and wire it into your game.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to make a 3D model in Godot with Ziva
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://ziva.sh/docs/installation" rel="noopener noreferrer"&gt;Install Ziva&lt;/a&gt; in your Godot project and sign in on a Pro or Ultra plan.&lt;/li&gt;
&lt;li&gt;Open &lt;strong&gt;Settings → Pro Features&lt;/strong&gt; and click &lt;strong&gt;Download&lt;/strong&gt; under &lt;strong&gt;3D Model Generator&lt;/strong&gt;. If you skip this, Ziva asks "Enable Ziva's 3D Model Generator?" with the download size the first time you ask for a model. It installs its own copy of Blender 5.2.2; on Linux the download was 365.5 MB.&lt;/li&gt;
&lt;li&gt;Ask for a model in the chat, with size, style and where to put it, for example "Make a stylized low-poly pine tree, about 3 meters tall, with a trunk and three cone layers of leaves, save it in &lt;code&gt;res://models/&lt;/code&gt; and add it to my test scene." You can attach a reference image as well.&lt;/li&gt;
&lt;li&gt;Ziva runs its Blender script, renders a preview from a camera in the Blender scene and shows it in the chat. Ask for changes ("make the lid open wider", "darker wood") and it can edit the same &lt;code&gt;.blend&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The model shows up in the FileSystem dock. Godot's own &lt;code&gt;.blend&lt;/code&gt; importer turns it into a scene you can drag into any level. On Godot 4.3 or later Ziva points Godot at its Blender if the &lt;strong&gt;Blender Path&lt;/strong&gt; editor setting is empty, so there is no setup dialog.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The 3D Model Generator runs on macOS (Apple silicon), Windows (x64 or ARM64) and Linux (x64).&lt;/p&gt;

&lt;h2&gt;
  
  
  An example: a treasure chest and a pine tree
&lt;/h2&gt;

&lt;p&gt;We tested this on 4 November 2026 in Godot 4.7.2 with a Pro account. One prompt was "Make a low-poly wooden treasure chest with metal corner bands and a slightly open lid for my game, save it in res://models/, and put it in a new scene res://scenes/chest_test.tscn so I can see it." The Blender script itself ran in about 2 seconds and Ziva showed this preview in the chat:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6vi1i7j4l4eczg81iyg2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6vi1i7j4l4eczg81iyg2.webp" alt="Blender preview of a low-poly wooden treasure chest with dark metal corner bands and an open lid, rendered by Ziva" width="512" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ziva saved &lt;code&gt;treasure_chest.blend&lt;/code&gt;, exported &lt;code&gt;treasure_chest.glb&lt;/code&gt;, set up the test scene with a floor, light and camera, and ran the scene to check the framing. A second prompt asked for "a stylized low-poly pine tree, about 3 meters tall, with a trunk and three cone layers of leaves" next to the chest. Here are both models running in Godot:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiytd88liy1857lj4szar.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiytd88liy1857lj4szar.webp" alt="The low-poly treasure chest and a 3 m low-poly pine tree with three green cone layers, running in a Godot scene" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The chest is 296 triangles with 3 materials and the tree is 70 triangles with 2, both flat colors with no textures. That is the style this approach is good at: simple, light props you can change by asking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Import an AI-generated GLB or a Blender file into Godot 4
&lt;/h2&gt;

&lt;p&gt;For files from Meshy, Tripo, Rodin or any other generator:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Format.&lt;/strong&gt; Godot's docs recommend glTF 2.0 (&lt;code&gt;.gltf&lt;/code&gt; or &lt;code&gt;.glb&lt;/code&gt;). Copy the file into your project folder and Godot imports it as a scene.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blender files.&lt;/strong&gt; Godot imports &lt;code&gt;.blend&lt;/code&gt; files by calling Blender to export glTF in the background. Set &lt;strong&gt;Editor Settings → FileSystem → Import → Blender → Blender Path&lt;/strong&gt; to your Blender executable. Teammates who open the project need Blender too, so also export a &lt;code&gt;.glb&lt;/code&gt; for teammates without Blender.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale.&lt;/strong&gt; Select the file, open the &lt;strong&gt;Import&lt;/strong&gt; dock and change &lt;strong&gt;Root Scale&lt;/strong&gt;, then click &lt;strong&gt;Reimport&lt;/strong&gt;. Generated models often come in at an arbitrary size, so check them next to a 1 m reference cube.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collision.&lt;/strong&gt; Before export, add &lt;code&gt;-col&lt;/code&gt; (mesh plus static collision) or &lt;code&gt;-colonly&lt;/code&gt; (collision only) to the end of a mesh's name. Or double-click the file to open &lt;strong&gt;Advanced Import Settings&lt;/strong&gt;, select the mesh and enable &lt;strong&gt;Physics&lt;/strong&gt; with a Body Type and a Shape Type. Trimesh shapes only work with Static bodies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Materials.&lt;/strong&gt; In &lt;strong&gt;Advanced Import Settings&lt;/strong&gt;, use &lt;strong&gt;Actions… → Extract Materials&lt;/strong&gt; to save them as &lt;code&gt;.tres&lt;/code&gt; files you can edit. Godot keeps your edits when the source file is reimported.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;AI-generated meshes often have far more triangles than a game needs. Decimate them in Blender, and keep &lt;strong&gt;Generate LODs&lt;/strong&gt; on in the import settings so distant copies draw fewer triangles.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Ziva's 3D Model Generator doesn't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No neural text-to-3D or image-to-3D generation. Models are built from shapes in Blender scripts, so detailed organic shapes and realistic sculpts are out of reach.&lt;/li&gt;
&lt;li&gt;A reference image guides the script; it is not reconstructed into a matching mesh.&lt;/li&gt;
&lt;li&gt;No built-in rigging, animation, retopology or LOD step.&lt;/li&gt;
&lt;li&gt;Pro and Ultra plans only. Hobby and Basic see an upgrade card.&lt;/li&gt;
&lt;li&gt;Intel Macs and Linux ARM64 are not supported.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When another tool is better
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Characters, creatures and organic props: Meshy, Tripo, Rodin or Scenario, then import the model.&lt;/li&gt;
&lt;li&gt;Free, offline image-to-3D on your own GPU: TRELLIS.2.&lt;/li&gt;
&lt;li&gt;Ready-made, consistent asset sets at no cost: Kenney, Quaternius or KayKit.&lt;/li&gt;
&lt;li&gt;Driving Blender from Claude Code, Codex or Cursor outside Godot: MCP for Blender, or &lt;a href="https://ziva.sh/docs/ziva-mcp-server" rel="noopener noreferrer"&gt;Ziva's MCP server&lt;/a&gt; for the Godot side.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ziva.sh/blogs/best-ai-3d-asset-generators" rel="noopener noreferrer"&gt;AI 3D asset generators for games: what to check before choosing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ziva.sh/blogs/godot-ai-sound-effects" rel="noopener noreferrer"&gt;How to generate sound effects for Godot with AI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ziva.sh/docs/creating#3d-models" rel="noopener noreferrer"&gt;Create and test: playtests, images, sound and 3D models&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full guide, with an FAQ, is at &lt;a href="https://ziva.sh/blogs/godot-ai-3d-models" rel="noopener noreferrer"&gt;https://ziva.sh/blogs/godot-ai-3d-models&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Where do the 3D models in your Godot game come from today? Drop your workflow in the comments.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>ai</category>
      <category>blender</category>
    </item>
    <item>
      <title>Can an AI assistant fix Godot C# build errors? We tested Ziva on a .NET project</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Wed, 04 Nov 2026 14:22:46 +0000</pubDate>
      <link>https://dev.to/ziva/can-an-ai-assistant-fix-godot-c-build-errors-we-tested-ziva-on-a-net-project-4ja2</link>
      <guid>https://dev.to/ziva/can-an-ai-assistant-fix-godot-c-build-errors-we-tested-ziva-on-a-net-project-4ja2</guid>
      <description>&lt;p&gt;A common test for any AI coding assistant on a Godot C# project: break the build on purpose and see whether the assistant notices. We ran that test on &lt;a href="https://ziva.sh/docs/godot-csharp" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt;, an AI assistant that works inside the Godot editor, with a small Godot 4.7 .NET project on 4 November 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; Ziva.sh makes Ziva. This is a write-up of our own test; the short version is our docs page &lt;a href="https://ziva.sh/docs/godot-csharp" rel="noopener noreferrer"&gt;Use Ziva with Godot C#&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Godot 4.7 (.NET version) and the .NET 8 SDK on Linux.&lt;/li&gt;
&lt;li&gt;A tiny project: a &lt;code&gt;Player&lt;/code&gt; (&lt;code&gt;Sprite2D&lt;/code&gt;) that moves with the arrow keys, one scene, and a &lt;code&gt;.csproj&lt;/code&gt; that Godot created.&lt;/li&gt;
&lt;li&gt;Ziva's default model, with approvals switched off so each turn ran to the end.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You need the .NET version of Godot and the .NET SDK for any C# work. If your project has no C# scripts yet, create the solution first with &lt;strong&gt;Project &amp;gt; Tools &amp;gt; C# &amp;gt; Create C# solution&lt;/strong&gt;. Ziva does not set up C# in a project.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Write C# from a plain request
&lt;/h2&gt;

&lt;p&gt;We asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is a C# Godot project. Add a coin the player can collect, written in C#: when the player touches the coin, the coin disappears and a score label on screen goes up by one. Then run the scene to check it works.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ziva wrote &lt;code&gt;Coin.cs&lt;/code&gt; (an &lt;code&gt;Area2D&lt;/code&gt; that listens to &lt;code&gt;AreaEntered&lt;/code&gt;) and &lt;code&gt;ScoreLabel.cs&lt;/code&gt; (a &lt;code&gt;Label&lt;/code&gt;), gave the player a hitbox, and added the coin and the label to the scene. Then it ran the scene while holding the right arrow key. The screenshot from that run showed &lt;strong&gt;Score: 1&lt;/strong&gt; and the coin was gone.&lt;/p&gt;

&lt;p&gt;In a later chat we asked for a patrolling enemy without naming a language. Ziva saw that the project used C# and wrote &lt;code&gt;Enemy.cs&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Break the build three ways
&lt;/h2&gt;

&lt;p&gt;We introduced one error at a time and asked Ziva about it in plain words.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error we added&lt;/th&gt;
&lt;th&gt;What we asked&lt;/th&gt;
&lt;th&gt;What Ziva did&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Called &lt;code&gt;label.AddPoints(1)&lt;/code&gt;, a method that doesn't exist (CS1061)&lt;/td&gt;
&lt;td&gt;"I edited Coin.cs and now the game won't start. Can you find the problem and fix it?"&lt;/td&gt;
&lt;td&gt;Read the scripts, ran &lt;code&gt;dotnet build&lt;/code&gt; to confirm CS1061, renamed the call, built again and ran the scene&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Score += amount * 1.5;&lt;/code&gt; on an &lt;code&gt;int&lt;/code&gt; (CS0266)&lt;/td&gt;
&lt;td&gt;"Run the game and check that collecting the coin still adds to the score."&lt;/td&gt;
&lt;td&gt;Ran the scene, got Godot's C# build error back, fixed the line, built again and checked the score went up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Removed &lt;code&gt;partial&lt;/code&gt; from &lt;code&gt;Enemy&lt;/code&gt; (GD0001)&lt;/td&gt;
&lt;td&gt;"Godot says 'Failed to build project' when I press Play. Can you fix it?"&lt;/td&gt;
&lt;td&gt;Ran &lt;code&gt;dotnet build&lt;/code&gt;, added &lt;code&gt;partial&lt;/code&gt;, built again and ran the scene&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When Ziva runs a scene and the build fails, it gets Godot's C# build errors with file, line and error code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;res://ScoreLabel.cs:15:9 CS0266: Cannot implicitly convert type 'double' to 'int'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These errors come from Godot's last C# build. If you edit code outside Godot and nothing has built since, there is nothing to read yet, so Ziva builds the project with &lt;code&gt;dotnet build&lt;/code&gt; or runs the scene to find the error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Ziva's tools from Claude Code, Cursor or Codex
&lt;/h2&gt;

&lt;p&gt;Ziva also runs as a local &lt;a href="https://ziva.sh/docs/ziva-mcp-server" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt;. On the same C# project, its &lt;code&gt;get_godot_errors&lt;/code&gt; and &lt;code&gt;run_scene&lt;/code&gt; tools returned the same C# build errors, for example &lt;code&gt;res://Enemy.cs:3:14 GD0001: Missing partial modifier on declaration of type 'Enemy'&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ziva doesn't set up C# in a project. Create the C# solution in Godot first.&lt;/li&gt;
&lt;li&gt;Ziva doesn't autocomplete C# as you type. Keep Visual Studio, VS Code or Rider for typing, and use Ziva for changes across scenes and scripts.&lt;/li&gt;
&lt;li&gt;Ziva checks C# changes by building and running your scenes. It can't run C# unit tests.&lt;/li&gt;
&lt;li&gt;Godot can't export C# projects to the web yet, so Ziva's Share web builds doesn't work for C# games.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full page, kept up to date: &lt;a href="https://ziva.sh/docs/godot-csharp" rel="noopener noreferrer"&gt;https://ziva.sh/docs/godot-csharp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do you use an AI assistant with Godot C#? Does it see your build errors, or do you paste them in?&lt;/p&gt;

</description>
      <category>godot</category>
      <category>csharp</category>
      <category>gamedev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
