Leo wouldn't stand still — Dr Paris Buttfield-Addison Paris Buttfield-Addison Technology · Policy · Research · Games · Arts · Education · WritingEvents Writing About PostsMay 21, 2026Leo wouldn't stand stillWe’re working on an adventure game, Leonardo’s Moon Ship. The player character, Leo, has the usual pile of animations: idle left and right, walk in four directions, a couple of stair anims. Since implementing the basics of the character, Leo has had this slightly cursed behaviour where he’d grow about 3% taller the moment he started walking, then shrink back when he stopped. His feet would also lift off the ground by up to 27 pixels depending on which way he was facing. Not enough to be obviously broken in a screenshot. Exactly enough to make the game feel wrong. That’s the bug overlaid on itself. The red ghost is the naive setup. The blue is the fixed version. When Leo’s idle they sit on top of each other. The second a walk anim kicks in the red drifts up and out of place.Why a single scale falls overThe animations came from the artists on wildly inconsistent canvases. Not the character, the canvas the character was painted on. Here’s the actual sizes:idle_right: 675x1095, character tight-croppedidle_left: 616x1094, basically the same character, slightly different cropwalk_right and walk_left: 1920x1200, but Leo himself is only about 700x1088 and floating somewhere inside all that transparent spacewalk_up: 664x1202walk_down: 664x1202This is normal. Artists work in whatever canvas size makes sense for the motion they’re drawing. Walk cycles need horizontal room for the stride, idle frames don’t. And good luck enforcing a single canvas size across hundreds of frames and constant revisions.The naive setup was a single animated-sprite node with one sprite_scale of 0.28 and one feet_offset_y of -340 applied globally. That only works when every animation has the same character pixel height and the character sits at the same place inside its canvas. Neither was true here. The walk_right character was about 305 visible pixels tall, idle_right was 296. Multiply both by 0.28, and one renders three pixels taller than the other every time Leo stops moving. And because walk_down had Leo floating higher inside his canvas, applying the same offset put his feet 27 pixels above the ground.Panel one shows the loose alpha bounding box that a stock image library hands you (in red) versus the actual visual feet line (in green). Panel two is what you got with the single global scale and offset: drift everywhere. Panel three is the fix.Levels in this game are tuned around the convention that Leo’s root node sits at his feet, so spawn points and collision shapes just work. That convention was being silently violated every time he changed animation. The collision body was fine. The visuals were lying about where the body actually was.The fix: per-anim metrics, computed at loadThe first instinct is to crop the bounding box and call it a day. Don’t. The stock alpha-bounding-box helper returns a rect containing every pixel with any alpha, which means it catches anti-aliasing halos and baked drop-shadow halos that extend a noticeable distance below the visible feet. Those halos vary per animation because the artist redrew the shadow each time. The loose bbox bottom is not where the feet are.So at load time, the character script walks every frame of every animation and runs a small scan. For each frame it grabs the loose alpha bbox, then scans rows from the bottom upward looking for the first row where at least 5% of the bbox width has alpha greater than 0.5. That row is the visual feet line. The scan bails the instant it finds an opaque row, so it’s basically free even across a few hundred frames.For each animation I average the visual-feet Y and the character pixel height across every frame, which means the walking bob doesn’t bias one frame and ruin the alignment. Then I pick idle_right as the reference, because the levels are already tuned to its feet position and I’m not redoing all that work. For every other animation:anim_scale = sprite_scale * (ref_char_h / this_anim_char_h) anim_offset_y = ref_feet_local_y - (this_anim_feet_y * anim_scale) That gives every animation a pre-computed scale and offset that makes its rendered character the same pixel height as idle_right and lands its visual feet at the same local Y. Then I hook the animation-change callback and apply the right values whenever the current animation flips. Naive on the left, fixed on the right, same animation timeline. The red line on the naive side tracks where the feet actually are frame to frame, which is depressing. The green line on the fixed side is locked, and Leo stays on it.The shadow came along for the rideLeo has a shadow, a separate ellipse sprite parented to him. Originally it was anchored to the node origin, which meant it floated somewhere around his knees and danced about as he changed animation. With the feet now pinned to a known constant Y in Leo’s local space, the shadow just sits at that Y. No per-anim shadow logic. It works for the stair animations too, which I didn’t even bother testing until a week later because I’d already mentally filed the problem as solved. It was.LeonardosmoonshipGamesGamedev2dProgramming← Previous Libraries Tasmania Research Fellowship Next → Games Are the Art Form of Our Time© 2026 Dr Paris Buttfield-Addison · Privacy |
The author discusses the challenges encountered while developing an adventure game called Leonardo's Moon Ship, specifically focusing on resolving visual inconsistencies related to character scaling and positioning across different animations. The initial problem stemmed from a fundamental mismatch between the character sprite dimensions and the canvases they were drawn on by the artists, which manifested as erratic behavior when implementing standard character rigging. When attempting to apply a single global scale and offset to the character, the system experienced an undesirable effect where the character would either grow or shrink during movement, and the feet would float relative to the ground, leading to a broken visual experience.
This initial failure was traced to the fact that animations were sourced from inconsistent canvases; for instance, walk cycles required more horizontal space than idle frames, and the pixel height of the character varied between different animation states. The naive solution involved applying a uniform scale and offset across all animations, which proved inadequate because it failed to account for these per-frame variations. The author details how using a single global scale and offset resulted in the character drifting because the reference points for the character’s feet were not consistently aligned within their respective canvases. The problem was compounded by issues with bounding boxes, as stock image libraries provide loosely defined alpha bounding boxes that included anti-aliasing halos and shadow remnants, which further complicated attempts at a simple visual correction.
To achieve a stable and consistent visual result, the author developed a more complex, data-driven system based on computing per-animation metrics at load time. Instead of simply cropping the bounding box, the approach involved a scan process across every frame of every animation. For each frame, the script analyzed the loose alpha bounding box to locate the actual visual feet line by searching for a row where at least five percent of the bounding box width exhibited an alpha value greater than zero point five, effectively identifying the visible feet line while ignoring artifacts like halos. This process allowed the system to determine the visual feet position accurately, independent of the potentially misleading bounding box.
Subsequently, the system computed the average visual feet Y position and the character pixel height across all frames for each animation. A reference point was established using the idle_right animation, as the game levels were already tuned around that state. Using these computed averages, the system calculated specific scale and offset values for every other animation. The scaling factor was determined by comparing the current animation’s character height to the reference height, and the vertical offset was calculated relative to the reference feet position, factoring in the specific feet position found in each frame. This methodology ensures that every animation is rendered with a precisely consistent pixel height and that the visual feet land at the identical local Y coordinate, thereby aligning the character consistently with the established spatial conventions of the game levels.
The implementation of this fixed, per-anim metric system also resolved an unexpected secondary issue regarding the character’s shadow. Because the feet position was now pinned to a known, constant Y coordinate within the character’s local space, the shadow, which was parented to the character, automatically settled at that fixed position. This eliminated the need for separate, per-animation shadow logic, allowing the shadow to correctly align with the character’s fixed foot position across all movement states, including stair animations. |