<?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: PatilRB</title>
    <description>The latest articles on DEV Community by PatilRB (@patilrb).</description>
    <link>https://dev.to/patilrb</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4083431%2Ffec512ba-ed01-4fd6-b8e2-c7bae032d764.webp</url>
      <title>DEV Community: PatilRB</title>
      <link>https://dev.to/patilrb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/patilrb"/>
    <language>en</language>
    <item>
      <title>Tap-to-move is harder than it looks</title>
      <dc:creator>PatilRB</dc:creator>
      <pubDate>Wed, 26 Aug 2026 08:57:50 +0000</pubDate>
      <link>https://dev.to/patilrb/tap-to-move-is-harder-than-it-looks-2al1</link>
      <guid>https://dev.to/patilrb/tap-to-move-is-harder-than-it-looks-2al1</guid>
      <description>&lt;p&gt;Tap-to-move is the first input scheme most 3D web games implement and the first place they get subtly wrong. Not broken — wrong in ways that feel like the game is cheap.&lt;/p&gt;

&lt;p&gt;The naive version is four lines and it mostly works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pointerdown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ndc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toNDC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;raycaster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFromCamera&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ndc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raycaster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;intersectObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ground&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;point&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;Here is what that misses, roughly in the order you'll discover it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Normalised device coordinates need the canvas, not the window
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;clientX&lt;/code&gt; is relative to the viewport. The raycaster wants coordinates relative to the canvas, in the range −1 to 1, with y flipped. If your canvas is inset, letterboxed, or sits below a header, using window dimensions puts every tap at a small offset from where the player aimed — and it will feel like input lag rather than a mapping error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientX&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientY&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Recompute the rect on resize and on orientation change, or cache it and invalidate — reading it per tap is fine, reading it per frame is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Raycast a plane, not the ground mesh
&lt;/h2&gt;

&lt;p&gt;Intersecting the visible ground works until the ground has holes, decorative geometry, or a mesh whose collider does not match its silhouette. Then taps near edges do nothing, which reads as unresponsiveness.&lt;/p&gt;

&lt;p&gt;Raycast a mathematical plane at the gameplay height instead. It always hits, it costs nothing, and it decouples input from art.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;pointerdown&lt;/code&gt;, and only pointer events
&lt;/h2&gt;

&lt;p&gt;Not &lt;code&gt;mousedown&lt;/code&gt; plus a touch path bolted on later. &lt;code&gt;pointerdown&lt;/code&gt; covers mouse, touch and pen in one code path. Also set &lt;code&gt;touch-action: none&lt;/code&gt; on the canvas, or mobile browsers will helpfully scroll and pinch-zoom your game while the player tries to play it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Move with delta time, and check the unit
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;              &lt;span class="c1"&gt;// dt in SECONDS&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;distanceTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addScaledVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// snap, or you oscillate forever&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two traps in four lines. &lt;code&gt;THREE.Clock.getDelta()&lt;/code&gt; gives seconds; a raw &lt;code&gt;requestAnimationFrame&lt;/code&gt; timestamp gives milliseconds, and passing the wrong one produces motion that is wrong by a factor of 1000 without erroring. And omitting the snap leaves the player jittering around the target forever, because it can never land exactly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that makes it feel good
&lt;/h2&gt;

&lt;p&gt;Everything above makes tap-to-move &lt;em&gt;correct&lt;/em&gt;. What makes it feel deliberate is the acknowledgement: a small burst at the tap point, fired on &lt;code&gt;pointerdown&lt;/code&gt;, before the character has moved at all.&lt;/p&gt;

&lt;p&gt;That single effect does more for perceived responsiveness than any amount of movement tuning, because it answers the player's real question — &lt;em&gt;did it hear me?&lt;/em&gt; — in the same frame as the input.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nixiefx.com/threejs-game-tutorial/" rel="noopener noreferrer"&gt;This tutorial&lt;/a&gt; goes end to end on the mobile-first version: renderer with a capped pixel ratio, scene and camera, the loop, pointer input, and a tap-burst effect wired into the same per-frame update.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone starting today
&lt;/h2&gt;

&lt;p&gt;Write the input layer yourself and get the mapping right, because it is small and everything else sits on top of it. Then borrow the feedback layer, because hand-rolling a particle system teaches you about object pooling rather than about your game.&lt;/p&gt;

</description>
      <category>threejs</category>
      <category>gamedev</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Zero allocation is a habit, not an optimisation pass</title>
      <dc:creator>PatilRB</dc:creator>
      <pubDate>Wed, 26 Aug 2026 08:51:26 +0000</pubDate>
      <link>https://dev.to/patilrb/zero-allocation-is-a-habit-not-an-optimisation-pass-51ln</link>
      <guid>https://dev.to/patilrb/zero-allocation-is-a-habit-not-an-optimisation-pass-51ln</guid>
      <description>&lt;p&gt;"We'll optimise later" works for most code and fails specifically for allocation, because by the time you notice, the allocations are structural.&lt;/p&gt;

&lt;p&gt;A garbage collection pause does not show up as a slow function. It shows up as a stutter with no obvious owner, at a moment unrelated to whatever caused it, and profiling tells you the collector ran — not who fed it. The fix is not a pass you schedule; it is a habit you hold in the paths that run every frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hot path, cold path
&lt;/h2&gt;

&lt;p&gt;The whole discipline rests on one distinction. A &lt;strong&gt;hot path&lt;/strong&gt; runs every frame, or per particle, per entity, per collision. A &lt;strong&gt;cold path&lt;/strong&gt; runs on load, on level start, on menu open.&lt;/p&gt;

&lt;p&gt;Cold paths can allocate freely. Write the clear, idiomatic version — &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt;, spread, objects wherever they help.&lt;/p&gt;

&lt;p&gt;Hot paths get treated as if allocation were a correctness bug. Not because a single object matters, but because a single object multiplied by 60 frames and 500 entities is 30,000 objects a second, and that is a collection you scheduled without meaning to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four habits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Indexed loops instead of iterator chains.&lt;/strong&gt; &lt;code&gt;arr.map(f).filter(g)&lt;/code&gt; allocates two intermediate arrays per call. In a per-frame path, write the loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// cold path: fine&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;visible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alive&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sprite&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// hot path: reuse a caller-owned buffer&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;collectVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&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="nx"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;alive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;sprite&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&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;&lt;strong&gt;Scratch objects instead of fresh ones.&lt;/strong&gt; Vector maths is the usual culprit — every &lt;code&gt;add()&lt;/code&gt; that returns a new vector is an allocation. Keep module-level scratch instances and write into them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bounded, prewarmed pools for anything bursty.&lt;/strong&gt; Particles, projectiles, damage numbers. Prewarm at load so the first burst does not allocate, and bound the pool so a pathological case degrades instead of exploding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update renderer objects, never recreate them.&lt;/strong&gt; Recreating a sprite or a material per frame is the most expensive version of a cheap operation, and it is easy to do accidentally inside a React-style render function.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that is not about allocation
&lt;/h2&gt;

&lt;p&gt;Two settings will outweigh all of the above on mobile, and both are one line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cap the device pixel ratio.&lt;/strong&gt; A phone reporting DPR 3 renders nine times the pixels for a difference nobody perceives on a moving particle. &lt;code&gt;Math.min(devicePixelRatio, 2)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bake vectors into atlases.&lt;/strong&gt; Procedural drawing each frame is the most expensive route to a shape you could have drawn once at load.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://nixiefx.com/html5-game-performance/" rel="noopener noreferrer"&gt;The full write-up is here&lt;/a&gt; — the pooling patterns, the draw-call guidance, and the debugging order to follow when something already stutters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I would actually enforce
&lt;/h2&gt;

&lt;p&gt;When a game already stutters: profile on a real phone, find the root cause, fix it. &lt;strong&gt;Do not fake the win by quietly shrinking the feature.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That last clause is the one worth taping to a monitor. Halving a particle count improves the frame time and makes the game worse, and because nobody writes down that the trade happened, six months later the effect is "just how it looks" — while the uncapped DPR that actually caused it is still there, waiting for the next feature.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>javascript</category>
      <category>gamedev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The reward moment is the product</title>
      <dc:creator>PatilRB</dc:creator>
      <pubDate>Wed, 26 Aug 2026 08:47:19 +0000</pubDate>
      <link>https://dev.to/patilrb/the-reward-moment-is-the-product-43h6</link>
      <guid>https://dev.to/patilrb/the-reward-moment-is-the-product-43h6</guid>
      <description>&lt;p&gt;Free-to-play UI is not decoration. It is the reward loop made visible, and the moment a player taps "claim" is closer to being the product than anything in your feature list.&lt;/p&gt;

&lt;p&gt;That sounds like marketing copy until you look at what actually differs between a game people keep opening and one they don't. It is rarely the systems. It is whether collecting something feels like collecting something.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reward moment, decomposed
&lt;/h2&gt;

&lt;p&gt;A reward tap that reads well is doing four things in about 400 milliseconds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Acknowledging the input&lt;/strong&gt; — something happens on press, not on release, so the tap never feels ignored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Showing the value&lt;/strong&gt; — the thing you earned is visibly &lt;em&gt;a thing&lt;/em&gt;, moving from where you claimed it to where it lives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marking significance&lt;/strong&gt; — scale, timing and particle density say "this was a big one" or "this was routine".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Getting out of the way&lt;/strong&gt; — it resolves fast enough that a player tapping through twenty of them is not fighting your animation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Point 4 is the one that gets missed. Juice is easy to add and hard to bound, and a reward sequence that delights on first sight becomes the thing players complain about on day thirty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the effort actually goes
&lt;/h2&gt;

&lt;p&gt;Not into the particle system. Into the &lt;em&gt;timing relationships&lt;/em&gt; between the layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;onClaim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// t=0    input acknowledged&lt;/span&gt;
  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fountain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tray&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// t=80  value appears, after the spark reads&lt;/span&gt;
  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;countUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;140&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// t=140 number climbs into place&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those offsets are the design. Fire everything at t=0 and it reads as one undifferentiated flash; space them too far and it reads as lag. The 60–150 ms band is where "responsive but legible" lives, and finding it is iteration, not calculation.&lt;/p&gt;

&lt;p&gt;Which is why the authoring loop matters more than the feature set. If changing an 80 to a 110 requires an engineer, a rebuild and a redeploy, nobody will tune it, and the reward moment will ship at whatever numbers the first implementation happened to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it tunable by the person with the taste
&lt;/h2&gt;

&lt;p&gt;Two properties get you there:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Effects as data, not code.&lt;/strong&gt; A named effect loaded from a bundle can be re-authored by whoever has the eye for it. A &lt;code&gt;drawCoinBurst()&lt;/code&gt; function can only be changed by whoever writes functions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timing in one place.&lt;/strong&gt; The offsets above belong in the presentation layer next to each other, not scattered across the handlers that trigger them. You want a person to be able to read the whole sequence at once.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nixiefx.com/pixijs-particle-effects/" rel="noopener noreferrer"&gt;This walkthrough&lt;/a&gt; builds the concrete version for PixiJS v8 — a claim button that sparks on press and a coin fountain for the reward popup, both authored in an editor and played from the ticker. What I'd take from it is less the particles than the shape: the game says a reward happened, and a separate layer owns how that reads.&lt;/p&gt;

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

&lt;p&gt;Ask whoever has the best instinct for game feel on your team to change how a reward reads — not to describe it, to change it.&lt;/p&gt;

&lt;p&gt;If they can do it alone in a few minutes, your pipeline is right. If they have to file a ticket, then the most commercially important 400 milliseconds in your game are being tuned by whoever has build access, which is not the same person and never was.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>ux</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I judge tools on the first ten minutes</title>
      <dc:creator>PatilRB</dc:creator>
      <pubDate>Wed, 26 Aug 2026 07:38:49 +0000</pubDate>
      <link>https://dev.to/patilrb/i-judge-tools-on-the-first-ten-minutes-24f2</link>
      <guid>https://dev.to/patilrb/i-judge-tools-on-the-first-ten-minutes-24f2</guid>
      <description>&lt;p&gt;I have started judging developer tools almost entirely on the first ten minutes, and I think the instinct is correct rather than lazy.&lt;/p&gt;

&lt;p&gt;Not because the first ten minutes are the important part of the work — they obviously are not. Because they are the only part you can evaluate before committing, and because a tool that respects them is usually a tool built by someone who watched a real person use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the first ten minutes are actually measuring
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Time to first visible result.&lt;/strong&gt; For a VFX tool, the moment something moves on screen that you caused. For a database library, the first successful query. Every step before that is unpaid setup, and users are entitled to resent it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many decisions you are forced to make while ignorant.&lt;/strong&gt; This is the one most tools get wrong. Asking me to choose a project name, a package manager, a rendering backend, and a directory layout before I have seen the thing work is asking me to make four decisions with no information. Every one of them is a chance to get stuck, and none of them can be made well yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whether the happy path is honest.&lt;/strong&gt; If the quickstart works only because it skips the part that will bite me — no error handling, no build step, a demo asset that hides the asset pipeline — then it was marketing, not onboarding, and I will discover this at the worst moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern I now look for
&lt;/h2&gt;

&lt;p&gt;The best onboarding I have encountered lately does something slightly unusual: it hands you a &lt;strong&gt;prompt for your coding agent&lt;/strong&gt; rather than a sequence of commands to run yourself.&lt;/p&gt;

&lt;p&gt;That looked like a gimmick until I thought about what it actually optimises. A command list requires you to be correct at every step; a spec handed to an agent means the tedious, error-prone parts — scaffolding, installing the right peer dependency, wiring the loop — happen in one shot and produce a diff you can read. The verification is at the end, where it belongs, rather than distributed across eight steps you might mistype.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nixiefx.com/quick-start/" rel="noopener noreferrer"&gt;NixieFX's quick start&lt;/a&gt; is built this way: copy one prompt into whichever agent you use, and it sets up a Vite + TypeScript project, installs the runtime and its renderer peer, scaffolds an effect, exports the bundle, plays it in a scene, and tells you how to run the result. Ten minutes with one decision in it — which renderer — rather than six.&lt;/p&gt;

&lt;p&gt;Two details in it are worth stealing regardless of your stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The prompt names the packages explicitly.&lt;/strong&gt; Removes an entire class of hallucinated dependency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The prompt links the docs and the machine-readable site index.&lt;/strong&gt; An agent with the real reference stops inventing option names, which is the single biggest cause of confidently wrong scaffolds.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where I would push back on my own heuristic
&lt;/h2&gt;

&lt;p&gt;The obvious failure mode: optimising for the first ten minutes can produce a tool that is delightful to start and miserable at month three. Magic scaffolding, hidden configuration, a golden path that collapses the moment your project stops resembling the tutorial.&lt;/p&gt;

&lt;p&gt;So the heuristic needs a second half. After the ten minutes, ask: &lt;strong&gt;can I see everything the tool did?&lt;/strong&gt; If the scaffold is a readable diff and the configuration is a file in my repo, the fast start cost me nothing. If it materialised state I cannot inspect, I have borrowed against my own future.&lt;/p&gt;

&lt;p&gt;Fast start, legible aftermath. Either one alone is a trap.&lt;/p&gt;

</description>
      <category>devtools</category>
      <category>productivity</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The curve editor is the actual interface</title>
      <dc:creator>PatilRB</dc:creator>
      <pubDate>Wed, 26 Aug 2026 07:34:23 +0000</pubDate>
      <link>https://dev.to/patilrb/the-curve-editor-is-the-actual-interface-2ogj</link>
      <guid>https://dev.to/patilrb/the-curve-editor-is-the-actual-interface-2ogj</guid>
      <description>&lt;p&gt;Most particle editors present themselves as a wall of numeric fields, and most tutorials teach you to fill them in. Both are a distraction from where the actual authoring happens, which is the curve editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Constants describe nothing that moves
&lt;/h2&gt;

&lt;p&gt;A particle's size is not a number. It is a shape over the particle's lifetime: bursting outward fast, holding, then shrinking as it fades. Its colour is not a value either, it is a gradient — white-hot at spawn, orange in the middle, dark smoke at the end.&lt;/p&gt;

&lt;p&gt;Author those as constants and you get the characteristic look of a first attempt: technically a fire effect, reads like a spray of orange dots. Nothing about the individual parameters is wrong. What is missing is that every interesting property &lt;em&gt;changes over the life of the thing&lt;/em&gt;, and a single number cannot express change.&lt;/p&gt;

&lt;p&gt;This is why the curve and gradient editors are the real interface. Everything else is plumbing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;size over lifetime     0 ▁▃▅▇▇▆▄▂▁ 1     ← fast rise, slow decay
alpha over lifetime    0 ▇▇▇▆▅▃▂▁▁ 1     ← hold, then fade late
colour over lifetime   [white ▸ orange ▸ ash]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three curves and a gradient is the difference between "orange dots" and "fire". No additional modules required.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules I have converged on
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Alpha should fade late, not linearly.&lt;/strong&gt; A linear fade reads as the particle being dimmed. A fade held flat and dropped in the last third reads as the particle &lt;em&gt;dying&lt;/em&gt;. Same duration, entirely different impression.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Size should not fade to zero at the same time as alpha.&lt;/strong&gt; If both hit zero together the particle vanishes twice, which reads as a pop. Let size finish first and alpha carry the tail, or the reverse — just not both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Put the variation on the curve, not the value.&lt;/strong&gt; Randomising a constant gives you particles that differ from each other. Randomising &lt;em&gt;where on the curve&lt;/em&gt; each particle starts gives you particles that differ over time, which is what makes a group look like a phenomenon rather than a set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gradients want three stops, not seven.&lt;/strong&gt; Every extra stop is a decision someone has to preserve when they tune it later. I have never regretted removing one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that took me longest to internalise
&lt;/h2&gt;

&lt;p&gt;The timeline is a separate axis from the curves, and confusing them causes a specific kind of frustration.&lt;/p&gt;

&lt;p&gt;A curve describes what happens over &lt;strong&gt;one particle's&lt;/strong&gt; lifetime. The timeline describes what happens over &lt;strong&gt;the effect's&lt;/strong&gt; duration — when each emitter starts, how long it runs, when the second stage fires. When an effect looks wrong "in the middle", the instinct is to reach for curves, and about half the time the actual problem is timeline: two emitters are overlapping when they should be sequential, or a burst is landing before the thing it is supposed to accent.&lt;/p&gt;

&lt;p&gt;Asking "is this a per-particle problem or a per-effect problem?" first has saved me a lot of pointless curve-dragging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the reference lives
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://nixiefx.com/editor-manual/" rel="noopener noreferrer"&gt;The NixieFX editor manual&lt;/a&gt; documents the curve and gradient editors, the timeline, and per-field tables for every emitter module — which is the part I actually needed, because particle systems are full of fields whose label suggests slightly more than the implementation does. Guessing from a slider and a name is how you lose an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  The transferable bit
&lt;/h2&gt;

&lt;p&gt;If you are building any authoring tool, the lesson generalises: &lt;strong&gt;find the parameters that are secretly functions of time, and give them a real editor.&lt;/strong&gt; Users will forgive a plain form for everything else. They will not produce good work while expressing a curve as a number, and most of them will not be able to tell you that is what is wrong.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>animation</category>
      <category>javascript</category>
      <category>ux</category>
    </item>
    <item>
      <title>Delegating VFX work to a coding agent: what actually made it reliable</title>
      <dc:creator>PatilRB</dc:creator>
      <pubDate>Mon, 24 Aug 2026 09:56:30 +0000</pubDate>
      <link>https://dev.to/patilrb/delegating-vfx-work-to-a-coding-agent-what-actually-made-it-reliable-2112</link>
      <guid>https://dev.to/patilrb/delegating-vfx-work-to-a-coding-agent-what-actually-made-it-reliable-2112</guid>
      <description>&lt;p&gt;I have spent the last few months handing real work to coding agents, and particle effects turned out to be an unexpectedly good test case. Not because the task is hard, but because it fails in a specific, informative way.&lt;/p&gt;

&lt;p&gt;Ask an agent to "add a fire burst to the boss intro" in a project it has not been taught about, and you get code that looks right and is wrong. Plausible imports. Invented option names. An update call in the wrong place in the host lifecycle. It compiles, it renders nothing, and the failure gives you no hint about which of the four layers broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure is a vocabulary problem
&lt;/h2&gt;

&lt;p&gt;The agent is not confused about JavaScript. It is guessing at three things it has no way to know:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Where authored assets live&lt;/strong&gt; in this project, and what a valid effect file looks like.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which capabilities the backend actually supports&lt;/strong&gt; — a 3D world effect and a 2D UI effect are not interchangeable, and the constraint is not visible from the call site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where in the host lifecycle&lt;/strong&gt; loading, per-frame simulation, and cleanup belong. This is the one that produces silent no-ops: the code is fine and it runs in the wrong place.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of that is inferable from a function signature. It is project knowledge, and if you do not supply it the model supplies a guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Splitting authoring from integration
&lt;/h2&gt;

&lt;p&gt;What worked was refusing to treat this as one task. Authoring an effect and wiring an effect into a game are different jobs with different failure modes, and giving the agent one combined instruction sheet made both worse.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nixiefx.com/skills/" rel="noopener noreferrer"&gt;NixieFX ships two agent skills&lt;/a&gt; split exactly on that seam:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;authoring&lt;/strong&gt; — create project-safe effect files, check them against what the backend supports, produce the export bundle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;runtime&lt;/strong&gt; — load an exported bundle, wire the providers the engine owns, keep simulation and cleanup in the right lifecycle hooks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Naming the one you want is what makes the run predictable:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use the authoring skill to create, validate, and export a fire burst for a PixiJS boss intro in this project.&lt;/p&gt;

&lt;p&gt;Use the runtime skill to load the exported fire-burst effect from &lt;code&gt;out/vfx&lt;/code&gt;, wire its texture provider, and update it once per frame.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two prompts, two reviewable diffs. When something is wrong you know which half to look at, which is most of the value.&lt;/p&gt;

&lt;h2&gt;
  
  
  The validator is what makes it safe
&lt;/h2&gt;

&lt;p&gt;The reason this works better than a longer prompt is that there is a machine-checkable step in the middle. The authoring loop is create, edit, validate, export — and validate is read-only and exits non-zero on error.&lt;/p&gt;

&lt;p&gt;That gives the agent a ground truth it can act on instead of a vibe. It is also the difference between reviewing an agent's VFX work in thirty seconds and reviewing it by launching the game and squinting. If &lt;code&gt;validate&lt;/code&gt; passes and &lt;code&gt;export&lt;/code&gt; produces a bundle, the authoring half is done; anything still broken is integration, and integration is the other skill's problem.&lt;/p&gt;

&lt;p&gt;I would generalise this past VFX: &lt;strong&gt;any task you want to delegate to an agent needs a check the agent can run itself.&lt;/strong&gt; Not a test you run afterwards — a command it can invoke, read the exit code from, and iterate against. Tasks with that property go well. Tasks without it produce confident, plausible, unverifiable diffs, and you end up doing the work twice.&lt;/p&gt;

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

&lt;p&gt;Judgement about whether the effect is any good. An agent can produce a valid fire burst that matches the spec and still looks wrong for the scene, because "reads clearly at 400 px on a phone during a screen shake" is not a property a validator can check.&lt;/p&gt;

&lt;p&gt;Authoring and wiring: delegate. Deciding it looks right: still a person, still watching it in context.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Curious whether others have found the same pattern — that the deciding factor in agent-delegated work is the presence of a self-service validation command rather than the quality of the prompt.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>coding</category>
      <category>programming</category>
    </item>
    <item>
      <title>The particle effect that looks different on every machine</title>
      <dc:creator>PatilRB</dc:creator>
      <pubDate>Mon, 24 Aug 2026 09:54:42 +0000</pubDate>
      <link>https://dev.to/patilrb/the-particle-effect-that-looks-different-on-every-machine-583h</link>
      <guid>https://dev.to/patilrb/the-particle-effect-that-looks-different-on-every-machine-583h</guid>
      <description>&lt;p&gt;"It looks different on my machine" is the worst bug report you can get about a particle effect, and it is also the most common one.&lt;/p&gt;

&lt;p&gt;Nobody can reproduce it, nothing is thrown, and the only artefact is a teammate insisting the explosion is "wetter" on their laptop. Then someone records a video, and they are right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two separate causes wearing the same costume
&lt;/h2&gt;

&lt;p&gt;Almost every one of these turns out to be one of two things, and they are worth separating because the fixes are unrelated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause one: you are feeding the simulation the wrong unit.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every runtime picks a convention for its update step, and the two popular ones differ by a factor of 1000. If the API wants seconds and your host loop hands it milliseconds, the effect does not error — it just runs a thousand times too fast, which reads as "the particles vanish instantly" or, at low emission rates, as "it looks kind of aggressive". On a fast machine with a 4 ms frame you get one appearance; on a throttled laptop at 30 ms you get another. Same code, different vibe, no exception.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// three.js: THREE.Clock.getDelta() already returns seconds&lt;/span&gt;
&lt;span class="nx"&gt;vfx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDelta&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// but a raw rAF timestamp does not&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vfx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;- the /1000 is the whole bug&lt;/span&gt;
  &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&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;I have written that missing &lt;code&gt;/ 1000&lt;/code&gt; more than once. It is worth a comment on the line, because it is invisible in review — both versions are a number being passed to a function that takes a number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause two: the simulation is genuinely random per run.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Particle systems are stochastic by design. If the randomness is seeded from the clock, then two machines, two reloads, and two QA passes all produce different motion. That is fine for ambient smoke and actively hostile for anything you need to verify, screenshot, or regression-test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeded runs are the feature to look for
&lt;/h2&gt;

&lt;p&gt;The property you want from a runtime is: same effect, same seed, same motion — on every machine and every backend. &lt;a href="https://nixiefx.com/threejs-runtime/" rel="noopener noreferrer"&gt;The NixieFX Three.js runtime&lt;/a&gt; exposes it directly on effect creation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vfx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fireBurst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1234&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass a seed and the run is reproducible. Leave it out and you get variety.&lt;/p&gt;

&lt;p&gt;That single parameter changes what is possible downstream:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Screenshot tests work.&lt;/strong&gt; Fixed seed plus fixed delta means frame 30 is byte-comparable across CI runs. You can diff VFX like you diff a component render.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bug reports become artefacts.&lt;/strong&gt; "Seed 1234, frame 30" is a reproduction. "It looks wetter on my laptop" is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A/B comparisons are honest.&lt;/strong&gt; Comparing two parameter sets under different random draws tells you nothing; under the same seed it tells you exactly what the parameter did.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variety stays available.&lt;/strong&gt; Seed per spawn from your own counter and every explosion differs, but deterministically — replayable from the same game state.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The discipline, compressed
&lt;/h2&gt;

&lt;p&gt;Two rules cover the whole class of bug:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Advance the simulation with &lt;strong&gt;seconds&lt;/strong&gt;, exactly once per frame, from the host's own clock — never from a value you computed twice or reused across systems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seed anything you intend to verify.&lt;/strong&gt; Ambient effects can float; anything in a test, a screenshot, or a bug report gets a number.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither is clever. Both are the kind of thing you only write down after losing an afternoon to a "wetter" explosion.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Related trap, for anyone chasing frame time rather than appearance: updating the simulation inside a loop that already runs per-object means you advance it N times per frame. It looks like a performance problem and it is actually a correctness one.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Put your particle effects in CI</title>
      <dc:creator>PatilRB</dc:creator>
      <pubDate>Mon, 24 Aug 2026 09:51:47 +0000</pubDate>
      <link>https://dev.to/patilrb/put-your-particle-effects-in-ci-46pl</link>
      <guid>https://dev.to/patilrb/put-your-particle-effects-in-ci-46pl</guid>
      <description>&lt;p&gt;Particle effects are the last asset class most web teams still treat as an opaque blob. Textures get optimized in CI, shaders get linted, JSON configs get schema-checked — and the VFX sit in a binary editor file that exactly one person on the team can open.&lt;/p&gt;

&lt;p&gt;That asymmetry has a cost you only notice on the day someone's effect stops rendering and the git history says &lt;code&gt;Modified: fire_burst.vfx (binary)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Text assets are reviewable assets
&lt;/h2&gt;

&lt;p&gt;The fix is not clever, it is just a format decision: keep effects as plain JSON in the repo, next to the code that loads them. Then all the tooling you already own starts working for free.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;git diff&lt;/code&gt; shows that someone raised the emission rate from 40 to 400, which is why the boss intro now costs 12 ms a frame.&lt;/li&gt;
&lt;li&gt;Code review can happen at all. "Why is this effect spawning 6 sub-emitters?" is a question a reviewer can ask on a line number.&lt;/li&gt;
&lt;li&gt;Merge conflicts become conflicts you can resolve instead of a coin flip between two binaries.&lt;/li&gt;
&lt;li&gt;Rollback is a revert, not an archaeology project in someone's Downloads folder.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The pipeline that makes it stick
&lt;/h2&gt;

&lt;p&gt;A text format only helps if there is a validation step, otherwise you have traded an unreadable file for a readable file that is also broken. The loop I settle on looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Author&lt;/strong&gt; — scaffold the effect file, then edit it in an editor or by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate&lt;/strong&gt; — read-only check over every effect in the project, non-zero exit on error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export&lt;/strong&gt; — compile to a bundle the game actually loads.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 2 is the one that earns its keep, because it is the step you can put in CI. A validator that exits non-zero is a merge gate. The same command a developer runs before committing is the command the pipeline runs on the pull request, which means "it worked on my machine" stops being a defence.&lt;/p&gt;

&lt;p&gt;The tool I use for this is &lt;a href="https://nixiefx.com/cli-reference/" rel="noopener noreferrer"&gt;the NixieFX CLI&lt;/a&gt; — three commands, no browser required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nixie-fx effect create &lt;span class="nt"&gt;--project&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"Fire Burst"&lt;/span&gt; &lt;span class="nt"&gt;--profile&lt;/span&gt; three-world-3d
npx nixie-fx validate &lt;span class="nb"&gt;.&lt;/span&gt;
npx nixie-fx &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;validate&lt;/code&gt; walks every effect, touches nothing, and exits 1 if anything is wrong. That is the whole contract, and it is enough.&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="c1"&gt;# .github/workflows/vfx.yml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx nixie-fx validate ./vfx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nine lines of pipeline config, and a broken effect can no longer reach &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the export step buys you
&lt;/h2&gt;

&lt;p&gt;Keeping the source readable does not mean shipping the source. &lt;code&gt;export&lt;/code&gt; writes a bundle — a manifest, compiled effect JSON, and byte copies of every referenced asset — and that bundle is what the game loads at runtime. Authoring format and shipping format are allowed to be different things, and separating them is what lets the authoring format stay verbose and diff-friendly.&lt;/p&gt;

&lt;p&gt;It also means the deploy artifact is self-contained. No "works locally, missing texture in staging", because the bundle carries its assets rather than pointing at wherever they happened to live on the author's disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I underestimated
&lt;/h2&gt;

&lt;p&gt;I expected the win to be about git hygiene. The actual win was that non-specialists could participate. When an effect is a text file with named fields and a validator that tells you when you are wrong, a gameplay programmer with no VFX background can adjust a lifetime or swap a texture and know within a second whether they broke it.&lt;/p&gt;

&lt;p&gt;The binary format was not protecting anything. It was just the reason only one person touched the effects.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you keep VFX in your repo, what is your validation story? I am curious whether anyone has gone further and put a perf budget in the same gate — failing the build when an effect exceeds a draw-call count would be the logical next step, and I have not built it yet.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>git</category>
      <category>performance</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>The particle features I stopped hand-rolling</title>
      <dc:creator>PatilRB</dc:creator>
      <pubDate>Thu, 20 Aug 2026 11:16:22 +0000</pubDate>
      <link>https://dev.to/patilrb/the-particle-features-i-stopped-hand-rolling-1jpd</link>
      <guid>https://dev.to/patilrb/the-particle-features-i-stopped-hand-rolling-1jpd</guid>
      <description>&lt;p&gt;Every web game I have worked on grew its own little particle system. It always starts the same way: a pool of sprites, a velocity, a lifetime, a fade. Two hundred lines and you have sparks.&lt;/p&gt;

&lt;p&gt;Then the requests arrive. Can the smoke curl? Can the trail taper? Can the explosion spawn embers that themselves fall and fade? Each one is reasonable, and each one is another week bolted onto code nobody wants to own.&lt;/p&gt;

&lt;p&gt;Here is the list of things I used to write by hand that an authored VFX pipeline just has, which is roughly why I stopped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Values that change over a lifetime
&lt;/h2&gt;

&lt;p&gt;Size, colour, alpha, rotation, velocity — all driven by curves and gradients rather than lerps hard-coded at the call site. Multi-point curves with per-point bezier tangents, colour and alpha gradients that can blend or step. The moment an artist can edit these without a rebuild, the iteration loop stops going through you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Emission shape
&lt;/h2&gt;

&lt;p&gt;Not just "spawn at a point". Shapes, with the direction of emission following from the shape. This is the one I always half-implemented, because a cone is easy and a ring is easy and then someone wants a mesh.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forces and noise
&lt;/h2&gt;

&lt;p&gt;Gravity is trivial. Turbulence is not, and turbulence is the difference between smoke that reads as smoke and smoke that reads as a sprite moving upward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trails, flipbooks and sub-emitters
&lt;/h2&gt;

&lt;p&gt;Sub-emitters are the honest breaking point for a hand-rolled system. Once a particle can spawn its own emitter with its own lifetime and its own modules, you are no longer maintaining a particle effect. You are maintaining a particle engine, and that was never the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  A material graph
&lt;/h2&gt;

&lt;p&gt;A node-based shader editor with live per-node previews is not something I was ever going to write for one game. I would have shipped three hand-written shaders and told the artist those were the options.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed
&lt;/h2&gt;

&lt;p&gt;The pitch I did not expect to care about is that the effects stay in my repo as plain JSON, in my own project folder, exported into a self-contained bundle the game loads. So this is not the usual trade where you adopt a tool and hand over your content in exchange. The diffs are still readable, the files are still mine, and the engine-side integration is a renderer, a create call and one update per frame.&lt;/p&gt;

&lt;p&gt;What I gave up is the two hundred lines. I do not miss them.&lt;/p&gt;

&lt;p&gt;The full module list — emission shapes, curves, gradients, forces, noise, trails, flipbooks, sub-emitters, the shader graph, and which of them each backend supports — is in the &lt;a href="https://nixiefx.com/vfx-runtime-docs/" rel="noopener noreferrer"&gt;NixieFX feature reference&lt;/a&gt;. The editor itself is free and runs in the browser at &lt;a href="https://nixiefx.com/editor/" rel="noopener noreferrer"&gt;nixiefx.com/editor&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>programming</category>
      <category>software</category>
    </item>
    <item>
      <title>A practical checklist for UI-triggered particle effects in PixiJS</title>
      <dc:creator>PatilRB</dc:creator>
      <pubDate>Wed, 19 Aug 2026 12:28:54 +0000</pubDate>
      <link>https://dev.to/patilrb/a-practical-checklist-for-ui-triggered-particle-effects-in-pixijs-251l</link>
      <guid>https://dev.to/patilrb/a-practical-checklist-for-ui-triggered-particle-effects-in-pixijs-251l</guid>
      <description>&lt;p&gt;Game-feel effects often start as a visual task: add a burst, tune the color, ship it. In browser games, the more durable question is how an effect enters and leaves the frame loop.&lt;/p&gt;

&lt;p&gt;For small PixiJS projects, I use a short checklist before adding any particle system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tie the trigger to a real interface event. Button success, a streak increment, or a completed quest gives the effect a clear job.&lt;/li&gt;
&lt;li&gt;Keep the emitter lifetime explicit. Create it, emit for a bounded interval, then destroy or return it to a pool.&lt;/li&gt;
&lt;li&gt;Pass the current delta time through the update path. A burst that looks good at 60 FPS but races on a high-refresh display is a hidden regression.&lt;/li&gt;
&lt;li&gt;Budget texture changes and blend-mode changes. The prettiest spark is not worth a stuttering menu.&lt;/li&gt;
&lt;li&gt;Test the “quiet” path too. Opening and closing the UI repeatedly should not grow memory or leave particles updating off-screen.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The implementation details and an editor-oriented workflow are in this &lt;a href="https://nixiefx.com/pixijs-particle-effects/" rel="noopener noreferrer"&gt;PixiJS particle-effects guide&lt;/a&gt;. The useful takeaway is not a particular emitter preset; it is treating visual feedback as part of the UI’s lifecycle.&lt;/p&gt;

&lt;p&gt;That framing also makes reviews easier. A designer can judge whether the cue communicates the intended state, while an engineer can verify that it is bounded, frame-rate independent, and removable. Small web games benefit from both kinds of clarity.&lt;/p&gt;

</description>
      <category>threejs</category>
    </item>
    <item>
      <title>Five hot-path rules for 60 FPS HTML5 games</title>
      <dc:creator>PatilRB</dc:creator>
      <pubDate>Wed, 19 Aug 2026 11:51:07 +0000</pubDate>
      <link>https://dev.to/patilrb/five-hot-path-rules-for-60-fps-html5-games-2kak</link>
      <guid>https://dev.to/patilrb/five-hot-path-rules-for-60-fps-html5-games-2kak</guid>
      <description>&lt;p&gt;A web game can look simple and still miss its frame budget. The usual culprit is not a single expensive feature; it is small amounts of work repeated every frame until the garbage collector, renderer, or GPU stalls.&lt;/p&gt;

&lt;p&gt;Here are five rules that consistently help.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Treat per-frame allocation as a bug
&lt;/h2&gt;

&lt;p&gt;Avoid creating vectors, arrays, closures, and temporary objects inside update loops. Reuse scratch values and keep object shapes stable. A tiny allocation multiplied by thousands of particles becomes visible hitching.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Prewarm pools
&lt;/h2&gt;

&lt;p&gt;Pooling only helps if the pool is ready before the action begins. Prewarm projectiles, particles, and transient UI effects during a loading phase. The first explosion should not also be the moment the game constructs hundreds of objects.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Use texture atlases deliberately
&lt;/h2&gt;

&lt;p&gt;Atlases reduce texture switches, but a giant atlas is not automatically better. Group assets that are rendered together, keep filtering and color-space settings consistent, and verify that batching actually improves in the profiler.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Budget draw calls, not just triangles
&lt;/h2&gt;

&lt;p&gt;Many transparent particle layers can become expensive even when the geometry is tiny. Watch draw calls, overdraw, blend modes, and material changes. Sort only when the visual result truly needs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Measure frame time, not average FPS
&lt;/h2&gt;

&lt;p&gt;An average can hide a periodic 50 ms hitch. Capture frame-time distributions and inspect the worst frames. Fix the spike before reducing the whole game's visual ambition.&lt;/p&gt;

&lt;p&gt;These constraints shaped the NixieFX runtime: authored effects are useful only when their update path stays predictable in a real HTML5 game.&lt;/p&gt;

&lt;p&gt;The full guide includes practical pooling, atlas, and profiling examples:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nixiefx.com/html5-game-performance/" rel="noopener noreferrer"&gt;https://nixiefx.com/html5-game-performance/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>threejs</category>
    </item>
    <item>
      <title>The dual prompt: why your AI task handoffs keep failing</title>
      <dc:creator>PatilRB</dc:creator>
      <pubDate>Tue, 18 Aug 2026 15:28:01 +0000</pubDate>
      <link>https://dev.to/patilrb/the-dual-prompt-why-your-ai-task-handoffs-keep-failing-2a0o</link>
      <guid>https://dev.to/patilrb/the-dual-prompt-why-your-ai-task-handoffs-keep-failing-2a0o</guid>
      <description>&lt;p&gt;A while back a teammate ran a task I wrote and it came back wrong. Not subtly wrong — it had refactored a file I never mentioned. The model was fine. My prompt was the problem: I had written one set of instructions for two completely different readers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two readers, one prompt
&lt;/h2&gt;

&lt;p&gt;When you hand an AI task to someone else — they run it on their own Claude Code or Codex subscription and send you the result — there are two audiences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The human&lt;/strong&gt; needs to know what this is, why it matters, what "done" looks like, and what to sanity-check before sending it back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The agent&lt;/strong&gt; needs exact scope, hard constraints, which files or systems are in play, and what it must not touch.&lt;/p&gt;

&lt;p&gt;A single prompt serving both ends up too vague for the agent and too jargon-dense for the person. And the second failure is the expensive one: if the person can't tell whether the output is right, they forward it unchecked. Your review becomes the only gate, which defeats the point of delegating at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the split looks like
&lt;/h2&gt;

&lt;p&gt;Here is the version I used to write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Clean up the analytics module and make sure the tests pass.
Should be quick.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the same task written for both readers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## For you (the human)&lt;/span&gt;
We're removing the last of the legacy event names before the
rename ships Friday. There are 11 call sites.

Done looks like: &lt;span class="sb"&gt;`rg "track_legacy" src/`&lt;/span&gt; returns nothing, and
the analytics test suite is green.

Before you send it back, check the diff does not touch
src/analytics/schema.ts — another team owns that file and
changing it will block the release.

&lt;span class="gu"&gt;## For the agent&lt;/span&gt;
Scope: src/analytics/&lt;span class="ge"&gt;**&lt;/span&gt;, excluding src/analytics/schema.ts

Task: replace every &lt;span class="sb"&gt;`track_legacy(&amp;lt;name&amp;gt;)`&lt;/span&gt; call with
&lt;span class="sb"&gt;`track(&amp;lt;name&amp;gt;)`&lt;/span&gt;, preserving arguments and ordering.

Constraints:
&lt;span class="p"&gt;-&lt;/span&gt; Do not modify schema.ts
&lt;span class="p"&gt;-&lt;/span&gt; Do not reformat or touch unrelated lines
&lt;span class="p"&gt;-&lt;/span&gt; Run &lt;span class="sb"&gt;`npm test -- analytics`&lt;/span&gt;; report failures rather than
  fixing ones outside this scope

Out of scope: renaming events, the dashboard package.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second is four times longer. It is also the difference between a delivery you can merge and one you have to redo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "for you" half matters more than it looks
&lt;/h2&gt;

&lt;p&gt;The reflex is to skip it. The human is just going to paste the agent half anyway, right?&lt;/p&gt;

&lt;p&gt;But the human half does something the agent half cannot: it gives the person enough context to &lt;strong&gt;reject&lt;/strong&gt; a bad result. An agent will confidently hand back something plausible. If the person running it has no idea what correct looks like, plausible and correct are indistinguishable. One sentence — "check the diff does not touch schema.ts" — turns a passive relay into an actual reviewer.&lt;/p&gt;

&lt;p&gt;That is the whole return on writing the second half.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint underneath
&lt;/h2&gt;

&lt;p&gt;There is a reason this pattern comes up at all. You cannot hand someone else's Claude or Codex subscription a task without handing over the login. So the work has to travel as &lt;em&gt;instructions&lt;/em&gt;, not as access — and that is exactly why the writing quality carries so much weight. With shared access you can course-correct mid-run. With a handoff, the prompt is the entire interface.&lt;/p&gt;

&lt;p&gt;We have been building &lt;a href="https://wagglet.com" rel="noopener noreferrer"&gt;Wagglet&lt;/a&gt; at Rockbite Games around this shape of work (disclosure: I work there). The dual prompt is a pattern we kept re-deriving by hand until we made it the default structure of a task. If you want the longer reasoning there is a &lt;a href="https://wagglet.com/blog/dual-prompt-human-agent-task-design" rel="noopener noreferrer"&gt;write-up on the dual prompt&lt;/a&gt;, and &lt;a href="https://wagglet.com/blog/wagglet-workflow-request-draft-ticket-delivery" rel="noopener noreferrer"&gt;the full request-to-delivery workflow&lt;/a&gt; if you want to see where it sits end to end. For wiring an agent in directly there is an &lt;a href="https://wagglet.com/docs/mcp" rel="noopener noreferrer"&gt;MCP endpoint&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;None of that is required to use the pattern, though. It is just two headings in a markdown file.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist that mostly works
&lt;/h2&gt;

&lt;p&gt;Before sending a task for someone else to run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can they tell, without asking you, whether the result is wrong?&lt;/li&gt;
&lt;li&gt;Have you named at least one thing that must &lt;strong&gt;not&lt;/strong&gt; change?&lt;/li&gt;
&lt;li&gt;Is scope stated as paths and systems, not as a description?&lt;/li&gt;
&lt;li&gt;Have you said what to do on failure — report it, or fix it?&lt;/li&gt;
&lt;li&gt;Would this still make sense to them in three days?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of those are a no, the prompt is not finished. It will come back wrong and you will blame the model.&lt;/p&gt;

&lt;p&gt;Curious whether other people have landed on the same split, or something better — particularly anyone running handoffs across a team rather than solo.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>llm</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
