بے اثر دہرائی کا پھندا
بات ایک بگ رپورٹ سے شروع ہوئی: تین پینلز پر بٹنوں کے اندرونی حاشیے عجیب لگ رہے تھے۔ یہ پہلے اس بات کی منظم اصلاح میں بدل گئی کہ UI جیومیٹری لکھی کیسے جائے، اور پھر پورے ایک دن کے تعاقب میں — اُس بگ کا تعاقب جو اسی اصلاح نے خود پیدا کیا تھا۔ دونوں حصے لکھنے کے قابل ہیں: پہلا وہ طریقہ ہے جو میں دوبارہ اپناؤں گا، دوسرا وہ پھندا ہے جس سے میں چاہوں گا کہ دوسرے بچ جائیں۔
مسئلہ: جیومیٹری بطور سنی سنائی بات
Unity کے uGUI میں کوئی اسٹائلنگ پرت نہیں ہے۔ بٹن کا سائز اس کے RectTransform میں
ننگے اعداد کی صورت پڑا ہوتا ہے، جو کسی نے کسی prefab میں کسی دن ٹائپ کر دیا تھا۔
اسے دو درجن prefabs اور ایک سال کی تکرار سے ضرب دیجیے، اور آپ کو ایسا بہاؤ مل جائے
گا جو کسی نے جان بوجھ کر متعارف نہیں کرایا۔
اپنا ناپنا خاصا ہوش دلانے والا تھا۔ پورے پروجیکٹ میں بٹنوں کی چوڑائیاں 400، 420، 430، 446، 450، 385 اور 360 پر جمع تھیں۔ اونچائیاں 186، 180، 175، 160، 150، 145، 120، 110 اور 100 پر۔ ایک موڈل پینل 1000×1400 تھا، یعنی چار انسٹینس والے 980×1360 کے جھرمٹ سے تقریباً 2% ہٹ کر۔ ان میں سے کوئی فرق فیصلہ نہیں تھا۔ یہ ٹائپنگ کی غلطیاں اور بہاؤ تھے، جو معقول شکل اوڑھے ہوئے تھے۔
پندرہ سال کے ویب کام کے بعد جواب کی شکل واضح تھی: design tokens اسی کے لیے ہوتے ہیں۔ CSS custom properties، Tailwind کا اسپیسنگ اسکیل، Material کا ٹائپ ریمپ — جائز اقدار کا ایک بند مجموعہ چنیے، انہیں نام دیجیے، اور اس مجموعے سے باہر کوئی عدد ٹائپ کرنا ناممکن بنا دیجیے۔
تین بند لغتیں
ہاتھ سے لکھی جیومیٹری کی جگہ تین اینم آ گئے:
ButtonSize— Small (120)، Medium (160)، Large (186)ButtonWidth— Narrow (210)، Compact (300)، Wide (446)ModalSize— پینل سطحیں، اور ساتھ ایکFullBleed
ہر ایک میں ایک UseRectTransform رکن ہے جس کا مطلب ہے "یہ عنصر اپنا سائز خود طے
کرتا ہے، اسے چھوڑ دو"۔ یہ قدر جان بوجھ کر ڈیفالٹ نہیں ہے۔ جس عنصر کا سائز کبھی
چنا ہی نہیں گیا، وہ اسی حالت میں رہتا ہے جس میں لکھا گیا تھا اور آڈیٹر اسے نشان زد
کر دیتا ہے — یہاں خاموشی سے کوئی سائز ڈال دینا ٹھیک اُسی آبادی کو چھپا دیتا جسے
ڈھونڈنے کے لیے یہ تبدیلی کی گئی تھی۔
مائیگریشن ٹولنگ نے یہ لغت تقریباً تیس prefabs پر لاگو کی، اور آگے کے لیے ایک آڈیٹر پہرہ دیتا ہے۔ اعداد ایک ہی فائل میں رہتے ہیں، تاکہ کوئی قدر تیس جگہ کے بجائے ایک جگہ بدلی جا سکے۔
وہ بصیرت جس نے اسے واقعی قابلِ قدر بنایا
اصل بگ — بٹن جو چھوٹے سائز پر "غلط لگتے تھے" — سائز کا مسئلہ تھا ہی نہیں۔ وہ تناسب کا مسئلہ تھا۔ ہر بٹن کے اندر رنگین پلیٹ بٹن کے کنارے سے ایک مقررہ پکسل حاشیے پر بیٹھی تھی۔ 186 کی ڈیزائن اونچائی پر یہ پلیٹ کو بٹن کا تقریباً 81% چھوڑتی تھی۔ اونچائی 120 پر 70%، اور 100 پر صرف 64%۔ بٹن چھوٹا ہوا اور پلیٹ غیر متناسب طور پر اس سے بھی زیادہ چھوٹی ہو گئی۔
چنانچہ اب حاشیہ انسٹینس سے برقرار رکھنے کے بجائے اونچائی سے اخذ ہوتا ہے:
public const float DesignHeight = 186f;
public const float PlateBottomRatio = 44.9f / DesignHeight;
public const float PlateTopRatio = 8.9f / DesignHeight;
اب چھوٹا بٹن لمبے بٹن کی طرح متناسب ہے، نہ کہ محض بھونڈے طریقے سے سکڑا ہوا۔ ان دو تناسبوں کو یاد رکھیے۔ یہ واپس آئیں گے۔
پھندا
لغت کو نافذ کرنے کے لیے کسی نہ کسی کو یہ اعداد واقعی transform پر لکھنے ہوتے ہیں۔
ہم نے یہ کام OnValidate میں کیا، تاکہ ایڈیٹر وہی دکھائے جو گیم دکھائے گا:
private void OnValidate()
{
if (Application.isPlaying) return;
EditorApplication.delayCall += () =>
{
Undo.RecordObject(transform, "Apply button size");
ApplySize();
EditorUtility.SetDirty(this);
EditorUtility.SetDirty(transform);
};
}
بظاہر بےضرر۔ اس نے ایک دن لے لیا۔
دو علامتیں۔ مین مینو سین محفوظ ہو کر رہتا ہی نہیں تھا — Ctrl+S فائل لکھ دیتا، ورژن
کنٹرول کوئی زیرِ التوا تبدیلی نہ دکھاتا، اور "غیر محفوظ" کا ستارہ فوراً واپس آ
جاتا، ایڈیٹر دوبارہ چلانے کے بعد بھی۔ الگ سے، بٹن رکھنے والا ہر prefab کھلتے ہی دو
نئے پراپرٹی اوور رائیڈ ثبت کر دیتا: m_fontSize: 44 اور m_TextStyleHashCode۔
چار غلط جواب
اس کے بعد جو ہوا، سبق آموز حصہ وہی ہے۔ چار مفروضے، سب سورس کوڈ پڑھ کر اخذ کیے گئے، سب غلط:
- محفوظ شدہ قدر پرانی ہے۔ سورس prefab کا
m_fontSize44 کر دیا تاکہ وہ انسٹینس کے حساب سے میل کھا جائے۔ اوور رائیڈ پھر بھی واپس آ گیا — اور اس کی قدر سورس کے بالکل برابر تھی۔ - کوئی ٹیکسٹ فٹنگ کمپوننٹ لکھ رہا ہے۔ کوڈ پڑھ کر ایک قرینِ قیاس ملزم ڈھونڈا، مگر کبھی جانچا ہی نہیں کہ وہ موجود بھی ہے یا نہیں۔ وہ متاثرہ prefabs میں سے کسی میں بھی نہیں تھا۔
- یہ ایک بار ثبت ہو کر ٹھہر جاتا ہے۔ کبھی ناپا ہی نہیں۔ سراسر غلط۔
- ایک idempotence گارڈ اسے ٹھیک کر دے گا۔ خیال درست تھا۔ کام نہیں کیا، اور اس کی وجہ الگ حصے کی مستحق ہے۔
ہر ایک نے ایک "بناؤ–چلاؤ–دیکھو" چکر لیا۔ کسی ایک میں بھی کچھ ناپا نہیں گیا۔
آخرکار، ناپنا
اس سلسلے کو ختم کرنے والی چیز ایڈیٹر کوڈ کی تیس سطریں تھیں، جو Unity کے تین ایونٹس
سنتی ہیں: Undo.postprocessModifications (ہم وقت — عین property path، پہلے اور بعد
کی اقدار، اور ایک اسٹیک ٹریس جو کال کرنے والے کا نام لیتا ہے)،
ObjectChangeEvents.changesPublished (وہ لکھائیاں پکڑتا ہے جو undo سسٹم سے بچ نکلتی
ہیں)، اور EditorSceneManager.sceneDirtied (بتاتا ہے کہ سین کو میلا کس نے کیا)۔
ایک کیپچر۔ ایک اسٹیک ٹریس:
UnityEditor.EditorUtility:SetDirty (UnityEngine.Object)
CandyRush.Runtime.UI.Buttons.ButtonBehavior:<OnValidate>b__54_0 ()
UnityEditor.EditorApplication:Internal_CallDelayFunctions ()
ہمارا اپنا کوڈ، نام کے ساتھ، عین اُسی سطر پر جو سین کو میلا کر رہی تھی۔ چار مفروضوں نے کچھ نہ دیا؛ ایک پیمائش نے جواب دے دیا۔
اصل میں ہو کیا رہا تھا
تین حقائق مل کر یہ بگ بناتے ہیں، اور ہر ایک الگ سے جاننے کے قابل ہے۔
1. RectTransform کا setter تفویض کرتا ہے، موازنہ نہیں
rect.sizeDelta میں بالکل وہی قدر لکھنا بھی layout کو میلا نشان زد کر دیتا ہے۔
Unity یہ نہیں دیکھتا کہ قدر بدلی بھی ہے یا نہیں۔ اس سے layout دوبارہ بنتا ہے، جو بٹن
کے کیپشن پر TextMeshPro کا auto-size حل کنندہ دوبارہ چلاتا ہے، اور وہ m_fontSize
کو تفویض کر دیتا ہے۔
2. Unity "بدلا گیا تھا" ریکارڈ کرتا ہے، "مختلف ہے" نہیں
یہی وہ بنیادی حقیقت ہے، اور یہ ایک ہی ساتھ ساری علامتوں کی وضاحت کر دیتی ہے۔ Unity کا prefab override نظام اُن پراپرٹیز کا کھاتہ ہے جنہیں کسی انسٹینس پر تفویض کیا گیا — نہ کہ سورس کے مقابلے میں کوئی زندہ diff۔ تفویض ایک اندراج بنا دیتی ہے، خواہ قدر prefab کی قدر کے عین برابر ہو۔
آپ اسے ہوتا دیکھ سکتے ہیں: کسی prefab انسٹینس پر ایک پراپرٹی بدلیے، پھر اسے اصل قدر پر واپس لے آئیے۔ Inspector میں نیلی override پٹی وہیں رہے گی۔ اندراج اب بھی موجود ہے؛ بس اب اس میں سورس ہی کی قدر ہے۔ اسے صرف صریح right-click → Revert ہٹاتا ہے۔
یہی ایک حقیقت پیچھے مڑ کر بتا دیتی ہے کہ پہلا مفروضہ کبھی کام کر ہی نہیں سکتا تھا۔ وہ فرق پر حملہ کر رہا تھا، حالانکہ محرک کبھی فرق تھا ہی نہیں۔
3. layout سے چلنے والی اقدار فائلوں میں پک رہی تھیں
راستے میں ہمیں ایک ContentSizeFitter ملا جو کسی LayoutGroup کے بچے پر لگا ہوا تھا
— ایسی ترتیب جسے Unity اپنے issue tracker کے چار الگ اندراجات میں دستاویز کرتا ہے کہ
یہ سین کو کھلتے ہی میلا کر دیتی ہے اور وہ کبھی صاف نہیں رہتا۔ Unity کا اپنا Inspector
خبردار کرتا ہے: "A child of a layout group should not have a Content Size Fitter
component." ہمارے والے کے دونوں محور Unconstrained تھے، یعنی وہ کچھ کرتا ہی نہیں
تھا، سوائے driven-property tracker میں اندراج کرانے اور سین کو میلا کرنے کے۔
ایک بےاثر کمپوننٹ ہٹانے سے شور کی ایک پوری دیوار ہٹ گئی۔
وہ فلوٹ جو YAML سے زندہ واپس نہ آ سکا
idempotence گارڈ — ہدف کا حساب لگاؤ، اور اگر پہلے سے میل کھاتا ہے تو جلد لوٹ جاؤ — درست حل تھا۔ اس نے کام نہیں کیا، اور اس کی وجہ اس سال کا میرا پسندیدہ بگ ہے۔
گارڈ پلیٹ کے حاشیے کا موازنہ عین فلوٹ برابری سے کر رہا تھا۔ یاد کیجیے
PlateBottomRatio = 44.9f / 186f۔ اسے بٹن کی اونچائی سے ضرب دیجیے اور محفوظ کیجیے۔
Unity کا YAML سات معنی خیز ہندسے رکھتا ہے۔ اب اسے واپس پڑھ کر تازہ حساب شدہ قدر سے
ملائیے:
| اونچائی | حساب شدہ | محفوظ شدہ | عین برابر؟ |
|---|---|---|---|
| 120 (Small) | 28.967742920 | 28.967740000 | نہیں |
| 160 (Medium) | 38.623657227 | 38.623660000 | نہیں |
| 186 (Large) | 44.900001526 | 44.900000000 | ہاں |
ہر Small اور Medium بٹن کے لیے گارڈ ہمیشہ "بدل گیا" کہتا رہا۔ وہ ہر deserialization
پر چلتا، rect لکھتا، سین میلا کرتا، اور auto-sizer دوبارہ چلا دیتا — یعنی عین وہی
رویہ جسے روکنے کے لیے وہ لکھا گیا تھا۔ ساتھ والا sizeDelta چیک ٹھیک تھا، کیونکہ
Unity Vector2 کے operator== کو epsilon کے ساتھ اوورلوڈ کرتا ہے۔ صرف وہ دو ننگے
فلوٹ موازنے غلط تھے۔
حل Mathf.Approximately ہے۔ اور سبق اس سے وسیع ہے: کسی حساب شدہ فلوٹ کا کسی محفوظ
شدہ فلوٹ سے == کے ذریعے موازنہ کبھی نہ کیجیے۔ محفوظ کرنے کا چکر نقصان دہ ہے، اور
آپ کا برابری کا امتحان نہیں۔
کیا اصل فیصلہ درست تھا؟
ایک ایسے مسئلے کی تلاش میں دن گزارنا جو آپ کے اپنے فنِ تعمیر نے پیدا کیا ہو، یہ خود ایک مضبوط دلیل ہے کہ فنِ تعمیر غلط تھا۔ میں یہ نہیں سمجھتا، اور وجہ ٹھیک ٹھیک بیان کرنے کے قابل ہے۔
حکمتِ عملی — بند لغتیں، سچائی کا واحد منبع، ایک آڈیٹر — وہی ہے جس پر پوری صنعت
پہنچی ہے۔ Unity کا اپنا UI Toolkit یہی کام USS اسٹائل شیٹس سے کرتا ہے، اور uGUI اب
دیکھ بھال کے مرحلے میں ہے۔ بڑی ٹیمیں اسے prefab variants، کمپوننٹ presets، یا نام کے
اصول پر بنے prefab میٹرکس سے حل کرتی ہیں؛ مائیکروسافٹ کا MRTK بٹن براہِ راست
PressableButton_SIZE_STYLE کی ترتیبوں کے طور پر بھیجتا ہے۔ ایسی کوئی دنیا نہیں جس
میں "ہر کوئی جو چاہے عدد ٹائپ کر لے" بہتر انتخاب ہو۔
نفاذ کا طریقہ ہی وہ جگہ ہے جہاں ہم نے دستیاب سب سے سخت لیور چنا۔ OnValidate سے
RectTransform لکھنا بیک وقت تین دستاویزی تیز دھاروں کو چھوتا ہے: SetDirty پورے سین
کو میلا کرتا ہے؛ جڑ کی RectTransform پراپرٹیز کو prefab نظام خصوصی سلوک دیتا ہے؛ اور
Unity کا مینوئل صراحتاً تقاضا کرتا ہے کہ انسٹینس پر Undo.RecordObject کے بعد
PrefabUtility.RecordPrefabInstancePropertyModifications بھی بلایا جائے — ایک کال جو
ہمارے کوڈ میں نہیں تھی۔
مگر uGUI اسے رکھنے کی کوئی اور جگہ دیتا ہی نہیں۔ اگر آپ uGUI میں tokens چاہتے ہیں تو
نفاذ خود بنانا پڑے گا، اور ایڈیٹر وقت کے OnValidate والا کوئی رن ٹائم کمپوننٹ وہ
SerializedObject راستہ استعمال نہیں کر سکتا جو Unity تجویز کرتا ہے، کیونکہ وہی
طریقہ Awake سے بھی چلتا ہے۔ اس فہرست کو دیکھتے ہوئے، انتخاب قابلِ دفاع تھا۔ خرابی
تقریباً دس سطریں تھی، ڈیزائن نہیں۔
تین چیزیں جو چرانے کے قابل ہیں
کوئی بھی ٹول جو اثاثے لکھتا ہے، جب کچھ نہ بدلا ہو تو اسے no-op ہونا چاہیے۔ یہ بہتری نہیں، درستی کی خاصیت ہے۔ جو ٹول ایک جیسی اقدار دوبارہ لکھتا ہے وہ فرضی diffs، میلی فائلیں، اور ہر اُس نظام میں ضمنی اثرات پیدا کرتا ہے جو ان اشیا کو دیکھ رہا ہو۔
دو ناکام اندازوں کے بعد اندازے بند کیجیے اور آلہ بنائیے۔ کوڈ پڑھ کر نتیجہ نکالنا پُرکشش ہے کیونکہ یہ پیش رفت جیسا لگتا ہے اور شروع میں مفت ہے۔ یہاں اس نے ایک دن لیا۔ پروب نے تیس منٹ لیے اور ایک نام والا اسٹیک ٹریس دیا۔ اب یہ ایک مستقل اصول ہے، اور پروب ابھی بھی پروجیکٹ میں موجود ہے۔
جانیے کہ آپ کے ایڈیٹر کے اشارے دراصل کہتے کیا ہیں۔ بہت سی الجھن تین غیر متعلق اشاروں کو ایک سمجھنے سے آئی: ستارہ (میموری میں تبدیلیاں جو ڈسک پر نہیں)، نیلی override پٹی (انسٹینس کی تبدیلی فہرست میں ایک اندراج موجود ہے)، اور قدر کی برابری (وہ اندراج سورس سے میل کھاتا ہے یا نہیں)۔ محفوظ کرنا کبھی نیلی پٹی نہیں ہٹاتا۔ یہ الگ نظام ہیں، اور انہیں ایک سمجھنا آپ کو غلط جگہ ڈھونڈتا چھوڑ دیتا ہے۔
لغت بچ گئی۔ آڈیٹر، واحد منبع والے اعداد، اور ایک ایسا تشخیصی آلہ بھی بچ گیا جو اس شکل کا اگلا بگ چار اندازوں کے بجائے ایک کیپچر میں ڈھونڈ لے گا۔ ایک دن کے بدلے یہ مناسب سودا ہے۔