<?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: DMITRII ZOLOTOV</title>
    <description>The latest articles on DEV Community by DMITRII ZOLOTOV (@dmitrii_zolotov_90fd95738).</description>
    <link>https://dev.to/dmitrii_zolotov_90fd95738</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%2F2351340%2F1dda64a1-3c19-4720-8be3-c43987ed520d.jpg</url>
      <title>DEV Community: DMITRII ZOLOTOV</title>
      <link>https://dev.to/dmitrii_zolotov_90fd95738</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dmitrii_zolotov_90fd95738"/>
    <language>en</language>
    <item>
      <title>I Wrote a 3D Game Engine for Flutter From Scratch — and You Can Ship a Game With It Today</title>
      <dc:creator>DMITRII ZOLOTOV</dc:creator>
      <pubDate>Tue, 01 Sep 2026 13:49:51 +0000</pubDate>
      <link>https://dev.to/dmitrii_zolotov_90fd95738/i-wrote-a-3d-game-engine-for-flutter-from-scratch-and-you-can-ship-a-game-with-it-today-1pia</link>
      <guid>https://dev.to/dmitrii_zolotov_90fd95738/i-wrote-a-3d-game-engine-for-flutter-from-scratch-and-you-can-ship-a-game-with-it-today-1pia</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a condensed version of the full story, &lt;a href="https://dzolotov.medium.com/i-wrote-a-3d-game-engine-for-flutter-from-scratch-and-you-can-ship-a-game-with-it-today-e635f1c1b8ed" rel="noopener noreferrer"&gt;originally published on Medium&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I built a 3D game engine for Flutter in pure Dart — renderer, physics, collision, audio, level format, no bindings, no native code. It's open source now: twenty-three packages on pub.dev, MIT-licensed, with three finished games you can &lt;a href="https://flutter3d.pleion.dev" rel="noopener noreferrer"&gt;play in your browser&lt;/a&gt;. Below: the architecture, the four bugs that taught me the most, and a platformer in under 600 lines.&lt;/p&gt;

&lt;p&gt;This is the fourth 3D engine I've written — x86 assembly, then Java, then C++. Every language I fall in love with gets the same test. I've been writing Flutter since 2018 and teaching it to over a thousand students, and one question kept coming back: &lt;em&gt;this framework draws anything at 120 fps, so where are the 3D games?&lt;/em&gt; The honest answer was always a list of bindings to somebody else's engine. So I wrote one that's actually Flutter's own. It's called &lt;strong&gt;flutter3d&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  One rule shaped everything
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The renderer never talks to a graphics API.&lt;/strong&gt; It talks to a hardware abstraction layer — &lt;code&gt;flutter3d_hardware&lt;/code&gt;, a small package of interfaces with a written compatibility promise. A rendering backend is just a package that implements them. Three ship out of the box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;flutter3d_impeller&lt;/code&gt; — production, on Flutter GPU (Metal, Vulkan)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flutter3d_webgl&lt;/code&gt; — WebGL2, what the browser demos run on&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flutter3d_cpu&lt;/code&gt; — a software rasterizer in pure Dart&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your game picks one with a single pubspec line. And because the HAL is a public contract, you can write your own backend without forking the engine.&lt;/p&gt;

&lt;p&gt;A software rasterizer in Dart sounds like a joke. Hold that thought.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6zpxndzssoctt4e8yf8y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6zpxndzssoctt4e8yf8y.png" alt="The flutter3d package map: apps on top, genre packages as vocabulary, a deterministic game layer with no Flutter and no GPU, and the drawing stack meeting at the hardware abstraction layer" width="799" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Four bugs that taught me more than the features did
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The racing game ran at under 1 fps for weeks.&lt;/strong&gt; Shrinking the frame changed nothing — which &lt;em&gt;was&lt;/em&gt; the clue: the cost wasn't per-pixel. A cube shadow atlas was sized from the sun's shadow settings and weighed &lt;strong&gt;402 MB&lt;/strong&gt; on a platform that doesn't have that to give. One setting fixed it. If shrinking the frame changes nothing, the money isn't in the frame.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk700sgfjwzf7nz2to6nr.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk700sgfjwzf7nz2to6nr.jpeg" alt="Two formula cars on a foggy track in the flutter3d racing game" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The shadow atlas was stored upside down — while every test passed.&lt;/strong&gt; Every check happened to view the atlas through a full-screen pass that flips its input, so the picture came back twice-flipped and matched the reference pixel-for-pixel. The lit pass sampled the texture directly and read rows nobody had drawn. Lesson: an instrument that cancels the error it's pointed at agrees with everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Every held weapon drew almost black — identically on two backends.&lt;/strong&gt; Identical wrongness across backends means the defect lives &lt;em&gt;above&lt;/em&gt; them. The weapon's own two-light "studio" was being encoded with the world's light buffer, so its lights were never uploaded. Right design, missing binding, and the difference was the entire picture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The "joke" rasterizer became the referee.&lt;/strong&gt; All three backends render the same golden scenes, compared pixel by pixel. When WebGL disagreed with Impeller by 19% on a particle scene, the CPU backend judged who was lying (WebGL — a leaked &lt;code&gt;vertexAttribDivisor&lt;/code&gt;). Today the worst divergence is &lt;strong&gt;0.55%&lt;/strong&gt;, and out of ~3000 tests only about thirty need a GPU — the whole golden suite runs headless in CI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs1ihmabwh1r8oh7sbdcv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs1ihmabwh1r8oh7sbdcv.png" alt="The same golden scene rendered by two different backends, side by side — the suite compares them pixel by pixel" width="800" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three games, zero engine edits
&lt;/h2&gt;

&lt;p&gt;The engine ships with a dungeon shooter, a platformer, and a racing game — and the claim I care about most: &lt;strong&gt;the second and third games didn't change a single line of the engine's packages.&lt;/strong&gt; Genre mechanics (weapons, monsters, double jump with coyote time, a tire model with a grip peak) live in genre packages on top of a deterministic fixed-step game layer. All three run in your browser: &lt;a href="https://flutter3d.pleion.dev" rel="noopener noreferrer"&gt;flutter3d.pleion.dev&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A platformer in under 600 lines
&lt;/h2&gt;

&lt;p&gt;The full walkthrough lives in a companion repo — &lt;a href="https://github.com/pleiondev/coin_climb" rel="noopener noreferrer"&gt;github.com/pleiondev/coin_climb&lt;/a&gt;, one commit per step, diff any step against the previous one. The condensed version:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One pubspec.&lt;/strong&gt; No graphics API in sight — &lt;code&gt;flutter3d_app&lt;/code&gt; picks the backend per platform:&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;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;flutter3d&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^0.4.0&lt;/span&gt;                 &lt;span class="c1"&gt;# renderer, scene, assets&lt;/span&gt;
  &lt;span class="na"&gt;flutter3d_game&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^0.4.0&lt;/span&gt;            &lt;span class="c1"&gt;# the fixed step, levels, actors&lt;/span&gt;
  &lt;span class="na"&gt;flutter3d_game_platformer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^0.4.0&lt;/span&gt; &lt;span class="c1"&gt;# the genre we're about to steal&lt;/span&gt;
  &lt;span class="na"&gt;flutter3d_bridge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^0.4.0&lt;/span&gt;          &lt;span class="c1"&gt;# where levels become meshes&lt;/span&gt;
  &lt;span class="na"&gt;flutter3d_app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^0.4.0&lt;/span&gt;             &lt;span class="c1"&gt;# input, settings, backend selection&lt;/span&gt;
  &lt;span class="na"&gt;flutter3d_audio&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^0.4.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A level is a 36-line JSON document.&lt;/strong&gt; Brushes become both the meshes you see and the colliders you stand on — one source, so they can't drift apart. Same format the level editor reads and writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"brushes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&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="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;20.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&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="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;20.0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"material"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ground"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"entities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"player_spawn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-7.0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"yaw"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"collectible"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&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="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-3.5&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"what"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"coin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assets/models/coin.glb"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Steal a genre.&lt;/strong&gt; This is the whole thesis. Load the level with the platformer's registry, and "collectible" suddenly means something — coin pickup, double jump, coyote time, the shrink-away animation, none of it your code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;loaded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;LevelLoader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kLevel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;device:&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;registry:&lt;/span&gt; &lt;span class="n"&gt;platformerRegistry&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// now "collectible" means something&lt;/span&gt;
  &lt;span class="nl"&gt;rules:&lt;/span&gt; &lt;span class="n"&gt;platformerRules&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;staged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loaded&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loaded&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;collision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;input:&lt;/span&gt; &lt;span class="n"&gt;_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;onFixture:&lt;/span&gt; &lt;span class="n"&gt;fixtures&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// every fixed step:&lt;/span&gt;
&lt;span class="n"&gt;staged&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The entire HUD is one &lt;code&gt;Text&lt;/code&gt; widget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'coins &lt;/span&gt;&lt;span class="si"&gt;${staged.runner.purse['coin']}&lt;/span&gt;&lt;span class="s"&gt; / &lt;/span&gt;&lt;span class="si"&gt;$kCoins&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Sound in three lines.&lt;/strong&gt; Positional audio — the coin rings from where the coin was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;_speakers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;openSpeakers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;bank:&lt;/span&gt; &lt;span class="n"&gt;SoundBank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;kCoinSound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kJumpSound&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;staged&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jumpedThisStep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kJumpSound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;staged&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Collectible&lt;/span&gt; &lt;span class="n"&gt;taken&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;staged&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;takenThisStep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kCoinSound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;taken&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;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;That's a playable platformer in under 600 lines of your own code — 36 of them the level, none of them engine code. And because the simulation is a deterministic fixed step, the repo's test suite plays the game headless in CI: holds forward, jumps, and asserts the purse actually gained a coin.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ru7tzjufwofn4f3llgh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ru7tzjufwofn4f3llgh.png" alt="Coin Climb running: floating platforms, golden coins, a " width="800" height="632"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three lessons, none about graphics
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Package boundaries are only real if a test enforces them.&lt;/strong&gt; The boundaries without tests had quietly dissolved by the time I looked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A number in the docs that nothing verifies is already wrong.&lt;/strong&gt; The README claimed 1,242 tests; the real number was more than double. Now a scan holds the README, the architecture doc, and the website to the same answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distrust any instrument that can't fail.&lt;/strong&gt; See the upside-down atlas. My golden tests now include mutation checks — break the thing on purpose, confirm the test notices.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Play them
&lt;/h2&gt;

&lt;p&gt;MIT-licensed. Packages on pub.dev under the &lt;a href="https://pub.dev/publishers/pleion.dev" rel="noopener noreferrer"&gt;pleion.dev publisher&lt;/a&gt;, docs at &lt;a href="https://flutter3d.pleion.dev" rel="noopener noreferrer"&gt;flutter3d.pleion.dev&lt;/a&gt;, source &lt;a href="https://github.com/pleiondev/flutter3d" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;. The war stories in full detail — the six-session upside-down atlas, the level format design, all of it — are in the &lt;a href="https://dzolotov.medium.com/i-wrote-a-3d-game-engine-for-flutter-from-scratch-and-you-can-ship-a-game-with-it-today-e635f1c1b8ed" rel="noopener noreferrer"&gt;Medium original&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But first: &lt;a href="https://flutter3d.pleion.dev" rel="noopener noreferrer"&gt;play the three games in your browser&lt;/a&gt;. They're the honest demo.&lt;/p&gt;

&lt;p&gt;And to every student who ever asked me whether Flutter can do a real 3D game: it can now. I checked.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>gamedev</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
