The Eight-Direction Sprite Bug That Changes Between Godot and Unity
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. Start with one direction contract Do not begin in the Animator or AnimationTree. Write the direction table first and use the same names in filenames, runtime code, and tests. | Direction | Godot 4 screen vector | Unity 6 world vector | |---|---|---| | East | (1, 0) | (1, 0) | | North-east | (0.7071, -0.7071) | (0.7071, 0.7071) | | North | (0, -1) | (0, 1) | | North-west | (-0.7071, -0.7071) | (-0.7071, 0.7071) | | West | (-1, 0) | (-1, 0) | | South-west | (-0.7071, 0.7071) | (-0.7071, -0.7071) | | South | (0, 1) | (0, -1) | | South-east | (0.7071, 0.7071) | (0.7071, -0.7071) | 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. Movement and facing are separate values 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. Use the raw, length-limited vector for movement. Normalize a copy and select the authored direction with the highest dot product: best direction = direction with max(dot(normalize(input), candidate)) This creates deterministic 45-degree sectors. It also avoids a long chain of angle comparisons whose edge cases become difficult to audit. In Godot 4, Input.get_vector() already limits the vector length. Send it to CharacterBody2D.velocity , then send the quantized copy to AnimationNodeBlendSpace2D.blend_position . In Unity 6, physical movement can remain analog while MoveX and MoveY 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. Idle needs an explicit policy When input returns to zero, zero is not a direction. Choose one of two designs: - Use one neutral idle at the center of the blend space. - Store lastFacing and use a separate directional idle state. The second option is required when a character should keep looking north-west after movement stops. Only update lastFacing when the input magnitude is above the dead zone. Otherwise small controller noise will cause idle flicker. Test the boundaries, not just the eight labels 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: - low-magnitude gamepad input; - rapid reversal; - simultaneous opposite keys; - movement stopping near a boundary; - transition duration at native game scale. 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. The blend tree cannot repair source-art drift Runtime setup is only half of the contract. Every directional clip also needs the same: - canvas dimensions; - semantic foot point; - pivot policy; - playback duration; - equipment placement; - direction naming order. 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. Complete engine implementations The FrameSprite workflow team published both versions with code, failure tables, official engine references, and a downloadable cross-engine vector CSV: The engine references used for the implementation are Godot's official AnimationNodeBlendSpace2D , AnimationTree , Input.get_vector , and CharacterBody2D documentation, plus Unity's official 2D Freeform Directional Blend Tree and Animator.SetFloat documentation. Disclosure: I work on FrameSprite. The full method is included here so the post remains useful without following either link. ๏ผ Top comments (0)
Comments
No comments yet. Start the discussion.