<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: CGT Rocket X Racer</title>
    <description>The latest articles on DEV Community by CGT Rocket X Racer (@cgtrocketx_dev).</description>
    <link>https://dev.to/cgtrocketx_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4199459%2Febf519b7-873f-41bf-b865-777d03388ab5.png</url>
      <title>DEV Community: CGT Rocket X Racer</title>
      <link>https://dev.to/cgtrocketx_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cgtrocketx_dev"/>
    <language>en</language>
    <item>
      <title>Our Godot web build drew its first frame at 1.5 s but showed a usable screen at 10.3 s — here's what moving things around did</title>
      <dc:creator>CGT Rocket X Racer</dc:creator>
      <pubDate>Wed, 28 Oct 2026 10:14:11 +0000</pubDate>
      <link>https://dev.to/cgtrocketx_dev/our-godot-web-build-drew-its-first-frame-at-15-s-but-showed-a-usable-screen-at-103-s-heres-14lj</link>
      <guid>https://dev.to/cgtrocketx_dev/our-godot-web-build-drew-its-first-frame-at-15-s-but-showed-a-usable-screen-at-103-s-heres-14lj</guid>
      <description>&lt;p&gt;Fan-made · Free in PC browser · Android on Google Play · iOS coming soon · Not affiliated&lt;/p&gt;

&lt;p&gt;A Godot web build can draw early and still feel slow. Our first WebGL frame came at a median 1,527 ms on a first visit, but a player saw nothing they could click until 10,344 ms. So the engine was ready to draw for almost nine seconds while the player waited behind a loading overlay.&lt;/p&gt;

&lt;p&gt;Article 9 looked at where the startup time goes. This one is about decisions: what has to load before the first frame, what can wait, and whether moving it actually helps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two finish lines, timed separately
&lt;/h2&gt;

&lt;p&gt;"Loaded" hides two different moments, so we timed them separately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;First real UI visible:&lt;/strong&gt; the first engine-drawn screen with something the player can interact with (a button), with no HTML overlay covering it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playable:&lt;/strong&gt; the game scene is running, its streamed level content has finished loading, and the overlay is gone. In our build the game prints a console line at exactly that point, and the intro with SKIP and the menu with PLAY follow from there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Speeding up one can slow down the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  The before-first-frame checklist
&lt;/h2&gt;

&lt;p&gt;Our web export already uses two packs. &lt;code&gt;index.pck&lt;/code&gt; (4,381,724 bytes) loads with the engine. &lt;code&gt;main.pck&lt;/code&gt; (50,860,652 bytes) is downloaded and mounted by the boot scene at runtime. A boot pack should contain only what the first screen needs: the boot scene, its script, a font, and the menu UI. Our rule for everything else: &lt;strong&gt;if the player can't see or use it in the first seconds, it doesn't go in the boot pack.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When we opened our boot pack with GDRE Tools, it failed that rule. Its biggest file was a 2,035,057-byte video that the boot scene played underneath an HTML overlay, so no player ever saw it. Next came 1,736,096 bytes of tutorial and guide textures. &lt;code&gt;main.pck&lt;/code&gt; contains 27,385,814 bytes of imported 3D models and 11,835,266 bytes of textures. The first menu needs none of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three levers, and what each one costs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Split packs with export filters and &lt;code&gt;load_resource_pack()&lt;/code&gt;.&lt;/strong&gt; Export presets can include or exclude files by filter, and &lt;code&gt;ProjectSettings.load_resource_pack()&lt;/code&gt; mounts a PCK at runtime (&lt;a href="https://docs.godotengine.org/en/stable/tutorials/export/exporting_projects.html" rel="noopener noreferrer"&gt;exporting projects&lt;/a&gt;, &lt;a href="https://docs.godotengine.org/en/stable/tutorials/export/exporting_pcks.html" rel="noopener noreferrer"&gt;exporting packs&lt;/a&gt;). The cost is that you now maintain two dependency lists, and a missing file only shows up at runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;ResourceLoader.load_threaded_request()&lt;/code&gt; + &lt;code&gt;load_threaded_get_status()&lt;/code&gt;.&lt;/strong&gt; This is the documented way to load in the background and poll progress (&lt;a href="https://docs.godotengine.org/en/stable/tutorials/io/background_loading.html" rel="noopener noreferrer"&gt;background loading&lt;/a&gt;). Our export is &lt;strong&gt;single-threaded&lt;/strong&gt;, so we measured what the call actually does there instead of assuming.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. When the overlay comes off.&lt;/strong&gt; Our HTML shell removed its overlay only after the scene was ready &lt;strong&gt;and&lt;/strong&gt; its loader video had finished. Removing it as soon as the scene is ready means the branding clip gets cut off, and players may see a plain engine loading screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment
&lt;/h2&gt;

&lt;p&gt;Every variant used the same public files (Godot 4.7.2 web export), served locally. These variants were built and measured locally; the public build hasn't been changed yet.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A:&lt;/strong&gt; the current build, unchanged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B:&lt;/strong&gt; only the overlay change. The page calls &lt;code&gt;reveal()&lt;/code&gt; at scene-ready instead of waiting for the video. Two lines of HTML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C:&lt;/strong&gt; B plus a new boot scene. It draws the real menu (title, PLAY button, status line) on its first frame. After that frame is drawn (&lt;code&gt;RenderingServer.frame_post_draw&lt;/code&gt;), it fetches &lt;code&gt;main.pck&lt;/code&gt;, mounts it with &lt;code&gt;load_resource_pack()&lt;/code&gt;, and loads the main scene with &lt;code&gt;load_threaded_request()&lt;/code&gt;, then &lt;code&gt;change_scene_to_packed()&lt;/code&gt; once PLAY has been pressed. The hidden boot video was removed, which brought the boot pack down to 2,570,428 bytes. &lt;code&gt;main.pck&lt;/code&gt; was left byte-identical.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Environment:&lt;/strong&gt; local headless Chrome 156 on a Linux box (8 vCPU), driven by Puppeteer over the Chrome DevTools Protocol. There was no GPU, so WebGL ran on SwiftShader. The files came from a local server on loopback, with &lt;strong&gt;no network throttling&lt;/strong&gt;. A first visit used an empty profile, and a repeat visit used the same profile with cache. Each variant got 3 runs, and every number below is the &lt;strong&gt;median&lt;/strong&gt;. In C, the test script clicked PLAY as soon as the menu appeared, like an impatient player.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First visit (ms from navigation, median of 3):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;A&lt;/th&gt;
&lt;th&gt;B&lt;/th&gt;
&lt;th&gt;C&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First WebGL frame&lt;/td&gt;
&lt;td&gt;1,527&lt;/td&gt;
&lt;td&gt;1,527&lt;/td&gt;
&lt;td&gt;1,646&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First real UI visible&lt;/td&gt;
&lt;td&gt;10,344&lt;/td&gt;
&lt;td&gt;5,444&lt;/td&gt;
&lt;td&gt;1,647&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playable&lt;/td&gt;
&lt;td&gt;11,196&lt;/td&gt;
&lt;td&gt;8,002&lt;/td&gt;
&lt;td&gt;5,972&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Longest freeze between UI and playable&lt;/td&gt;
&lt;td&gt;383&lt;/td&gt;
&lt;td&gt;2,150&lt;/td&gt;
&lt;td&gt;1,200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Repeat visit:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;A&lt;/th&gt;
&lt;th&gt;B&lt;/th&gt;
&lt;th&gt;C&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First WebGL frame&lt;/td&gt;
&lt;td&gt;1,047&lt;/td&gt;
&lt;td&gt;1,012&lt;/td&gt;
&lt;td&gt;1,095&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First real UI visible&lt;/td&gt;
&lt;td&gt;9,768&lt;/td&gt;
&lt;td&gt;4,836&lt;/td&gt;
&lt;td&gt;1,105&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playable&lt;/td&gt;
&lt;td&gt;10,612&lt;/td&gt;
&lt;td&gt;6,608&lt;/td&gt;
&lt;td&gt;5,439&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Longest freeze between UI and playable&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;1,700&lt;/td&gt;
&lt;td&gt;817&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Before running anything, we set a success criterion: C must show real UI at least 2 s earlier than A, and C's playable time must be no more than 10% later than A's. &lt;strong&gt;It passed.&lt;/strong&gt; On first visits, C's UI came 8,697 ms earlier and C was playable at 0.533 of A's time. On repeat visits, the figures were 8,663 ms and 0.513.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually moved the numbers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The two-line overlay fix did most of the work.&lt;/strong&gt; On first visits, B alone brought real UI forward by 4,900 ms and playable by 3,194 ms. The rest of the pipeline was fine; the delay came from an overlay waiting for a video to finish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The boot menu was the second step.&lt;/strong&gt; Going from B to C brought real UI forward by another 3,797 ms and playable by 2,030 ms. The menu showed up 7 ms after the first WebGL frame. We did not isolate why C also became playable sooner than B, because C changed several things at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On a single-threaded build, &lt;code&gt;load_threaded_request()&lt;/code&gt; blocked anyway.&lt;/strong&gt; After mounting, the call took a median 413 ms to return, the page went without a new frame for 467 ms, and the scene already reported LOADED on the first poll. The progress API still worked, but loading was not happening in the background. A threaded export may behave differently. We didn't test one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Costs we measured:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;C's first WebGL frame came 119 ms later than A's on first visits. Its boot scene also loads a 220,634-byte menu texture; we did not isolate the cause.&lt;/li&gt;
&lt;li&gt;C shows the menu early, and then the menu can freeze for up to 1,200 ms while the game scene starts. In A, nothing on screen is interactive before 10.3 s, but once it is, the longest freeze is 383 ms.&lt;/li&gt;
&lt;li&gt;On repeat visits, C mounted &lt;code&gt;main.pck&lt;/code&gt; at 4,130 ms against 2,729 ms for A. We haven't found out why.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When it's not worth it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If your HTML or engine loading screen waits for an animation, change that first. It took two lines for us and gave the largest single gain.&lt;/li&gt;
&lt;li&gt;A boot scene plus a separate pack is two dependency lists to keep in sync. For a small game that ships one pack, our numbers don't show a reason to split.&lt;/li&gt;
&lt;li&gt;Showing a menu that freezes after the first click can feel worse than an honest progress bar. We didn't measure how players perceive it.&lt;/li&gt;
&lt;li&gt;These numbers come from local headless Chrome with software WebGL and no throttling. They tell us about this build's startup pipeline. They are not predictions for real GPUs, real networks, or other devices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's in your boot scene that the first frame doesn't need?&lt;/p&gt;

&lt;p&gt;PC browser: &lt;a href="https://cgt.monster/x/devto10" rel="noopener noreferrer"&gt;https://cgt.monster/x/devto10&lt;/a&gt;&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>webassembly</category>
      <category>performance</category>
    </item>
    <item>
      <title>Before you rewrite GDScript: prove the hitch is texture upload (Chrome Performance)</title>
      <dc:creator>CGT Rocket X Racer</dc:creator>
      <pubDate>Sun, 25 Oct 2026 13:55:21 +0000</pubDate>
      <link>https://dev.to/cgtrocketx_dev/before-you-rewrite-gdscript-prove-the-hitch-is-texture-upload-chrome-performance-595a</link>
      <guid>https://dev.to/cgtrocketx_dev/before-you-rewrite-gdscript-prove-the-hitch-is-texture-upload-chrome-performance-595a</guid>
      <description>&lt;p&gt;Fan-made · Desktop-only · Not affiliated with any car maker&lt;/p&gt;

&lt;h2&gt;
  
  
  0. The hitch is real. The diagnosis is still open.
&lt;/h2&gt;

&lt;p&gt;You enter the garage. The game catches for a moment. You open the script, spot a loop, and start rewriting it.&lt;/p&gt;

&lt;p&gt;That is an understandable reaction. It is also a diagnosis made before collecting evidence.&lt;/p&gt;

&lt;p&gt;For a desktop-browser project like CGT Rocket X Racer, the useful question is narrower: &lt;strong&gt;what was the browser doing when the frame stalled?&lt;/strong&gt; If the hitch coincides with a texture's first use, another GDScript pass may target the wrong work.&lt;/p&gt;

&lt;p&gt;This is a verification workflow for Godot Compatibility builds in desktop Chrome. It starts with a short Performance recording and ends with a decision about where to investigate next.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Why “the script is busy” is not enough
&lt;/h2&gt;

&lt;p&gt;A script hotspot tells you where script execution costs time. It does not automatically explain a particular missed frame in the browser.&lt;/p&gt;

&lt;p&gt;Godot's documented web path uses Compatibility and WebGL 2.0. That makes browser-side rendering work part of the investigation, alongside gameplay execution. See the &lt;a href="https://docs.godotengine.org/en/4.4/tutorials/export/exporting_for_web.html" rel="noopener noreferrer"&gt;Godot web export documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A texture can be downloaded and loaded without all the work associated with its first visible use having finished. Resource preparation, transfer to graphics memory, and rendering can make that first encounter different from later ones. A large texture or atlas is therefore a suspect, not a verdict.&lt;/p&gt;

&lt;p&gt;With a single-threaded build, gameplay and rendering-related CPU work can also compete on the same thread. A long block around an engine frame is not necessarily expensive gameplay logic.&lt;/p&gt;

&lt;p&gt;Keep three possibilities separate: recurring script work, first-use resource work, and browser presentation work. The trace should help distinguish them before any rewrite begins.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Record one transition, then repeat it
&lt;/h2&gt;

&lt;p&gt;Choose a reproducible action: entering the garage for the first time, or showing a previously unseen level skin. Avoid recording a whole play session and hunting through unrelated activity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Establish the capture conditions
&lt;/h3&gt;

&lt;p&gt;Write down the build, Chrome version, desktop hardware, window size, and whether the page was freshly loaded. Keep throttling settings unchanged between captures. A fresh page is not necessarily an empty browser cache; describe what you actually reset.&lt;/p&gt;

&lt;p&gt;Open DevTools, select &lt;strong&gt;Performance&lt;/strong&gt;, and start recording before the action. Perform it once, wait until the scene settles, then stop. Enable screenshots if they help identify when the scene changes, while remembering that recording itself adds overhead.&lt;/p&gt;

&lt;p&gt;Chrome's &lt;a href="https://developer.chrome.com/docs/devtools/performance/reference/" rel="noopener noreferrer"&gt;Performance reference&lt;/a&gt; describes the Main flame chart, GPU activity section, and inspection views used here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Locate the hitch before naming it
&lt;/h3&gt;

&lt;p&gt;Find the transition using the captured frames and screenshots where available. Select a narrow interval around the stall. Inspect the Main track, then compare the timing with GPU activity if your recording exposes it.&lt;/p&gt;

&lt;p&gt;Select the expensive events and inspect their details and call stacks. Use &lt;strong&gt;Call tree&lt;/strong&gt; for the calling path and &lt;strong&gt;Bottom-up&lt;/strong&gt; to see which recorded activities account for time within that interval.&lt;/p&gt;

&lt;p&gt;Do not treat the widest rectangle as a diagnosis. Ask what work it represents and whether it overlaps the visible hitch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recognize a family of work, not a magic label
&lt;/h3&gt;

&lt;p&gt;Look for evidence of image preparation, texture transfer, or graphics work clustered around the resource's first appearance. Depending on the build and capture, relevant clues might include image decoding, bitmap preparation, or WebGL texture API calls visible in a stack.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;texImage2D&lt;/code&gt; and &lt;code&gt;texSubImage2D&lt;/code&gt; are API names to recognize &lt;strong&gt;if exposed&lt;/strong&gt;, not guaranteed Performance event labels. Likewise, an &lt;code&gt;ImageBitmap&lt;/code&gt; reference alone would not prove a GPU upload. Godot resources do not all travel through the same browser image path.&lt;/p&gt;

&lt;p&gt;Chrome versions, platforms, symbols, and capture settings change what is visible. A generic WebAssembly block or GPU activity band does not identify a texture by itself. Upload submission and GPU execution can also occur at different times.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compare the second encounter
&lt;/h3&gt;

&lt;p&gt;Without reloading the page, leave and repeat the same transition. Keep the camera, visible content, and interaction sequence comparable. Check that the resource stayed resident; unloading it would change the experiment.&lt;/p&gt;

&lt;p&gt;If the first encounter hitches and the second does not, that supports a &lt;strong&gt;first-use&lt;/strong&gt; explanation. It does not isolate upload from decoding, shader compilation, scene initialization, or other one-time work.&lt;/p&gt;

&lt;p&gt;Strengthen the case with a controlled development build: keep gameplay logic and scene structure constant, but substitute a smaller test texture. Repeat the captures. If the relevant activity and hitch consistently change together, the upload hypothesis becomes stronger. This is a diagnostic experiment, not a shipping prescription.&lt;/p&gt;

&lt;p&gt;Godot profiler data can help identify script or physics activity during the same action. Use it as corroboration; the browser recording remains the evidence for the browser-visible stall.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Let the evidence choose the next investigation
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Evidence around the hitch&lt;/th&gt;
&lt;th&gt;Next step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First-use spike, identifiable texture-related activity, and a controlled resource test that changes the stall&lt;/td&gt;
&lt;td&gt;Investigate resource preparation, texture dimensions, and when resources become visible or resident.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeated script or physics work aligned with repeated stalls&lt;/td&gt;
&lt;td&gt;Identify the responsible function or subsystem before changing GDScript.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Layout or compositing work aligned with the transition&lt;/td&gt;
&lt;td&gt;Inspect the page wrapper, fullscreen changes, and canvas sizing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opaque engine activity with no useful attribution&lt;/td&gt;
&lt;td&gt;Improve the capture or isolate variables; keep the cause unresolved.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are investigation routes, not mutually exclusive causes. A scene transition can trigger several kinds of work together.&lt;/p&gt;

&lt;p&gt;Resource changes come after evidence. Import decisions and atlas sizing may deserve attention then, but this article does not prescribe a compression or export-settings recipe. The immediate deliverable is a defensible diagnosis.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Be precise about what you proved
&lt;/h2&gt;

&lt;p&gt;“Only the first visit stutters” is an observation. “First-use work is implicated” is an inference. “Texture upload causes this hitch” needs stronger attribution and a repeatable comparison.&lt;/p&gt;

&lt;p&gt;Save the recordings with their capture conditions. Note what changed between tests and what stayed constant. If your trace cannot separate upload from other graphics initialization, report that limitation instead of promoting a guess into a finding.&lt;/p&gt;

&lt;p&gt;No particular time saving follows from this workflow. It helps choose the next experiment. These checks also concern desktop Chrome; they establish no mobile-browser performance claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Evidence before the rewrite
&lt;/h2&gt;

&lt;p&gt;Before changing GDScript, capture the hitch, compare first and repeat encounters, and test the most plausible explanation. Rewrite when the evidence points to script work.&lt;/p&gt;

&lt;p&gt;For your last web hitch, did you open Performance first, or change the script first?&lt;/p&gt;

&lt;p&gt;Desktop build: &lt;a href="https://cgt.monster/x/devto8" rel="noopener noreferrer"&gt;https://cgt.monster/x/devto8&lt;/a&gt;&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>webdev</category>
      <category>indiedev</category>
    </item>
    <item>
      <title>Readable hazards: why first-run deaths feel fair in a browser runner</title>
      <dc:creator>CGT Rocket X Racer</dc:creator>
      <pubDate>Sun, 25 Oct 2026 13:17:26 +0000</pubDate>
      <link>https://dev.to/cgtrocketx_dev/readable-hazards-why-first-run-deaths-feel-fair-in-a-browser-runner-2jlb</link>
      <guid>https://dev.to/cgtrocketx_dev/readable-hazards-why-first-run-deaths-feel-fair-in-a-browser-runner-2jlb</guid>
      <description>&lt;p&gt;First-run drop-off in a browser runner is often blamed on difficulty. In practice, many exits happen after a death the player cannot explain. If a press silhouette or sweeping indoor arm never reads before the commit window, Boost and roll tricks cannot save the session. In CGT Rocket X Racer — a fan-made rocket-car runner for desktop browsers — we treat hazards as a readability system first, and a difficulty dial second. A fair death says “I read late.” An opaque one says “the game lied,” and there is no second run after that.&lt;/p&gt;

&lt;p&gt;Fan-made · Desktop-only · Not affiliated with any car maker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real first-run failure modes
&lt;/h2&gt;

&lt;p&gt;Three failure modes turn a miss into a betrayal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Muddy silhouette.&lt;/strong&gt; The press that is about to stamp, or the arm that is about to sweep, never presents a clear outline early enough. Particles and motion blur win the frame; the lethal shape loses. The player dies inside noise and has no mental replay of what hit them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase that lies after Boost.&lt;/strong&gt; Hazard timing that feels honest at cruise speed can feel dishonest when approach speed jumps. If telegraph stays locked to a slow-car rhythm while the car is already closing faster, the same stamp that felt fair becomes a surprise clip. The player does not blame their Boost timing — they blame the obstacle for “changing.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rewards buried in the kill path.&lt;/strong&gt; When a part pickup sits inside a hazard’s motion arc, death feels like the game baited a collect. The HUD may have been honest about which bin was weak; the track placement still taught the wrong lesson: greed was punished by geometry the player could not negotiate.&lt;/p&gt;

&lt;p&gt;Product conclusion: each of these converts “I mistimed” into “this cheated.” First-run trust dies with the car.&lt;/p&gt;

&lt;h2&gt;
  
  
  The readability contract
&lt;/h2&gt;

&lt;p&gt;We nail these rules as a checklist, not as engine lore.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Silhouette before spectacle.&lt;/strong&gt; Press and arm outlines outrank particles. If the lethal shape is unreadable, the VFX is wrong even when it looks expensive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safe lane stays visible.&lt;/strong&gt; Indoor sweep patterns keep a lane the eye can hold. A pretty kill box with no readable escape is not difficulty — it is fog.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distance sampling, not wall-clock.&lt;/strong&gt; Hazard phase follows track distance so Boost changes approach speed without rewriting the beat the player already learned. We treat this as a readability fix, not a separate Boost essay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Death attribution stays readable.&lt;/strong&gt; After a crash, the player should be able to replay “I read late” or “I cut late,” not invent a story that the capsule quietly resized.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Landscape framing under Boost.&lt;/strong&gt; At Boost speed the camera still has to keep forward hazards legible in a wide desktop frame. Readable approach is a framing job, not a portrait crop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That contract is what we mean by fair. Hard is allowed. Opaque is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback loop: before, at, and after death
&lt;/h2&gt;

&lt;p&gt;Readable hazards are a three-beat loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before death.&lt;/strong&gt; Telegraph — rotation, outline, motion phase — must land before the commit window. The player needs time to choose a lane or hold Boost, not time to invent a conspiracy. Surprise without telegraph is cheap; telegraph that arrives after commit is worse, because it looks like feedback while offering none.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At death.&lt;/strong&gt; Feedback stays short and specific. A flash or hit read that still shows &lt;em&gt;what&lt;/em&gt; you hit beats a full-screen wash that erases the culprit. If the frame after impact cannot answer “press or arm?”, the death screen is decorating failure instead of teaching it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After death.&lt;/strong&gt; A one-line, soft attribution is enough: something that points at late read or late cut, without a lecture. The goal is the next run’s attention, not a tutorial modal. Weak bin red on the collect HUD is a collecting lesson; hazard readability is a survival lesson. Those two lessons should not fight for the same visual channel. When they collide, players stop knowing which skill they failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Knobs we are still turning
&lt;/h2&gt;

&lt;p&gt;Honesty over polish theater.&lt;/p&gt;

&lt;p&gt;Compatibility mode plus frame drops still ask how heavy telegraph must be before it reads “in time” on a tired tab. A stamp that is clear at 60 fps can smear into a lie when the tab hitching hides the outline for two frames. Boost hold stretches how much track distance fits inside the same commit window, so we keep asking whether telegraph should lean slightly earlier as speed rises — without turning every stamp into a neon billboard. We also keep auditing pickup placement against hazard arcs so the collect loop never reintroduces baited deaths.&lt;/p&gt;

&lt;p&gt;If you ship Godot 4 browser runners, we want the cheap signals: outline weight, earlier phase, softer post-death copy, or anything that makes a first-run death feel like the player’s miss instead of the build’s lie.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why readable hazards are the first-run floor
&lt;/h2&gt;

&lt;p&gt;Readable hazards are infrastructure for first-run trust on the desktop web build. Without them, every other system — Boost, collect bins, end-of-run payoff — spends trust it never earned. With them, a death can still sting and still earn one more try.&lt;/p&gt;

&lt;p&gt;Desktop build on cgt.monster.&lt;/p&gt;

&lt;p&gt;When a first-run death hits, do players more often blame &lt;em&gt;unclear telegraph&lt;/em&gt; or &lt;em&gt;unfair judgment&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cgt.monster/x/devto7" rel="noopener noreferrer"&gt;Play on desktop&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fan-made · Desktop-only · Not affiliated with any car maker.&lt;/p&gt;

</description>
      <category>godotgamedevwebdevindiedev</category>
    </item>
    <item>
      <title>Packing a garage fleet from one car visual under Compatibility</title>
      <dc:creator>CGT Rocket X Racer</dc:creator>
      <pubDate>Sun, 25 Oct 2026 10:58:52 +0000</pubDate>
      <link>https://dev.to/cgtrocketx_dev/packing-a-garage-fleet-from-one-car-visual-under-compatibility-4gbg</link>
      <guid>https://dev.to/cgtrocketx_dev/packing-a-garage-fleet-from-one-car-visual-under-compatibility-4gbg</guid>
      <description>&lt;p&gt;A runner that ends with a single victory pose wastes the collecting loop. In CGT Rocket X Racer — a fan-made rocket-car runner for desktop browsers — the payoff is a garage pack: the cars you built line up, then leave. The hard part is not inventing a second vehicle system. It is packing a readable fleet from the same visual the player just flew, without cloning physics cars or blowing a Compatibility + no-threads budget.&lt;/p&gt;

&lt;p&gt;Fan-made · Desktop-only · Not affiliated with any car maker.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pack size is already decided mid-run
&lt;/h2&gt;

&lt;p&gt;During the run you collect five part types: body, chip, motor, thrust, and energy. Cars-built is the minimum of those five counters. The HUD marks the weakest bin in red, so the player already knows which part is capping the fleet before the finish garage opens. The end sequence does not invent a new score. It stages a number the run already earned.&lt;/p&gt;

&lt;p&gt;That contract matters for packing. The pack is honest only if the lineup count tracks the same min rule the HUD used. If the garage showed a fixed parade regardless of collection, the red counter would feel decorative. If it tried to spawn a full physics car per built unit, a strong run would hitch the tab right when the player wants a clean reward.&lt;/p&gt;

&lt;h2&gt;
  
  
  One source visual, many quiet replicas
&lt;/h2&gt;

&lt;p&gt;The garage display path starts from the car the player actually finished with — after hit-flash alpha is restored — not from a separate showcase prefab. Design intent: duplicate the visual tree, park replicas on berths inside the delivery-hub frame, and disable processing on those copies so they do not keep running gameplay systems.&lt;/p&gt;

&lt;p&gt;Dynamic nodes get stripped from each replica before it sits on a plinth: particle emitters, audio players, cameras, lights, animation players. The mesh stays. The thruster language players already learned mid-run — side nozzle mist locked to body markers, underbody hover jets that read as flight — can be reused for the leave beat without carrying live VFX graphs on every copy. Shadows stay off on display meshes. Compatibility already limits what a browser tab can spend; a row of cars is not an excuse to multiply draw cost by accident.&lt;/p&gt;

&lt;p&gt;Controls during the run stay the same keyboard contract the rest of the loop uses: A/D for lane cuts, W or Space for the jump-flip, S for ground roll, Shift for boost. None of that input path belongs on a parked replica. Packing is presentation, not a second simulation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cap what the camera shows, keep the true count
&lt;/h2&gt;

&lt;p&gt;A strong collecting run can push cars-built past what a landscape framing can read at a glance. Design intent for the garage pack is a hard display cap: show up to a small row that fits the delivery hub, while the results UI still reports the real cars-built total. Icons and the count-up carry the full number. The 3D pack carries silhouette and motion.&lt;/p&gt;

&lt;p&gt;That split avoids two failure modes. First, an endless row that shrinks into illegible dots at the back of the garage. Second, a fake “full fleet” that ignores the min-of-five rule. Players should be able to glance at the pack, then confirm the exact total on the stock-style results line without arguing with the camera.&lt;/p&gt;

&lt;p&gt;Berths are staggered left/right and stepped back along the garage depth so overlapping meshes do not read as one blob. The delivery hub itself is a lit box the car flies into and settles in — not a hard cut to a menu — so the pack appears inside a space the player already understands as finish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lift-off is delivery, not fireworks
&lt;/h2&gt;

&lt;p&gt;After results, the front-of-garage beat is a pack lift: cars leave together, but not as a synced parade. Design intent is departure with variation — peel timing and direction differ so the eye reads “these are leaving,” not “a particle fountain of identical silhouettes.” Some peel off. Most keep going. The fleet should exit the frame rather than hover forever as celebration confetti with thrusters.&lt;/p&gt;

&lt;p&gt;That motion reuses the same flight tells as the run. Underbody hover jets and marker-locked mist sell flight without new post-process. Compatibility and the no-threads web template already push us toward motion and camera over heavy effects; the pack lift is the same philosophy at the end of the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What packing must never break
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cars-built stays &lt;code&gt;min(body, chip, motor, thrust, energy)&lt;/code&gt; from the run — the pack does not invent units.&lt;/li&gt;
&lt;li&gt;Replicas do not keep physics, boost, or input alive on the plinth.&lt;/li&gt;
&lt;li&gt;Display count can be capped for readability; the results total stays truthful.&lt;/li&gt;
&lt;li&gt;Thruster markers stay on the mesh; mist that drifts off the body is worse when multiplied across a row.&lt;/li&gt;
&lt;li&gt;Landscape framing stays readable: the pack is a wide beat, not a portrait crop of one car.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this sits after fair hazards
&lt;/h2&gt;

&lt;p&gt;A short pack only motivates if players trust the run that fed it. Presses and sweeping indoor arms are audited so pickups are not buried inside hazard motion and indoor patterns keep a safe lane. Distance-sampled hazards keep boost from scrambling approach phase. When a missing energy part is the player’s miss, a thin garage row is coaching. When a miss feels like a dishonest stamp, the same row feels like the game cheated the reward.&lt;/p&gt;

&lt;p&gt;We are still tightening how many replicas stay lit under Compatibility when the leave beat starts, and how aggressively we strip VFX before duplication. Feedback from people shipping Godot 4 browser showcases — especially fleet outros that reuse one player mesh — is welcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Play the desktop browser build: &lt;a href="https://www.cgt.monster/x/devto6" rel="noopener noreferrer"&gt;CGT Rocket X Racer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fan-made · Desktop-only · Not affiliated with any car maker.&lt;/p&gt;

</description>
      <category>godotgamedevwebdevindiedev</category>
    </item>
    <item>
      <title>Mid-air flips and side nozzles: selling flight without rewriting the hitbox</title>
      <dc:creator>CGT Rocket X Racer</dc:creator>
      <pubDate>Thu, 22 Oct 2026 20:23:03 +0000</pubDate>
      <link>https://dev.to/cgtrocketx_dev/mid-air-flips-and-side-nozzles-selling-flight-without-rewriting-the-hitbox-2d3m</link>
      <guid>https://dev.to/cgtrocketx_dev/mid-air-flips-and-side-nozzles-selling-flight-without-rewriting-the-hitbox-2d3m</guid>
      <description>&lt;p&gt;In a landscape browser runner, a jump that only changes Y position feels like a hop. A flip that rotates the car, keeps underbody jets lit, and throws mist from the side nozzles feels like flight. In CGT Rocket X Racer — a fan-made rocket-car runner for desktop browsers — we spent a stretch making that mid-air flip read as intentional without letting the cosmetics decide when you live or die.&lt;/p&gt;

&lt;p&gt;Fan-made · Desktop-only · Not affiliated with any car maker.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flip is a commit, not a hover
&lt;/h2&gt;

&lt;p&gt;Controls stay simple on purpose: A/D for lane cuts, W or Space for the jump-flip, S for ground roll, Shift for boost. Boost has no cooldown. A tap holds for about 1.5 seconds and climbs toward 370; hold keeps that speed; release eases back over about one second. The flip shares that same keyboard timing indoors and outdoors. That matters because the dodge window against a press or a low barrier is muscle memory. If the flip apex changed between biomes, players would blame the car instead of their timing.&lt;/p&gt;

&lt;p&gt;We treat the flip as a short commit. Once W or Space is accepted, the car is in the air for a fixed window. There is no mid-flip cancel that lets you sit above a stamp forever. Ground roll is the opposite commit: stay low, trade height for a different clearance. Mash both and the first accepted input wins for that window, so the browser never has to resolve a half-flip, half-roll state that looks broken on camera.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side nozzles are markers, not thrusters
&lt;/h2&gt;

&lt;p&gt;Side nozzle mist is locked to model markers on the car. When the flip starts, those markers already know where mist should emit relative to the mesh. We do not spawn free-floating particles at world origin and hope they track. Underbody hover jets stay on for the whole airborne window so the silhouette still reads as flying rather than falling with style.&lt;/p&gt;

&lt;p&gt;None of that rewrites collision. The hit volume for the flip is the same capsule family we use on the ground, raised and slightly rotated with the mesh. If mist clipped a press before the capsule did, the death would feel dishonest. If the capsule clipped while mist sailed clear, the death would feel like a bug. Cosmetics follow the mesh; collision follows the capsule; both share the same flip timeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser budget shapes the feel work
&lt;/h2&gt;

&lt;p&gt;The build uses Godot’s Compatibility renderer and the no-threads web template. Compatibility keeps the game running on more desktop machines in a tab. No-threads keeps the host as a plain static site without special cross-origin headers. Neither choice is an excuse to skip feel work. They do mean we cannot lean on heavy post-process tells or runtime probes that burn frame time while the car is upside down.&lt;/p&gt;

&lt;p&gt;Telegraph stays cheap: silhouette rotation, nozzle mist on markers, hover jets that stay lit, and a camera that keeps the landscape framing readable at boost speed. We verify flip clearance offline against press cycles and low barriers at several travel speeds, then ship the timings. The browser runner only plays back what already passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Input buffer: forgive jitter, not pre-queue
&lt;/h2&gt;

&lt;p&gt;Browser tabs drop frames. A short input buffer on lane cuts and on the flip press forgives that without letting players stack three moves through a dense hazard row. Enough for one dropped frame after you already pressed. Not enough to queue a flip from half a second early and coast through a stamp on luck.&lt;/p&gt;

&lt;p&gt;The same rule applies when boost is held. At higher speed the flip still lasts the same commit window in game time, so the distance you cover in the air grows. Players learn that holding Shift makes the flip a longer jump in track space, not a longer animation. Boost changes how far the commit takes you, not whether the commit exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a clean flip should read as
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;W or Space starts a readable rotation; the car does not pop up as a flat sprite.&lt;/li&gt;
&lt;li&gt;Side nozzle mist stays glued to body markers through the spin.&lt;/li&gt;
&lt;li&gt;Underbody hover jets stay lit so flight still reads as flight.&lt;/li&gt;
&lt;li&gt;Landing recovers into the same lane logic as a ground-roll exit — no surprise lane snap.&lt;/li&gt;
&lt;li&gt;A miss after a late flip should feel late, not like the mist lied about clearance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why the rest of the loop depends on this
&lt;/h2&gt;

&lt;p&gt;When the flip is trustworthy, the collecting loop can stay strict. Five part types — body, chip, motor, thrust, energy — drive cars-built as the minimum of the five, with the weakest counter marked red on the HUD. The results screen draws a stock-style line that dips where you took damage. Ten skins wait behind a garage CONFIRM. None of that lands if a mid-air death feels like the nozzles cheated.&lt;/p&gt;

&lt;p&gt;We are still tightening flip recovery into ground roll, and how much mist we allow under Compatibility before the silhouette gets noisy. Feedback from people shipping Godot 4 browser builds — especially on flip timing that stays stable under variable tab frame time — is welcome.&lt;/p&gt;

&lt;p&gt;Fan-made · Desktop-only · Not affiliated with any car maker.&lt;/p&gt;

</description>
      <category>indiedev</category>
      <category>gamedev</category>
      <category>webdev</category>
      <category>godot</category>
    </item>
    <item>
      <title>Auditing stamp presses and robot arms for a fair browser runner</title>
      <dc:creator>CGT Rocket X Racer</dc:creator>
      <pubDate>Thu, 22 Oct 2026 15:25:25 +0000</pubDate>
      <link>https://dev.to/cgtrocketx_dev/auditing-stamp-presses-and-robot-arms-for-a-fair-browser-runner-14hm</link>
      <guid>https://dev.to/cgtrocketx_dev/auditing-stamp-presses-and-robot-arms-for-a-fair-browser-runner-14hm</guid>
      <description>&lt;p&gt;Fairness is easy to claim and hard to prove in a 3D runner. In CGT Rocket X Racer, a fan-made rocket-car runner that plays in a desktop browser, the two hazards players blame first are giant presses and sweeping robot arms. If either feels random, every other system — parts, boost, flip — gets blamed with it. This post is about how we audit those patterns so a miss reads as a player choice, not a coin flip.&lt;/p&gt;

&lt;p&gt;Fan-made · Desktop-only · Not affiliated with any car maker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why presses need a cycle audit
&lt;/h2&gt;

&lt;p&gt;A press looks simple: a stamp comes down, a stamp goes up. At one speed that is true. At three speeds it is not. The safe window shrinks or stretches with travel speed, and a pattern that felt generous in the editor can clip the car once boost is held. We treat every press as a full-cycle object: down, dwell, up, reset. An automated audit steps each pattern through that cycle at several speeds and flags any frame where the hit volume overlaps a legal lane at the moment a player could already be committed.&lt;/p&gt;

&lt;p&gt;That audit is not a balance spreadsheet. It answers one question: if the player is already in the lane and the press starts its stroke, is there still a readable escape — flip over, ground roll under, or a side cut — before the stamp closes?&lt;/p&gt;

&lt;h2&gt;
  
  
  Robot arms and the safe-lane rule
&lt;/h2&gt;

&lt;p&gt;Indoor halls use sweeping robot arms. Arms that cover every lane at once feel clever for one run and cheap forever after. Our rule is blunt: every indoor arm pattern must leave at least one safe lane for the whole sweep. We sample the arm from distance along the track rather than wall-clock time, so the safe lane stays the same whether the car is coasting or holding boost. Outdoors the fog drops and the sky opens, but the same rule still applies when an arm stretch appears near a hall exit — the player should not have to guess whether the outdoor lighting changed the hitbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Input feel is part of fairness
&lt;/h2&gt;

&lt;p&gt;A fair hazard still fails if input arrives late. Controls are simple on purpose: A/D for lane cuts, W or Space for jump-flip, S for ground roll, Shift for boost. Boost has no cooldown. A tap holds for about 1.5 seconds and climbs toward 370; hold keeps that speed; release eases back over about one second. Because flip and boost share the same keyboard timing indoors and outdoors, the press audit can assume a stable commit window instead of a different curve per biome.&lt;/p&gt;

&lt;p&gt;We keep a short input buffer on lane cuts so a single dropped frame in the browser tab does not eat a dodge that was already pressed. That buffer is small on purpose — enough to forgive browser jitter, not enough to let players pre-queue three moves through a dense press row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser budget shapes the audit, not the excuse
&lt;/h2&gt;

&lt;p&gt;The build uses Godot's Compatibility renderer and the no-threads web template. Compatibility keeps the game running on more desktop machines in a tab; no-threads keeps the host as a plain static site without special cross-origin headers. Neither choice is a reason to skip fairness work. They do mean the audit itself has to stay light: we verify patterns offline and ship the verified timings, rather than running heavy probes at runtime in the browser.&lt;/p&gt;

&lt;p&gt;Effects stay cheap too. Side nozzle mist is locked to model markers, and underbody hover jets stay on so flight reads as flight. Those visuals do not change hitboxes. They only make a successful dodge feel like it came from the car.&lt;/p&gt;

&lt;h2&gt;
  
  
  What fairness unlocks downstream
&lt;/h2&gt;

&lt;p&gt;When presses and arms are trustworthy, the rest of the loop can stay honest. Five part types — body, chip, motor, thrust, energy — drive cars-built as the minimum of the five, with the weakest counter marked red on the HUD. The results screen draws a stock-style line that dips where you took damage. Ten skins wait behind a garage CONFIRM. None of that lands if the player thinks the stamp cheated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways for other browser runners
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Audit hazards as full cycles at several speeds, not as a single editor preview.&lt;/li&gt;
&lt;li&gt;Indoor sweeping hazards should always leave a safe lane; sample by track distance when speed varies.&lt;/li&gt;
&lt;li&gt;Keep boost and flip timing the same across biomes so dodge windows stay learnable.&lt;/li&gt;
&lt;li&gt;A tiny input buffer helps browser jitter; a large one hides bad level design.&lt;/li&gt;
&lt;li&gt;Under Compatibility + no-threads, prove fairness offline and ship the timings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We are still tightening press dwell windows and arm telegraphs. Feedback from people shipping Godot 4 browser builds is welcome.&lt;/p&gt;

&lt;p&gt;Optional peek: desktop browser build at cgt.monster&lt;/p&gt;

&lt;p&gt;Fan-made · Desktop-only · Not affiliated with any car maker.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>webdev</category>
      <category>indiedev</category>
    </item>
    <item>
      <title>Designing a fleet lift-off end screen for a browser runner</title>
      <dc:creator>CGT Rocket X Racer</dc:creator>
      <pubDate>Thu, 22 Oct 2026 14:59:48 +0000</pubDate>
      <link>https://dev.to/cgtrocketx_dev/designing-a-fleet-lift-off-end-screen-for-a-browser-runner-270o</link>
      <guid>https://dev.to/cgtrocketx_dev/designing-a-fleet-lift-off-end-screen-for-a-browser-runner-270o</guid>
      <description>&lt;p&gt;Most runners end the same way: you crash or cross the line, a panel slides in, and you press retry. That works, but it throws away the best moment to show players what their run produced. In CGT Rocket X Racer, a fan-made rocket-car runner that plays in a desktop browser, we tried to make the end of a run feel like a delivery instead of a game-over screen. This post covers how that end sequence is put together, the constraints a browser build puts on it, and the trade-offs we made.&lt;/p&gt;

&lt;p&gt;Fan-made · Desktop-only · Not affiliated with Tesla.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the run produces
&lt;/h2&gt;

&lt;p&gt;The end screen only means something if the run feeds into it. During a run you collect five part types: body, chip, motor, thrust and energy. The number of cars you can build equals the lowest of the five counters. The HUD shows your weakest counter in red, so you always know which part is holding you back. This rule matters for the end screen. Since the number of cars depends on your weakest part, a row of finished cars directly shows how balanced your collecting was, not just how far you got.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sequence, step by step
&lt;/h2&gt;

&lt;p&gt;The end of a run happens in several steps rather than one hard cut.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Arrival.&lt;/strong&gt; The route alternates between indoor factory halls and open air, and it finishes by flying into an end garage. The car brakes and hovers there, and the camera orbits around it. That orbit gives the player a moment to look at the car before any numbers appear.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Results.&lt;/strong&gt; The results screen draws a stock-style price line over your run, with dips where you took damage. A line chart is easier to read at a glance than a table of numbers. You can see a clean stretch, a bad collision and a recovery without reading anything.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Delivery.&lt;/strong&gt; After the results, the scene moves to the front of the garage, where a row of cars lifts off together. This is the payoff: the parts you collected become a group of vehicles leaving the factory, not a number in a corner.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The garage decision.&lt;/strong&gt; From there the player can go into the garage, browse the skin picker (ten skins), and press CONFIRM to save a choice and apply it to the car.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why a fleet and not a single car
&lt;/h2&gt;

&lt;p&gt;A single car taking off looks like any victory animation. A row of cars taking off together says something different: you built these. Linking the visuals to the cars-built rule keeps the screen honest. It also gives players a clear reason to change how they play next time. If the row looks short, the red counter on the HUD already showed which part to look for.&lt;/p&gt;

&lt;p&gt;There is a readability reason too. During the run, the side nozzles and the always-on underbody hover jets are the main sign that the car is flying rather than driving. Reusing the same thruster language for the lift-off at the end ties the end screen to the feel of the run, so no new visual vocabulary has to be learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser constraints shaped the design
&lt;/h2&gt;

&lt;p&gt;The game is built in Godot and exported to the web. Two decisions from the rest of the project directly limited what the end screen could do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compatibility renderer.&lt;/strong&gt; Everything uses Godot's Compatibility renderer, which runs on more machines in the browser but gives up some heavier lighting and post-processing. So the end screen cannot depend on expensive effects for impact. It depends on motion instead: a camera orbit, a group lifting together, and thruster effects reused from gameplay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No-threads web template.&lt;/strong&gt; The web build uses the no-threads template, so it runs on a plain static host without the special cross-origin headers threaded builds need. The trade-off is less room for parallel work. That pushes us to keep the end sequence light. It reuses vehicle assets and effects already loaded for the run instead of loading a separate showcase scene with its own large assets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Effects in the right place.&lt;/strong&gt; We spend effects where players actually look. During development the side nozzles were moved back behind the front wheel arch, and a check tool confirms every mist emitter sits exactly on its marker in the model. The same discipline applies to a row of cars: if emitters drift off their markers, the mistakes repeat across every car in the row and get much easier to spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fairness upstream, payoff downstream
&lt;/h2&gt;

&lt;p&gt;An end screen that celebrates the run only works if players believe the run was fair. The hazards along the way are giant presses and sweeping robot arms, plus several debuffs. An automated audit tests every press pattern across its full cycle at several speeds. Pickups are never placed inside an obstacle's motion, and indoor arm patterns always leave a safe lane. When players trust that a missing part was their own mistake, a short row at the end works as motivation instead of feeling unfair.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pacing: keeping it skippable in spirit
&lt;/h2&gt;

&lt;p&gt;Runners live on quick retries, and a long outro gets in the way. We keep the end sequence in short, separate steps (arrival, results, delivery, garage), so each step does one job and players always know what they are looking at. The results chart carries the information. The lift-off is the reward. The garage is the next decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we would tell other browser devs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Connect the end screen to a rule the player already understands. Ours is "cars built = lowest part counter".&lt;/li&gt;
&lt;li&gt;Under the Compatibility renderer, motion and camera are cheaper than post-processing and read better anyway.&lt;/li&gt;
&lt;li&gt;Reuse in-run assets for the outro so a lightweight web build doesn't have to load a showcase scene.&lt;/li&gt;
&lt;li&gt;Use tooling to check effect placement. Small offsets multiply when you show many vehicles at once.&lt;/li&gt;
&lt;li&gt;Show performance as a shape, not a spreadsheet. A line with dips is quick to read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We are still tuning the order and timing of these steps, and feedback from people building web runners is very welcome.&lt;/p&gt;

&lt;p&gt;If you want to see the sequence yourself, the fan-made game runs in a desktop browser at &lt;a href="http://www.cgt.monster" rel="noopener noreferrer"&gt;www.cgt.monster&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Fan-made · Desktop-only · Not affiliated with Tesla.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>webdev</category>
      <category>indiedev</category>
    </item>
    <item>
      <title>Shipping a 3D Godot 4 game to the desktop browser: what we changed for performance</title>
      <dc:creator>CGT Rocket X Racer</dc:creator>
      <pubDate>Thu, 22 Oct 2026 05:31:36 +0000</pubDate>
      <link>https://dev.to/cgtrocketx_dev/shipping-a-3d-godot-4-game-to-the-desktop-browser-what-we-changed-for-performance-30ol</link>
      <guid>https://dev.to/cgtrocketx_dev/shipping-a-3d-godot-4-game-to-the-desktop-browser-what-we-changed-for-performance-30ol</guid>
      <description>&lt;p&gt;CGT Rocket X Racer is a fan-made 3D runner we build in Godot 4 and play in a desktop browser. You fly a hovering rocket car through a large factory made of many separate halls, then out over the rooftops and back in. That is a lot of geometry for a web build. This post collects the performance decisions that mattered most for us. I'm describing techniques, not quoting benchmark numbers, because results depend heavily on the hardware.&lt;/p&gt;

&lt;p&gt;Fan-made, not affiliated with Tesla. Desktop-only.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Pick the renderer once, and use it everywhere
&lt;/h2&gt;

&lt;p&gt;Godot's web export only supports the Compatibility renderer (OpenGL ES 3 / WebGL 2). We made the same choice for every platform. The game looks identical everywhere, it runs on older GPUs, and we only have one set of materials and lighting to tune.&lt;/p&gt;

&lt;p&gt;The trade-off is real: the Mobile and Forward+ renderers give better lighting and post-processing. Switching native builds later is a one-line project setting, but every material and light would need checking again. For us, "looks the same in the browser as in testing" was worth more than the extra effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The no-threads web template
&lt;/h2&gt;

&lt;p&gt;We export with the no-threads web template. It doesn't need SharedArrayBuffer, so it also doesn't need the cross-origin isolation headers (COOP/COEP) that threaded builds require. In practice that means the build runs on any static host and is easier to embed or deploy.&lt;/p&gt;

&lt;p&gt;The cost is that you can't rely on threads in the browser the way you can on native. Anything that loads a lot of data has to be designed so it doesn't stall the main loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Render 3D below native resolution, keep the UI sharp
&lt;/h2&gt;

&lt;p&gt;Fill rate is the first wall you hit in a browser on a high-DPI screen. Our approach on web and mobile is to render only the 3D viewport at a reduced scale, while the UI canvas stays at native resolution. The scale comes from the device pixel ratio and the window's pixel count, clamped to a sensible range, with bilinear filtering. There's also a project-level fallback scale in case the script never runs.&lt;/p&gt;

&lt;p&gt;The result: the world is a little softer, but text and HUD stay crisp, and crisp text is what players notice. On a normal desktop window the scale stays at full resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Near and far versions of every hall
&lt;/h2&gt;

&lt;p&gt;The factory is made of many hall models. Each hall exists twice: a detailed "near" file and a cheap "far" shell that keeps the silhouette and landmarks. A site streamer swaps between them by distance.&lt;/p&gt;

&lt;p&gt;Obstacles use the same idea with Godot visibility ranges. The detailed LOD0 mesh is only drawn close to the camera, and LOD1 takes over beyond that. Both levels share one animated hierarchy: the LOD1 meshes are grafted onto the LOD0 nodes. Animation runs once, and collision reads the same transforms.&lt;/p&gt;

&lt;p&gt;This matters most when the route climbs out above the roofs, where you can suddenly see the whole site.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Fewer surfaces, fewer draw calls
&lt;/h2&gt;

&lt;p&gt;Draw calls are expensive in WebGL. A few habits helped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Merge by material at import.&lt;/strong&gt; A post-import script merges the car, coins and parts into one mesh with one surface per material and regenerates LODs. It keeps the named marker nodes we need, like nozzle positions, as Marker3D.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MultiMesh for pickups.&lt;/strong&gt; Coins and parts are drawn with MultiMesh instead of one node per pickup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bake debuffs per pivot.&lt;/strong&gt; The debuff obstacles are baked into a few merged meshes per moving pivot, to stay inside our draw budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bake instanced site geometry.&lt;/strong&gt; The site was originally authored elsewhere. Our export script bakes each instanced mesh into one merged mesh per material with vertex colours before it becomes a GLB.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Keep textures in check
&lt;/h2&gt;

&lt;p&gt;Obstacle textures come in as KTX2. Godot imports them and re-compresses to VRAM formats for the web build (S3TC and ETC2 in the pack). We also cap texture sizes per map type: base colour gets the most resolution, while ORM and emissive maps get far less. When two LOD levels had byte-identical textures, we imported the far one without images and let its meshes reuse the near materials. That avoids loading the same texture twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Cheap effects
&lt;/h2&gt;

&lt;p&gt;Mist from the car's nozzles and pickup sparkles use CPUParticles3D, and the emitters are pooled rather than created per event. Pickup sparkles are parented to the car so they keep up at high speed without extra work. The crash effect is a short screen flash and a decaying camera shake. Both are cheap, and both do a lot for feel.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Audio that respects the browser
&lt;/h2&gt;

&lt;p&gt;Browsers block audio until the user interacts with the page. Instead of fighting that, we don't create any audio players or play anything until the first real tap, click or key press. That avoids autoplay warnings and wasted work at startup. Our placeholder sound effects are also generated procedurally by a small script, so there are no downloaded audio files to ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Measure at fixed points, not just "it feels fine"
&lt;/h2&gt;

&lt;p&gt;The project has a bench mode. It runs the game under a virtual framebuffer at a fixed resolution and frame rate, and records performance monitors at specific route points: a heavy press, the sign approaches, the garage and a worst-case combination of hazards. There's a separate mode for measuring the in-race HUD alone. Fixed checkpoints mean "this change made the press area slower" is something you can actually see, instead of a vibe.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone starting a 3D Godot web game
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Decide on Compatibility and no-threads early, and design around them.&lt;/li&gt;
&lt;li&gt;Split 3D resolution from UI resolution.&lt;/li&gt;
&lt;li&gt;Build near/far versions of big environment chunks before you need them.&lt;/li&gt;
&lt;li&gt;Merge meshes by material and use MultiMesh for anything repeated.&lt;/li&gt;
&lt;li&gt;Make performance measurable at named points on your level.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to see how it holds up in your own browser, the game is playable on desktop at &lt;a href="https://cgt.monster/x/devto2" rel="noopener noreferrer"&gt;https://cgt.monster/x/devto2&lt;/a&gt;. Feedback on frame pacing is very welcome.&lt;/p&gt;

&lt;p&gt;Fan-made, not affiliated with Tesla. Desktop-only.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>godot</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Making a hover car feel like it flies: notes from a Godot 4 runner</title>
      <dc:creator>CGT Rocket X Racer</dc:creator>
      <pubDate>Thu, 22 Oct 2026 03:48:07 +0000</pubDate>
      <link>https://dev.to/cgtrocketx_dev/making-a-hover-car-feel-like-it-flies-notes-from-a-godot-4-runner-55j3</link>
      <guid>https://dev.to/cgtrocketx_dev/making-a-hover-car-feel-like-it-flies-notes-from-a-godot-4-runner-55j3</guid>
      <description>&lt;p&gt;CGT Rocket X Racer is a fan-made, desktop-only rocket-car runner we have been building in Godot 4. It is not affiliated with any car maker. You steer a hovering car through a giant factory: lane changes, jumps, rolls and a boost, with presses and robot arms trying to ruin your run. This post is about three problems that turned out to matter far more than we expected: making the car feel airborne, switching between indoor and outdoor sections without breaking the camera, and proving that every obstacle is actually fair.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Flight feel is mostly about thrust you can see
&lt;/h2&gt;

&lt;p&gt;Our first builds had a car that simply floated at a fixed height above the track. It worked, but it looked like a sticker sliding on glass. The fix was not physics, it was feedback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Continuous underbody jets.&lt;/strong&gt; The car always has mist jets running underneath it. They never turn off, so the player's eye always has something tying the car to the ground below. That alone made the hover height believable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nozzles that sit where they should.&lt;/strong&gt; The car model has named marker nodes for every nozzle. At import time a post-import script merges the meshes by material but keeps those markers, and the car script reads the nozzle positions from the imported model at runtime. We moved the side nozzles back behind the front wheel arch so side mist reads as steering rather than exhaust. Then we wrote a small check tool that renders the car and confirms every mist emitter sits exactly on its marker. It sounds pedantic, but when skins swap the car model, the nozzles still line up, because both the base car and the skins are read the same way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A boost with no cooldown.&lt;/strong&gt; This is the rule in the current build, and it went through several iterations before we were happy with it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A tap boosts for 1.5 seconds, ramping linearly up to 370.&lt;/li&gt;
&lt;li&gt;Holding the button keeps boosting at about 370, and it holds there.&lt;/li&gt;
&lt;li&gt;When you release, speed eases back down to natural speed over 1 second.&lt;/li&gt;
&lt;li&gt;It behaves the same indoors and outdoors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We tried cooldown-based boosts earlier. They made players wait instead of play. Removing the cooldown and putting the skill into &lt;em&gt;when&lt;/em&gt; you release turned boost into a steering tool: you feather it to line up a gap, then let it ease off before a hazard. The linear ramp matters too. An instant jump to top speed felt like a teleport; a short ramp feels like thrust building.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small, cheap juice.&lt;/strong&gt; Pickups get a short sparkle burst parented to the car so it keeps up at high speed, and the coin itself pops in scale for a fraction of a second. A crash is a single screen flash (no strobe) plus a short decaying camera shake. None of this is expensive, and all of it adds to the sense of speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Indoor and outdoor switching
&lt;/h2&gt;

&lt;p&gt;The route is one long closed loop through a factory site made of many separate hall models. The run keeps alternating: inside a hall, burst out above the roofs, fly through a big SUPER FACTORY sign tower, dive back in, and eventually fly into an end garage where the car brakes, hovers and the camera orbits before the results screen.&lt;/p&gt;

&lt;p&gt;Three things made this workable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streaming and LOD.&lt;/strong&gt; Each hall exists as a detailed "near" model and a cheap "far" shell. A site streamer loads halls on a background thread and swaps LOD by distance. Without this, flying above the roofs, where you can suddenly see everything, would tank the frame rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A rail-locked camera, with its own test.&lt;/strong&gt; The camera follows the route frame rather than free physics. Transitions between tight halls and open sky are exactly where cameras clip into walls, so we added a dedicated camera test mode that flies the whole loop in all three lanes and reports clipping. Running it after every route change is far cheaper than finding clipping by hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Outdoors changes the look, not the rules.&lt;/strong&gt; Outside, the sky is brighter and there is much less fog, so leaving a hall feels like a release. We deliberately do not change speed outdoors, and boost behaves exactly the same. Players should never have to relearn the controls because the ceiling disappeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Proving obstacles are fair
&lt;/h2&gt;

&lt;p&gt;Runners live or die on fairness. If a player crashes and thinks "that was impossible", they quit. So we stopped trusting our eyes and started auditing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collision that matches the visuals.&lt;/strong&gt; The car uses three oriented boxes fitted to its body, including during banking and barrel rolls. Obstacles use boxes attached to their animated nodes: the press head, posts, top beam and die plate; the arm's upper arm, forearm, wrist and gripper. Because collision reads the same transforms as the visuals, there is no invisible hitbox. On a hit, the car and the obstacle clock are bisected back to the moment of contact and frozen, so nothing is ever drawn inside the car on the crash screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deterministic obstacle timing.&lt;/strong&gt; The robot arm animation is sampled by the car's distance along the track, not by wall-clock time. That means every arm pattern has fixed safe lanes and jump windows at any speed, which makes them auditable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An audit mode.&lt;/strong&gt; A headless audit run tests every press pattern at every phase of its cycle at several speeds, and checks that the free lane is never hit. It also measures arm jump windows and rejects patterns where the window is too short to be fair. Indoor arm patterns always leave a stay-safe lane.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pickups never sit inside danger.&lt;/strong&gt; Each candidate coin or part is tested against the obstacle's boxes over its whole motion, and dropped if it touches. Nothing is worse than a coin that baits you into a press.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debuffs with their own colors.&lt;/strong&gt; Besides presses and arms there are debuffs such as red tape, a roadside media wave, an approval gate and a charging bear. Each has a distinct warning color family, so players can read what is coming before they reach it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few smaller notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Everything runs on Godot's Compatibility renderer, and the web build uses the no-threads template, so it runs from a plain static host with no special headers.&lt;/li&gt;
&lt;li&gt;Along the way you collect five part types: body, chip, motor, thrust and energy.&lt;/li&gt;
&lt;li&gt;There are ten skins in the current build; confirming a skin saves it and applies it to the car.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you want to feel the boost rule for yourself, CGT Rocket X Racer is playable in a desktop browser (desktop-only for now):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.cgt.monster/x/devto" rel="noopener noreferrer"&gt;https://www.cgt.monster/x/devto&lt;/a&gt;&lt;/strong&gt; · invite code &lt;strong&gt;48WYTRMMDW&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fan-made · Not affiliated · Desktop-only. Feedback on flight feel and obstacle readability is the most useful thing you can send us.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>indiedev</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
