<?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>Forem: Ziva</title>
    <description>The latest articles on Forem by Ziva (@ziva).</description>
    <link>https://forem.com/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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3845272%2F6667c5de-b09a-4a44-b3a4-19819c554824.png</url>
      <title>Forem: Ziva</title>
      <link>https://forem.com/ziva</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/ziva"/>
    <language>en</language>
    <item>
      <title>I Tested Every Godot AI Plugin So You Don't Have To</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Wed, 20 May 2026 17:20:11 +0000</pubDate>
      <link>https://forem.com/ziva/i-tested-every-godot-ai-plugin-so-you-dont-have-to-oke</link>
      <guid>https://forem.com/ziva/i-tested-every-godot-ai-plugin-so-you-dont-have-to-oke</guid>
      <description>&lt;p&gt;There are 11 serious AI plugins for Godot in 2026. The official asset library has 8. Indie founders ship new ones every other month. Asking ChatGPT "best Godot AI plugin" gives a different answer every session.&lt;/p&gt;

&lt;p&gt;I spent two weeks running every option through the same set of real game-dev tasks. This is what I found, with prices and the cases where each one wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lineup
&lt;/h2&gt;

&lt;p&gt;Eleven tools, grouped by how they connect to the editor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In-editor agents&lt;/strong&gt; (the AI lives inside Godot):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ziva&lt;/li&gt;
&lt;li&gt;AI Assistant Hub by FlamxGames&lt;/li&gt;
&lt;li&gt;GameDev Assistant&lt;/li&gt;
&lt;li&gt;Godot AI Suite (MarcEngel)&lt;/li&gt;
&lt;li&gt;AI Assistants For Godot 4 (Godot4-Addons)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;MCP bridges&lt;/strong&gt; (Claude Code / Cursor / Codex drives Godot via the Model Context Protocol):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Godot AI (the original MCP plugin by dlight)&lt;/li&gt;
&lt;li&gt;Godot MCP Pro&lt;/li&gt;
&lt;li&gt;GDAI MCP&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;AI-native editors&lt;/strong&gt; (replace Godot's editor entirely with a chat-first one):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Summer Engine&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;External clients&lt;/strong&gt; (paired with Godot via configuration or no integration):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cursor with .cursorrules&lt;/li&gt;
&lt;li&gt;GitHub Copilot via the lrdcxdes community plugin&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;p&gt;I built the same minimum scene five times: a 2D platformer with a player CharacterBody2D, a TileMapLayer level, two enemies with a shared StateMachine, and a coin pickup with a signal-based score counter. Each plugin had to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate the player script from a description&lt;/li&gt;
&lt;li&gt;Add the two enemies and wire their state machines&lt;/li&gt;
&lt;li&gt;Paint a small tilemap programmatically&lt;/li&gt;
&lt;li&gt;Generate a coin sprite and import it correctly&lt;/li&gt;
&lt;li&gt;Read a runtime error and propose a fix&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Real tasks, identical scope, ~30 minutes of agent time each.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin&lt;/th&gt;
&lt;th&gt;Player script&lt;/th&gt;
&lt;th&gt;Adds enemies&lt;/th&gt;
&lt;th&gt;Paints tilemap&lt;/th&gt;
&lt;th&gt;Sprite gen&lt;/th&gt;
&lt;th&gt;Reads errors&lt;/th&gt;
&lt;th&gt;Setup time&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;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes (Retrodiffusion)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI Assistant Hub&lt;/td&gt;
&lt;td&gt;yes (chat only)&lt;/td&gt;
&lt;td&gt;no (you paste)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no (you paste)&lt;/td&gt;
&lt;td&gt;5 min + Ollama&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GameDev Assistant&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes (limited)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;4 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Godot AI Suite&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no (masterprompt)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;3 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI Assistants For Godot 4&lt;/td&gt;
&lt;td&gt;yes (chat only)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;3 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Godot AI (MCP)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;20+ min (MCP setup)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Godot MCP Pro&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;25+ min (MCP + 162 tools)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GDAI MCP&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;20+ min (MCP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Summer Engine&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;new editor learning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor (.cursorrules)&lt;/td&gt;
&lt;td&gt;yes (you copy back)&lt;/td&gt;
&lt;td&gt;no (you do it)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no (you paste)&lt;/td&gt;
&lt;td&gt;10 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Copilot&lt;/td&gt;
&lt;td&gt;tab completion only&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What actually mattered in practice
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Does the AI touch the editor, or just answer questions?
&lt;/h3&gt;

&lt;p&gt;Cursor, Copilot, AI Assistant Hub, and GameDev Assistant (in tutor mode) are answer-questions tools. You paste context, get suggestions, do the clicks yourself. The other seven are act-on-the-editor tools. The act-on-editor group saved me roughly two hours per day during this test compared to the answer-questions group.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Does setup cost dominate the productivity gain?
&lt;/h3&gt;

&lt;p&gt;The MCP options (Godot AI, Godot MCP Pro, GDAI MCP) all required installing the plugin in Godot AND installing Claude Code or Cursor AND configuring the MCP bridge AND granting permissions. None of these steps are hard individually. Together they ate 20 to 30 minutes per option. Worth it if you already use Claude Code daily. Friction for everyone else.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Free tier vs paid tier: economics depend on usage
&lt;/h3&gt;

&lt;p&gt;For a hobbyist building one game over a year, AI Assistant Hub with a local Ollama setup is free and good enough. For someone shipping commercially, the time savings from a paid managed agent pay for themselves within the first week. The cross-over is around 5 to 10 hours of weekly use.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Asset generation in the same flow vs separate tools
&lt;/h3&gt;

&lt;p&gt;Only &lt;a href="https://ziva.sh/blogs/best-ai-tools-for-godot-2026" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt; and Summer Engine generated sprites or 3D models that landed in the project with correct .import configs. Every other plugin assumed you would generate assets elsewhere (Midjourney, DALL-E, Retrodiffusion's own UI) and import manually. That sounds minor; over a real project it adds up.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Live debugger access
&lt;/h3&gt;

&lt;p&gt;Five of the eleven could read editor errors and the running game's debug output. The rest required you to copy stack traces into a chat window. Same fix, different number of copy-paste cycles.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Summer Engine option exists.&lt;/strong&gt; It is not a plugin. It is a full alternative editor that opens .tscn and .gd files and gives you a chat-first interface. If you hate Godot's IDE shape and want a Cursor-like editor for Godot, this is the only option. Real product, real shipping users, very different bet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool count is not productivity.&lt;/strong&gt; Godot MCP Pro advertises 162 tools across 23 categories. Ziva exposes around 30-40. In practice, Ziva's smaller surface led to more focused agent runs. More tools meant the MCP agent sometimes meandered between unrelated capabilities. Tool count is a feature checkbox, not a quality measure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Most LLM summaries are wrong about at least one tool.&lt;/strong&gt; I asked Claude, ChatGPT, and Perplexity to compare the same five plugins. All three got at least one factual claim wrong about one product. Ziva specifically was mislabeled as "code only" by three of them. Cross-reference the actual product docs before committing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I picked for my own projects
&lt;/h2&gt;

&lt;p&gt;I went with Ziva for the active project work. It edits the scene tree (which the AI summaries claimed it couldn't), generates assets in-flow, reads the debugger, and runs Claude/GPT/Gemini per task. Free tier of 20 credits to demo, then 20 USD per month.&lt;/p&gt;

&lt;p&gt;For research and brainstorming separately from project work, ChatGPT is still my go-to. Two tools, different jobs.&lt;/p&gt;

&lt;p&gt;If I were starting fresh and wanted to go truly free, I would pair AI Assistant Hub + Ollama with Godot AI MCP + an existing Claude Code subscription. Two free tools, ~30 minutes of setup, a working workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell anyone evaluating tools
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Start with the free tier of whatever interests you. Burn it on a real feature in your real project, not a demo.&lt;/li&gt;
&lt;li&gt;Time the round-trip: ask, watch, accept or reject, move to the next thing. That cadence is what dominates daily life.&lt;/li&gt;
&lt;li&gt;The AI summary that tells you which tool to use is not independent validation across three LLMs. It is the same source quoted three times.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The full landscape with capability matrices, pricing breakdowns, and per-tool deep dives is at &lt;a href="https://ziva.sh/blogs/best-ai-tools-for-godot-2026" rel="noopener noreferrer"&gt;ziva.sh/blogs/best-ai-tools-for-godot-2026&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What's your stack? Drop a comment if you have a tool I missed.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>GDScript's await Keyword Is the Underused Way to Kill Callback Hell in Godot</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Mon, 18 May 2026 00:43:01 +0000</pubDate>
      <link>https://forem.com/ziva/gdscripts-await-keyword-is-the-underused-way-to-kill-callback-hell-in-godot-1oei</link>
      <guid>https://forem.com/ziva/gdscripts-await-keyword-is-the-underused-way-to-kill-callback-hell-in-godot-1oei</guid>
      <description>&lt;p&gt;In Godot 4, &lt;code&gt;await&lt;/code&gt; is the single GDScript feature that flattens the messiest part of your codebase. It replaces signal-callback chains with linear top-to-bottom code, and it's still the first thing most tutorials skip past on their way to the next node-tree screenshot.&lt;/p&gt;

&lt;p&gt;This is a quick tour of where &lt;code&gt;await&lt;/code&gt; actually pays off, with code you can paste into a 2D project today.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of &lt;code&gt;await&lt;/code&gt; in GDScript
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.html" rel="noopener noreferrer"&gt;The official GDScript reference&lt;/a&gt; calls &lt;code&gt;await&lt;/code&gt; exactly once in the keyword list: "Waits for a signal or a coroutine to finish." That's the whole API. Two forms:&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;# Form 1: await a signal directly&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.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;timeout&lt;/span&gt;

&lt;span class="c1"&gt;# Form 2: await another function that itself awaits&lt;/span&gt;
&lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;play_intro_sequence&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Form 1 yields control until the signal emits. Form 2 turns any function with an &lt;code&gt;await&lt;/code&gt; inside it into a coroutine that the caller can also await, which is how you compose multi-step animations and transitions without nesting callbacks.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yield&lt;/code&gt;, the Godot 3 ancestor, is still in the keyword list "for transition." Do not use it in new 4.x code; every modern API expects &lt;code&gt;await&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five places &lt;code&gt;await&lt;/code&gt; saves real code
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Timed delays without a separate Timer node
&lt;/h3&gt;

&lt;p&gt;The single most common pattern:&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;func&lt;/span&gt; &lt;span class="nf"&gt;flash_warning&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;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Watch out!"&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="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;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This replaces a 12-line state-machine that other tutorials show. The timer is created, awaited once, and garbage-collected when the function returns. The &lt;a href="https://docs.godotengine.org/en/stable/classes/class_scenetreetimer.html" rel="noopener noreferrer"&gt;Godot timer docs&lt;/a&gt; call this exact pattern out as the supported way to do "fire and forget" delays.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Sequencing animations
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;play_intro&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;AnimationPlayer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;animation_finished&lt;/span&gt;
    &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;AnimationPlayer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"zoom_in"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;AnimationPlayer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;animation_finished&lt;/span&gt;
    &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;AnimationPlayer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"fade_out"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;AnimationPlayer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;animation_finished&lt;/span&gt;
    &lt;span class="n"&gt;queue_free&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three animations in sequence, in eight lines, with no nested callbacks. The pre-await version of this code is what most "how to chain animations in Godot" tutorials ship, and it is twice as long with three times the bug surface.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Waiting on player input inside a coroutine
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;wait_for_jump&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="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;await&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;input_event&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_action_pressed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"jump"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what tutorial games and dialogue systems actually need: pause execution until the player presses a specific key, then resume. Without &lt;code&gt;await&lt;/code&gt;, this is a &lt;code&gt;_input&lt;/code&gt; handler plus a state flag plus a polling check.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Turn-based game flow
&lt;/h3&gt;

&lt;p&gt;Reddit user heyitsdoodler filed &lt;a href="https://github.com/godotengine/godot-proposals/issues/13597" rel="noopener noreferrer"&gt;godot-proposals#13597&lt;/a&gt; describing the use case better than I can: "I'm waiting for multiple characters to finish tasks of variable lengths before continuing turn order succession." Their working solution is a sequence of &lt;code&gt;await&lt;/code&gt;s on each character's &lt;code&gt;task_finished&lt;/code&gt; signal. The proposal asks for built-in &lt;code&gt;all()&lt;/code&gt; and &lt;code&gt;any()&lt;/code&gt; helpers, which would close the last gap. Until those land, write a tiny utility:&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;func&lt;/span&gt; &lt;span class="nf"&gt;await_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signals&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="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;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;signals&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;s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order is arbitrary but the function returns only after every signal has fired.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Async HTTP requests without a callback
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;fetch_high_scores&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Array&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;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HTTPRequest&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;add_child&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;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://api.example.com/scores"&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;await&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_completed&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queue_free&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;JSON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&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="n"&gt;get_string_from_utf8&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two yields, one return value, no class-level state machine. &lt;a href="https://docs.godotengine.org/en/stable/classes/class_httprequest.html" rel="noopener noreferrer"&gt;HTTPRequest emits &lt;code&gt;request_completed&lt;/code&gt;&lt;/a&gt; with a four-element array; awaiting on the signal gets you exactly that array back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas every tutorial leaves out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Awaiting a signal on a freed object hangs forever.&lt;/strong&gt; If you &lt;code&gt;await some_node.signal_name&lt;/code&gt; and then &lt;code&gt;queue_free()&lt;/code&gt; the node before the signal fires, your coroutine never resumes. Wrap critical awaits in a timeout pattern using &lt;code&gt;any()&lt;/code&gt;-style helpers, or check &lt;code&gt;is_instance_valid()&lt;/code&gt; after the await returns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coroutines do not catch exceptions across the yield.&lt;/strong&gt; A &lt;code&gt;push_error()&lt;/code&gt; before the await fires normally, but a runtime crash in the resumed half is reported with a partial stack trace. Profile suspicious sequences with &lt;a href="https://docs.godotengine.org/en/stable/tutorials/scripting/debug/the_profiler.html" rel="noopener noreferrer"&gt;the Godot Profiler tab&lt;/a&gt; before you trust the line numbers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Awaiting inside &lt;code&gt;_ready()&lt;/code&gt; works, but the parent finishes loading before you do.&lt;/strong&gt; If another script reads state your &lt;code&gt;_ready&lt;/code&gt; is still building, the values it reads are pre-await. Set defaults before the first &lt;code&gt;await&lt;/code&gt; in &lt;code&gt;_ready&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI assistants keep writing callback hell instead
&lt;/h2&gt;

&lt;p&gt;If you ask a generic AI assistant to "wait three seconds and then play an animation in Godot," most produce a &lt;code&gt;Timer&lt;/code&gt; node, a &lt;code&gt;timeout&lt;/code&gt; signal connection, and a callback function. Correct, verbose, and ten years out of date.&lt;/p&gt;

&lt;p&gt;The pattern goes deeper than that single example. AI tools trained on web tutorials tend to reach for callback patterns from JavaScript, Python's &lt;code&gt;asyncio.create_task&lt;/code&gt;, or C# events. GDScript's &lt;code&gt;await&lt;/code&gt; is closer to Python's &lt;code&gt;await&lt;/code&gt; and C# &lt;code&gt;await&lt;/code&gt;, but with Godot-specific signal types that web examples do not cover. Domain-specific tools like &lt;a href="https://ziva.sh" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt; that live inside the Godot editor see your actual signals and emit &lt;code&gt;await&lt;/code&gt;-based code; generic tools fall back to whatever pattern is most common in their training set.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to skip &lt;code&gt;await&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Two cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hot per-frame code.&lt;/strong&gt; Use &lt;code&gt;_process&lt;/code&gt; or &lt;code&gt;_physics_process&lt;/code&gt; for things that need to run every frame. &lt;code&gt;await&lt;/code&gt; is for sequenced one-shot logic, not animation curves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-scene communication.&lt;/strong&gt; A signal connected through &lt;code&gt;connect()&lt;/code&gt; is still the right answer when two unrelated nodes need to talk to each other reactively. &lt;code&gt;await&lt;/code&gt; works inside one coroutine; &lt;code&gt;connect&lt;/code&gt; is the pub-sub between systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mental model that works: &lt;code&gt;await&lt;/code&gt; is for code that reads top-to-bottom but needs to wait. &lt;code&gt;connect&lt;/code&gt; is for code that reacts whenever something happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;await&lt;/code&gt; for timers, animations, input prompts, turn order, and HTTP. Wrap critical awaits in timeout helpers. Watch out for freed nodes. Set defaults before awaiting in &lt;code&gt;_ready&lt;/code&gt;. And if your AI assistant still emits &lt;code&gt;connect("pressed", _on_pressed)&lt;/code&gt; chains for these patterns, it's reading from a 2021 tutorial.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>Godot 4 Save Systems: 5 Patterns from Real Shipped Games</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Thu, 14 May 2026 01:00:26 +0000</pubDate>
      <link>https://forem.com/ziva/godot-4-save-systems-5-patterns-from-real-shipped-games-3g23</link>
      <guid>https://forem.com/ziva/godot-4-save-systems-5-patterns-from-real-shipped-games-3g23</guid>
      <description>&lt;p&gt;Every Godot tutorial pretends save systems are easy. They are not. The choice you make on day one quietly decides whether your save format survives a refactor, whether modders can edit a config, and whether your players lose their progress when you ship a patch.&lt;/p&gt;

&lt;p&gt;I have shipped two Godot games and dug through the docs and source of a few more. Here are the five save patterns that actually show up in production Godot games, ranked by where they make sense and where they bite you.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. ConfigFile for settings, not state
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ConfigFile&lt;/code&gt; writes INI-style files: &lt;code&gt;[section]&lt;/code&gt; blocks with &lt;code&gt;key = value&lt;/code&gt; pairs. The &lt;a href="https://docs.godotengine.org/en/stable/classes/class_configfile.html" rel="noopener noreferrer"&gt;official docs&lt;/a&gt; describe it as "creating simple configuration files."&lt;/p&gt;

&lt;p&gt;It is good at one thing: settings. Audio volumes, resolution, keybinds, accessibility toggles. The file is human-readable, easy to ship as a &lt;code&gt;user://settings.cfg&lt;/code&gt;, and trivially editable by tech-savvy players.&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;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ConfigFile&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;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"audio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"master"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"controls"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"jump"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"space"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"user://settings.cfg"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where it bites: it does not handle nested data well. Save your game progress in &lt;code&gt;ConfigFile&lt;/code&gt; and you end up flattening dictionaries by hand. &lt;a href="https://www.gdquest.com/library/cheatsheet_save_systems/" rel="noopener noreferrer"&gt;GDQuest's save cheatsheet&lt;/a&gt; explicitly recommends ConfigFile only for "small data like settings."&lt;/p&gt;

&lt;h2&gt;
  
  
  2. JSON with FileAccess for hand-edited data
&lt;/h2&gt;

&lt;p&gt;JSON is the format every web developer knows, and it works in Godot via &lt;code&gt;JSON.stringify&lt;/code&gt; / &lt;code&gt;JSON.parse_string&lt;/code&gt; with &lt;code&gt;FileAccess&lt;/code&gt;. &lt;a href="https://godotlearning.com/blog/godot-4-save-system-tutorial/" rel="noopener noreferrer"&gt;Godot Learning's January 2026 tutorial&lt;/a&gt; walks through a full implementation with auto-save and multiple slots.&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"hp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"pos"&lt;/span&gt;&lt;span class="p"&gt;:&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="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s2"&gt;"inventory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sword"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"potion"&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;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FileAccess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"user://save_01.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FileAccess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WRITE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where it bites: JSON does not natively support Godot types. &lt;code&gt;Vector2(10, 20)&lt;/code&gt; becomes &lt;code&gt;[10, 20]&lt;/code&gt; and you write conversion code by hand both ways. Miss one type and the load silently produces an &lt;code&gt;Array&lt;/code&gt;, not a &lt;code&gt;Vector2&lt;/code&gt;, and your character spawns at &lt;code&gt;null.x&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Use JSON when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need humans (modders, QA, designers) to read the save file&lt;/li&gt;
&lt;li&gt;Save data is mostly primitives (strings, numbers, arrays)&lt;/li&gt;
&lt;li&gt;You are syncing with a web backend that speaks JSON anyway&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Binary serialization with &lt;code&gt;store_var&lt;/code&gt; / &lt;code&gt;get_var&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Most overlooked native option. &lt;a href="https://docs.godotengine.org/en/stable/tutorials/io/binary_serialization_api.html" rel="noopener noreferrer"&gt;&lt;code&gt;FileAccess.store_var()&lt;/code&gt; and &lt;code&gt;FileAccess.get_var()&lt;/code&gt;&lt;/a&gt; write Variant types directly to a binary file. Vector2 stays Vector2. Dictionary stays Dictionary. No conversion 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="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FileAccess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"user://save_01.dat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FileAccess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WRITE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store_var&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"hp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"pos"&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;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The docs note this format is "secure by default because it prevents saving and loading objects, which are what enable code execution in Godot." That security comes from refusing to deserialize objects, which is also its limit: you cannot save scene references or class instances directly.&lt;/p&gt;

&lt;p&gt;Where it bites: binary files cannot be diffed in version control. If your save data is config-like and you need PR review, this is the wrong tool. Use it for hot-path saves where speed matters and you do not need humans reading the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Custom Resources + ResourceSaver
&lt;/h2&gt;

&lt;p&gt;The pattern Godot itself wants you to use. Define a &lt;code&gt;Resource&lt;/code&gt; subclass with &lt;code&gt;@export&lt;/code&gt; properties for every saved field, populate an instance, and call &lt;code&gt;ResourceSaver.save()&lt;/code&gt;. &lt;a href="https://www.gdquest.com/library/save_game_godot4/" rel="noopener noreferrer"&gt;GDQuest's resource save guide&lt;/a&gt; calls this "the most concise method with full type safety."&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;SaveData&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Resource&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;hp&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;100&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;pos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Vector2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Vector2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ZERO&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;inventory&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;String&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;/code&gt;&lt;/pre&gt;

&lt;/div&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;save&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SaveData&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;save&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;
&lt;span class="n"&gt;ResourceSaver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"user://save_01.tres"&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;loaded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SaveData&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;"user://save_01.tres"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Static typing. Code completion. Automatic conversion. Most importantly, you can save the same Resource class to either &lt;code&gt;.tres&lt;/code&gt; (text, diffable) or &lt;code&gt;.res&lt;/code&gt; (binary, smaller) by changing the extension.&lt;/p&gt;

&lt;p&gt;Where it bites: schema migration. Adding a field is fine, but renaming or removing fields breaks existing saves. You either keep the old field around forever or write migration code. &lt;a href="https://docs.godotengine.org/en/stable/tutorials/io/saving_games.html" rel="noopener noreferrer"&gt;Godot's official saving docs&lt;/a&gt; flag this as the trade-off you accept in exchange for type safety.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The hybrid pattern: Resource for state, ConfigFile for settings
&lt;/h2&gt;

&lt;p&gt;This is what shipping games actually do. I have not seen a single non-trivial Godot game use just one of the four patterns above. The pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;user://settings.cfg&lt;/code&gt; (ConfigFile) for audio, controls, display&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user://save_01.tres&lt;/code&gt; (custom Resource) for game state&lt;/li&gt;
&lt;li&gt;Optional: &lt;code&gt;user://stats.json&lt;/code&gt; (JSON) for analytics or stuff you want to inspect manually&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://godotengine.org/showcase/slay-the-spire-2/" rel="noopener noreferrer"&gt;Slay the Spire 2&lt;/a&gt; saves to the standard Godot user folder, with &lt;a href="https://www.xmodhub.com/info/xmod-blog/slay-the-spire-2-save-file-location/" rel="noopener noreferrer"&gt;run state and persistent unlocks in separate files&lt;/a&gt; so a run crash does not nuke your meta-progress. Splitting state by lifecycle (per-run, per-profile, per-install) is the actual lesson, not the format choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI assistants get wrong here
&lt;/h2&gt;

&lt;p&gt;This is a 2026 problem: ask ChatGPT or Claude for a Godot 4 save system and you will almost always get one of three patterns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;func _save():&lt;/code&gt; with &lt;code&gt;var file = File.new()&lt;/code&gt;.&lt;/strong&gt; That is Godot 3 syntax. &lt;code&gt;File&lt;/code&gt; was removed in 4.0. The replacement is &lt;code&gt;FileAccess.open()&lt;/code&gt; as a static call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON with manual Vector serialization but no &lt;code&gt;@export&lt;/code&gt; annotation suggestion.&lt;/strong&gt; Generic models do not know that &lt;code&gt;@export&lt;/code&gt; on a Resource subclass is the modern path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;store_line()&lt;/code&gt; for save data.&lt;/strong&gt; That works for plain text but loses every Godot type. It also encourages the JSON-with-manual-conversion antipattern.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the same drift I covered in my earlier post on &lt;a href="https://dev.to/ziva"&gt;Godot 4 API calls AI assistants still get wrong&lt;/a&gt;. The fix is the same: use a Godot-aware tool that reads your project's &lt;code&gt;project.godot&lt;/code&gt; and knows which Godot version you are actually on. Tools like &lt;a href="https://ziva.sh" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt; inject the current Godot docs into the model at inference time, which is how you avoid the &lt;code&gt;File.new()&lt;/code&gt; rabbit hole on a 4.7 project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick decision table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Use for&lt;/th&gt;
&lt;th&gt;Avoid for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ConfigFile&lt;/td&gt;
&lt;td&gt;Settings, keybinds&lt;/td&gt;
&lt;td&gt;Game state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;Modder-editable saves, web sync&lt;/td&gt;
&lt;td&gt;Type-heavy state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Binary &lt;code&gt;store_var&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Fast saves, big state&lt;/td&gt;
&lt;td&gt;Files you need to diff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom Resource&lt;/td&gt;
&lt;td&gt;Type-safe game state&lt;/td&gt;
&lt;td&gt;Schema-volatile data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid&lt;/td&gt;
&lt;td&gt;Real shipped games&lt;/td&gt;
&lt;td&gt;Tiny prototypes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you are starting a new Godot 4 project today, default to &lt;strong&gt;Custom Resources for state and ConfigFile for settings&lt;/strong&gt;. Migrate the analytics/debug stuff to JSON only if you actually need to read it by hand.&lt;/p&gt;

&lt;p&gt;The cost of getting this wrong is not "your save file is ugly." It is "you ship a patch in month six and 5% of your players post one-star Steam reviews because their save vanished." Pick the pattern that survives that month-six rewrite.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Multijugador en Godot 4: Guía práctica con MultiplayerSpawner</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Fri, 08 May 2026 18:38:43 +0000</pubDate>
      <link>https://forem.com/ziva/multijugador-en-godot-4-guia-practica-con-multiplayerspawner-4jem</link>
      <guid>https://forem.com/ziva/multijugador-en-godot-4-guia-practica-con-multiplayerspawner-4jem</guid>
      <description>&lt;p&gt;El sistema multijugador de Godot 4 cambió mucho desde Godot 3. Si vienes de la versión anterior o de Unity, vas a ver nodos nuevos como &lt;code&gt;MultiplayerSpawner&lt;/code&gt; y &lt;code&gt;MultiplayerSynchronizer&lt;/code&gt; que hacen casi todo el trabajo pesado por ti. Lo bueno: bien usados, ahorran cientos de líneas de código. Lo malo: la documentación oficial los explica mal, y la mayoría de los tutoriales en YouTube están desactualizados.&lt;/p&gt;

&lt;p&gt;Esta guía cubre lo que sí funciona en Godot 4.4+ para multijugador peer-to-peer simple, los errores más comunes y dónde la IA generativa todavía falla cuando la pides escribir código de red.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué viene incluido en Godot 4
&lt;/h2&gt;

&lt;p&gt;Godot 4 trae un stack de red completo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ENetMultiplayerPeer&lt;/code&gt;&lt;/strong&gt;: capa de transporte UDP confiable. Funciona en LAN, WAN y a través de NAT (con port forwarding).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WebSocketMultiplayerPeer&lt;/code&gt;&lt;/strong&gt;: alternativa para builds web/HTML5.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WebRTCMultiplayerPeer&lt;/code&gt;&lt;/strong&gt;: peer-to-peer real con NAT traversal automático, pero requiere un signaling server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;MultiplayerSpawner&lt;/code&gt;&lt;/strong&gt;: replica nodos automáticamente cuando se spawnean en el host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;MultiplayerSynchronizer&lt;/code&gt;&lt;/strong&gt;: replica propiedades específicas (posición, salud, etc.) en cada frame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RPCs (&lt;code&gt;@rpc&lt;/code&gt;)&lt;/strong&gt;: llamadas remotas a funciones, con tres modos de autoridad.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Para la mayoría de los juegos indie, la combinación es: ENet + Spawner + Synchronizer + algunos RPCs. El resto (lobbies, matchmaking, autenticación) lo escribes tú o lo delegas a un servicio externo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configurando una sala simple
&lt;/h2&gt;

&lt;p&gt;Vamos a hacer una sala donde el primer jugador es el host y los demás se conectan a él. El código mínimo es sorprendentemente corto.&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;Node&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7777&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MAX_PLAYERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;host_game&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;peer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ENetMultiplayerPeer&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;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_server&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;MAX_PLAYERS&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;error&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="n"&gt;push_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"No se pudo crear el servidor: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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;multiplayer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;multiplayer_peer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;peer&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;"Servidor escuchando en el puerto "&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;func&lt;/span&gt; &lt;span class="nf"&gt;join_game&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;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;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;peer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ENetMultiplayerPeer&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;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_client&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;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;error&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="n"&gt;push_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"No se pudo conectar: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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;multiplayer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;multiplayer_peer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;peer&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;"Conectando a "&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;func&lt;/span&gt; &lt;span class="nf"&gt;_on_peer_connected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&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;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;"Jugador conectado: "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&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;_on_peer_disconnected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&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;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;"Jugador desconectado: "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&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;_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="n"&gt;multiplayer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peer_connected&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;_on_peer_connected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;multiplayer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peer_disconnected&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;_on_peer_disconnected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eso es todo. El host llama a &lt;code&gt;host_game()&lt;/code&gt;, los clientes llaman a &lt;code&gt;join_game("192.168.1.42")&lt;/code&gt;. Las señales &lt;code&gt;peer_connected&lt;/code&gt; y &lt;code&gt;peer_disconnected&lt;/code&gt; te avisan cuando alguien entra o sale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replicando jugadores con MultiplayerSpawner
&lt;/h2&gt;

&lt;p&gt;El nodo &lt;code&gt;MultiplayerSpawner&lt;/code&gt; es la forma idiomática en Godot 4 de spawnear personajes. Reemplaza el viejo patrón de Godot 3 donde tenías que llamar &lt;code&gt;rpc("spawn_player", id)&lt;/code&gt; y replicar manualmente.&lt;/p&gt;

&lt;p&gt;Configuración:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;En tu escena de juego, añade un nodo &lt;code&gt;MultiplayerSpawner&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;En el inspector, asigna su propiedad &lt;code&gt;spawn_path&lt;/code&gt; a un Node2D o Node3D donde van los jugadores (algo como &lt;code&gt;$Players&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Añade tu escena de jugador (&lt;code&gt;player.tscn&lt;/code&gt;) a la lista &lt;code&gt;auto_spawn_list&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Cuando el host instancie un jugador como hijo del nodo &lt;code&gt;Players&lt;/code&gt;, el Spawner replica automáticamente esa instancia en todos los clientes.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="c1"&gt;# En el host, despues de que un peer se conecta:&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_on_peer_connected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&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;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;multiplayer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_server&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;player&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"res://scenes/player.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;player&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# importante: el name debe ser el id del peer&lt;/span&gt;
    &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;Players&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;player&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El detalle clave es que el nombre del nodo (&lt;code&gt;player.name&lt;/code&gt;) debe ser el ID del peer. Esto le dice a Godot a quién pertenece ese nodo. Si no lo haces, los inputs del jugador 1 controlarán a todos los personajes a la vez.&lt;/p&gt;

&lt;h2&gt;
  
  
  MultiplayerSynchronizer para movimiento
&lt;/h2&gt;

&lt;p&gt;El Synchronizer replica propiedades específicas de un nodo. Para un personaje que se mueve, querrás replicar &lt;code&gt;position&lt;/code&gt; y quizás &lt;code&gt;rotation&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Dentro de tu escena de jugador, añade un &lt;code&gt;MultiplayerSynchronizer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;En el inspector, abre el editor de Replication.&lt;/li&gt;
&lt;li&gt;Añade &lt;code&gt;position&lt;/code&gt; (replicado en cada frame) y &lt;code&gt;rotation&lt;/code&gt; si aplica.&lt;/li&gt;
&lt;li&gt;Configura &lt;code&gt;replication_interval&lt;/code&gt; a 0 para movimiento crítico, o a 0.05 (20 Hz) para reducir tráfico de red.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;El Synchronizer hace lo siguiente: en cada tick, el peer que tiene autoridad sobre el nodo envía las propiedades replicadas a todos los demás peers, que las aplican localmente. La autoridad por defecto es el host (peer ID 1), pero puedes cambiarla:&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;# En _ready() del nodo player:&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="c1"&gt;# Cada jugador es dueno de su propio personaje&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;peer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_int&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;set_multiplayer_authority&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Con esto, cada jugador controla su personaje y el host no tiene que procesar los inputs de todos.&lt;/p&gt;

&lt;h2&gt;
  
  
  RPCs para acciones puntuales
&lt;/h2&gt;

&lt;p&gt;Para cosas que pasan ocasionalmente (disparos, daño, chat) usa RPCs:&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="n"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"any_peer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"call_local"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"reliable"&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;chat_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&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;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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;multiplayer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_remote_sender_id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s2"&gt;"]: "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Llamar desde cualquier peer:&lt;/span&gt;
&lt;span class="n"&gt;chat_message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Hola a todos"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Las tres anotaciones importantes son:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"any_peer"&lt;/code&gt; o &lt;code&gt;"authority"&lt;/code&gt;: quién puede llamar la función. &lt;code&gt;authority&lt;/code&gt; es solo el dueño del nodo; &lt;code&gt;any_peer&lt;/code&gt; permite que cualquiera la llame.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"call_local"&lt;/code&gt; o &lt;code&gt;"call_remote"&lt;/code&gt;: si el llamante también ejecuta la función localmente. Para chat sí, para movimiento no.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"reliable"&lt;/code&gt; o &lt;code&gt;"unreliable"&lt;/code&gt;: TCP-style o UDP-style. Acciones críticas usan reliable; movimiento puro usa unreliable_ordered.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Donde la IA generativa todavía falla
&lt;/h2&gt;

&lt;p&gt;He pedido código multiplayer a varios LLMs y los errores se repiten:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Mezcla de APIs Godot 3 y Godot 4.&lt;/strong&gt; Los modelos generales fueron entrenados con muchos tutoriales viejos, así que te darán código con &lt;code&gt;rpc("function_name")&lt;/code&gt; en lugar de &lt;code&gt;function_name.rpc()&lt;/code&gt;, o &lt;code&gt;set_network_master()&lt;/code&gt; en vez de &lt;code&gt;set_multiplayer_authority()&lt;/code&gt;. El primero compila pero no hace lo correcto; el segundo ya ni existe en Godot 4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. RPC sin anotaciones.&lt;/strong&gt; Olvidan poner &lt;code&gt;@rpc("any_peer")&lt;/code&gt; y luego no entienden por qué el cliente no puede llamar la función. Por defecto, los RPCs son &lt;code&gt;authority&lt;/code&gt;-only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Authority mal configurada.&lt;/strong&gt; Los clientes intentan modificar nodos de los que no son autoridad, las modificaciones se sobrescriben en el siguiente sync, y el código parece "casi funcionar".&lt;/p&gt;

&lt;p&gt;Herramientas como &lt;a href="https://ziva.sh" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt; que conocen específicamente la API de Godot 4 evitan estos tres problemas porque consultan la base de clases del editor en tiempo real, no patrones aprendidos de tutoriales viejos. Para multiplayer en particular, donde el modo authority + el tipo de RPC + el reliable/unreliable importan mucho, esto es la diferencia entre "compila" y "funciona en producción".&lt;/p&gt;

&lt;h2&gt;
  
  
  Errores comunes y cómo depurarlos
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;El cliente conecta pero no se ve el otro jugador.&lt;/strong&gt; Casi siempre es porque el &lt;code&gt;MultiplayerSpawner&lt;/code&gt; no tiene la escena en su &lt;code&gt;auto_spawn_list&lt;/code&gt;, o porque el nombre del nodo no coincide con el peer ID.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Los inputs del jugador 1 controlan a todos.&lt;/strong&gt; Te falta &lt;code&gt;set_multiplayer_authority()&lt;/code&gt; en &lt;code&gt;_ready()&lt;/code&gt; del personaje.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cliente y host se desincronizan.&lt;/strong&gt; Probablemente estás modificando una propiedad replicada en ambos lados a la vez. Solo la autoridad debe modificar las propiedades sincronizadas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lag visible aún en LAN.&lt;/strong&gt; Sube &lt;code&gt;replication_interval&lt;/code&gt; a 0 (cada frame) o usa interpolación del lado del cliente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cuándo usar otra cosa
&lt;/h2&gt;

&lt;p&gt;ENet con MultiplayerSpawner funciona para:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Co-op de hasta ~8 jugadores&lt;/li&gt;
&lt;li&gt;Juegos PvP simples sin matchmaking complejo&lt;/li&gt;
&lt;li&gt;Juegos donde el host puede ser un jugador (sin servidor dedicado)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No es la opción correcta para:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MMOs (necesitas servidores autoritarios escalables)&lt;/li&gt;
&lt;li&gt;Juegos competitivos con anti-cheat (necesitas servidor dedicado, no peer authority)&lt;/li&gt;
&lt;li&gt;Browser builds (usa WebRTC + signaling server)&lt;/li&gt;
&lt;li&gt;Cross-platform mobile (PlayFab, Photon o Nakama suelen ser mejores)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recursos para profundizar
&lt;/h2&gt;

&lt;p&gt;La &lt;a href="https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html" rel="noopener noreferrer"&gt;documentación oficial de Godot Multiplayer&lt;/a&gt; cubre todo lo anterior con más profundidad, aunque tiende a saltar entre Godot 3 y Godot 4 sin marcarlo. Lee dos veces antes de implementar.&lt;/p&gt;

&lt;p&gt;Si estás empezando con multiplayer en Godot, este código alcanza para un proyecto pequeño funcional en una tarde. Las complicaciones reales (matchmaking, anti-cheat, persistencia) vienen después y se resuelven con servicios externos, no con más código de Godot.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>spanish</category>
      <category>espanol</category>
    </item>
    <item>
      <title>Godot 4 für Web-Spiele: Export, WASM und Browser-Performance</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Fri, 08 May 2026 18:33:55 +0000</pubDate>
      <link>https://forem.com/ziva/godot-4-fur-web-spiele-export-wasm-und-browser-performance-4315</link>
      <guid>https://forem.com/ziva/godot-4-fur-web-spiele-export-wasm-und-browser-performance-4315</guid>
      <description>&lt;p&gt;Godot 4 kann seit Version 4.3 wieder zuverlässig in den Browser exportieren. Wer 2023 mit dem Web-Export gekämpft hat, sollte sich die aktuelle Version anschauen. Das Threading-Modell wurde überarbeitet, die Build-Größe ist gesunken, und auf modernen Browsern läuft die Performance brauchbar.&lt;/p&gt;

&lt;p&gt;Dieser Artikel ist eine praktische Übersicht: was funktioniert, was nicht, und wie du den Web-Export sauber einrichtest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status quo: was geht in Godot 4.4+ wirklich
&lt;/h2&gt;

&lt;p&gt;Der Web-Export von Godot kompiliert zu WebAssembly (WASM) und nutzt WebGL 2 als Renderer. In der aktuellen Stable (4.4) und Beta (4.7) sind die folgenden Punkte gelöst, die bis vor zwei Jahren noch Probleme gemacht haben:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Threading.&lt;/strong&gt; Godot nutzt jetzt SharedArrayBuffer mit den korrekten COOP/COEP-Headern. Das ist Pflicht, ohne entsprechend konfigurierten Server bricht der Export beim Start ab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build-Größe.&lt;/strong&gt; Ein leeres Godot-4.4-Projekt liefert ungefähr 35 MB komprimierter WASM-Code aus. Mit Asset-Pack-Stripping kannst du das auf etwa 25 MB reduzieren. Vergleiche das mit Unity WebGL bei rund 12-15 MB für ein Mini-Projekt: Godot ist größer, aber nicht mehr inakzeptabel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio.&lt;/strong&gt; Web Audio funktioniert. Latenz liegt bei ~80-150 ms, was für die meisten Casual Games reicht. Für rhythmische Spiele bleibt es ein Kompromiss.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Was 2026 immer noch nicht klappt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3D-Spiele mit komplexen Shadern. WebGL 2 ist nicht WebGPU. Wenn dein Spiel Compute Shader oder Forward+ Rendering nutzt, vergiss den Browser.&lt;/li&gt;
&lt;li&gt;Saves über Local Storage hinaus. Browser-Quotas sind unzuverlässig. Plan ein, dass Spielerdaten verloren gehen können.&lt;/li&gt;
&lt;li&gt;Mobile Browser auf iOS. Safari hat lange WASM-Probleme gehabt. Aktuell läuft es, aber Bug-Reports kommen häufiger als auf Desktop.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Die Server-Konfiguration: hier scheitern die meisten
&lt;/h2&gt;

&lt;p&gt;Das größte Problem beim Web-Export ist nicht der Build, sondern das Hosting. Godot benötigt zwei spezifische HTTP-Header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ohne diese Header lädt der Browser keinen SharedArrayBuffer, und Godot bricht mit einem Threading-Fehler ab. Auf itch.io sind die Header standardmäßig gesetzt; auf einem eigenen Server musst du sie konfigurieren.&lt;/p&gt;

&lt;p&gt;Für Nginx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/spiel/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cross-Origin-Embedder-Policy&lt;/span&gt; &lt;span class="s"&gt;require-corp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cross-Origin-Opener-Policy&lt;/span&gt; &lt;span class="s"&gt;same-origin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Für Apache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;Location&lt;/span&gt;&lt;span class="sr"&gt; "/spiel/"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Cross-Origin-Embedder-Policy "require-corp"
    &lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Cross-Origin-Opener-Policy "same-origin"
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub Pages, Vercel und Netlify unterstützen diese Header über Konfigurationsdateien (&lt;code&gt;_headers&lt;/code&gt;-Datei bzw. &lt;code&gt;vercel.json&lt;/code&gt;). Cloudflare Pages braucht ein zusätzliches Worker-Skript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Den Web-Export einrichten
&lt;/h2&gt;

&lt;p&gt;Schritt 1: Im Godot-Editor unter &lt;code&gt;Project &amp;gt; Export&lt;/code&gt; den Web-Export hinzufügen. Falls die Export-Templates fehlen, lade sie über &lt;code&gt;Editor &amp;gt; Manage Export Templates&lt;/code&gt; herunter.&lt;/p&gt;

&lt;p&gt;Schritt 2: Im Export-Dialog die folgenden Optionen prüfen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Variant:&lt;/strong&gt; Threads (empfohlen). Singlethreaded läuft auch ohne COOP/COEP-Header, ist aber deutlich langsamer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VRAM Texture Compression:&lt;/strong&gt; ETC2 für mobile Browser, BPTC/S3TC für Desktop. Beide aktivieren, wenn du beides bedienst.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom HTML Shell:&lt;/strong&gt; leer lassen, falls du keine eigene Shell hast. Die Default-Shell macht alles, was du brauchst.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Schritt 3: &lt;code&gt;Export Project&lt;/code&gt; und einen Ordner wählen. Godot erzeugt fünf Dateien: &lt;code&gt;index.html&lt;/code&gt;, &lt;code&gt;index.js&lt;/code&gt;, &lt;code&gt;index.wasm&lt;/code&gt;, &lt;code&gt;index.pck&lt;/code&gt; (das Asset-Pack) und &lt;code&gt;index.audio.worklet.js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Schritt 4: Lokal testen mit einem CORS-fähigen Server. Der Python-Einzeiler reicht nicht, weil die Header fehlen. Stattdessen:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CrossOriginIsolatedHandler&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-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="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="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="nc"&gt;HTTPServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;localhost&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;CrossOriginIsolatedHandler&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;Speichern als &lt;code&gt;serve.py&lt;/code&gt;, im Export-Ordner ausführen, dann &lt;code&gt;http://localhost:8000&lt;/code&gt; im Browser öffnen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance-Tipps für den Browser
&lt;/h2&gt;

&lt;p&gt;Der Browser ist nicht der Desktop. Was auf dem Editor mit 60 FPS läuft, kann im Browser auf 25 FPS einbrechen, wenn du nicht aufpasst.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Texturen reduzieren.&lt;/strong&gt; WebGL 2 hat begrenzten VRAM auf mobilen Geräten. 4K-Texturen sind im Browser selten sinnvoll. Skaliere Assets auf 1024x1024 oder kleiner, wo möglich.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Polygon-Anzahl beobachten.&lt;/strong&gt; Browser-WebGL ist langsamer als nativer OpenGL-Treiber. Eine 100.000-Polygon-Szene, die nativ flüssig läuft, ruckelt im Browser. Halte die sichtbare Polygon-Zahl unter 50.000.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Audio-Streams limitieren.&lt;/strong&gt; Jeder gleichzeitig spielende AudioStreamPlayer kostet im Browser mehr als auf Desktop. Maximal 8-10 simultane Streams für Casual Games, weniger für Mobile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. &lt;code&gt;_process&lt;/code&gt; minimieren.&lt;/strong&gt; Jede Frame-Logik in &lt;code&gt;_process&lt;/code&gt; kostet im Browser mehr. Nutze Signale für ereignisgesteuerte Logik statt jeden Frame zu pollen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Pre-loading testen.&lt;/strong&gt; Der Browser lädt das &lt;code&gt;.pck&lt;/code&gt;-File komplett, bevor das Spiel startet. Bei 100 MB Assets sind das 30-60 Sekunden Wartezeit auf langsamen Verbindungen. Asset-Splitting via &lt;a href="https://docs.godotengine.org/en/stable/tutorials/export/exporting_pcks.html" rel="noopener noreferrer"&gt;PCK Embedding&lt;/a&gt; kann helfen, ist aber mit Aufwand verbunden.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hosting-Optionen im Vergleich
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plattform&lt;/th&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Bandbreite&lt;/th&gt;
&lt;th&gt;Custom Domain&lt;/th&gt;
&lt;th&gt;Preis&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;itch.io&lt;/td&gt;
&lt;td&gt;1 Klick&lt;/td&gt;
&lt;td&gt;unbegrenzt&lt;/td&gt;
&lt;td&gt;nein&lt;/td&gt;
&lt;td&gt;gratis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Pages&lt;/td&gt;
&lt;td&gt;mittel&lt;/td&gt;
&lt;td&gt;100 GB/Monat&lt;/td&gt;
&lt;td&gt;ja&lt;/td&gt;
&lt;td&gt;gratis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;mittel&lt;/td&gt;
&lt;td&gt;100 GB/Monat&lt;/td&gt;
&lt;td&gt;ja&lt;/td&gt;
&lt;td&gt;gratis Tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Pages&lt;/td&gt;
&lt;td&gt;hoch (Worker nötig)&lt;/td&gt;
&lt;td&gt;unbegrenzt&lt;/td&gt;
&lt;td&gt;ja&lt;/td&gt;
&lt;td&gt;gratis Tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netlify&lt;/td&gt;
&lt;td&gt;mittel&lt;/td&gt;
&lt;td&gt;100 GB/Monat&lt;/td&gt;
&lt;td&gt;ja&lt;/td&gt;
&lt;td&gt;gratis Tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eigener Server&lt;/td&gt;
&lt;td&gt;hoch&lt;/td&gt;
&lt;td&gt;je nach Provider&lt;/td&gt;
&lt;td&gt;ja&lt;/td&gt;
&lt;td&gt;variabel&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Für Game Jams und Demos: itch.io. Für eine eigene Marke mit Custom Domain und Analytics: Vercel oder Netlify. Für Produktion mit hohem Traffic: Cloudflare Pages, weil die Bandbreite nicht limitiert ist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fallstricke, die ich gesehen habe
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;iOS Safari Audio.&lt;/strong&gt; iOS verlangt eine Nutzeraktion (Klick), bevor Audio abspielt. Wenn dein Spiel Audio im &lt;code&gt;_ready()&lt;/code&gt; startet, hörst du auf iOS nichts. Workaround: einen "Start"-Button anzeigen, Audio erst nach dem Klick initialisieren.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gamepad-Inputs.&lt;/strong&gt; Der Browser-Gamepad-API ist standardisiert, aber Godot mappt nicht alle Buttons konsistent. Teste mit echten Controllern, nicht nur Tastatur-Emulation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory Leaks.&lt;/strong&gt; Lange Web-Sessions können Speicher leaken, weil die WASM-Heap nicht so aggressiv aufgeräumt wird wie der native Heap. Für Spiele unter 30 Minuten Spielzeit kein Problem; für Endless-Games solltest du den RAM-Verbrauch im DevTools-Performance-Tab überwachen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebGL-Kontext-Verlust.&lt;/strong&gt; Bei Tab-Wechsel oder Mobile-Hintergrund kann der WebGL-Kontext verloren gehen. Godot fängt das in den meisten Fällen ab, aber Texturen müssen neu geladen werden. Plan ein paar Sekunden Reload-Pause ein.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wann Web-Export sinnvoll ist und wann nicht
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sinnvoll:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2D-Casual-Games für Game Jams&lt;/li&gt;
&lt;li&gt;Demos und Trailer-Versionen, die direkt im Browser laufen&lt;/li&gt;
&lt;li&gt;Kleine Puzzle- und Strategiespiele&lt;/li&gt;
&lt;li&gt;Educational Games für den Schulkontext&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Nicht sinnvoll:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3D-AAA-artige Projekte&lt;/li&gt;
&lt;li&gt;Spiele mit echten Multiplayer-Anforderungen (WebSockets gehen, aber Latenz ist höher)&lt;/li&gt;
&lt;li&gt;Große Open-World-Spiele&lt;/li&gt;
&lt;li&gt;Alles mit Compute-Shader-Bedarf&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Fazit
&lt;/h2&gt;

&lt;p&gt;Der Web-Export von Godot 4 ist 2026 brauchbar geworden, wenn du die Einschränkungen kennst. Die Server-Konfiguration ist die häufigste Fehlerquelle. Wenn du einmal saubere COOP/COEP-Header eingerichtet hast, wirst du nicht mehr daran scheitern.&lt;/p&gt;

&lt;p&gt;Für Game Jams und Demos lohnt es sich. Für Vollpreis-Releases im Browser bist du wahrscheinlich besser bei Steam oder einem nativen Mobile-Build aufgehoben.&lt;/p&gt;

&lt;p&gt;Wer mehr zur &lt;a href="https://docs.godotengine.org/de/" rel="noopener noreferrer"&gt;offiziellen Godot-Dokumentation auf Deutsch&lt;/a&gt; sucht, findet dort die meisten Web-Export-Themen ausführlich behandelt.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>german</category>
      <category>deutsch</category>
    </item>
    <item>
      <title>Static Typing in GDScript: The 30 Minutes That Saved Me 30 Hours</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Fri, 08 May 2026 18:11:27 +0000</pubDate>
      <link>https://forem.com/ziva/static-typing-in-gdscript-the-30-minutes-that-saved-me-30-hours-1m8k</link>
      <guid>https://forem.com/ziva/static-typing-in-gdscript-the-30-minutes-that-saved-me-30-hours-1m8k</guid>
      <description>&lt;p&gt;Last month I spent an evening adding type hints to a 4,000-line GDScript codebase that had been running fine for a year. I expected nothing. By the time I finished, the editor had flagged 12 latent bugs I had never noticed: wrong return types, methods called with stale signatures, a &lt;code&gt;Vector2&lt;/code&gt; being passed where the function expected &lt;code&gt;Vector3&lt;/code&gt;. Every single one of those would have eventually crashed in production.&lt;/p&gt;

&lt;p&gt;Three of them already had: I just blamed them on something else when the bug reports came in.&lt;/p&gt;

&lt;p&gt;Static typing in GDScript is one of those features that sounds boring on the docs page and turns out to be the biggest quality-of-life upgrade you can make to a Godot 4 project. It is faster too. Independent benchmarks put the gain at &lt;a href="https://www.beep.blog/2024-02-14-gdscript-typing/" rel="noopener noreferrer"&gt;28-59% on hot paths&lt;/a&gt;, driven by the engine bypassing Variant dispatch when types are known at compile time.&lt;/p&gt;

&lt;p&gt;This post is the case for using type hints everywhere, including the gotchas I hit, and how to retrofit a typed style into a codebase that does not have it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What static typing actually catches
&lt;/h2&gt;

&lt;p&gt;Static typing in GDScript is opt-in per declaration. You can mix typed and untyped code freely in the same file. The type system catches:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Wrong type to a function&lt;/strong&gt;: &lt;code&gt;move_to(player.position_string)&lt;/code&gt; when &lt;code&gt;position_string&lt;/code&gt; does not exist on &lt;code&gt;player&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong return type&lt;/strong&gt;: a function declared &lt;code&gt;-&amp;gt; int&lt;/code&gt; that returns a &lt;code&gt;String&lt;/code&gt; somewhere down a branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Misspelled property names&lt;/strong&gt;: &lt;code&gt;player.helath&lt;/code&gt; instead of &lt;code&gt;player.health&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stale method signatures&lt;/strong&gt;: you renamed &lt;code&gt;attack(target)&lt;/code&gt; to &lt;code&gt;attack(target, damage)&lt;/code&gt; six months ago and the editor finds the one call site you missed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Null misuses&lt;/strong&gt;: assigning a &lt;code&gt;null&lt;/code&gt; to a non-nullable typed variable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first three I catch every time I run the project. The last two I never catch in dynamic GDScript because they only blow up under specific game state.&lt;/p&gt;

&lt;p&gt;I added types using the &lt;a href="https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/static_typing.html" rel="noopener noreferrer"&gt;official syntax&lt;/a&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;# Before (untyped, "I'll figure it out at runtime")&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;hp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;enemies&lt;/span&gt; &lt;span class="o"&gt;=&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;damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;multiplier&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;hp&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;multiplier&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hp&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="c1"&gt;# After (typed)&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;hp&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;100&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;enemies&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="n"&gt;Enemy&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;func&lt;/span&gt; &lt;span class="nf"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&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;multiplier&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="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;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;hp&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;amount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;multiplier&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;hp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;Array[Enemy]&lt;/code&gt;. Typed arrays are a Godot 4 feature and they catch passing the wrong kind of array into a function that expects &lt;code&gt;Array[Player]&lt;/code&gt;. This single check found four bugs in my project where I had been silently mixing entity types.&lt;/p&gt;

&lt;h2&gt;
  
  
  The performance angle
&lt;/h2&gt;

&lt;p&gt;I would argue for typing on bug-catching grounds alone. The performance angle is a free bonus that turns out to be substantial.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.beep.blog/2024-02-14-gdscript-typing/" rel="noopener noreferrer"&gt;The beep.blog benchmark&lt;/a&gt; ran 1 billion iterations across common GDScript operations on an M2 Max:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Untyped (ns)&lt;/th&gt;
&lt;th&gt;Typed (ns)&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Integer addition&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;1.57x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector2 add&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;2.07x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Method call&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;1.42x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engine API call&lt;/td&gt;
&lt;td&gt;73&lt;/td&gt;
&lt;td&gt;51&lt;/td&gt;
&lt;td&gt;1.43x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The speedup comes from typed GDScript generating optimized opcodes that skip Variant unwrapping. In dynamic GDScript, every operation has to: check the type tag, dispatch to the right operator, perform the operation, wrap the result back in a Variant. Typed code knows the types ahead of time and just does the operation.&lt;/p&gt;

&lt;p&gt;The biggest wins are in vector math and engine API calls, which is exactly what game code does in &lt;code&gt;_process&lt;/code&gt; and &lt;code&gt;_physics_process&lt;/code&gt;. If your game has any per-frame work, typing the hot loops is a free 30%+ speedup with no algorithm changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch with AI-generated code
&lt;/h2&gt;

&lt;p&gt;I run a lot of code through LLMs while working in Godot. The pattern I have noticed: most cloud LLMs default to dynamic GDScript even when the surrounding file is typed. They generate this:&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;func&lt;/span&gt; &lt;span class="nf"&gt;get_nearest_enemy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&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;closest&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;var&lt;/span&gt; &lt;span class="n"&gt;closest_dist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;999999&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;enemies&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;e&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;distance_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&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;closest_dist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;closest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
            &lt;span class="n"&gt;closest_dist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&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;distance_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&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;closest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the rest of the file looks like this:&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;func&lt;/span&gt; &lt;span class="nf"&gt;get_nearest_enemy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Enemy&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;closest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Enemy&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;var&lt;/span&gt; &lt;span class="n"&gt;closest_dist&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="bp"&gt;INF&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Enemy&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;enemies&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;d&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;e&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;distance_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&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;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;closest_dist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;closest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
            &lt;span class="n"&gt;closest_dist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;closest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dynamic version compiles. It even runs. But it skips every benefit you set up the typed style for: no IDE autocomplete on the return value, no type-mismatch checks, no opcode optimization. Worse, the next person editing the file has to mentally track which variables are which type.&lt;/p&gt;

&lt;p&gt;This is one reason game-dev-specific tools beat generic chat assistants. Tools like &lt;a href="https://ziva.sh" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt; (an AI agent built into the Godot editor) read your existing code style before generating new code, so a typed file gets typed completions. Generic assistants train on whatever GDScript samples were on the open web at scrape time, which skews dynamic.&lt;/p&gt;

&lt;p&gt;If you are using a generic assistant, the workaround is to put the typed signature in the prompt: "write &lt;code&gt;get_nearest_enemy(pos: Vector2) -&amp;gt; Enemy:&lt;/code&gt; that does X". The constraint is enough to flip the generation style.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to enforce typing in your project
&lt;/h2&gt;

&lt;p&gt;Godot has &lt;a href="https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/warning_system.html" rel="noopener noreferrer"&gt;a project setting for warnings on untyped declarations&lt;/a&gt;. Turn the relevant ones on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Project Settings -&amp;gt; Debug -&amp;gt; GDScript&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Untyped&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Declaration"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Warn&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Inferred&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Declaration"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Warn&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unsafe&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Method&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Access"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Error&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Return&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Value&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Discarded"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Warn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set "Treat Warnings as Errors" if you want hard enforcement. I treat untyped declarations as warnings only because mixing typed and untyped is occasionally pragmatic. You might want a generic helper that takes any value. But unsafe method access on untyped variables (the kind that crashes at runtime) is always an error.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://allenwp.com/blog/2023/10/03/how-to-enforce-static-typing-in-gdscript/" rel="noopener noreferrer"&gt;Allen Pestaluky has a deeper guide&lt;/a&gt; on each warning and what to set it to.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed in my workflow
&lt;/h2&gt;

&lt;p&gt;After the typing audit I made three habit changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every new function declaration starts with a return type. Even &lt;code&gt;-&amp;gt; void&lt;/code&gt;. The editor refuses to autocomplete properly without it.&lt;/li&gt;
&lt;li&gt;Every &lt;code&gt;var&lt;/code&gt; either gets a type annotation or uses &lt;code&gt;:=&lt;/code&gt; for inference. Bare &lt;code&gt;var x = ...&lt;/code&gt; is now a code smell I look for in PRs.&lt;/li&gt;
&lt;li&gt;Class members at the top of a script always have explicit types. Inference inside functions is fine; inference on class state means the next reader has to scroll to the assignment to figure out what the type is.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The cost of these habits is roughly one extra word per declaration. The benefit is a compiler that finds bugs before I do.&lt;/p&gt;

&lt;p&gt;If you maintain a Godot 4 project that has not had a typing pass, give it one evening. The bugs you find will pay for the time. The performance will be a nice surprise.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>gdscript</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How to Profile GDScript Performance in Godot 4: A 2026 Guide</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Thu, 07 May 2026 14:52:28 +0000</pubDate>
      <link>https://forem.com/ziva/how-to-profile-gdscript-performance-in-godot-4-a-2026-guide-16jn</link>
      <guid>https://forem.com/ziva/how-to-profile-gdscript-performance-in-godot-4-a-2026-guide-16jn</guid>
      <description>&lt;p&gt;Profiling GDScript is one of those things every Godot developer eventually learns the hard way. The first time you ship a build that drops to 22 fps on a mid-tier laptop, you discover the profiler exists, you open it, and the numbers stare back at you with no obvious next step.&lt;/p&gt;

&lt;p&gt;This is the guide I wish I had when I started. It covers what changed in &lt;a href="https://www.gdquest.com/library/godot_4_6_workflow_changes/" rel="noopener noreferrer"&gt;Godot 4.6's unified docking&lt;/a&gt;, how to use the built-in profiler properly, when to reach for external tracing profilers like Tracy, and the four common GDScript bottlenecks that cause most of the frame-time damage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things I got wrong before I read the docs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Profiling in the editor.&lt;/strong&gt; The Godot editor adds overhead to every frame, so the numbers you see when you press F6 inside the editor are not the numbers your players see. &lt;a href="https://docs.godotengine.org/en/stable/tutorials/scripting/debug/the_profiler.html" rel="noopener noreferrer"&gt;Godot's own docs are explicit about this: "for accurate performance numbers, profile an exported build"&lt;/a&gt;. Half the optimizations I "made" in 2024 were undoing imaginary problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treating frame time as one number.&lt;/strong&gt; Frame time has at least three layers: CPU work in your scripts, CPU work in the engine, and GPU work in the renderer. The Monitor tab gives you a single FPS number that mashes all three together. The Profiler tab is what splits it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not enabling typed code.&lt;/strong&gt; The single largest source of GDScript performance complaints I see on the forums is untyped code. From the &lt;a href="https://github.com/godotengine/godot-docs/blob/master/tutorials/performance/cpu_optimization.rst" rel="noopener noreferrer"&gt;Godot performance docs on CPU optimization&lt;/a&gt;: "Untyped variables require the runtime to determine the type and dispatch the correct operation on every operation, while typed variables skip this resolution." Same for arrays. If your hot loops use &lt;code&gt;var arr = []&lt;/code&gt; instead of &lt;code&gt;var arr: Array[int] = []&lt;/code&gt;, that is your low-hanging fruit before you ever touch the profiler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Open the right panel
&lt;/h2&gt;

&lt;p&gt;The profiling tools live in the Debugger panel at the bottom of the editor. With Godot 4.6, &lt;a href="https://godotengine.org/releases/4.6/" rel="noopener noreferrer"&gt;the Debugger is now a regular dock that can be moved or floated&lt;/a&gt;, which is useful if you have a vertical monitor and want the profiler in a side column.&lt;/p&gt;

&lt;p&gt;Three tabs matter for performance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Profiler.&lt;/strong&gt; Per-script function call timings, sorted by self time or total time. This is where you find the slow function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitors.&lt;/strong&gt; Live graphs of FPS, memory, draw calls, physics steps, and more. Good for catching trends over a play session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual Profiler.&lt;/strong&gt; Per-frame breakdown of the renderer cost. &lt;a href="https://www.linuxcompatible.org/story/godot-47-dev-4-released/" rel="noopener noreferrer"&gt;Godot 4.7 dev 4 added folding support to the Visual Profiler tree&lt;/a&gt;, which makes it usable on complex frames.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Profiler does not record by default. You have to click "Start" before you reproduce the slow scenario, and "Stop" when you are done. &lt;a href="https://docs.godotengine.org/en/stable/tutorials/scripting/debug/the_profiler.html" rel="noopener noreferrer"&gt;The docs note that profiling is performance-intensive because it instruments every frame&lt;/a&gt;, so leaving it on the whole session will distort the readings of the very thing you are measuring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Find the slow function
&lt;/h2&gt;

&lt;p&gt;Sort the Profiler results by &lt;strong&gt;Self Time&lt;/strong&gt; first, not Total Time. Self Time is what the function itself does, excluding everything it calls. Total Time can be misleading: a function that calls a slow library will show high Total Time but the fix is downstream.&lt;/p&gt;

&lt;p&gt;Common patterns I see in real Godot 4 projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;_process&lt;/code&gt; callback that runs &lt;code&gt;get_tree().get_nodes_in_group(...)&lt;/code&gt; every frame.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;_physics_process&lt;/code&gt; doing string concatenation for debug output that ships in release builds.&lt;/li&gt;
&lt;li&gt;A loop that calls &lt;code&gt;Vector2.distance_to&lt;/code&gt; instead of &lt;code&gt;distance_squared_to&lt;/code&gt; for a comparison check.&lt;/li&gt;
&lt;li&gt;A signal connected in &lt;code&gt;_ready&lt;/code&gt; that fires multiple times per frame because it was also connected in &lt;code&gt;_enter_tree&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The profiler tells you which function. Reading the function tells you which of these patterns it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Use custom monitors for things the built-in graphs miss
&lt;/h2&gt;

&lt;p&gt;The built-in Monitors tab has roughly 30 metrics. None of them know what your game does. If you want to track "active enemies on screen" or "outstanding network requests" or "items in the loot pool," you have to add a custom monitor.&lt;/p&gt;

&lt;p&gt;The API is straightforward. From &lt;a href="https://docs.godotengine.org/en/stable/tutorials/scripting/debug/custom_performance_monitors.html" rel="noopener noreferrer"&gt;the custom performance monitors docs&lt;/a&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;Node&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="n"&gt;Performance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_custom_monitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"game/active_enemies"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_count_enemies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Performance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_custom_monitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"game/loot_drops_per_minute"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_loot_rate&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;_count_enemies&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;int&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;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;get_nodes_in_group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"enemies"&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_loot_rate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;float&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;loot_log&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="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time_played_minutes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The metric appears under your category in the Monitors tab next session. Custom monitors are the cheapest way to validate that what you think your game is doing matches what it actually does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Reach for external tracers when the built-in profiler isn't enough
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://godotengine.org/releases/4.6/" rel="noopener noreferrer"&gt;Godot 4.6 added support for tracing profilers like Tracy, Perfetto, and Apple Instruments&lt;/a&gt;. These give you per-frame, per-thread, microsecond-level visibility into the engine internals, useful when the bottleneck is in the renderer or physics step rather than your script.&lt;/p&gt;

&lt;p&gt;When to reach for external profilers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Try first&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Slow function in your code&lt;/td&gt;
&lt;td&gt;Built-in Profiler tab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FPS drops on specific scenes&lt;/td&gt;
&lt;td&gt;Visual Profiler tab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stutters that don't show up in script timings&lt;/td&gt;
&lt;td&gt;Tracy or Perfetto&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mac-specific performance problem&lt;/td&gt;
&lt;td&gt;Apple Instruments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frame pacing or VRR issues&lt;/td&gt;
&lt;td&gt;RenderDoc or PIX (per platform)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tracy is the one most commonly reported as worth the setup cost for indie projects. The build needs to be compiled with the Tracy hooks enabled, which is heavier than the built-in tools, but the resolution is in a different league.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Fix the four GDScript patterns that account for most of the damage
&lt;/h2&gt;

&lt;p&gt;After you have a profiler trace and you know which function is slow, the fix is usually one of four things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Untyped variables and arrays.&lt;/strong&gt; Add types. &lt;code&gt;var hp: int = 100&lt;/code&gt; is faster than &lt;code&gt;var hp = 100&lt;/code&gt;. &lt;code&gt;var enemies: Array[Enemy] = []&lt;/code&gt; is faster than &lt;code&gt;var enemies = []&lt;/code&gt;. The cost is one annotation per declaration. The benefit is the runtime skipping the type-resolve step on every access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;get_node&lt;/code&gt;, &lt;code&gt;get_tree&lt;/code&gt;, &lt;code&gt;get_nodes_in_group&lt;/code&gt; in hot loops.&lt;/strong&gt; These walk the scene tree on every call. Cache the result in &lt;code&gt;_ready&lt;/code&gt; and store it as &lt;code&gt;@onready var hud: HUD = $UI/HUD&lt;/code&gt;. Same for &lt;code&gt;get_tree().get_root()&lt;/code&gt; and group lookups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;String operations in &lt;code&gt;_process&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;str(x) + " " + str(y)&lt;/code&gt; allocates two strings per frame. Use &lt;code&gt;"%d %d" % [x, y]&lt;/code&gt; if you need formatting, or move the string work behind an &lt;code&gt;if Engine.is_editor_hint()&lt;/code&gt; so it only runs in debug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Signal duplication.&lt;/strong&gt; &lt;code&gt;connect()&lt;/code&gt; does not deduplicate. If &lt;code&gt;_ready&lt;/code&gt; and &lt;code&gt;_enter_tree&lt;/code&gt; both connect to the same signal, the slot fires twice per emit. The fix is to check &lt;code&gt;is_connected()&lt;/code&gt; first, or only connect in one lifecycle hook.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Read the profile, not the code, for confirmation
&lt;/h2&gt;

&lt;p&gt;The discipline that took me longest to internalize: do not optimize from intuition. Profile, change one thing, profile again, confirm the number moved in the direction you expected. Half the "optimizations" that look obvious in code do nothing for frame time, and a few make it worse.&lt;/p&gt;

&lt;p&gt;If you have an AI assistant in your editor, this is one of the few places where it can pull weight without making things worse. Ask it to read the profiler dump and the slow function side by side and propose a hypothesis. &lt;a href="https://ziva.sh" rel="noopener noreferrer"&gt;Tools like Ziva&lt;/a&gt; that run inside Godot can read the actual scene tree and the actual project structure, which matters here because most GDScript performance problems are about how a script interacts with the rest of the project, not about the script in isolation. Generic chat tools that only see the script text miss the cross-cutting issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to skip
&lt;/h2&gt;

&lt;p&gt;A few things are not worth your time at the start:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Premature &lt;code&gt;static&lt;/code&gt; typing of every single variable in your codebase. Type the hot loops first.&lt;/li&gt;
&lt;li&gt;Refactoring &lt;code&gt;Vector2&lt;/code&gt; math to use a custom struct. The engine version is fine; your bottleneck is elsewhere.&lt;/li&gt;
&lt;li&gt;Replacing GDScript with C# for "performance." &lt;a href="https://docs.godotengine.org/en/stable/tutorials/performance/index.html" rel="noopener noreferrer"&gt;GDScript and C# benchmarks are competitive in Godot 4 for most game logic&lt;/a&gt;. C# helps when you have CPU-bound math kernels. It does not help when your problem is &lt;code&gt;get_nodes_in_group&lt;/code&gt; in &lt;code&gt;_process&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two-line summary: profile exported builds, type your hot loops, cache scene tree lookups in &lt;code&gt;_ready&lt;/code&gt;, and use external tracers only after the built-in profiler tells you the bottleneck is not in your code. Most performance problems in shipped Godot projects are one of these four things, and the profiler will tell you which one if you let it.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>performance</category>
      <category>programming</category>
    </item>
    <item>
      <title>VirtualJoystick in Godot 4.7: Endlich ein nativer Touchscreen-Stick</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Fri, 01 May 2026 17:41:44 +0000</pubDate>
      <link>https://forem.com/ziva/virtualjoystick-in-godot-47-endlich-ein-nativer-touchscreen-stick-1enb</link>
      <guid>https://forem.com/ziva/virtualjoystick-in-godot-47-endlich-ein-nativer-touchscreen-stick-1enb</guid>
      <description>&lt;p&gt;Bis vor kurzem hattest du in Godot zwei Optionen, wenn du einen virtuellen Joystick auf dem Smartphone wolltest: Plugin installieren oder selbst bauen. Beides hatte den gleichen Effekt: Edge Cases mit Multitouch, Resolution-Scaling von Hand, und am Ende eine Klasse mit 200 Zeilen, die du bei jedem Engine-Update neu testen musstest.&lt;/p&gt;

&lt;p&gt;Mit &lt;a href="https://godotengine.org/article/dev-snapshot-godot-4-7-beta-1/" rel="noopener noreferrer"&gt;Godot 4.7 beta 1&lt;/a&gt; (24. April 2026) ist das vorbei. Der neue &lt;code&gt;VirtualJoystick&lt;/code&gt;-Knoten ist Teil der Engine, hat drei Modi und fügt sich sauber ins Action-Mapping-System ein.&lt;/p&gt;

&lt;h2&gt;
  
  
  Das Problem, das &lt;code&gt;TouchScreenButton&lt;/code&gt; nicht gelöst hat
&lt;/h2&gt;

&lt;p&gt;Godot hatte schon vorher einen Knoten namens &lt;code&gt;TouchScreenButton&lt;/code&gt; für mobile Steuerungen. Das Problem: Er erbt von &lt;code&gt;Node2D&lt;/code&gt;. Das klingt harmlos, ist es aber nicht. Es bedeutet, dass der Button keine Anchors verwenden kann, also nicht relativ zum Bildschirmrand positioniert werden kann.&lt;/p&gt;

&lt;p&gt;Der Reviewer Calinou hat es im Pull Request knapp formuliert: &lt;a href="https://github.com/godotengine/godot/pull/110933" rel="noopener noreferrer"&gt;"TouchScreenButton inherits from Node2D, which means it can't make use of anchors."&lt;/a&gt; Für ein Element, das auf jedem Display-Format ordentlich aussehen soll, ist das ein Showstopper.&lt;/p&gt;

&lt;p&gt;Im &lt;a href="https://github.com/godotengine/godot-proposals/issues/11192" rel="noopener noreferrer"&gt;ursprünglichen Proposal&lt;/a&gt; stand es so:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"When creating a mobile game, you often need a virtual joystick so the player can move around. However, this is nontrivial to implement correctly."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Das Resultat: Jedes mobile Godot-Projekt hatte entweder eine Asset-Library-Abhängigkeit oder eine selbst geschriebene Joystick-Klasse, die in vielen Fällen ein paar Edge Cases falsch handhabte. Multitouch-Tracking, Resolution-Scaling und das Verhalten beim Verlassen des Sticks sind die häufigsten Stolpersteine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was der neue &lt;code&gt;VirtualJoystick&lt;/code&gt; ist
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;VirtualJoystick&lt;/code&gt; erbt von &lt;code&gt;Control&lt;/code&gt;. Damit funktionieren Anchors, &lt;code&gt;size_flags&lt;/code&gt;, &lt;code&gt;theme_override&lt;/code&gt;, alles was Control-Nodes können. Das ist der wichtige Unterschied zum alten &lt;code&gt;TouchScreenButton&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Der Knoten zeichnet sich prozedural, also nicht als Bitmap. Das bedeutet: Egal ob 1080p oder 4K-Tablet, der Stick sieht identisch scharf aus. Der Look lässt sich über Theme-Properties anpassen (Hintergrund, Knopf, Farben).&lt;/p&gt;

&lt;p&gt;Hinzu kommen vier Signale, die du im Editor verbinden kannst:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tapped&lt;/code&gt;: losgelassen, ohne dass der Stick bewegt wurde&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;released&lt;/code&gt;: der Finger hat den Bildschirm verlassen&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flicked&lt;/code&gt;: der Stick wurde aus der Deadzone heraus bewegt&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flick_canceled&lt;/code&gt;: ein Flick wurde initiiert, aber wieder zurückgezogen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Das ist mehr als die meisten Plugin-Implementierungen anbieten. Vor allem &lt;code&gt;tapped&lt;/code&gt; ist nützlich: Damit lässt sich der Joystick auch als Action-Button verwenden, wenn der Spieler nur kurz tippt statt zu schieben.&lt;/p&gt;

&lt;h2&gt;
  
  
  Die drei Modi: Fixed, Dynamic, Following
&lt;/h2&gt;

&lt;p&gt;Hier ist der Punkt, an dem die meisten Tutorials oberflächlich werden. Die drei Modi bestimmen, wie sich der Stick beim Berühren verhält.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;JOYSTICK_FIXED&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Der Stick bleibt da, wo du ihn platziert hast. Klassisch, simpel, vorhersehbar. Wenn der Spieler nicht direkt auf den Stick tippt, passiert nichts.&lt;/p&gt;

&lt;p&gt;Use Case: Action-Spiele mit fester UI, bei denen der Stick immer sichtbar ist. Spieler gewöhnen sich an die Position.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;JOYSTICK_DYNAMIC&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Tippt der Spieler in den Bereich des Sticks (also die &lt;code&gt;size&lt;/code&gt;-Region des Control-Nodes), springt der Knopf zur Berührungsposition. Der Stick selbst bewegt sich nicht weiter über den Berührungspunkt hinaus.&lt;/p&gt;

&lt;p&gt;Use Case: Mehr Komfort als &lt;code&gt;FIXED&lt;/code&gt;, weil man nicht millimetergenau treffen muss. Gut für Mobile-Ports von Joypad-Spielen.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;JOYSTICK_FOLLOWING&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Wie &lt;code&gt;DYNAMIC&lt;/code&gt;, aber der Stick folgt dem Finger auch über die ursprüngliche Bounding-Box hinaus. Beim Loslassen springt er zurück.&lt;/p&gt;

&lt;p&gt;Use Case: Twin-Stick-Shooter und alles, wo der Spieler den Daumen wandern lässt. Das fühlt sich auf großen Displays am natürlichsten an.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Modus&lt;/th&gt;
&lt;th&gt;Stick-Position&lt;/th&gt;
&lt;th&gt;Bewegung über Bounds&lt;/th&gt;
&lt;th&gt;Typischer Einsatz&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;JOYSTICK_FIXED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;unverändert&lt;/td&gt;
&lt;td&gt;nein&lt;/td&gt;
&lt;td&gt;klassische UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;JOYSTICK_DYNAMIC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;springt zur Berührung&lt;/td&gt;
&lt;td&gt;nein&lt;/td&gt;
&lt;td&gt;Mobile-Ports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;JOYSTICK_FOLLOWING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;springt zur Berührung&lt;/td&gt;
&lt;td&gt;ja&lt;/td&gt;
&lt;td&gt;Twin-Stick&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Praktischer Setup-Code
&lt;/h2&gt;

&lt;p&gt;Im Editor legst du den &lt;code&gt;VirtualJoystick&lt;/code&gt; als Kind eines &lt;code&gt;CanvasLayer&lt;/code&gt; an, damit er nicht mit der Welt-Kamera scrollt. Dann mappst du die Richtungen auf Input-Actions und liest die Werte wie gewohnt aus.&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;CharacterBody2D&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;move_speed&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="mf"&gt;200.0&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;var&lt;/span&gt; &lt;span class="n"&gt;input_dir&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&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;get_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"move_left"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"move_right"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"move_up"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"move_down"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;velocity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_dir&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;move_speed&lt;/span&gt;
    &lt;span class="n"&gt;move_and_slide&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Das ist alles. Im &lt;code&gt;VirtualJoystick&lt;/code&gt;-Inspector trägst du &lt;code&gt;move_left&lt;/code&gt;, &lt;code&gt;move_right&lt;/code&gt;, &lt;code&gt;move_up&lt;/code&gt; und &lt;code&gt;move_down&lt;/code&gt; als Action-Properties ein. Der Knoten triggert die Actions automatisch mit der entsprechenden Stärke (zwischen 0.0 und 1.0). Dein Spiel-Code muss nichts vom Joystick wissen, das ist plattform-agnostisch und funktioniert auch mit Gamepad oder Tastatur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was nicht drin ist (und warum)
&lt;/h2&gt;

&lt;p&gt;Bewusst weggelassen wurden zwei Features, die viele Plugins haben:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deadzone-Konfiguration im Knoten selbst.&lt;/strong&gt; Die Deadzone gehört zur Input-Action, nicht zum Joystick. Godot hat dafür schon ein Feld in den Project Settings unter "Input Map". Doppelte Konfiguration wäre nur eine Quelle für Bugs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clamp-Zone.&lt;/strong&gt; Eine Zone, in der der Stick "gefangen" bleibt. Im &lt;a href="https://github.com/godotengine/godot-proposals/issues/11192" rel="noopener noreferrer"&gt;Original-Proposal&lt;/a&gt; als optional markiert, in der finalen Version dann doch rausgeflogen.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Beides sind Design-Entscheidungen, keine Lücken, die später gefüllt werden. Wenn du das brauchst, kannst du den Knoten beerben und es selbst hinzufügen, aber im Standard-Workflow ist es nicht notwendig.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vor 4.7 vs. ab 4.7
&lt;/h2&gt;

&lt;p&gt;Wenn du schon Mobile-Spiele in Godot baust, hast du wahrscheinlich eine eigene &lt;code&gt;VirtualJoystick.gd&lt;/code&gt;-Klasse oder ein Asset aus der Library. Die meisten dieser Implementierungen liegen bei 100 bis 300 Zeilen Code, abhängig davon, wie viele Edge Cases sie abdecken.&lt;/p&gt;

&lt;p&gt;Die &lt;a href="https://godotengine.org/asset-library/asset?filter=joystick" rel="noopener noreferrer"&gt;Godot Asset Library&lt;/a&gt; listet aktuell 20 konkurrierende Joystick-Plugins. Genau das ist das Problem, das eine native Lösung beendet: Du musst nicht mehr entscheiden, welches Plugin am wenigsten Bugs hat oder welches noch zu Godot 4.6 kompatibel ist. Der Knoten ist da, gewartet vom Engine-Team, und verschwindet beim nächsten Update nicht.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lohnt sich der Umstieg für Mobile-Projekte?
&lt;/h2&gt;

&lt;p&gt;Wenn dein Spiel auf Touchscreen zielt, ja. &lt;a href="https://www.game.de/en/record-revenue-with-mobile-games-in-germany/" rel="noopener noreferrer"&gt;Mobile Games erzielten 2024 in Deutschland Rekord-Umsätze von 3 Milliarden Euro&lt;/a&gt;, 63 Prozent mehr als 2019, und das Smartphone ist seit Jahren die meistgenutzte Gaming-Plattform hierzulande. Viele Indie-Studios machen einen Hybrid-Ansatz: Desktop-Release zuerst, dann Mobile-Port. Genau in diesem Mobile-Port-Schritt war Touchscreen-Steuerung bisher ein Tag Handarbeit.&lt;/p&gt;

&lt;p&gt;4.7 ist gerade in der Beta-Phase, der stabile Release wird in etwa zwei Monaten erwartet. Wenn du an einem Mobile-Spiel arbeitest, lohnt sich der Sprung früh genug, um den Joystick vor dem Launch zu testen. Klein, aber konkret.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>german</category>
      <category>deutsch</category>
    </item>
    <item>
      <title>Generación procedural en Godot 4: guía práctica con GDScript</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Tue, 28 Apr 2026 23:46:37 +0000</pubDate>
      <link>https://forem.com/ziva/generacion-procedural-en-godot-4-guia-practica-con-gdscript-bdn</link>
      <guid>https://forem.com/ziva/generacion-procedural-en-godot-4-guia-practica-con-gdscript-bdn</guid>
      <description>&lt;p&gt;La generación procedural es una de las características que más diferencia un juego indie en Steam de un juego de portfolio. Roguelikes, sandboxes, dungeon crawlers, todos dependen de algún sistema procedural. En Godot 4 hay tres patrones que cubren el 80 por ciento de los casos de uso, y aún así hay sutilezas que la mayoría de tutoriales pasan por alto.&lt;/p&gt;

&lt;p&gt;Voy a recorrer los tres patrones, las trampas reales que aparecen al implementarlos, y por qué los asistentes de IA tienden a generar código procedural que parece correcto pero produce mundos rotos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patrón 1: Generación basada en ruido (Perlin/Simplex)
&lt;/h2&gt;

&lt;p&gt;Godot 4 incluye &lt;code&gt;FastNoiseLite&lt;/code&gt; con varios tipos de ruido (Perlin, Simplex, Value, Cellular). Es el camino más corto para generar terrenos, mapas de calor, distribuciones de recursos.&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;noise&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FastNoiseLite&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;func&lt;/span&gt; &lt;span class="nf"&gt;_ready&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;noise&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;noise_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FastNoiseLite&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TYPE_SIMPLEX_SMOOTH&lt;/span&gt;
    &lt;span class="n"&gt;noise&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;seed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;randi&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;noise&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frequency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;get_height&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&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;y&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="kt"&gt;float&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;noise&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_noise_2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La trampa principal: la frecuencia. Demasiado baja (0.001) y obtienes una superficie casi plana. Demasiado alta (0.5) y obtienes ruido aleatorio sin estructura. La mayoría de problemas de "mi terreno se ve raro" son problemas de frecuencia mal calibrada.&lt;/p&gt;

&lt;p&gt;Otra trampa común: usar &lt;code&gt;randi()&lt;/code&gt; como seed sin guardarla. Si quieres reproducibilidad (necesario para multiplayer y depuración), guarda la seed que usaste.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patrón 2: Generación basada en celdas (cellular automata)
&lt;/h2&gt;

&lt;p&gt;Para cuevas, islas, distribuciones orgánicas, los autómatas celulares son superiores al ruido. El algoritmo clásico:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Llena una grilla aleatoriamente con piedra (1) y aire (0).&lt;/li&gt;
&lt;li&gt;Para N iteraciones: para cada celda, cuenta sus vecinos. Si tiene &amp;gt;= 4 vecinos sólidos, se vuelve sólida. Si no, se vuelve aire.&lt;/li&gt;
&lt;li&gt;Después de 4-5 iteraciones, los grupos pequeños desaparecen y emergen cuevas naturales.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;cellular_step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Array&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;new_grid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;duplicate&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;grid&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;grid&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;size&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;neighbors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;count_neighbors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;new_grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;y&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;if&lt;/span&gt; &lt;span class="n"&gt;neighbors&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;new_grid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trampa: la primera iteración necesita cuidado en los bordes. Si tu &lt;code&gt;count_neighbors&lt;/code&gt; no maneja los bordes correctamente, las cuevas pegadas al borde del mapa se rompen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patrón 3: BSP (Binary Space Partitioning)
&lt;/h2&gt;

&lt;p&gt;Para mazmorras estructuradas con habitaciones y pasillos, BSP es el estándar de la industria. Divides el mapa recursivamente en dos, paras cuando los rectángulos son del tamaño de una habitación, colocas una habitación en cada hoja, y conectas habitaciones con pasillos siguiendo la jerarquía del árbol.&lt;/p&gt;

&lt;p&gt;Este es el patrón más complicado de los tres, porque combina recursión, geometría 2D, y un grafo de conectividad. Es también donde más fallan los asistentes de IA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué la IA genera código procedural roto
&lt;/h2&gt;

&lt;p&gt;Cuando le pides a ChatGPT, Claude o Cursor que genere un sistema procedural en GDScript, suele producir código que compila y ejecuta, pero genera mundos visiblemente rotos. Las razones recurrentes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frecuencias de ruido sin calibrar.&lt;/strong&gt; El modelo elige un valor "estándar" (0.1) que solo funciona para escalas específicas. Si tu mapa es de 1000x1000 tiles, no va a verse bien con esa frecuencia.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bucles sin guardas.&lt;/strong&gt; El cellular automata necesita un número fijo de iteraciones. La IA a veces escribe &lt;code&gt;while not stable:&lt;/code&gt; sin definir bien &lt;code&gt;stable&lt;/code&gt;, y el bucle nunca termina o termina muy pronto.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manejo de bordes inconsistente.&lt;/strong&gt; Wrap, clamp, o ignorar son tres opciones válidas según el tipo de mundo. La IA mezcla las tres en el mismo proyecto.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seed no guardada.&lt;/strong&gt; El código genera un mundo bonito una vez y luego nunca puedes reproducirlo. La IA omite el almacenamiento de la seed por defecto.&lt;/p&gt;

&lt;p&gt;Esto encaja con el patrón general de fallos silenciosos en código generado por IA. El &lt;a href="https://www.sonarsource.com/state-of-code-developer-survey-report.pdf" rel="noopener noreferrer"&gt;reporte Sonarsource State of Code 2026&lt;/a&gt; reporta que el 60 por ciento de los fallos en código generado por IA son "fallos silenciosos": código que compila, parece correcto, y produce resultados incorrectos. La generación procedural es un caso particularmente afectado, porque el "resultado correcto" es subjetivo (un mundo visualmente plausible) y no hay test unitario que lo capture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo verificar generación procedural
&lt;/h2&gt;

&lt;p&gt;Tres reglas prácticas:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visualiza siempre.&lt;/strong&gt; No confíes en que un mundo "se ve bien" porque el código corre. Genera 10 mundos, mira los 10. Las trampas de calibración aparecen como "mundos demasiado parecidos" o "mundos demasiado caóticos."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Guarda la seed que produjo el bug.&lt;/strong&gt; Cuando un mundo procedural se vea raro, copia la seed que lo generó al portapapeles. Sin la seed, no puedes reproducir el bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Usa herramientas que ejecuten el código en Godot.&lt;/strong&gt; Una clase emergente de herramientas (proyectos como &lt;a href="https://ziva.sh" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt; específicamente para Godot) ejecuta el código generado dentro del editor, observa el resultado, y reacciona cuando el mundo se ve roto. Eso cierra el bucle entre "el código compila" y "el código produce un mundo jugable."&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;Los tres patrones (ruido, celular, BSP) cubren la mayoría de necesidades procedurales en juegos indie. Cada uno tiene trampas que la IA tiende a pasar por alto. La defensa más fuerte es siempre la misma: ejecuta el código en Godot, observa el resultado, no confíes en que "el código compila" significa "el mundo está bien."&lt;/p&gt;

&lt;p&gt;Para tu próximo proyecto procedural en Godot, empieza con &lt;code&gt;FastNoiseLite&lt;/code&gt; y calibra la frecuencia visualmente. Cuando necesites estructura (habitaciones, conectividad), pasa a BSP. Para orgánicos, autómatas celulares. Y guarda esa seed.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>godot</category>
      <category>gdscript</category>
      <category>programming</category>
    </item>
    <item>
      <title>KI-Tools in Godot 2026: Was funktioniert, was scheitert</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Tue, 28 Apr 2026 23:44:04 +0000</pubDate>
      <link>https://forem.com/ziva/ki-tools-in-godot-2026-was-funktioniert-was-scheitert-2o2b</link>
      <guid>https://forem.com/ziva/ki-tools-in-godot-2026-was-funktioniert-was-scheitert-2o2b</guid>
      <description>&lt;p&gt;Wer 2026 mit Godot anfängt und nach KI-Hilfe sucht, stößt auf ein Problem, das in Tutorials selten erwähnt wird: ChatGPT und Claude können GDScript zwar gut schreiben, aber sie wissen nicht, wie das Godot-Projekt strukturiert ist, das du gerade aufbaust. Sie raten. Manchmal richtig, oft daneben.&lt;/p&gt;

&lt;p&gt;Ich nutze KI-Tools seit Jahren beim Webentwicklung und seit etwa 8 Monaten in Godot. Die Erfahrung ist deutlich anders. Hier eine ehrliche Übersicht, was 2026 in Godot mit KI funktioniert und was nicht.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was funktioniert
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Boilerplate für gängige Patterns.&lt;/strong&gt; Ein Player-Controller mit &lt;code&gt;CharacterBody2D&lt;/code&gt;, Eingabebehandlung über &lt;code&gt;Input.get_axis()&lt;/code&gt;, Sprung-Logik mit Coyote-Time, all das schreiben generische KI-Tools korrekt. Wenn du ein Standard-Pattern willst, ist das schneller als selbst tippen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Erklärung von Engine-Features.&lt;/strong&gt; Wenn du nicht verstehst, was &lt;code&gt;_physics_process&lt;/code&gt; vs &lt;code&gt;_process&lt;/code&gt; macht, oder wann du &lt;code&gt;queue_free()&lt;/code&gt; statt &lt;code&gt;free()&lt;/code&gt; verwendest, sind LLMs eine sinnvolle Lernhilfe. Die Grundkonzepte sitzen ausreichend gut im Trainingsmaterial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GDScript-Syntax und Typen.&lt;/strong&gt; Godot 4.x Typing (&lt;code&gt;var hp: int = 100&lt;/code&gt;) wird korrekt gehandhabt. &lt;code&gt;@export&lt;/code&gt;-Annotationen, &lt;code&gt;@onready&lt;/code&gt;, das funktioniert in 95 Prozent der Fälle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refactoring von eigenen Skripten.&lt;/strong&gt; Ein vorhandenes Skript in mehrere Klassen aufteilen, Magic Numbers extrahieren, Funktionen umbenennen, das macht generische KI gut.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was nicht funktioniert
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Signale, die zur Laufzeit gebunden werden.&lt;/strong&gt; Die KI sieht nur den Quellcode. Sie kann nicht überprüfen, ob ein &lt;code&gt;connect()&lt;/code&gt;-Aufruf zum Zeitpunkt der Emission noch gültig ist, ob der Receiver freigegeben wurde, ob die Verbindung doppelt existiert. Ein Großteil der "fast richtigen" Godot-Bugs sind Signal-Bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AnimationTree-Verkabelung.&lt;/strong&gt; Der &lt;code&gt;parameters/playback&lt;/code&gt;-Pfad ist ein String. Generische KI tippt diese Strings souverän falsch ab und merkt es nicht. Die Animation läuft einfach nicht. Kein Fehler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Szenenpfade in &lt;code&gt;load()&lt;/code&gt;.&lt;/strong&gt; Die KI nimmt an, dein Projekt sei strukturiert wie das erste Tutorial, das sie gelernt hat. Wenn deine &lt;code&gt;Player.tscn&lt;/code&gt; woanders liegt, gibt &lt;code&gt;load()&lt;/code&gt; &lt;code&gt;null&lt;/code&gt; zurück, und die KI merkt es nicht.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Autoload-Referenzen.&lt;/strong&gt; &lt;code&gt;AudioManager.play("hit")&lt;/code&gt; setzt voraus, dass &lt;code&gt;AudioManager&lt;/code&gt; als Autoload registriert ist. KI-generierte Code überspringt diesen Check standardmäßig. Du bekommst NIL-Errors zur Laufzeit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plattform-spezifischer Export-Code.&lt;/strong&gt; Web-Export, Mobile-Export, Konsolen-Build-Konfigurationen sind in den Trainingsdaten unterrepräsentiert. Hier rät die KI sehr.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warum das passiert
&lt;/h2&gt;

&lt;p&gt;Der zugrundeliegende Grund ist einfach: 60 Prozent der Fehler in KI-generiertem Code sind laut &lt;a href="https://www.sonarsource.com/state-of-code-developer-survey-report.pdf" rel="noopener noreferrer"&gt;Sonarsource State of Code Report 2026&lt;/a&gt; "stille Fehler". Code, der kompiliert, plausibel aussieht und in der Produktion das Falsche tut. In Godot ist "Produktion" der erste Druck auf F5.&lt;/p&gt;

&lt;p&gt;Die &lt;a href="https://stackoverflow.blog/2025/12/29/developers-remain-willing-but-reluctant-to-use-ai-the-2025-developer-survey-results-are-here/" rel="noopener noreferrer"&gt;Stack Overflow Developer Survey 2025&lt;/a&gt; zeigt zudem: 84 Prozent der Entwickler nutzen KI, nur 29 Prozent vertrauen den Ergebnissen. Bei Godot-Devs ist das Vertrauensniveau noch niedriger, weil die Fehler stiller sind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was 2026 hilft
&lt;/h2&gt;

&lt;p&gt;Drei praktische Tipps, die wirklich Zeit sparen:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run-then-paste statt paste-then-run.&lt;/strong&gt; Lass die KI Code generieren, paste ihn ein, drücke F5 BEVOR du committest. Das Output-Panel von Godot zeigt die meisten stillen Fehler innerhalb von Sekunden.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;is_connected()&lt;/code&gt;-Guards bei &lt;code&gt;disconnect()&lt;/code&gt;.&lt;/strong&gt; Das ist die billigste Defensiv-Praxis, die du KI-Code hinzufügen kannst. Sie verhindert eine ganze Klasse von "signal not connected"-Fehlern beim Szenenwechsel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Engine-bewusste KI-Tools, wo möglich.&lt;/strong&gt; Eine kleine, aber wachsende Klasse von Tools (Projekte wie &lt;a href="https://ziva.sh" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt; für Godot speziell) integriert sich direkt in den Godot-Editor. Der Agent kann die Szene laden, F5 drücken, die Ausgabe lesen, und reagieren, wenn das Signal nicht gefeuert hat. Das schließt die Lücke zwischen "Code sieht richtig aus" und "Code funktioniert."&lt;/p&gt;

&lt;h2&gt;
  
  
  Fazit für deutsche Godot-Entwickler
&lt;/h2&gt;

&lt;p&gt;Generische KI-Tools sind 2026 nicht überflüssig, aber sie sind nicht das Endgame. Die nächste Generation von Godot-Tools ist engine-aware, kennt die Szenenhierarchie, kann F5 drücken. Wenn du gerade mit KI-gestützter Godot-Entwicklung anfängst, würde ich empfehlen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generische LLMs für Konzepte und GDScript-Syntax verwenden.&lt;/li&gt;
&lt;li&gt;Kritische Engine-Pfade (Signale, AnimationTree, Autoloads) IMMER manuell testen.&lt;/li&gt;
&lt;li&gt;Ein engine-aware Tool ausprobieren, wenn du regelmäßig Godot-Code schreibst.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Die Stunden, die du beim Debuggen "fast richtigen" KI-Codes sparst, summieren sich schnell. Was ich als Faustregel gelernt habe: KI-Code, der nicht in der Engine ausgeführt wurde, ist KI-Code, dem du nicht trauen solltest.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>godot</category>
      <category>ai</category>
      <category>gdscript</category>
    </item>
    <item>
      <title>Testing Godot Code Is Harder Than Testing a Webapp. Here's What Helps.</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Tue, 28 Apr 2026 23:23:26 +0000</pubDate>
      <link>https://forem.com/ziva/testing-godot-code-is-harder-than-testing-a-webapp-heres-what-helps-5gb1</link>
      <guid>https://forem.com/ziva/testing-godot-code-is-harder-than-testing-a-webapp-heres-what-helps-5gb1</guid>
      <description>&lt;p&gt;If you've come from web development, you have muscle memory for testing. Spin up Jest, mock the database, hit the route, assert. Or use Playwright to drive a headless browser through a flow. CI runs the suite on every PR. You know the loop.&lt;/p&gt;

&lt;p&gt;Then you open a Godot project. The web testing playbook breaks in five places before you finish your morning coffee.&lt;/p&gt;

&lt;p&gt;I'm writing this because the gap between "AI wrote my Godot code" and "the code actually works" is mostly testing, and most of the testing tooling for Godot is one or two layers behind what web devs are used to. Here's what's different and what currently helps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Godot doesn't have a default headless test mode that just works
&lt;/h2&gt;

&lt;p&gt;A web project ships with a test framework on day one. Jest is built into Create React App. Vitest is the Vite default. You inherit a working test runner before you've written a line of business logic.&lt;/p&gt;

&lt;p&gt;A Godot project ships with the editor and &lt;code&gt;_ready()&lt;/code&gt;. There is no default test framework. The community standard, &lt;a href="https://github.com/bitwes/Gut" rel="noopener noreferrer"&gt;GUT (Godot Unit Testing)&lt;/a&gt;, is a third-party plugin you install yourself. It's good. It's also one of those "obvious in hindsight, surprising on day one" gaps when you arrive from a webdev background.&lt;/p&gt;

&lt;p&gt;There's also &lt;a href="https://github.com/chickensoft-games/GodotTestDriver" rel="noopener noreferrer"&gt;GodotTestDriver&lt;/a&gt; for integration tests, and Godot does support a &lt;code&gt;--headless&lt;/code&gt; mode for running scenes without a window. But none of this is wired up out of the box, and the headless mode has quirks: rendering-dependent code (anything that uses &lt;code&gt;Viewport.get_texture()&lt;/code&gt;, for example) silently produces empty results.&lt;/p&gt;

&lt;h2&gt;
  
  
  The state of your scene tree is invisible to most test runners
&lt;/h2&gt;

&lt;p&gt;Web tests deal with three kinds of state: the DOM, your store, and the database. All three are introspectable. You can &lt;code&gt;screen.getByRole('button')&lt;/code&gt;, you can read Redux state, you can &lt;code&gt;SELECT *&lt;/code&gt; from the test DB.&lt;/p&gt;

&lt;p&gt;Godot has a fourth kind: the scene tree. Nodes have parents. Signals connect nodes to other nodes. The active StateMachine state is buried inside an AnimationTree's &lt;code&gt;parameters/playback&lt;/code&gt; property. None of this surfaces in a stack trace. None of it is in a test database. You can write a unit test that verifies your signal-emitting function fires the signal, and still have a broken game because the receiving node was freed two frames earlier.&lt;/p&gt;

&lt;p&gt;This is the failure mode that bit me hardest moving from web to Godot: tests that pass in isolation, gameplay that breaks at runtime because the test runner doesn't know what the scene tree looked like when the bug happened.&lt;/p&gt;

&lt;p&gt;GodotTestDriver helps here by providing a &lt;code&gt;Fixture&lt;/code&gt; class that owns scene nodes during a test and tears them down cleanly. But you have to write integration tests that exercise actual scene behavior, not just unit tests that exercise pure functions. Most game logic is not pure functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most AI-generated Godot code never gets run before it ships
&lt;/h2&gt;

&lt;p&gt;The 2026 &lt;a href="https://www.sonarsource.com/state-of-code-developer-survey-report.pdf" rel="noopener noreferrer"&gt;Sonarsource State of Code report&lt;/a&gt; found that 60% of faults in AI-generated code are "silent failures." Code that compiles, looks right, and produces wrong results in production. The 2025 &lt;a href="https://stackoverflow.blog/2025/12/29/developers-remain-willing-but-reluctant-to-use-ai-the-2025-developer-survey-results-are-here/" rel="noopener noreferrer"&gt;Stack Overflow Developer Survey&lt;/a&gt; shows trust in AI output dropped from 40% to 29%, with 66% of devs citing "almost-right" code as their top frustration.&lt;/p&gt;

&lt;p&gt;For a webdev, this hurts a little. Type-check catches some of it. Failing test catches more. The user-visible failure mode is a 500 error and a Sentry alert.&lt;/p&gt;

&lt;p&gt;For a Godot dev, the same code can ship without anything obvious going wrong. The build succeeds. The editor doesn't complain. You press Play, the scene loads, your character moves around. Then you realize the death animation isn't playing because the signal was connected to a node that gets freed before the signal fires. There's no exception. There's no log line. The gameplay is just slightly wrong.&lt;/p&gt;

&lt;p&gt;Pasting AI-generated code into Godot without running it in the engine first is the equivalent of merging an untested PR straight to production. Web devs would never do that. Godot devs do it constantly, because the tooling makes it the path of least resistance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What helps
&lt;/h2&gt;

&lt;p&gt;A few things narrow the gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run a smoke test scene before pasting AI output.&lt;/strong&gt; Open the project, open the scene, press F5. If the AI's change broke node references or signal wiring, you'll see it in the output panel within seconds. This sounds obvious. It's also the thing most people skip because the dev/AI/dev/AI loop has too many context switches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add GUT and write integration tests for systems that touch the scene tree.&lt;/strong&gt; Pure-function tests are not enough for Godot. You need tests that load a scene, fire input, advance the frame, and assert on the resulting state. GUT supports this with &lt;code&gt;add_child_autofree()&lt;/code&gt; and the &lt;code&gt;await&lt;/code&gt; keyword for waiting on signals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use AI tools that integrate with the engine, not just the editor.&lt;/strong&gt; Most AI coding tools edit text files and stop. They have no view into the scene tree, no way to press Play, no read access to the Godot output panel. A small but growing class of tools (projects like &lt;a href="https://ziva.sh" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt; for Godot specifically) wire the AI agent to the engine itself, so the same model that writes the code can run the scene, watch the output, and react when a signal didn't fire. That's the part of the loop that closes the gap between "code looks right" and "code works."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat headless CI as a goal, not a starting point.&lt;/strong&gt; You will not have headless CI on day one of a Godot project. That's fine. Get to the point where you can press Play and watch a smoke test scene first. Build up to running tests on a CI runner with &lt;code&gt;--headless&lt;/code&gt; later. Web devs are used to that order being reversed; in Godot it's almost always smoke-test-first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest summary
&lt;/h2&gt;

&lt;p&gt;Game dev testing is harder than webapp testing in 2026. The frameworks are younger. The state model is more complex. The default failure mode is silent. AI-generated code raises the floor for syntax correctness and lowers the floor for runtime correctness in ways that are particularly bad for game projects, because game projects already had a runtime correctness problem.&lt;/p&gt;

&lt;p&gt;If you're a web dev experimenting with Godot and you're confused why your AI-assisted prototype keeps almost-but-not-quite working, this is most of the answer. The fix is upstream of the AI: better integration between the model writing the code and the engine running the code. The community is figuring this out in real time, and the tooling is improving fast.&lt;/p&gt;

&lt;p&gt;In the meantime, press Play before you commit.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>godot</category>
      <category>ai</category>
      <category>testing</category>
    </item>
    <item>
      <title>Domain-Specific AI Beats General AI on Niche Code. Here is Why.</title>
      <dc:creator>Ziva</dc:creator>
      <pubDate>Fri, 24 Apr 2026 21:34:02 +0000</pubDate>
      <link>https://forem.com/ziva/domain-specific-ai-beats-general-ai-on-niche-code-here-is-why-305f</link>
      <guid>https://forem.com/ziva/domain-specific-ai-beats-general-ai-on-niche-code-here-is-why-305f</guid>
      <description>&lt;p&gt;If your AI coding assistant is great at React but keeps hallucinating on your Django ORM, or fine with Python but useless on your Rust lifetimes, or solid with webdev but writes broken Godot scripts, this post is for you. The pattern is consistent across domains: general-purpose AI coding tools have a frontier they do well inside, and everything outside that frontier gets progressively worse.&lt;/p&gt;

&lt;p&gt;This post is about why, what to do about it, and when staying with a general tool is still the right call.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;You ask a general AI assistant to write code for a framework or engine that is not in the top 20 most-discussed on Stack Overflow. The code looks plausible. It compiles sometimes. When it runs, it breaks in ways that make you question your sanity because the error message points somewhere unrelated to the actual bug.&lt;/p&gt;

&lt;p&gt;You paste the error back. The AI apologizes and writes a different wrong version. Forty minutes in, you give up and do it yourself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://code.claude.com/docs/en/best-practices" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt;, GitHub Copilot, and &lt;a href="https://cursor.com" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt; all hit this wall. It is not about which model is better. It is about the distribution of training data and the lack of runtime context for the specific project you are working on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;Three overlapping causes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Training data frequency.&lt;/strong&gt; LLMs get better at code patterns that appear often in their training corpus. A framework with 100K GitHub repos gets way more attention than one with 1K. This is why React code is almost always right and Godot GDScript code is often half wrong. &lt;a href="https://dev.to/t/gamedev"&gt;DEV.to's community analysis of gamedev engagement&lt;/a&gt; shows the gap: gamedev posts get 10 to 20 reactions while webdev posts regularly clear 200.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context window limits plus "lost in the middle".&lt;/strong&gt; Context windows grew to &lt;a href="https://claude5.com/news/context-window-race-2026-how-200k-to-1m-tokens-transform-ai" rel="noopener noreferrer"&gt;200K, 1M, and beyond over 2024-2026&lt;/a&gt;, which sounded like it would fix everything. It did not. Studies of long-context retrieval found that information buried in the middle of a large context window is retrieved worse than information at the start or end. Paste your whole codebase, and the model will happily ignore the autoload registration on line 11,000 that breaks everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No runtime state.&lt;/strong&gt; A pasted codebase is static. The actual project has scene trees, registered globals, input maps, environment variables, database schemas, and build outputs that the AI cannot see through pasting alone. For niche frameworks where the runtime state matters a lot, the AI is guessing at half of the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "domain-specific" actually means
&lt;/h2&gt;

&lt;p&gt;There are three tiers of AI coding tool, and the names get confusing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;What it knows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;General-purpose&lt;/td&gt;
&lt;td&gt;ChatGPT, Claude, basic Copilot&lt;/td&gt;
&lt;td&gt;Syntax, patterns, public docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IDE-integrated&lt;/td&gt;
&lt;td&gt;Cursor, &lt;a href="https://code.claude.com" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt;, full Copilot&lt;/td&gt;
&lt;td&gt;Syntax plus your codebase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain-specific&lt;/td&gt;
&lt;td&gt;Tools like &lt;a href="https://ziva.sh" rel="noopener noreferrer"&gt;Ziva&lt;/a&gt; for Godot, framework-specific helpers&lt;/td&gt;
&lt;td&gt;Syntax, codebase, and runtime state for one ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third tier is what this post is about. A domain-specific AI is one whose training, retrieval, and context gathering are built around a single ecosystem. It reads the framework's current release notes, the runtime state of your specific project, and the niche APIs that general AI glosses over.&lt;/p&gt;

&lt;p&gt;The narrow bet is: on the domain it covers, it outperforms general AI. Off-domain, it does nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  When general AI is still the right call
&lt;/h2&gt;

&lt;p&gt;Domain-specific tools are a tradeoff. If your work spans 5 different stacks on a given week, you do not want 5 different AI assistants. The context switching and subscription overhead kills the win.&lt;/p&gt;

&lt;p&gt;General AI is the right call when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your code is in a high-training-frequency ecosystem (React, Django, Rails, Spring, Express).&lt;/li&gt;
&lt;li&gt;You work across many stacks and need one tool that handles all of them.&lt;/li&gt;
&lt;li&gt;The niche parts of your work are small and you can fix them yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Domain-specific AI starts earning its keep when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You spend the majority of your week in one ecosystem.&lt;/li&gt;
&lt;li&gt;That ecosystem has framework-specific patterns the AI keeps getting wrong.&lt;/li&gt;
&lt;li&gt;You have hit the "AI wrote 40 lines of confident-looking garbage" wall more than once.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The practical test
&lt;/h2&gt;

&lt;p&gt;Before committing to a domain-specific tool, run this test. Take three tasks you did this week in your niche framework. Hand them to a general AI with the full relevant file pasted in. Measure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Did the generated code compile or run on the first attempt?&lt;/li&gt;
&lt;li&gt;Did it use APIs that actually exist in your framework version?&lt;/li&gt;
&lt;li&gt;Did it reference existing project structures (globals, singletons, shared resources) correctly?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you get two out of three, you are fine. General AI is working for you.&lt;/p&gt;

&lt;p&gt;If you are below that, the cost of a specialized tool is probably worth it. At that point, you are paying in compounding time loss, not just single-task friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;For Godot specifically, &lt;a href="https://ziva.sh" rel="noopener noreferrer"&gt;tools like Ziva&lt;/a&gt; that run inside the editor and read project state directly get the autoload, scene-tree, and signal context that a chat-based AI cannot. For Rust, tools that understand borrow-check state across your project do better than generic completion. For Django, ORM-aware tools outperform generic ones on query optimization.&lt;/p&gt;

&lt;p&gt;The pattern holds across ecosystems. The bet is the same: narrow your AI's surface area so the context it has is actually useful, and accept that it does not work outside that surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would not do
&lt;/h2&gt;

&lt;p&gt;Do not buy a specialized AI for every framework you touch. The tradeoff only makes sense when the time you save exceeds the time you lose to juggling tools.&lt;/p&gt;

&lt;p&gt;Do not expect a domain-specific AI to fix a problem that is not domain-specific. If your issue is bad code architecture or unclear requirements, a better AI will not save you.&lt;/p&gt;

&lt;p&gt;Do not assume "domain-specific" means better. It means narrow. On the ecosystem it covers, it should outperform general AI. Off the ecosystem, it does nothing. Measure before you switch, and measure after.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;General AI is great at code patterns that show up a lot in its training data. It gets progressively worse on niche frameworks, version-specific features, and anything that requires runtime project state. Bigger context windows do not fix this. Domain-specific tools exist for the cases where the gap is costing you time.&lt;/p&gt;

&lt;p&gt;Whether you need one depends on how much of your week is in the niche. Run the three-task test before you decide.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>godot</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
