<?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: Vlad Yefremov</title>
    <description>The latest articles on DEV Community by Vlad Yefremov (@vladosskin).</description>
    <link>https://dev.to/vladosskin</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%2F4227520%2F978e33cb-69bc-4b28-9fe8-2fc8acfe42a8.jpg</url>
      <title>DEV Community: Vlad Yefremov</title>
      <link>https://dev.to/vladosskin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vladosskin"/>
    <language>en</language>
    <item>
      <title>Preparing a Godot 4 game for Google Play without the pain (and a CLI that does the boring checks)</title>
      <dc:creator>Vlad Yefremov</dc:creator>
      <pubDate>Thu, 05 Nov 2026 14:43:18 +0000</pubDate>
      <link>https://dev.to/vladosskin/preparing-a-godot-4-game-for-google-play-without-the-pain-and-a-cli-that-does-the-boring-checks-3h08</link>
      <guid>https://dev.to/vladosskin/preparing-a-godot-4-game-for-google-play-without-the-pain-and-a-cli-that-does-the-boring-checks-3h08</guid>
      <description>&lt;p&gt;I'm making Kosti 3D, a survivor-like set in Slavic folklore: top-down 3D, auto-aim, a hero fighting his way through four chapters to Koschei the Deathless. It's built in Godot 4.6 and aimed at Android phones. My test floor is a Redmi 9. It isn't on Play yet; I'm getting it there.&lt;/p&gt;

&lt;p&gt;Writing the game was the comfortable part. Getting it into a shape Google Play accepts is where I lost evenings. None of the problems were hard; they were boring, easy to forget, and showed up as vague export errors or a build that was far too big.&lt;/p&gt;

&lt;p&gt;After hitting the same issues a few times, I turned the checks into a small CLI called &lt;strong&gt;godot-ship-kit&lt;/strong&gt; (&lt;code&gt;gsk&lt;/code&gt;). The problems come first here, because they're useful even if you never install anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. SDK, JDK and export templates
&lt;/h2&gt;

&lt;p&gt;Godot's Android export needs three things in the right place: the Android SDK (with platform-tools, build-tools and cmdline-tools), JDK 17, and export templates for &lt;em&gt;exactly&lt;/em&gt; your Godot version. When I moved the project to a new build server, every error came down to wrong paths or templates for a different patch version.&lt;/p&gt;

&lt;p&gt;A worse case: a cleanup script deleted the project's &lt;code&gt;android/build/&lt;/code&gt; folder, which is the Gradle build template. The next export failed with "build template not installed". It took a while to connect the two.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gsk doctor&lt;/code&gt; checks all of this up front: the Godot binary against the project's version, templates for that exact version (including .NET builds), each SDK component, the JDK version, and whether the Gradle template matches Godot. Every finding comes with a &lt;code&gt;fix:&lt;/code&gt; line. &lt;code&gt;gsk configure&lt;/code&gt; writes the SDK and JDK paths into Godot's editor settings for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The keystore is the one file you can't lose
&lt;/h2&gt;

&lt;p&gt;Once a game is on Play, every update must be signed with the same upload key. I keep mine outside the project with an offline backup, and I never want its password sitting in &lt;code&gt;export_presets.cfg&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;doctor&lt;/code&gt; looks for a release keystore from environment variables, the preset or export credentials, checks that the password and alias are present (it never prints them), and fails if a keystore, &lt;code&gt;export_credentials.cfg&lt;/code&gt; or a plain-text password ends up tracked by git. &lt;code&gt;gsk build&lt;/code&gt; takes signing from the standard Godot environment variables, so CI can use secrets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GODOT_ANDROID_KEYSTORE_RELEASE_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/secure/upload.keystore
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GODOT_ANDROID_KEYSTORE_RELEASE_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;upload
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GODOT_ANDROID_KEYSTORE_RELEASE_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...   &lt;span class="c"&gt;# from your secret store&lt;/span&gt;
gsk build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A release build stops before exporting if no keystore is configured.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. AAB, target API and 16 KB pages
&lt;/h2&gt;

&lt;p&gt;I tested Kosti as an APK for months because it's easy to sideload. But new apps on Play need an AAB, which means the Gradle build, which means the build template from point 1.&lt;/p&gt;

&lt;p&gt;Then there are the numbers Google keeps moving. The target API level requirement is now 36 (since 31 Aug 2026), and 64-bit native libraries need to support 16 KB memory pages. The editor won't tell you.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gsk build&lt;/code&gt; exports headless and then verifies the result: zip integrity, signature (&lt;code&gt;apksigner&lt;/code&gt;) and &lt;code&gt;zipalign&lt;/code&gt; (when build-tools are installed), the manifest (package, version, target SDK, permissions), and 16 KB alignment, read straight from the ELF headers of each 64-bit &lt;code&gt;.so&lt;/code&gt;. You can also run that part alone on any APK or AAB with &lt;code&gt;gsk verify&lt;/code&gt;. One detail I learned the hard way: Godot's exit code isn't a reliable signal after a failed export, so &lt;code&gt;gsk build&lt;/code&gt; treats a missing or stale output file as a failure and keeps the log.&lt;/p&gt;

&lt;p&gt;All Play numbers live in one file, each with its source and the date it was checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A 651 MB APK
&lt;/h2&gt;

&lt;p&gt;At one point the Kosti APK was 651 MB. My first thought was the 3D models. It wasn't, mostly.&lt;/p&gt;

&lt;p&gt;The size of your PNG or JPEG doesn't matter much. What ships is the copy Godot makes in &lt;code&gt;.godot/imported&lt;/code&gt;, and that size comes from the &lt;code&gt;.import&lt;/code&gt; settings. A texture imported &lt;em&gt;Lossless&lt;/em&gt; at full resolution can be much bigger than its source (especially a JPEG), and far bigger in GPU memory. AI-generated 3D models made this worse for me: they come with large textures and import with heavy defaults.&lt;/p&gt;

&lt;p&gt;Re-importing about 400 textures with VRAM compression (ETC2) and size caps, plus removing duplicate assets, brought the APK from &lt;strong&gt;651 MB to 273 MB&lt;/strong&gt;. The caps do most of the download savings; ETC2 is mainly a GPU-memory win and can even be bigger on disk. I did that by hand the first time. &lt;code&gt;gsk slim&lt;/code&gt; is the automated version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it measures every imported file and maps it back to its source,&lt;/li&gt;
&lt;li&gt;proposes a &lt;strong&gt;safe&lt;/strong&gt; tier (VRAM compression for textures used in 3D, normal-map compression) and, with &lt;code&gt;--aggressive&lt;/code&gt;, a &lt;strong&gt;review&lt;/strong&gt; tier (size caps, lossy WebP for big 2D art, QOA for long WAVs),&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--apply&lt;/code&gt; backs up every &lt;code&gt;.import&lt;/code&gt; it touches, &lt;code&gt;--undo&lt;/code&gt; restores them, and &lt;code&gt;--reimport&lt;/code&gt; runs Godot and prints the measured result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also lists unreferenced assets and heavy GLB models. Estimates assume ETC2, so trust the &lt;code&gt;--reimport&lt;/code&gt; numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The menu that was cut off on 20:9 phones
&lt;/h2&gt;

&lt;p&gt;My menus were designed at 9:16 and looked fine in the editor. On a modern 20:9 phone, the menu sat between black bars and the bottom button's label was cut off.&lt;/p&gt;

&lt;p&gt;I already had a rule, after sending test builds with visual changes I hadn't looked at: render the game headless on the server and look before building. I use AI coding agents a lot, and an agent can't see a phone. So that habit became &lt;code&gt;gsk eyes&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gsk eyes&lt;/code&gt; starts Godot under a virtual display with software OpenGL (Mesa llvmpipe), so it needs no GPU. It loads each scene at real phone resolutions, can tap buttons or call methods first, and saves a screenshot. It uses the renderer your project uses &lt;em&gt;on mobile&lt;/em&gt;, not the desktop one. Then it compares each capture against a committed reference image and reports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;similarity, changed pixels and the largest changed region (so one wrong digit in a price still fails),&lt;/li&gt;
&lt;li&gt;any Control that sticks out past the screen edge, with the overflow in pixels,&lt;/li&gt;
&lt;li&gt;black frames, meshes with no material, a 3D scene with no camera, a rigged model whose animations aren't playing, and script errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Favmn5tbhudrp886hgmvw.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Favmn5tbhudrp886hgmvw.gif" alt="gsk eyes loop: a restyled button, eyes reports a change, the crop shows the label became invisible, then the fix" width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You get an HTML report with a before/after slider, plus &lt;code&gt;report.md&lt;/code&gt; and &lt;code&gt;report.json&lt;/code&gt; for agents with full-resolution crops of changed regions. The crops matter: a 1080x2400 screenshot gets downscaled until small text is unreadable to an agent; the crop is where it notices the clipped label.&lt;/p&gt;

&lt;p&gt;Software rendering is deterministic, which is what makes reference images work in CI. The repo's CI runs the capture on Godot 4.2, 4.4, 4.5 and 4.6.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"godot-ship-kit[eyes] @ git+https://github.com/vladosskins178/godot-ship-kit"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GODOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/path/to/godot      &lt;span class="c"&gt;# if godot isn't on PATH&lt;/span&gt;

&lt;span class="nb"&gt;cd &lt;/span&gt;my-game
gsk doctor                                 &lt;span class="c"&gt;# what blocks an Android release?&lt;/span&gt;
gsk slim                                   &lt;span class="c"&gt;# what makes the build big?&lt;/span&gt;
gsk eyes &lt;span class="nt"&gt;--scene&lt;/span&gt; res://ui/menu.tscn        &lt;span class="c"&gt;# what does it look like on a phone?&lt;/span&gt;
gsk build &lt;span class="nt"&gt;--debug&lt;/span&gt;                          &lt;span class="c"&gt;# export and verify&lt;/span&gt;
gsk play-checklist &lt;span class="nt"&gt;-o&lt;/span&gt; PLAY_CHECKLIST.md    &lt;span class="c"&gt;# Play Console launch checklist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;doctor&lt;/code&gt;, &lt;code&gt;slim&lt;/code&gt;, &lt;code&gt;build&lt;/code&gt; and &lt;code&gt;play-checklist&lt;/code&gt; use only the Python standard library (Python 3.11+). &lt;code&gt;eyes&lt;/code&gt; adds Pillow and numpy, and on headless Linux needs xvfb and Mesa.&lt;/p&gt;

&lt;p&gt;The repo is also a GitHub Action (run &lt;code&gt;eyes --strict&lt;/code&gt; or &lt;code&gt;doctor --strict&lt;/code&gt; on every push), there's an MCP server (&lt;code&gt;gsk mcp&lt;/code&gt;) whose &lt;code&gt;gsk_eyes&lt;/code&gt; returns the crops as images, and an agent skill (&lt;code&gt;gsk skill print &amp;gt;&amp;gt; AGENTS.md&lt;/code&gt; for Codex, Cursor and others).&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't show performance.&lt;/strong&gt; Software rendering shows layout, colour, composition and missing assets. Frame rate and GPU-specific bugs still need a real phone. I still test on the Redmi.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It isn't an editor bridge.&lt;/strong&gt; Tools like godot-mcp drive the editor while you build the game. gsk is for the part after that, and the two work fine side by side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Animated scenes and Godot upgrades need care.&lt;/strong&gt; Moving scenes need a fixed &lt;code&gt;wait&lt;/code&gt; and a looser threshold; references must be regenerated when you upgrade Godot (gsk records the version and warns).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;eyes&lt;/code&gt;, &lt;code&gt;build&lt;/code&gt; and &lt;code&gt;slim --reimport&lt;/code&gt; run your project's scripts&lt;/strong&gt;, same as opening it in the editor. Use them on projects you trust.&lt;/li&gt;
&lt;li&gt;It's version 0.1, developed mainly on Linux with Godot 4.6.3. &lt;code&gt;doctor&lt;/code&gt;, &lt;code&gt;slim&lt;/code&gt;, &lt;code&gt;build&lt;/code&gt; and &lt;code&gt;verify&lt;/code&gt; also run on macOS and Windows.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Feedback welcome
&lt;/h2&gt;

&lt;p&gt;The code is MIT-licensed: &lt;strong&gt;&lt;a href="https://github.com/vladosskins178/godot-ship-kit" rel="noopener noreferrer"&gt;https://github.com/vladosskins178/godot-ship-kit&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you've shipped a Godot game to Play, tell me which check you wish you'd had, or which one gives a wrong answer on your project. Issues and PRs are welcome; a bug report with a small repro project is the most useful thing you can send.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written with the help of an AI assistant; the tool and the experiences are mine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>android</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
