<?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: FrameSprite</title>
    <description>The latest articles on DEV Community by FrameSprite (@framesprite).</description>
    <link>https://dev.to/framesprite</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%2F4097018%2F2bcf1248-21d7-4e92-8223-632e4a9b99e2.png</url>
      <title>DEV Community: FrameSprite</title>
      <link>https://dev.to/framesprite</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/framesprite"/>
    <language>en</language>
    <item>
      <title>Stop changing your sprite sheet to fix animation speed</title>
      <dc:creator>FrameSprite</dc:creator>
      <pubDate>Sat, 05 Sep 2026 03:15:50 +0000</pubDate>
      <link>https://dev.to/framesprite/stop-changing-your-sprite-sheet-to-fix-animation-speed-2ohf</link>
      <guid>https://dev.to/framesprite/stop-changing-your-sprite-sheet-to-fix-animation-speed-2ohf</guid>
      <description>&lt;p&gt;An eight-frame animation does not have a fixed duration. At 8 fps it lasts one second; at 12 fps it lasts two-thirds of a second; at 16 fps it lasts half a second. Before drawing or generating more frames, check whether the problem is missing poses or the time each pose stays on screen.&lt;/p&gt;

&lt;p&gt;We maintain FrameSprite, a browser workspace for game assets. This is a timing and export note, not a claim that a particular frame count makes AI animation reliable. The equations work with hand-drawn sprites too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three numbers that are easy to mix up
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Source FPS&lt;/strong&gt; describes how a recording was sampled. &lt;strong&gt;Frame count&lt;/strong&gt; is the number of entries you put in an animation. &lt;strong&gt;Playback FPS&lt;/strong&gt; controls how fast those entries advance in the game. A 24 fps source video can provide eight selected poses that you play at 12 fps. You do not need to preserve every source frame.&lt;/p&gt;

&lt;p&gt;For equal holds, forward playback and a speed multiplier of 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;duration_seconds = frame_count / playback_fps
frame_hold_ms = 1000 / playback_fps
fps_for_target = frame_count * 1000 / target_duration_ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Same eight frames&lt;/th&gt;
&lt;th&gt;Hold per frame&lt;/th&gt;
&lt;th&gt;Full loop&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8 fps&lt;/td&gt;
&lt;td&gt;125 ms&lt;/td&gt;
&lt;td&gt;1.000 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12 fps&lt;/td&gt;
&lt;td&gt;83.333… ms&lt;/td&gt;
&lt;td&gt;0.667 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 fps&lt;/td&gt;
&lt;td&gt;62.5 ms&lt;/td&gt;
&lt;td&gt;0.500 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You changed the cadence without changing one pixel of the sprite sheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  A test you can reproduce
&lt;/h2&gt;

&lt;p&gt;Use the &lt;a href="https://www.framesprite.com/guides/downloads/bone-dragon-8-frame-sample.zip" rel="noopener noreferrer"&gt;public eight-frame sample&lt;/a&gt;. Keep the same frames, order, canvas and pivot for all three trials. Change only playback FPS between 8, 12 and 16.&lt;/p&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%2Fq6h5mbfln2gr8onmxc1y.webp" 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%2Fq6h5mbfln2gr8onmxc1y.webp" alt="Eight bone-dragon frames in a fixed 4-by-2 grid, used for the timing experiment" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check the animation alone at its intended game size.&lt;/li&gt;
&lt;li&gt;Run it beside actual movement or attack timing.&lt;/li&gt;
&lt;li&gt;If cadence improves but a foot or weapon still jumps, inspect the missing phase instead of raising FPS again.&lt;/li&gt;
&lt;li&gt;If every frame jumps by a small amount, inspect canvas and pivot alignment.&lt;/li&gt;
&lt;li&gt;If the pause happens only at the seam, look for an accidental duplicate endpoint.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The sample makes the arithmetic test repeatable. It is not evidence that eight frames is the right budget for every character or action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not accumulate rounded timestamps
&lt;/h2&gt;

&lt;p&gt;At 24 fps, one hold is 41.666… milliseconds. Storing 41.67 ms and repeatedly adding it introduces rounding drift. Calculate each boundary from its index instead:&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;function&lt;/span&gt; &lt;span class="nf"&gt;timingRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frameCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fps&lt;/span&gt;&lt;span class="p"&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="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frameCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;frameCount&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;frameCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Frame count must be an integer from 1 to 1000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;fps&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;fps&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FPS must be between 0.01 and 240&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;frameCount&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;startMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;endMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&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="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;,&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;For 60 frames at 24 fps, the last entry is index 59. It starts at approximately 2458.333 ms and ends at 2500 ms. Round when displaying or serializing, not before calculating the next boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transfer the schedule, not an assumption
&lt;/h2&gt;

&lt;p&gt;In Godot, &lt;code&gt;SpriteFrames.set_animation_speed()&lt;/code&gt; sets animation FPS. Individual frames also have relative durations: a duration of 2 holds twice as long as 1. The simple equation above assumes all those durations are 1 and playback speed has not been changed. See the &lt;a href="https://docs.godotengine.org/en/stable/classes/class_spriteframes.html" rel="noopener noreferrer"&gt;official SpriteFrames reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In Phaser, animation &lt;code&gt;frameRate&lt;/code&gt; controls frame advancement; it is separate from the game's rendering frame rate. Check overrides such as extra frame duration, delays and playback speed. See the &lt;a href="https://docs.phaser.io/api-documentation/class/animations-animation" rel="noopener noreferrer"&gt;official Animation reference&lt;/a&gt;. Both references were rechecked on 2026-09-10.&lt;/p&gt;

&lt;p&gt;Variable holds, ping-pong traversal, transition blending and runtime stalls need additional reasoning. A spreadsheet of equal frame holds cannot diagnose them.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small handoff that prevents rework
&lt;/h2&gt;

&lt;p&gt;When handing work to another developer, record the frame count, playback FPS, intended duration and loop setting in the asset's README or issue. For this example: “Eight frames at 12 fps, equal holds, forward loop, no duplicate endpoint; approximately 0.667 seconds per cycle.”&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Correction (2026-09-10): An earlier version incorrectly stated that FrameSprite's timing calculator offered CSV export and shareable settings. It currently provides on-page timing calculations only. The equations and examples above are unchanged. This correction was prepared with AI assistance and checked against the current tool implementation and the linked engine documentation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A Seven-Field Prompt Contract for Usable 2D Game Assets</title>
      <dc:creator>FrameSprite</dc:creator>
      <pubDate>Thu, 03 Sep 2026 13:03:20 +0000</pubDate>
      <link>https://dev.to/framesprite/a-seven-field-prompt-contract-for-usable-2d-game-assets-2epi</link>
      <guid>https://dev.to/framesprite/a-seven-field-prompt-contract-for-usable-2d-game-assets-2epi</guid>
      <description>&lt;p&gt;An AI image prompt becomes more useful for game development when every sentence maps to something you can inspect in the output. Instead of asking for a generic “game asset,” define seven fields: the deliverable, subject, art contract, camera, composition, background, and exclusions. Then test the result against the needs of that specific asset class.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why “game asset” is too broad
&lt;/h2&gt;

&lt;p&gt;A full-body character, inventory icon, UI panel, map, VFX sheet, and terrain tile can share a visual style, but they cannot share one output contract.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A character needs a readable silhouette, complete limbs, and room for later motion.&lt;/li&gt;
&lt;li&gt;An icon has to survive at its smallest runtime size.&lt;/li&gt;
&lt;li&gt;UI artwork needs complete borders and quiet areas for real text and states.&lt;/li&gt;
&lt;li&gt;A map needs a declared camera and readable routes.&lt;/li&gt;
&lt;li&gt;VFX needs ordered motion inside fixed cells.&lt;/li&gt;
&lt;li&gt;A tile needs exact edges that repeat without a visible seam.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why adding more adjectives rarely fixes a failed asset. “Beautiful, cinematic, detailed, professional” does not tell the model which camera to use, how much empty margin to preserve, or whether opposite edges must match.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seven-field contract
&lt;/h2&gt;

&lt;p&gt;Use this order:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;What it controls&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deliverable&lt;/td&gt;
&lt;td&gt;The actual canvas and acceptance test&lt;/td&gt;
&lt;td&gt;Full-body character, inventory icon, true-overhead map&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subject&lt;/td&gt;
&lt;td&gt;Visible identity and gameplay role&lt;/td&gt;
&lt;td&gt;Young knight, common healing potion, boss attack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Art contract&lt;/td&gt;
&lt;td&gt;Shared visual language&lt;/td&gt;
&lt;td&gt;Clean line art, fixed palette, cel shading, one light direction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Camera&lt;/td&gt;
&lt;td&gt;View, facing, and perspective&lt;/td&gt;
&lt;td&gt;Strict side view facing screen-right; 90-degree orthographic overhead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Composition&lt;/td&gt;
&lt;td&gt;Scale, placement, and safe space&lt;/td&gt;
&lt;td&gt;Centered, complete body, generous margin on every side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background&lt;/td&gt;
&lt;td&gt;How the asset will be isolated or used&lt;/td&gt;
&lt;td&gt;Uniform solid background; naturally opaque terrain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exclusions&lt;/td&gt;
&lt;td&gt;Common failure modes to prevent&lt;/td&gt;
&lt;td&gt;No text, crop, duplicate equipment, horizon, or extra character&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Keep the art-contract wording stable across related assets. Change the deliverable-specific fields separately. That makes a correction diagnosable: if a map shows wall fronts, strengthen the camera block rather than rewriting the palette, materials, and layout at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worked example: a side-facing game character
&lt;/h2&gt;

&lt;p&gt;Here is a compact structure:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One full-body 2D game character: a young knight with blue-white armor, a red cape, and one sword. Clean cel-shaded fantasy art with a fixed palette and clear material separation. Strict side view facing screen-right. One centered character with complete limbs and equipment, plus generous empty margin on every side. Uniform plain background. No text, scenery, ground shadow, crop, extra character, or duplicate equipment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every clause is testable. You can check the sword count, facing direction, crop, margin, and background before deciding whether the image is attractive.&lt;/p&gt;

&lt;p&gt;If the result is cropped, repair composition only: strengthen “complete limbs and equipment” and add an explicit safe-margin target. If the character turns toward the camera, repair the camera field. Keep the identity and art contract unchanged so the next attempt is still comparable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reuse the style, not the entire prompt
&lt;/h2&gt;

&lt;p&gt;For an inventory icon, retain the palette, shading, materials, and light direction, then replace the rest of the character contract:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Inventory icon: one brass healing potion for a common item tier. [Shared art contract.] Fixed three-quarter product angle, one centered object, strong silhouette at 48 pixels, consistent light, and safe padding. Isolated background. No text, hand, scenery, border, or duplicate object.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The important test now happens at 48 pixels, not at the source-image zoom level.&lt;/p&gt;

&lt;p&gt;For a true top-down map, the contract changes again:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;2D game map: a ruined sun temple with readable routes, blocked areas, and an open playable center. [Shared art contract.] True 90-degree orthographic overhead, no horizon, no vanishing point, and no visible front faces. Naturally opaque environment. No characters, UI, labels, or perspective tilt.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The map image still does not contain collision, navigation, spawns, or editable tile metadata. Those remain engine data.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small approval loop beats a large batch
&lt;/h2&gt;

&lt;p&gt;Before generating a pack, approve one representative source master and review it at the size used in the game.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check the deliverable-specific constraint first: facing for a character, native-size silhouette for an icon, edge continuity for a tile.&lt;/li&gt;
&lt;li&gt;Compare palette, line treatment, light, and materials with the project's accepted art contract.&lt;/li&gt;
&lt;li&gt;Keep the successful fields unchanged.&lt;/li&gt;
&lt;li&gt;Rewrite only the smallest block responsible for the visible failure.&lt;/li&gt;
&lt;li&gt;Preserve the accepted source and create runtime sizes or atlases as derivatives.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This loop does not guarantee a passing generation. Ambiguous references, complex anatomy, sampling variation, and text artifacts can still cause failures. Its value is that each retry has a reason and a measurable acceptance check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep image generation inside its boundary
&lt;/h2&gt;

&lt;p&gt;Generated artwork can be a source asset, but it does not replace runtime behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UI art still needs real text, focus states, input behavior, and responsive layout.&lt;/li&gt;
&lt;li&gt;Maps still need collision, navigation, triggers, and spawn data.&lt;/li&gt;
&lt;li&gt;Tilesets still need tile IDs and terrain rules.&lt;/li&gt;
&lt;li&gt;Character art still needs pivots, hitboxes, animation states, and import settings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat those boundaries as part of the prompt review. A visually strong image can still be the wrong deliverable.&lt;/p&gt;

&lt;p&gt;I work on FrameSprite, where we separated the Create workspace into character, prop/icon, UI, map, VFX, and tileset lanes. The deeper guide contains six copyable prompt structures and a downloadable QA checklist:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.framesprite.com/guides/ai-game-art-prompt-examples" rel="noopener noreferrer"&gt;https://www.framesprite.com/guides/ai-game-art-prompt-examples&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The structure is model-neutral: use it anywhere you can control a prompt, then keep the engine handoff explicit.&lt;br&gt;
``&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>gamedev</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why a 60-Frame Sprite Sheet Can Break Browser Canvas Export</title>
      <dc:creator>FrameSprite</dc:creator>
      <pubDate>Thu, 03 Sep 2026 07:35:30 +0000</pubDate>
      <link>https://dev.to/framesprite/why-a-60-frame-sprite-sheet-can-break-browser-canvas-export-37o1</link>
      <guid>https://dev.to/framesprite/why-a-60-frame-sprite-sheet-can-break-browser-canvas-export-37o1</guid>
      <description>&lt;p&gt;A 60-frame animation can contain perfectly valid PNGs and still fail when a browser combines them into one sprite sheet. The failure is often not the compressed file size. It is the decoded canvas size, the source frames held in memory, and the encoder's working allocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the final pixel budget
&lt;/h2&gt;

&lt;p&gt;For a fixed-cell sheet without margins or gutters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sheet width = columns × cell width&lt;/li&gt;
&lt;li&gt;sheet height = rows × cell height&lt;/li&gt;
&lt;li&gt;pixel count = sheet width × sheet height&lt;/li&gt;
&lt;li&gt;one RGBA buffer ≈ pixel count × 4 bytes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With 60 frames at 960×960 arranged 6×10:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sheet size&lt;/td&gt;
&lt;td&gt;5760×9600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixels&lt;/td&gt;
&lt;td&gt;55,296,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One RGBA buffer&lt;/td&gt;
&lt;td&gt;about 210.94 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That estimate covers only one uncompressed RGBA buffer. A real export can also retain decoded source images, intermediate canvases, encoder memory and the final Blob. A 10 MB PNG on disk therefore does not imply a 10 MB browser operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve resolution by changing delivery layout
&lt;/h2&gt;

&lt;p&gt;The resilient fallback is a numbered PNG sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run_001.png
run_002.png
...
run_060.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a small manifest with frame count and intended playback FPS. If the engine requires atlases, pack smaller pages per action or per texture budget later. The source sequence remains editable and complete.&lt;/p&gt;

&lt;p&gt;This is better than shrinking every source frame simply to make one giant sheet encode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate before allocating a canvas
&lt;/h2&gt;

&lt;p&gt;Export code should check width, height and total pixel area before creating the canvas. If the request is too large, offer sequence export immediately instead of attempting an allocation that may fail or freeze the page.&lt;/p&gt;

&lt;p&gt;When using &lt;code&gt;HTMLCanvasElement.toBlob&lt;/code&gt;, also handle a &lt;code&gt;null&lt;/code&gt; result. Preserve the user's frames and fall back to the sequence rather than producing an empty download.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capacity and CORS are different failures
&lt;/h2&gt;

&lt;p&gt;A large allocation is a capacity problem. A tainted canvas is an origin-security problem caused by drawing cross-origin pixels without the required CORS permission. Downscaling does not repair missing CORS headers, and changing CORS settings does not make a 55-million-pixel sheet cheap.&lt;/p&gt;

&lt;p&gt;Report these conditions separately so users get the right recovery path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical export checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Calculate final width, height and pixel area.&lt;/li&gt;
&lt;li&gt;Keep full-resolution numbered frames as the production master.&lt;/li&gt;
&lt;li&gt;Split sheets by action or engine texture-page budget.&lt;/li&gt;
&lt;li&gt;Handle a null &lt;code&gt;toBlob&lt;/code&gt; result.&lt;/li&gt;
&lt;li&gt;Report CORS failures separately from capacity failures.&lt;/li&gt;
&lt;li&gt;Verify ZIP count plus first, middle and last frame.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I work on FrameSprite, where this calculation came from a real 60-frame browser-export case. The complete 12/24/60-frame comparison and failure table are here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.framesprite.com/guides/sprite-sheet-too-large-browser-canvas?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=large_canvas_export_20260903" rel="noopener noreferrer"&gt;https://www.framesprite.com/guides/sprite-sheet-too-large-browser-canvas?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=large_canvas_export_20260903&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A Fixed-Cell Contract for Sprite Sheets That Do Not Jitter</title>
      <dc:creator>FrameSprite</dc:creator>
      <pubDate>Thu, 03 Sep 2026 03:18:29 +0000</pubDate>
      <link>https://dev.to/framesprite/a-fixed-cell-contract-for-sprite-sheets-that-do-not-jitter-3167</link>
      <guid>https://dev.to/framesprite/a-fixed-cell-contract-for-sprite-sheets-that-do-not-jitter-3167</guid>
      <description>&lt;p&gt;Generating or drawing a good sequence is only half the job. A sprite sheet stops jittering when every frame obeys the same &lt;strong&gt;fixed-cell contract&lt;/strong&gt;: identical canvas size, one semantic pivot, stable baseline, explicit reading order, and preserved trim offsets. Treat those values as runtime data, not as visual cleanup.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Disclosure: I work on FrameSprite. The method below is tool-independent, and the measurements come from a public, downloadable FrameSprite test strip.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why equal-looking frames can still jitter
&lt;/h2&gt;

&lt;p&gt;A renderer places a sprite rectangle around an origin. If every frame is cropped to its visible pixels, that rectangle changes as arms, capes, wings, or weapons move. Centering each new rectangle at the same transform does &lt;strong&gt;not&lt;/strong&gt; center the original character. It centers a different crop on every frame.&lt;/p&gt;

&lt;p&gt;That creates two separate problems that are often confused:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Artwork drift:&lt;/strong&gt; the character moves relative to the logical cell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime drift:&lt;/strong&gt; the engine changes placement through pivots, offsets, transforms, camera motion, or texture sampling.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Filtering can soften shimmer, but it cannot repair a moving pivot. Repacking can reduce atlas size, but it cannot reconstruct discarded source offsets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fixed-cell contract
&lt;/h2&gt;

&lt;p&gt;Write these fields down before export:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Contract&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cell width and height&lt;/td&gt;
&lt;td&gt;Identical for every frame in the action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reading order&lt;/td&gt;
&lt;td&gt;Explicit: row-major, column-major, or named frames&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pivot meaning&lt;/td&gt;
&lt;td&gt;A gameplay point, such as ground contact or torso root&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pivot coordinates&lt;/td&gt;
&lt;td&gt;Measured in the same source canvas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;Shared for grounded actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alpha policy&lt;/td&gt;
&lt;td&gt;Real transparency, not a painted checkerboard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trim metadata&lt;/td&gt;
&lt;td&gt;Original size plus X/Y offset if cells are packed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playback&lt;/td&gt;
&lt;td&gt;Frame order, duration, and loop policy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key word is &lt;strong&gt;semantic&lt;/strong&gt;. “Center” is not automatically a useful pivot. A grounded walk cycle often wants bottom-center or a designed foot-contact point. A flying creature may use a torso root. An attack may keep the body root stable while the weapon extends far outside the idle silhouette.&lt;/p&gt;

&lt;h2&gt;
  
  
  A measured example
&lt;/h2&gt;

&lt;p&gt;We measured four frames from a public 4×4 strip inside identical 360×360 cells. Their visible widths changed from &lt;strong&gt;145 px to 189 px&lt;/strong&gt;, while the lowest alpha-bound varied by only &lt;strong&gt;1 px&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That result matters because it separates silhouette change from root drift:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the moving limbs legitimately change the visible width;&lt;/li&gt;
&lt;li&gt;the outer cell remains 360×360;&lt;/li&gt;
&lt;li&gt;the canvas center remains 180×180;&lt;/li&gt;
&lt;li&gt;the shared bottom region stays coherent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If those four frames were independently trimmed and re-centered, the apparent character position would move even though the original fixed-cell sequence was stable.&lt;/p&gt;

&lt;p&gt;You can inspect the full method and download the reference measurements in the &lt;a href="https://www.framesprite.com/guides/sprite-pivot-anchor-origin" rel="noopener noreferrer"&gt;sprite pivot, anchor and origin guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A renderer-neutral playback model
&lt;/h2&gt;

&lt;p&gt;For a fixed grid, frame selection should change only the source rectangle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SpriteContract&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cellWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;cellHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;pivotX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;pivotY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&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;frameRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SpriteContract&lt;/span&gt;&lt;span class="p"&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;column&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columns&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;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;column&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cellWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cellHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cellWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cellHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pivotX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pivotX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pivotY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pivotY&lt;/span&gt;&lt;span class="p"&gt;,&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;The world transform does not move when the frame changes. Only the sampled rectangle changes.&lt;/p&gt;

&lt;p&gt;Engine terminology differs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unity stores a Sprite pivot in source-texture pixels.&lt;/li&gt;
&lt;li&gt;Phaser commonly exposes a normalized origin from 0 to 1.&lt;/li&gt;
&lt;li&gt;Godot AnimatedSprite2D combines centered drawing with a pixel offset.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The names and units differ, but the contract is the same: convert one semantic source point into each engine’s representation without redefining it per frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about trimmed atlases?
&lt;/h2&gt;

&lt;p&gt;Trimming is safe when the atlas keeps enough data to reconstruct the fixed source cell:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;untrimmed source width and height;&lt;/li&gt;
&lt;li&gt;packed rectangle width and height;&lt;/li&gt;
&lt;li&gt;trim offset X and Y;&lt;/li&gt;
&lt;li&gt;pivot in source coordinates;&lt;/li&gt;
&lt;li&gt;rotation flag, if packing allows rotated regions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A normalized origin of &lt;code&gt;0.5, 0.5&lt;/code&gt; is not sufficient by itself. If each trimmed rectangle has a different size, its normalized center refers to a different place in the original cell.&lt;/p&gt;

&lt;p&gt;If the exporter drops source size and trim offsets, keep the untrimmed fixed cells instead. A larger texture is often cheaper than debugging intermittent visual drift across several engines.&lt;/p&gt;

&lt;h2&gt;
  
  
  A five-minute diagnostic
&lt;/h2&gt;

&lt;p&gt;Use this order before changing import settings:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Freeze the character at one world position and stop the camera.&lt;/li&gt;
&lt;li&gt;Overlay all animation frames at the same top-left coordinate.&lt;/li&gt;
&lt;li&gt;Draw the logical cell border, pivot, and gameplay baseline.&lt;/li&gt;
&lt;li&gt;Confirm every frame uses the same outer canvas or valid trim offsets.&lt;/li&gt;
&lt;li&gt;Test frame swapping before testing movement.&lt;/li&gt;
&lt;li&gt;Move the character and camera separately.&lt;/li&gt;
&lt;li&gt;Only then inspect filtering, mipmaps, pixel snapping, and compression.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If jitter remains while both the character and camera are stationary, start with the art, cell, pivot, or atlas metadata. If it appears only during camera movement, investigate fractional transforms and pixel scaling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this contract should not be over-applied
&lt;/h2&gt;

&lt;p&gt;Not every opaque pixel should remain fixed. Jumps, recoil, squash and stretch, flying motion, and deliberate root translation can move the center of mass. The contract stabilizes the &lt;strong&gt;gameplay anchor&lt;/strong&gt;; it does not erase intentional motion.&lt;/p&gt;

&lt;p&gt;Likewise, one pivot cannot represent every gameplay attachment. Weapon sockets, hitboxes, effect origins, and collision shapes may need separate metadata or child transforms.&lt;/p&gt;

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

&lt;p&gt;Give the exported sheet to a developer who did not create it. If they cannot identify the cell size, reading order, pivot meaning, loop behavior, and trim reconstruction without asking the artist, the asset package is incomplete.&lt;/p&gt;

&lt;p&gt;For a visual overlay workflow, failure table, and downloadable alignment strip, see &lt;a href="https://www.framesprite.com/guides/fix-sprite-animation-jitter" rel="noopener noreferrer"&gt;How to fix sprite animation jitter and pivot drift&lt;/a&gt;. You can also test an existing sheet in FrameSprite’s &lt;a href="https://www.framesprite.com/tools/sprite-sheet" rel="noopener noreferrer"&gt;free browser sprite-sheet tools&lt;/a&gt;; those tools run without generation credits.&lt;/p&gt;

&lt;p&gt;A stable sprite animation is not defined by how tightly each drawing is cropped. It is defined by whether every frame can be placed from the same contract.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>tutorial</category>
      <category>debugging</category>
      <category>programming</category>
    </item>
    <item>
      <title>Stop Choosing Sprite Animation Frames by FPS</title>
      <dc:creator>FrameSprite</dc:creator>
      <pubDate>Wed, 02 Sep 2026 01:44:24 +0000</pubDate>
      <link>https://dev.to/framesprite/stop-choosing-sprite-animation-frames-by-fps-5cgi</link>
      <guid>https://dev.to/framesprite/stop-choosing-sprite-animation-frames-by-fps-5cgi</guid>
      <description>&lt;p&gt;If you are deciding how many frames a sprite animation needs, do not start from the source video's frame rate. Start from the poses the player must read.&lt;/p&gt;

&lt;p&gt;Frame count and playback FPS answer different questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frame count&lt;/strong&gt; controls pose coverage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playback FPS&lt;/strong&gt; controls how quickly those poses advance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An eight-frame attack at 8 fps lasts one second. The same eight poses at 16 fps last half a second. Adding another eight near-duplicate images does not automatically improve the attack; it mostly increases the sheet, review work and opportunities for character drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Name the phases before drawing or generating frames
&lt;/h2&gt;

&lt;p&gt;Write the action as a short sequence of readable states. An attack normally needs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;anticipation,&lt;/li&gt;
&lt;li&gt;strike,&lt;/li&gt;
&lt;li&gt;impact,&lt;/li&gt;
&lt;li&gt;recovery.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A walk normally needs contact, down, passing and up. A death action needs loss of balance, the fall and a stable final pose. These are not rigid animation laws. They are a practical test: if two neighboring frames communicate the same phase with the same silhouette, one may not be earning its place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a small first-pass budget
&lt;/h2&gt;

&lt;p&gt;For a first playable pass, I use these ranges:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Start with&lt;/th&gt;
&lt;th&gt;What earns another frame&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Idle&lt;/td&gt;
&lt;td&gt;4–6&lt;/td&gt;
&lt;td&gt;Breathing, cloth or equipment pops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Walk&lt;/td&gt;
&lt;td&gt;6–8&lt;/td&gt;
&lt;td&gt;A foot teleports or the passing pose disappears&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run&lt;/td&gt;
&lt;td&gt;6–8&lt;/td&gt;
&lt;td&gt;Flight time or secondary motion collapses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attack&lt;/td&gt;
&lt;td&gt;8–12&lt;/td&gt;
&lt;td&gt;Weapon path, impact or recovery lacks a phase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cast&lt;/td&gt;
&lt;td&gt;8–12&lt;/td&gt;
&lt;td&gt;Hands, prop or effect skip a necessary state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hit&lt;/td&gt;
&lt;td&gt;3–6&lt;/td&gt;
&lt;td&gt;Contact or recoil cannot be read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Death&lt;/td&gt;
&lt;td&gt;8–12&lt;/td&gt;
&lt;td&gt;The fall teleports or never settles cleanly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are starting budgets for unique poses. A hand-drawn boss with broad arcs may need more. Deliberately stepped pixel art may need fewer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tune time separately
&lt;/h2&gt;

&lt;p&gt;Duration is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;duration in seconds = unique frames / playback FPS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight unique frames at 12 fps last about 0.667 seconds. If the animation already contains every important pose but feels floaty, increase playback FPS before adding images. If it moves at the right speed but the foot jumps from behind the body to the front, add the missing passing pose.&lt;/p&gt;

&lt;p&gt;This separation also makes engine debugging easier. A wrong duration is a timing problem. A missing silhouette is a pose problem. A character that shifts around the canvas is an anchor or pivot problem. A sword striking behind a right-facing character is a direction or clip-selection problem. More frames do not solve the last three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test at game scale
&lt;/h2&gt;

&lt;p&gt;A full-resolution preview can hide a weak silhouette. Preview the loop at the actual on-screen size and next to gameplay velocity, hit timing and state transitions.&lt;/p&gt;

&lt;p&gt;For each pass, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can I name the phase represented by every frame?&lt;/li&gt;
&lt;li&gt;Does the center of mass move continuously?&lt;/li&gt;
&lt;li&gt;Do feet and weapons keep believable contact points?&lt;/li&gt;
&lt;li&gt;Does the last unique frame return cleanly to the first?&lt;/li&gt;
&lt;li&gt;Does raising FPS fix the problem without adding poses?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only add a frame when the answer points to a missing visual state.&lt;/p&gt;

&lt;p&gt;We published the complete seven-action matrix as downloadable CSV and JSON, with its method and limitations, in the &lt;a href="https://www.framesprite.com/guides/sprite-animation-frame-count" rel="noopener noreferrer"&gt;FrameSprite frame-count guide&lt;/a&gt;. It is useful as a production checklist even if you draw every frame by hand and never use a generation tool.``&lt;/p&gt;

</description>
      <category>indiegame</category>
      <category>gamedev</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>Godot sprite blurry when moving? Diagnose the right layer first</title>
      <dc:creator>FrameSprite</dc:creator>
      <pubDate>Tue, 01 Sep 2026 11:03:37 +0000</pubDate>
      <link>https://dev.to/framesprite/godot-sprite-blurry-when-moving-diagnose-the-right-layer-first-36e</link>
      <guid>https://dev.to/framesprite/godot-sprite-blurry-when-moving-diagnose-the-right-layer-first-36e</guid>
      <description>&lt;p&gt;A sprite that is blurry while standing still and a sprite that only shimmers while moving are usually suffering from different bugs. Treating both as “bad filtering” leads to hours of random import changes.&lt;/p&gt;

&lt;p&gt;Here is the shortest diagnostic path I use for Godot 4 pixel art.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Freeze the scene before touching import settings
&lt;/h2&gt;

&lt;p&gt;Pause the character on a single frame and stop the camera.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Soft edges while everything is still&lt;/strong&gt; usually indicate texture filtering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sharp while still, unstable while moving&lt;/strong&gt; usually indicate fractional movement, fractional scale, or camera interpolation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A thin strip from the neighboring pose&lt;/strong&gt; indicates sprite-sheet grid or atlas bleeding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The whole body jumps between frames&lt;/strong&gt; indicates inconsistent canvases, pivots, or foot baselines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are four different layers. Change one at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Fix stationary blur with Nearest filtering
&lt;/h2&gt;

&lt;p&gt;For pixel art, select the &lt;code&gt;Sprite2D&lt;/code&gt; or &lt;code&gt;AnimatedSprite2D&lt;/code&gt; node and set:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CanvasItem → Texture → Filter → Nearest&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Nearest sampling chooses the closest source pixel rather than blending surrounding pixels. If every pixel-art asset in the project needs the same policy, set the project default instead of editing every node.&lt;/p&gt;

&lt;p&gt;Do not assume mipmaps are always wrong. A sprite displayed at native size or an integer enlargement usually does not need them. If the camera shrinks sprites below one source pixel per screen pixel, mipmaps can reduce distant shimmer. Test the real camera scale.&lt;/p&gt;

&lt;p&gt;Godot reference: &lt;a href="https://docs.godotengine.org/en/stable/classes/class_canvasitem.html" rel="noopener noreferrer"&gt;CanvasItem texture filtering&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. If it is sharp while still, inspect motion
&lt;/h2&gt;

&lt;p&gt;Nearest filtering cannot repair fractional transforms.&lt;/p&gt;

&lt;p&gt;Log the final rendered position of both the character and &lt;code&gt;Camera2D&lt;/code&gt;. Then test four cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Character still, camera still&lt;/li&gt;
&lt;li&gt;Character moving, camera still&lt;/li&gt;
&lt;li&gt;Character still, camera moving&lt;/li&gt;
&lt;li&gt;Character and camera moving together&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If only cases 3 and 4 fail, the camera path is the likely source. If case 2 fails as well, inspect the character transform and the relationship between physics updates and rendering.&lt;/p&gt;

&lt;p&gt;For deliberately crisp pixel art, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the final screen position lands on whole pixels;&lt;/li&gt;
&lt;li&gt;scale is an integer at the intended resolution;&lt;/li&gt;
&lt;li&gt;character and camera are not being rounded independently in opposing directions;&lt;/li&gt;
&lt;li&gt;physics-step movement and render interpolation are not fighting each other;&lt;/li&gt;
&lt;li&gt;the pixel-snap settings match the project's movement model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Godot's own guide separates jitter and stutter from texture filtering: &lt;a href="https://docs.godotengine.org/en/stable/tutorials/rendering/jitter_stutter.html" rel="noopener noreferrer"&gt;Fixing jitter, stutter and input lag&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Check odd-sized frames and centering
&lt;/h2&gt;

&lt;p&gt;An odd-width or odd-height frame with &lt;code&gt;centered = true&lt;/code&gt; can place the visual center between pixels.&lt;/p&gt;

&lt;p&gt;That does not mean every pixel sprite must disable centering. Even-sized frames that stay on integer positions can work correctly. But if an odd-sized frame deforms or shimmers, compare these two tests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;disable &lt;code&gt;Centered&lt;/code&gt; and align with an integer &lt;code&gt;offset&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;keep centering, but adjust the authored canvas so its anchor lands on a whole pixel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is a consistent gameplay anchor, not a universal checkbox rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Diagnose neighboring-frame bleed as atlas math
&lt;/h2&gt;

&lt;p&gt;If the wrong color appears only at a cell boundary, verify the sheet geometry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sheet width = cell width × columns + gutter × (columns - 1)
sheet height = cell height × rows + gutter × (rows - 1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exported pixels and the slicer settings must agree. Adding “padding” only in the editor does not create missing source pixels.&lt;/p&gt;

&lt;p&gt;For nearest-filtered, non-mipmapped pixel art, a real 1 px transparent gutter is a reasonable test starting point. Bilinear filtering, downscaling, compression, and mipmaps may require more isolation or alpha-aware edge dilation.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Separate frame jitter from transform jitter
&lt;/h2&gt;

&lt;p&gt;Overlay every animation frame on one fixed canvas. Mark one shared foot baseline and one pivot.&lt;/p&gt;

&lt;p&gt;Limbs, capes, and weapons can extend. The gameplay anchor should not wander unless the action intentionally translates the character. Cropping each frame to its opaque bounds and then centering it independently is a common way to create apparent bouncing.&lt;/p&gt;

&lt;p&gt;If the overlay is stable but the running game is not, return to transforms and the camera. If the overlay itself jumps, repack the source frames before changing engine settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  A compact symptom table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;First place to inspect&lt;/th&gt;
&lt;th&gt;Likely fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Soft while still&lt;/td&gt;
&lt;td&gt;Texture filter&lt;/td&gt;
&lt;td&gt;Use Nearest for pixel art&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sharp still, shimmers in motion&lt;/td&gt;
&lt;td&gt;Transform/camera&lt;/td&gt;
&lt;td&gt;Test integer placement and update timing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Neighboring pose at cell edge&lt;/td&gt;
&lt;td&gt;Sheet grid/gutters&lt;/td&gt;
&lt;td&gt;Correct cell math and export real spacing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Character bounces each frame&lt;/td&gt;
&lt;td&gt;Canvas/pivot&lt;/td&gt;
&lt;td&gt;Normalize canvas, baseline, and pivot&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important principle is simple: &lt;strong&gt;filtering, motion, atlas sampling, and frame alignment are separate systems&lt;/strong&gt;. Classify the symptom first, then change only the matching layer.&lt;/p&gt;

&lt;p&gt;I maintain FrameSprite, and we turned this into a more detailed, engine-focused checklist with failure cases and related sprite-sheet tests:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.framesprite.com/guides/godot-sprite-sheet-blurry-fix?utm_source=devto&amp;amp;utm_medium=technical_article&amp;amp;utm_campaign=godot_blur_20260901" rel="noopener noreferrer"&gt;Godot sprite blurry when moving? Fix blur and jitter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you are debugging one of these cases now, post which of the four motion tests fails. That usually narrows the problem faster than another round of import changes.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>debugging</category>
      <category>tutorial</category>
      <category>performance</category>
    </item>
    <item>
      <title>I tested 0, 1, 2 and 4 pixel sprite-sheet gutters instead of guessing</title>
      <dc:creator>FrameSprite</dc:creator>
      <pubDate>Mon, 31 Aug 2026 07:36:21 +0000</pubDate>
      <link>https://dev.to/framesprite/i-tested-0-1-2-and-4-pixel-sprite-sheet-gutters-instead-of-guessing-1935</link>
      <guid>https://dev.to/framesprite/i-tested-0-1-2-and-4-pixel-sprite-sheet-gutters-instead-of-guessing-1935</guid>
      <description>&lt;p&gt;Sprite-sheet advice often collapses to one number: “add two pixels of padding.” That is easy to repeat and hard to evaluate. Padding only works in relation to the sampler, output scale, mipmap path and atlas geometry.&lt;/p&gt;

&lt;p&gt;I wanted a smaller question that could be reproduced: &lt;strong&gt;when two different sprite cells touch, how much does nearest or bilinear sampling let the right cell influence the left one?&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;The test starts with the first two cells from a public FrameSprite bone-dragon sheet. Each character image is alpha-trimmed, placed in an equal-sized cell and edge-aligned. The pair is then repacked with 0, 1, 2 or 4 fully transparent pixels between cells.&lt;/p&gt;

&lt;p&gt;For each gutter, the script samples at 100%, 50% and 25% using nearest and bilinear filtering. The final sample is positioned a quarter source pixel inside the left cell boundary. A same-neighbor control supplies the baseline. If replacing the neighbor with a different frame changes a sampled RGBA channel by more than eight, that pixel is counted as contaminated.&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%2Fjbm7c6h1s0d9505lknwa.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%2Fjbm7c6h1s0d9505lknwa.png" alt="Sprite-sheet gutter comparison across nearest and bilinear sampling" width="800" height="126"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the controlled test found
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gutter&lt;/th&gt;
&lt;th&gt;Nearest&lt;/th&gt;
&lt;th&gt;Bilinear&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0 px&lt;/td&gt;
&lt;td&gt;No measured contamination&lt;/td&gt;
&lt;td&gt;156 / 78 / 40 affected rows at 100% / 50% / 25%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 px&lt;/td&gt;
&lt;td&gt;No measured contamination&lt;/td&gt;
&lt;td&gt;No measured contamination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 px&lt;/td&gt;
&lt;td&gt;No measured contamination&lt;/td&gt;
&lt;td&gt;No measured contamination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 px&lt;/td&gt;
&lt;td&gt;No measured contamination&lt;/td&gt;
&lt;td&gt;No measured contamination&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One pixel separated the two bilinear taps used here. It would be tempting to stop at “one pixel is enough,” but that conclusion would exceed the experiment. The script did &lt;strong&gt;not&lt;/strong&gt; generate mipmaps, apply texture compression, use repeating wrap modes or send the sheet through Unity's or Godot's atlas packer. Each of those can change how far a sample reaches or how edge colors are reconstructed.&lt;/p&gt;

&lt;h2&gt;
  
  
  A more useful rule than one universal number
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Export real transparent space. An editor spacing field cannot create pixels that are absent from the PNG.&lt;/li&gt;
&lt;li&gt;Keep logical frame size separate from gutter size.&lt;/li&gt;
&lt;li&gt;Enter the exact exported spacing in the engine slicer.&lt;/li&gt;
&lt;li&gt;Test the target filter, smallest display scale and platform build.&lt;/li&gt;
&lt;li&gt;For bilinear filtering or mipmaps, test a larger gutter and alpha-aware edge dilation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Edge dilation matters because a fully transparent pixel still has RGB channels. Extending the nearest visible edge color into transparent gutter pixels, while keeping alpha at zero, can prevent black or colored halos during interpolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce or challenge the result
&lt;/h2&gt;

&lt;p&gt;The full benchmark publishes the JSON rows, CSV, comparison image, source bounds, threshold and limitations. It also maps the result to Unity and Godot settings:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.framesprite.com/guides/sprite-sheet-padding-gutters?utm_source=devto&amp;amp;utm_medium=technical_article&amp;amp;utm_campaign=sprite_gutter_benchmark_20260831" rel="noopener noreferrer"&gt;https://www.framesprite.com/guides/sprite-sheet-padding-gutters?utm_source=devto&amp;amp;utm_medium=technical_article&amp;amp;utm_campaign=sprite_gutter_benchmark_20260831&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you run the data through a mipmap or platform-compression test, share the atlas settings and the first mip level where the result changes. That is the next experiment this small test does not answer.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>godot</category>
      <category>pixelart</category>
      <category>unity3d</category>
    </item>
    <item>
      <title>The Eight-Direction Sprite Bug That Changes Between Godot and Unity</title>
      <dc:creator>FrameSprite</dc:creator>
      <pubDate>Sun, 30 Aug 2026 07:04:20 +0000</pubDate>
      <link>https://dev.to/framesprite/the-eight-direction-sprite-bug-that-changes-between-godot-and-unity-3o84</link>
      <guid>https://dev.to/framesprite/the-eight-direction-sprite-bug-that-changes-between-godot-and-unity-3o84</guid>
      <description>&lt;p&gt;Eight-direction sprite animation usually fails at the boundary between input, engine coordinates, and authored art. Godot 4 treats screen-up as negative Y, while a typical Unity 6 2D world treats up as positive Y. Keep movement analog, quantize only the visual-facing vector, and preserve the last non-zero direction while idle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with one direction contract
&lt;/h2&gt;

&lt;p&gt;Do not begin in the Animator or AnimationTree. Write the direction table first and use the same names in filenames, runtime code, and tests.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;th&gt;Godot 4 screen vector&lt;/th&gt;
&lt;th&gt;Unity 6 world vector&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;East&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(1, 0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(1, 0)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;North-east&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(0.7071, -0.7071)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(0.7071, 0.7071)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;North&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(0, -1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(0, 1)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;North-west&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(-0.7071, -0.7071)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(-0.7071, 0.7071)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;West&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(-1, 0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(-1, 0)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;South-west&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(-0.7071, 0.7071)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(-0.7071, -0.7071)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;South&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(0, 1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(0, -1)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;South-east&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(0.7071, 0.7071)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(0.7071, -0.7071)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This sign change is easy to miss during an engine port because every clip can still be named correctly. The wrong animation plays only when the vertical input reaches the runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Movement and facing are separate values
&lt;/h2&gt;

&lt;p&gt;An analog stick can produce hundreds of angles. An eight-view sprite can show only eight. Treating the same raw vector as both physical movement and visual selection creates unstable diagonals and makes idle behavior depend on input noise.&lt;/p&gt;

&lt;p&gt;Use the raw, length-limited vector for movement. Normalize a copy and select the authored direction with the highest dot product:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;best direction = direction with max(dot(normalize(input), candidate))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates deterministic 45-degree sectors. It also avoids a long chain of angle comparisons whose edge cases become difficult to audit.&lt;/p&gt;

&lt;p&gt;In Godot 4, &lt;code&gt;Input.get_vector()&lt;/code&gt; already limits the vector length. Send it to &lt;code&gt;CharacterBody2D.velocity&lt;/code&gt;, then send the quantized copy to &lt;code&gt;AnimationNodeBlendSpace2D.blend_position&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In Unity 6, physical movement can remain analog while &lt;code&gt;MoveX&lt;/code&gt; and &lt;code&gt;MoveY&lt;/code&gt; receive either the normalized input or the selected eight-way unit vector. Continuous input suits smooth motion. Pixel-art and hand-drawn views often read more clearly when the visual value snaps to one authored direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idle needs an explicit policy
&lt;/h2&gt;

&lt;p&gt;When input returns to zero, zero is not a direction. Choose one of two designs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use one neutral idle at the center of the blend space.&lt;/li&gt;
&lt;li&gt;Store &lt;code&gt;lastFacing&lt;/code&gt; and use a separate directional idle state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The second option is required when a character should keep looking north-west after movement stops. Only update &lt;code&gt;lastFacing&lt;/code&gt; when the input magnitude is above the dead zone. Otherwise small controller noise will cause idle flicker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the boundaries, not just the eight labels
&lt;/h2&gt;

&lt;p&gt;Testing N, NE, E, SE, S, SW, W, and NW proves only the obvious cases. Each sector changes at a 22.5-degree boundary. Probe values immediately on both sides of every boundary, then test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;low-magnitude gamepad input;&lt;/li&gt;
&lt;li&gt;rapid reversal;&lt;/li&gt;
&lt;li&gt;simultaneous opposite keys;&lt;/li&gt;
&lt;li&gt;movement stopping near a boundary;&lt;/li&gt;
&lt;li&gt;transition duration at native game scale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If selection is still unstable, add a small amount of directional hysteresis so a new sector must beat the current sector by a margin before it wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  The blend tree cannot repair source-art drift
&lt;/h2&gt;

&lt;p&gt;Runtime setup is only half of the contract. Every directional clip also needs the same:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;canvas dimensions;&lt;/li&gt;
&lt;li&gt;semantic foot point;&lt;/li&gt;
&lt;li&gt;pivot policy;&lt;/li&gt;
&lt;li&gt;playback duration;&lt;/li&gt;
&lt;li&gt;equipment placement;&lt;/li&gt;
&lt;li&gt;direction naming order.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A different foot baseline can look like a BlendSpace2D or Animator bug. Compare the first frame of all eight clips in one scene before tuning transitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complete engine implementations
&lt;/h2&gt;

&lt;p&gt;The FrameSprite workflow team published both versions with code, failure tables, official engine references, and a downloadable cross-engine vector CSV:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.framesprite.com/guides/godot-4-eight-direction-animation-blendspace2d?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=eight_direction_runtime" rel="noopener noreferrer"&gt;Godot 4 BlendSpace2D setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.framesprite.com/guides/unity-6-eight-direction-animation-blend-tree?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=eight_direction_runtime" rel="noopener noreferrer"&gt;Unity 6 Blend Tree setup&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The engine references used for the implementation are Godot's official &lt;code&gt;AnimationNodeBlendSpace2D&lt;/code&gt;, &lt;code&gt;AnimationTree&lt;/code&gt;, &lt;code&gt;Input.get_vector&lt;/code&gt;, and &lt;code&gt;CharacterBody2D&lt;/code&gt; documentation, plus Unity's official 2D Freeform Directional Blend Tree and &lt;code&gt;Animator.SetFloat&lt;/code&gt; documentation.&lt;/p&gt;

&lt;p&gt;Disclosure: I work on FrameSprite. The full method is included here so the post remains useful without following either link.&lt;br&gt;
&lt;code&gt;，&lt;/code&gt;&lt;/p&gt;

</description>
      <category>godot</category>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>AI Game Art Is Not Engine-Ready Until You Define the Asset Contract</title>
      <dc:creator>FrameSprite</dc:creator>
      <pubDate>Thu, 27 Aug 2026 22:03:08 +0000</pubDate>
      <link>https://dev.to/framesprite/ai-game-art-is-not-engine-ready-until-you-define-the-asset-contract-33bo</link>
      <guid>https://dev.to/framesprite/ai-game-art-is-not-engine-ready-until-you-define-the-asset-contract-33bo</guid>
      <description>&lt;p&gt;An AI image can look finished and still be unusable in a game.&lt;/p&gt;

&lt;p&gt;The failure usually is not visual quality. It is the missing agreement between art generation and the engine: dimensions, pivots, camera, stretch behavior, naming, frame cadence, and acceptance tests.&lt;/p&gt;

&lt;p&gt;That agreement is the &lt;strong&gt;asset contract&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;An asset contract is a small, explicit specification that says what the engine expects before anyone generates or draws the asset. It turns "make a fantasy panel" into "make a panel whose corners never deform, whose center can stretch, and whose text remains inside a known safe area."&lt;/p&gt;

&lt;p&gt;This matters even more with generative tools. They can produce many plausible images quickly, which makes it easy to postpone technical decisions. Then the team discovers that the outputs cannot share a prefab, animation controller, atlas, or UI layout.&lt;/p&gt;

&lt;p&gt;Here is how I define the contract for five common 2D asset families.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. UI: define the 9-slice behavior before the decoration
&lt;/h2&gt;

&lt;p&gt;A menu panel is not just a rectangle with an ornate border. It is a resizable component.&lt;/p&gt;

&lt;p&gt;Before generating it, specify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the native canvas size;&lt;/li&gt;
&lt;li&gt;the left, right, top, and bottom cap widths;&lt;/li&gt;
&lt;li&gt;which center regions may stretch or tile;&lt;/li&gt;
&lt;li&gt;the text and icon safe area;&lt;/li&gt;
&lt;li&gt;the smallest and largest intended aspect ratios;&lt;/li&gt;
&lt;li&gt;required states such as default, hover, pressed, disabled, and selected;&lt;/li&gt;
&lt;li&gt;whether corners and edge ornaments may overlap adjacent UI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fastest acceptance test is to apply the 9-slice settings in the engine and render the panel at three deliberately awkward sizes: tall, wide, and close to the minimum. Corners should retain their shape, edge motifs should not become rubbery, and the center should not reveal seams.&lt;/p&gt;

&lt;p&gt;If the panel only works at the dimensions of the concept image, it is a mockup, not a component.&lt;/p&gt;

&lt;p&gt;This &lt;a href="https://www.framesprite.com/guides/ai-game-ui-generator" rel="noopener noreferrer"&gt;AI game UI workflow&lt;/a&gt; shows the FrameSprite approach to generating around those implementation constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Icons: review at runtime size, not presentation size
&lt;/h2&gt;

&lt;p&gt;A 1024-pixel icon preview can hide every problem that matters in play.&lt;/p&gt;

&lt;p&gt;The contract should name the real display sizes: perhaps 16, 24, 32, and 64 pixels. It should also lock:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;silhouette complexity;&lt;/li&gt;
&lt;li&gt;outer padding and alpha bounds;&lt;/li&gt;
&lt;li&gt;background treatment;&lt;/li&gt;
&lt;li&gt;stroke weight;&lt;/li&gt;
&lt;li&gt;palette and contrast range;&lt;/li&gt;
&lt;li&gt;lighting direction;&lt;/li&gt;
&lt;li&gt;rarity or state markers;&lt;/li&gt;
&lt;li&gt;atlas cell size and naming.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Review the icon set at 100% scale against the lightest and darkest surfaces in the actual UI. If the object class is not recognizable in a quick glance, extra detail will not save it.&lt;/p&gt;

&lt;p&gt;Consistency across a set matters more than isolated polish. A sword, potion, key, and quest item should look as though the same visual system produced them.&lt;/p&gt;

&lt;p&gt;The practical examples in this &lt;a href="https://www.framesprite.com/guides/ai-game-icon-generator" rel="noopener noreferrer"&gt;game icon generation guide&lt;/a&gt; focus on building that system rather than chasing one impressive image.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Eight-direction characters: directions are animation data
&lt;/h2&gt;

&lt;p&gt;"Generate eight views" is underspecified.&lt;/p&gt;

&lt;p&gt;First define the camera elevation and the canonical direction order. For example:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;S, SW, W, NW, N, NE, E, SE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then define the contract shared by every direction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;identical canvas dimensions;&lt;/li&gt;
&lt;li&gt;a fixed feet pivot;&lt;/li&gt;
&lt;li&gt;stable character scale and bounding box;&lt;/li&gt;
&lt;li&gt;the same number of frames per action;&lt;/li&gt;
&lt;li&gt;matching contact, passing, recoil, and recovery beats;&lt;/li&gt;
&lt;li&gt;rules for handedness and asymmetric equipment;&lt;/li&gt;
&lt;li&gt;direction and action naming conventions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The feet pivot is especially important. When it drifts between directions, the character appears to slide or jump during turns even if each frame looks good alone.&lt;/p&gt;

&lt;p&gt;Test the full directional set in a simple controller before producing every action. Walk a circle, change direction rapidly, and overlay the pivot. This exposes scale drift, camera drift, and inconsistent timing early.&lt;/p&gt;

&lt;p&gt;Our &lt;a href="https://www.framesprite.com/guides/eight-direction-character-sprite-generator" rel="noopener noreferrer"&gt;eight-direction character sprite guide&lt;/a&gt; goes deeper on keeping identity and structure stable across the complete directional set.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. True top-down characters: 90 degrees is a production choice
&lt;/h2&gt;

&lt;p&gt;"Top-down" often gets interpreted as a high three-quarter view. That may look more expressive, but it is not the same camera.&lt;/p&gt;

&lt;p&gt;A true 90-degree overhead character changes which planes are visible, how the head overlaps the torso, how weapons point, and how the sprite communicates facing. The contract should explicitly state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;camera pitch: 90 degrees overhead;&lt;/li&gt;
&lt;li&gt;no visible horizon and no three-quarter facial view;&lt;/li&gt;
&lt;li&gt;character center and ground-contact convention;&lt;/li&gt;
&lt;li&gt;allowed shadow shape;&lt;/li&gt;
&lt;li&gt;facing cues for head, shoulders, weapon, and feet;&lt;/li&gt;
&lt;li&gt;rules for tall hats, backpacks, capes, and long weapons;&lt;/li&gt;
&lt;li&gt;collision footprint versus visual footprint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Place the sprite on a checkerboard map with props and narrow passages. If it appears to lean toward the camera or exposes too much of the front plane, the camera has drifted back toward three-quarter.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://www.framesprite.com/guides/top-down-character-sprite-generator" rel="noopener noreferrer"&gt;true top-down sprite workflow&lt;/a&gt; is useful when the project needs overhead readability rather than an isometric-looking compromise.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Portraits: derive identity from the full-body master
&lt;/h2&gt;

&lt;p&gt;Generating a portrait and a full-body character independently often creates two believable people who are not quite the same person.&lt;/p&gt;

&lt;p&gt;Treat the approved full-body design as the identity master. Extract a short list of invariants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;face shape and skin tone;&lt;/li&gt;
&lt;li&gt;hair silhouette and color;&lt;/li&gt;
&lt;li&gt;headgear construction;&lt;/li&gt;
&lt;li&gt;distinctive marks;&lt;/li&gt;
&lt;li&gt;costume neckline and shoulder details;&lt;/li&gt;
&lt;li&gt;core palette;&lt;/li&gt;
&lt;li&gt;age, expression range, and rendering style.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The portrait contract then defines crop, head angle, eye line, lighting, background, expression variants, and output sizes. It should also say which costume details must remain visible to preserve recognition.&lt;/p&gt;

&lt;p&gt;Compare portrait and full-body views side by side at their real UI sizes. Do not accept a portrait just because it is attractive; accept it when players can reliably connect it to the in-world character.&lt;/p&gt;

&lt;p&gt;This &lt;a href="https://www.framesprite.com/guides/character-portrait-from-full-body-reference" rel="noopener noreferrer"&gt;portrait-from-full-body workflow&lt;/a&gt; demonstrates the reference-first method we use.&lt;/p&gt;

&lt;h2&gt;
  
  
  A compact contract template
&lt;/h2&gt;

&lt;p&gt;Before generating an asset family, write down:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Runtime role&lt;/strong&gt; — where and how the asset appears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output geometry&lt;/strong&gt; — canvas, cell, crop, padding, and alpha rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;View rules&lt;/strong&gt; — camera, direction, pose, lighting, and scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engine behavior&lt;/strong&gt; — pivot, stretch zones, animation timing, and collision relationship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set consistency&lt;/strong&gt; — palette, line weight, naming, and variant rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Acceptance tests&lt;/strong&gt; — the exact engine scenes and sizes used for review.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This can fit on one page. The important part is that it exists before the first batch is generated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generation is the middle of the pipeline
&lt;/h2&gt;

&lt;p&gt;A reliable workflow looks like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contract -&amp;gt; reference/master -&amp;gt; generation -&amp;gt; normalization -&amp;gt; engine import -&amp;gt; acceptance test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Prompting is only one step. Normalization and engine validation are what turn images into assets.&lt;/p&gt;

&lt;p&gt;If a generated image fails the contract, regenerate or repair it before it enters the content library. Do not let exceptions accumulate inside prefabs and controller code. Technical debt in art pipelines is still technical debt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I work on FrameSprite. The five links above point to FrameSprite guides. I included them because they document the concrete workflows behind these contract examples, not as neutral third-party recommendations.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>ai</category>
      <category>tutorial</category>
      <category>graphics</category>
    </item>
    <item>
      <title>From a 4 4 AI VFX Sheet to 16 Reproducible Game Frames</title>
      <dc:creator>FrameSprite</dc:creator>
      <pubDate>Thu, 27 Aug 2026 08:56:51 +0000</pubDate>
      <link>https://dev.to/framesprite/from-a-4x4-ai-vfx-sheet-to-16-reproducible-game-frames-51do</link>
      <guid>https://dev.to/framesprite/from-a-4x4-ai-vfx-sheet-to-16-reproducible-game-frames-51do</guid>
      <description>&lt;p&gt;An attractive VFX contact sheet is not automatically a usable game animation. The useful artifact is a sequence with an explicit contract that another developer can reproduce.&lt;/p&gt;

&lt;p&gt;For one current FrameSprite sample, that contract is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a 4×4 source sheet;&lt;/li&gt;
&lt;li&gt;sixteen frames in left-to-right, top-to-bottom order;&lt;/li&gt;
&lt;li&gt;a 512×512 canvas for every frame;&lt;/li&gt;
&lt;li&gt;transparent PNG output numbered 000–015;&lt;/li&gt;
&lt;li&gt;a one-second preview at 16 fps.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Validate motion before export
&lt;/h2&gt;

&lt;p&gt;Reviewing sixteen thumbnails independently misses sequencing problems. I review the result as an onset, buildup, peak and decay. The exact phase boundaries can vary, but the sequence should not restart or reverse unexpectedly halfway through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate alpha separately
&lt;/h2&gt;

&lt;p&gt;A PNG can contain an alpha channel and still show a bright or dark halo in an engine. Preview the same frames on both light and dark backgrounds. RGB edge colors, premultiplication and the engine blend mode can produce different results even when the cutout looks clean in a browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the engine boundary honest
&lt;/h2&gt;

&lt;p&gt;The exported PNGs do not contain a Unity material, a Godot scene, a particle lifetime or a blend mode. They are lossless source frames. Those engine choices should remain explicit rather than being hidden behind a “game-ready” label.&lt;/p&gt;

&lt;p&gt;I published the original frame pack, measured dimensions and the complete workflow here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.framesprite.com/guides/game-vfx-sprite-sheet-generator?utm_source=devto&amp;amp;utm_medium=technical_article&amp;amp;utm_campaign=vfx_16frames_20260827" rel="noopener noreferrer"&gt;https://www.framesprite.com/guides/game-vfx-sprite-sheet-generator?utm_source=devto&amp;amp;utm_medium=technical_article&amp;amp;utm_campaign=vfx_16frames_20260827&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclosure: I build FrameSprite. I would especially value reproducible feedback about the first engine/import combination you would test.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>webdev</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
