Решили: масштабный инвариант сборки юнита — 1 пиксель фигуры (1x) = 0.01 мировой единицы, подошва фигуры на y = 0, ось симметрии на x = 0. Отсюда человек тира 128 занимает ровно 1.27 юнита, и это проверяется габаритом собранного префаба, а не верой.

Почему именно 0.01: арт экспортируется в ×10 (export_bone_parts.lua, канон ради вращения без мыла), а spritePixelsToUnits у частей 1000 — 10/1000 и даёт сотую. Число не выбиралось заново: обе половины уже стояли, надо было только их перемножить и записать, чтобы позиции частей считались, а не подгонялись глазом.

Инвариант живёт между тремя местами и потому опасен: масштаб экспорта в lua, PPU в пресете импорта BonePartSprite.preset, координаты в генераторе раскладки. Сменить одно, забыв два других, означает фигуру верной формы и неверного размера — самая тихая из возможных поломок. Генератор раскладки держит формулу в шапке и считает всё от неё.

Грабли:

  • Newtonsoft.Json в execute_code не собирается: JObject и JArray существуют одновременно в Newtonsoft.Json и в Unity.Localization.ThirdParty.Editor, компилятор жалуется на неоднозначность, а alias в теле метода не объявить. Обход — не тащить JSON внутрь Unity вовсе: раскладка передаётся построчным текстом с разделителем |, который парсится string.Split.
  • Object в execute_code тоже неоднозначен (UnityEngine.Object против object) — писать полным именем.
  • Данные для сборки генерируются скриптом из .aseprite, а не набираются руками: девятнадцать узлов и восемнадцать позиций с точностью до сотой не набираются без ошибок.

Владелец правды: Assets/_Project/Prefabs/Bones/BoneUnit_Human128.prefab (собранный риг), Aseprite/scripts/export_bone_parts.lua (масштаб ×10), Assets/_Project/Art/Sprites/Bone Animations/BonePartSprite.preset (PPU 1000), docs/unit-128-rig-progress.md (живой заход, временный).