ایک ہی سیڈ، چار aspect ratios، چار مختلف گیمز
CandyRush ایک ڈیٹرمنسٹک سمولیشن ہے: ایک رن ایک سیڈ اور کھلاڑی کے ان پٹس کا مجموعہ ہے، اور اسی ریکارڈنگ کو دوبارہ چلانے سے ہر tick پر وہی رن بننا چاہیے۔ اپریل میں اس وعدے میں ایک سوراخ تھا۔ دشمن کہاں اسپان ہوں گے، یہ کھلاڑی کی اسکرین کی شکل اور فریم ریٹ پر منحصر تھا، اور CPU پر بھی منحصر ہو سکتا تھا، اس لیے 16:9 مانیٹر اور 4:3 مانیٹر پر ایک ہی سیڈ ایک جیسا رن نہیں بناتا تھا۔ جیسے ہی ایک اسپان مختلف ہوا، اس کے بعد کا ہر اسپان بھی مختلف ہو گیا۔
25 اپریل کو پل ریکوئسٹ #240 نے اسے بند کر دیا:
| پہلے | #240 کے بعد | |
|---|---|---|
| اسپان کے لیے کنارہ چننے کی حد | لائیو کیمرے سے aspect / (aspect + 1) | ایک مقررہ حوالہ aspect سے ایک مستقل قدر |
| ہر اسپان کوشش لوپ میں RNG draws | متغیر، پہلی جائز جگہ پر رک جاتا ہے | مقررہ، ہمیشہ 10 کوششیں |
| اسپان کالز | Unity Timeline، ہر رینڈر فریم میں ایک بار | قطار میں، مقررہ سمولیشن tick پر نکالی جاتی ہیں |
| اسکرین سے باہر ٹیلی پورٹ کا ذیلی مجموعہ | Time.frameCount % n | tick % n |
| وتر اور گردش کا حساب | Mathf.Sqrt، Quaternion.Euler | Fixed64.Sqrt، FixedMath.Rotate |
پل ریکوئسٹ نے 21 فائلیں بدلیں (+1,115 / -74) اور 29 EditMode ٹیسٹس شامل کیے۔ دو دن بعد #251 نے اسکرین کے aspect کو ایک session-locked ان پٹ بنا دیا جو ری پلے ہیڈر میں ریکارڈ ہوتا ہے، جس سے وہ واحد سودا ٹھیک ہو گیا جو #240 نے قبول کیا تھا۔
کوڈ AI کوڈنگ ایجنٹس نے issue کی بنیاد پر لکھا؛ کام تین درجوں میں بانٹا گیا، اور میں نے ہر درجے کا جائزہ لیا اور مرج کی منظوری دی۔
یہ کیسے سامنے آیا
یہ اتفاق سے ملا۔ 11 اپریل کو میں نے flow-field پاتھ فائنڈنگ کا کام (#218) مرج کیا، جس سے ہر دشمن کو کھلاڑی کا سیدھی لکیر میں پیچھا کرنے کے بجائے ایک مشترکہ گرڈ سے سمت مل جاتی ہے۔ اس کمٹ کے پیغام میں لاگت کا اندازہ تقریباً 5 ms CPU فی سیکنڈ لگایا گیا اور 74 EditMode ٹیسٹس شامل کیے گئے۔ اس کے ہر دشمن کے اسپان کی تشخیصی لاگز پڑھتے ہوئے لاگ نے کچھ اور دکھایا: ایک بیچ کا پہلا دشمن ہر رن میں ایک ہی جگہ اسپان ہوتا تھا، اور اس کے بعد ہر دشمن مختلف ہو جاتا تھا۔ issue #227 اسی دن کھولا گیا۔
بنیادی وجہ: اسکرین کے سمولیشن میں رسنے کے تین راستے
1. اسپان برانچ کیمرے کا aspect پڑھتی تھی
دشمن نظر آنے والے علاقے کے بالکل باہر اسپان ہوتے ہیں۔ کون سا کنارہ چننا ہے، اس کے لیے کیمرے کا کوڈ ایک بے ترتیب عدد نکالتا اور اسے لائیو کیمرے سے اخذ کی گئی ایک حد سے موازنہ کرتا تھا:
if (rng.Value(RngScopes.Spawning) > mainCamera.aspect / (mainCamera.aspect + 1))
یہ حد کھلاڑی کی اسکرین پر منحصر ہے:
| اسکرین | حد |
|---|---|
| 1:1 | 0.500 |
| 4:3 | 0.571 |
| 16:9 | 0.640 |
| 21:9 | 0.700 |
0.6 کا بے ترتیب عدد 4:3 اسکرین پر دائیں بائیں کا کنارہ اور 16:9 اسکرین پر اوپر یا نیچے کا کنارہ چنتا ہے۔ وہی سیڈ، وہی draw، دشمن کی مختلف جگہ۔ نقطہ رکھنے کے لیے استعمال ہونے والی حدود بھی aspect پر منحصر تھیں، کیونکہ نظر آنے والی چوڑائی orthographic سائز ضرب aspect ہوتی ہے۔
2. ری ٹرائی لوپ نے فرق کو پھیلا دیا
کوئی جگہ رکاوٹ پر بھی پڑ سکتی تھی، اس لیے اسپانر 10 بار تک دوبارہ کوشش کرتا اور پہلی جائز جگہ پر رک جاتا تھا:
while (triesCount < maxTriesCount) // up to 10
{
triesCount++;
float variation = rng.Value(RngScopes.Spawning) * 0.2f;
position = cameraManager.GetRandomPointOutsideCamera(0.5f + variation);
if (stageFieldManager.ValidatePosition(position, Vector2.zero))
{
foundPosition = true;
break; // early exit: the RNG offset now depends on the retry count
}
}
ہر کوشش چار بے ترتیب اعداد استعمال کرتی تھی۔ اگر پہلے دشمن کو ایک اسکرین پر دو کوششیں اور دوسری پر تین لگیں، تو بے ترتیب اعداد کی دھارا چار draws آگے پیچھے ہو جاتی، اور بعد کا ہر اسپان مختلف اعداد پڑھتا۔ یہ لاگ کے پیٹرن سے میل کھاتا ہے: بیچ کا پہلا دشمن یکساں تھا، اور اس کے بعد والے نہیں۔ ایک غلط قدر ایک چھوٹا سا bug ہوتی؛ دھارے کا غلط آفسیٹ اسے ایک مختلف گیم بنا دیتا ہے۔
3. ہر وہ چیز جو اپنا سائز کیمرے سے لیتی تھی
یہی لائیو کیمرا حدود دو اور نظاموں کو بھی جاتی تھیں۔ flow-field گرڈ کیمرے کے علاقے اور ایک بفر کا احاطہ کرتا تھا، اس لیے چوڑی اسکرین رکاوٹوں کا ایک مختلف مجموعہ درج کرتی، اور دشمن مختلف راستے لیتے اور مختلف طرح ٹکراتے۔ خالی ٹیسٹ اسٹیجز نے یہ چھپا دیا، کیونکہ props نہ ہونے کی وجہ سے اختلاف کی کوئی چیز ہی نہیں تھی۔ لامتناہی اسٹیج بھی لائیو حدود سے موازنہ کر کے طے کرتا تھا کہ منظر کے chunks، اور ان پر موجود props، کب بنانے اور ہٹانے ہیں۔
aspect کے مسائل کے علاوہ اسی راستے میں فریم ریٹ سے دو جوڑ بھی تھے۔ wave behaviours، Unity
Timeline کے ProcessFrame سے Spawn کو کال کرتے تھے، جو ہر سمولیشن tick کے بجائے ہر رینڈر
فریم پر ایک بار چلتا ہے، اور ایک ایسی کیمرا پوزیشن پڑھتے تھے جو ہر فریم پر interpolate ہوتی
ہے۔ اسکرین سے باہر ٹیلی پورٹ کی جانچ، جو پیچھے رہ جانے والے دشمنوں کو کھلاڑی کے سامنے واپس
لاتی ہے، دشمنوں کا ذیلی مجموعہ Time.frameCount سے چنتی تھی۔ 30 fps اور 144 fps پر یہ مختلف
نتائج دیتے ہیں۔
حل، تین درجوں میں
پل ریکوئسٹ تین کمٹس کی صورت میں آیا، ہر ایک اپنے ٹیسٹس کے ساتھ۔
درجہ 1، aspect سے آزاد۔ ایک نیا خالص ہیلپر، DeterministicSpawnBounds، اسپان کا سارا
حساب 16:9 کے مقررہ حوالہ aspect کے مقابل کرتا ہے۔ کیمرا اب بھی کھلاڑی کی اصل اسکرین دکھاتا
ہے؛ صرف سمولیشن نے اسے دیکھنا بند کر دیا۔ ہیلپر جو بھی برانچ لے، ٹھیک تین بے ترتیب اعداد
نکالتا ہے۔ ری ٹرائی لوپ مقررہ 10 دہرائیوں کا ہو گیا: یہ پہلا جائز امیدوار رکھتا ہے مگر بجٹ
ختم ہونے تک draws جاری رکھتا ہے، اس لیے ہر اسپان میں RNG کا استعمال مستقل ہے۔ flow-field گرڈ،
chunk کی فعالیت کی جانچ اور ٹیلی پورٹ کا فاصلہ، سب انہی ڈیٹرمنسٹک حدود پر منتقل ہو گئے۔
درجہ 2، فریم ریٹ سے آزاد۔ wave behaviours اب EnqueueSpawn کال کرتے ہیں۔ اسپانر یہ
قطار Simulate(tick, dt) کے اندر نکالتا ہے، اس لیے فریم کتنی ہی تیزی سے رینڈر ہوں، بے
ترتیب draws اور پوزیشن کی ریڈنگ مقررہ tick پر ہوتی ہیں۔ ٹیلی پورٹ کی جانچ بھی Simulate میں
منتقل ہو گئی اور tick % n کے حساب سے قدم بڑھاتی ہے۔
درجہ 3، CPU سے آزاد۔ floating-point کے جذر اور مثلثیاتی فنکشنز x87، SSE2 اور ARM NEON کے
درمیان، یا multiply-add کو مختلف طرح ملانے والے JITs کے درمیان، bit بہ bit یکساں ہونے کی
ضمانت نہیں رکھتے۔ ایک ULP کا فرق کسی سخت less-than موازنے کو پلٹنے اور ایک مشین پر دشمن کو
ٹیلی پورٹ کرنے مگر دوسری پر نہ کرنے کے لیے کافی ہے۔ وتر اب Fixed64.Sqrt استعمال کرتا ہے،
ٹیلی پورٹ کی گردش FixedMath.Rotate استعمال کرتی ہے، اور میری مشترکہ UTools لائبریری میں ایک
جوڑی دار تبدیلی (7079b05f) نے DeterministicRng کے unit-circle ہیلپرز کو fixed-point
sine، cosine اور جذر پر منتقل کر دیا، draws کی تعداد اور ترتیب برقرار رکھتے ہوئے۔
درجہ 3 کے ٹیسٹس عین Fixed64.RawValue نتائج کی تصدیق کرتے ہیں، رواداری (tolerance) کی نہیں۔
پل ریکوئسٹ میں دی گئی دلیل: ڈیٹرمنزم کے معاہدے پر رواداری خاموشی سے ایسی واپسی قبول کر لیتی
ہے جو ہر پلیٹ فارم پر صرف تقریباً درست floating-point حساب پر لوٹ آئے۔ پوری تقسیم یہ ہے:
DeterministicSpawnBoundsTests میں 24 ٹیسٹس اور WaveBehaviorEnqueueTests میں 5، جن میں ایک
ٹیسٹ یہ جانچتا ہے کہ دونوں اسپان برانچز draws کی یکساں تعداد استعمال کرتی ہیں۔ پل ریکوئسٹ
کے مطابق 2,602 میں سے 2,600 EditMode ٹیسٹس پاس ہوئے، اور دو ناکامیاں dev پر پہلے سے موجود
تھیں؛ issue کی اسٹیٹس اپ ڈیٹ ایک دستی جانچ درج کرتی ہے کہ 16:9، 4:3، 1:1 اور ایک الٹرا وائیڈ
ریزولوشن پر دشمنوں کی ورلڈ پوزیشنز bit بہ bit یکساں تھیں۔
اسی دن ایک الگ کمٹ (8d519867e) نے اسی خاندان کے دو چھوٹے رساؤ بند کیے: اسپان کی jitter کو
اس کی اپنی بے ترتیب دھارا دی گئی تاکہ وہ مرکزی اسپاننگ سلسلے کو نہ کھسکا سکے، اور دشمن کا
ٹکراؤ رداس اب Collider2D.bounds نہیں پڑھتا، جو فزکس انجن کی ایک قدر ہے اور پلیٹ فارمز کے
درمیان آخری bit میں مختلف ہو سکتی ہے۔ اس نے مزید 26 ٹیسٹس شامل کیے۔
سودا، اور فالو اپ
مقررہ 16:9 حوالے کی ایک معلوم قیمت تھی، جو ہیلپر کے اپنے تبصرے میں لکھی ہے: چوڑی اسکرین پر دشمن دائیں اور بائیں کناروں پر نظر آنے والے علاقے کے اندر اسپان ہو سکتے تھے، اور منظر کے chunks دیر سے فعال ہوتے تھے۔
27 اپریل کے فالو اپ #251 نے ڈیٹرمنزم چھوڑے بغیر اسے ٹھیک کیا۔ حوالہ aspect اب مستقل نہیں رہا۔ یہ ہر سیشن میں کھلاڑی کی سیٹنگز سے چنا جاتا ہے، ایک اسٹیج کی مدت کے لیے لاک ہوتا ہے، اور ری پلے ہیڈر کے ایک نئے ورژن میں لکھا جاتا ہے۔ پلے بیک اسے ہیڈر سے پڑھتا ہے، اس لیے ری پلے وہی aspect استعمال کرتا ہے جس پر وہ ریکارڈ ہوا تھا، چاہے اسے کسی بھی اسکرین پر دیکھا جائے۔ اس فیلڈ کے بغیر پرانے ری پلے 16:9 مانے جاتے ہیں، جس پر وہ ریکارڈ ہوئے تھے۔ اس نے 31 ٹیسٹس شامل کیے، جن میں ہر ری پلے ہیڈر ورژن کے round trips شامل ہیں۔
اس پوری قسم کے bugs کا عمومی حل یہی ہے: جو چیز بھی سمولیشن کو بدل سکتی ہے، یا تو اسے سمولیشن سے نکال دیا جائے یا اسے ان پٹ کے طور پر ریکارڈ کیا جائے۔
تصدیق شدہ اسکورز کے لیے یہ کیوں اہم ہے
سال کے آخر میں CandyRush کا ہفتہ وار لیڈر بورڈ کسی اسکور کو تب ہی قبول کرنے لگا جب ایک سرور ریکارڈنگ کو شائع شدہ گیم بلڈ میں دوبارہ چلائے اور سمولیشن کی حالت کے rolling digest کا موازنہ کرے۔ The Verified Score اس نظام کو بیان کرتا ہے۔ یہ صرف تبھی کام کرتا ہے جب ریکارڈنگ میں وہ سب کچھ ہو جو رن پر اثر ڈالتا ہے۔ ان اصلاحات سے پہلے، الٹرا وائیڈ مانیٹر پر بنائی گئی اور کسی دوسری ریزولوشن کے headless براؤزر میں تصدیق کی گئی ریکارڈنگ دو مختلف گیمز ہوتی۔ ایک ایماندار کھلاڑی مسترد ہو جاتا، اور صرف اسکور دیکھ کر کوئی نہیں بتا سکتا تھا کہ کیوں۔
اسی دن #239 نے ٹیسٹنگ cheats کو پروڈکشن بلڈز سے ہٹا دیا: cheat کے methods ایڈیٹر اور ڈویلپمنٹ بلڈز کے باہر compile ہو کر غائب ہو جاتے ہیں، اور cheat پینل لوڈ ہوتے ہی خود کو تباہ کر دیتا ہے۔ اگر ری پلے بنانے والا بلڈ invulnerability ٹوگل کے ساتھ شائع ہوا ہو تو تصدیق شدہ ری پلے کا کوئی خاص مطلب نہیں رہتا۔
جو بات عام طور پر لاگو ہوتی ہے
ڈیٹرمنزم کے رساؤ پہلی کامیابی کے پیچھے چھپتے ہیں۔ جس لاگ نے اسے پکڑا، اس میں ہر بیچ کا پہلا دشمن یکساں تھا۔ صرف پہلا نہیں، دوسرا، دسواں اور سوواں اسپان بھی جانچیں۔
صرف قدر نہیں، دھارا ٹھیک کریں۔ aspect پر منحصر برانچ نے ایک غلط پوزیشن پیدا کی۔ جلد
break نے اسے ایک کھسکی ہوئی بے ترتیب دھارا اور ایک مختلف گیم میں بدل دیا۔ ہر عمل میں RNG کا
مستقل استعمال ایک ایسا معاہدہ ہے جسے براہِ راست ٹیسٹ کرنا چاہیے۔
کھلاڑی کی مشین یا تو ریکارڈ ہوتی ہے یا نظر انداز۔ اسکرین کی شکل، فریم ریٹ اور CPU سب وہ چیزیں ہیں جو کھلاڑی کی مشین طے کرتی ہے۔ ہر ایک یا تو سمولیشن سے باہر رہتی ہے یا ری پلے میں جاتی ہے۔
ڈیٹرمنزم کے ٹیسٹس عین ہونے چاہییں۔ ڈیٹرمنزم ٹیسٹ میں رواداری ٹھیک اسی بہاؤ کو قبول کر لیتی ہے جسے پکڑنے کے لیے وہ موجود ہے۔