World Wide Maze एक ऐसा गेम है जिसमें आपको अपने स्मार्टफ़ोन का इस्तेमाल करके, वेबसाइटों से बनाई गई 3D मेज़ में एक रोलिंग बॉल को नेविगेट करना होता है. ऐसा करके, आपको गेम के लक्ष्य पॉइंट तक पहुंचना होता है.
इस गेम में HTML5 की सुविधाओं का ज़्यादा इस्तेमाल किया गया है. उदाहरण के लिए, DeviceOrientation इवेंट, स्मार्टफ़ोन से झुकाव का डेटा हासिल करता है. इसके बाद, इस डेटा को वेबसोकेट के ज़रिए पीसी पर भेजा जाता है. यहां खिलाड़ियों को WebGL और वेब वर्कर्स की मदद से बनाए गए 3D स्पेस में अपना रास्ता मिलता है.
इस लेख में, हम इन सुविधाओं के इस्तेमाल के तरीके, डेवलपमेंट की पूरी प्रोसेस, और ऑप्टिमाइज़ेशन के मुख्य पॉइंट के बारे में पूरी जानकारी देंगे.
DeviceOrientation
स्मार्टफ़ोन से झुकाव का डेटा पाने के लिए, DeviceOrientation इवेंट (उदाहरण) का इस्तेमाल किया जाता है. जब addEventListener
का इस्तेमाल DeviceOrientation
इवेंट के साथ किया जाता है, तो DeviceOrientationEvent
ऑब्जेक्ट के साथ कॉलबैक को नियमित अंतराल पर आर्ग्युमेंट के तौर पर लागू किया जाता है. इंटरवल, इस्तेमाल किए जा रहे डिवाइस के हिसाब से अलग-अलग होते हैं. उदाहरण के लिए, iOS + Chrome और iOS + Safari में, कॉलबैक हर 1/20 सेकंड में ट्रिगर होता है. वहीं, Android 4 + Chrome में यह हर 1/10 सेकंड में ट्रिगर होता है.
window.addEventListener('deviceorientation', function (e) {
// do something here..
});
DeviceOrientationEvent
ऑब्जेक्ट में, X
, Y
, और Z
अक्ष के लिए, डिग्री (रेडियन में नहीं) में झुकाव का डेटा होता है (HTML5Rocks के बारे में ज़्यादा पढ़ें). हालांकि, इस्तेमाल किए गए डिवाइस और ब्राउज़र के कॉम्बिनेशन के हिसाब से भी रिटर्न वैल्यू अलग-अलग होती हैं. रिटर्न की असल वैल्यू की रेंज, नीचे दी गई टेबल में दी गई है:
सबसे ऊपर नीले रंग से हाइलाइट की गई वैल्यू, W3C की खास बातों में बताई गई हैं. हरे रंग में हाइलाइट किए गए एलिमेंट, इन स्पेसिफ़िकेशन से मेल खाते हैं. वहीं, लाल रंग में हाइलाइट किए गए एलिमेंट, इन स्पेसिफ़िकेशन से मेल नहीं खाते. हैरानी की बात है कि सिर्फ़ Android-Firefox कॉम्बिनेशन ने ऐसी वैल्यू दिखाईं जो स्पेसिफ़िकेशन से मेल खाती थीं. हालांकि, लागू करने के मामले में, अक्सर आने वाली वैल्यू को शामिल करना ज़्यादा सही होता है. इसलिए, World Wide Maze, iOS की रिटर्न वैल्यू को स्टैंडर्ड के तौर पर इस्तेमाल करता है और Android डिवाइसों के हिसाब से अडजस्ट करता है.
if android and event.gamma > 180 then event.gamma -= 360
हालांकि, यह सुविधा अब भी Nexus 10 पर काम नहीं करती. Nexus 10, अन्य Android डिवाइसों की तरह ही वैल्यू की रेंज दिखाता है. हालांकि, इसमें एक गड़बड़ी है, जो बीटा और गामा वैल्यू को उलट देता है. इस समस्या को अलग से ठीक किया जा रहा है. (क्या यह डिफ़ॉल्ट रूप से लैंडस्केप ओरिएंटेशन में है?)
इस उदाहरण से पता चलता है कि भौतिक डिवाइसों से जुड़े एपीआई के लिए, स्पेसिफ़िकेशन सेट किए गए हों, तब भी इस बात की कोई गारंटी नहीं है कि रिटर्न की गई वैल्यू उन स्पेसिफ़िकेशन से मेल खाएंगी. इसलिए, इन्हें सभी संभावित डिवाइसों पर टेस्ट करना ज़रूरी है. इसका मतलब यह भी है कि अनचाही वैल्यू डाली जा सकती हैं. इन वैल्यू को ठीक करने के लिए, आपको कुछ तरीके अपनाने होंगे. World Wide Maze में, पहली बार खेलने वाले लोगों को ट्यूटोरियल के पहले चरण के तौर पर, अपने डिवाइसों को कैलिब्रेट करने के लिए कहा जाता है. हालांकि, अगर डिवाइस के झुकने की अप्रत्याशित वैल्यू मिलती हैं, तो यह शून्य पोज़िशन पर सही तरीके से कैलिब्रेट नहीं होगा. इसलिए, इसकी एक समयसीमा होती है. अगर इस समयसीमा के अंदर कैलिब्रेट नहीं हो पाता है, तो प्लेयर को कीबोर्ड कंट्रोल पर स्विच करने के लिए कहा जाता है.
WebSocket
World Wide Maze में, आपका स्मार्टफ़ोन और पीसी, वेबसोकेट के ज़रिए कनेक्ट होते हैं. असल में, ये डिवाइस एक-दूसरे से किसी रिले सर्वर के ज़रिए कनेक्ट होते हैं. जैसे, स्मार्टफ़ोन से सर्वर से पीसी. ऐसा इसलिए है, क्योंकि WebSocket में ब्राउज़र को सीधे तौर पर एक-दूसरे से कनेक्ट करने की सुविधा नहीं होती. (WebRTC डेटा चैनलों का इस्तेमाल करके, पीयर-टू-पीयर कनेक्टिविटी की सुविधा मिलती है. साथ ही, रिले सर्वर की ज़रूरत भी नहीं पड़ती. हालांकि, लागू करने के समय इस तरीके का इस्तेमाल सिर्फ़ Chrome Canary और Firefox Nightly के साथ किया जा सकता था.)
मैंने Socket.IO (v0.9.11) नाम की लाइब्रेरी का इस्तेमाल करके इसे लागू करने का विकल्प चुना है. इसमें कनेक्शन टाइम आउट या कनेक्शन टूटने की स्थिति में, फिर से कनेक्ट करने की सुविधाएं शामिल हैं. मैंने इसका इस्तेमाल NodeJS के साथ किया, क्योंकि NodeJS + Socket.IO कॉम्बिनेशन ने WebSocket लागू करने के कई टेस्ट में, सर्वर-साइड की सबसे अच्छी परफ़ॉर्मेंस दिखाई.
नंबर के हिसाब से जोड़ना
- आपका पीसी सर्वर से कनेक्ट हो जाता है.
- सर्वर आपके पीसी को एक रैंडम नंबर देता है और नंबर और पीसी के कॉम्बिनेशन को याद रखता है.
- अपने मोबाइल डिवाइस से कोई नंबर डालें और सर्वर से कनेक्ट करें.
- अगर दिया गया नंबर, कनेक्ट किए गए पीसी का है, तो इसका मतलब है कि आपका मोबाइल डिवाइस उस पीसी से जोड़ा गया है.
- अगर कोई पीसी नहीं चुना गया है, तो गड़बड़ी का मैसेज दिखता है.
- जब आपके मोबाइल डिवाइस से डेटा आता है, तो उसे उस पीसी पर भेज दिया जाता है जिससे वह डिवाइस जोड़ा गया है. इसके अलावा, जब पीसी से डेटा आता है, तो उसे मोबाइल डिवाइस पर भेज दिया जाता है.
इसके बजाय, शुरुआती कनेक्शन अपने मोबाइल डिवाइस से भी किया जा सकता है. ऐसे में, डिवाइसों को बस बदल दिया जाता है.
टैब सिंक करना
Chrome पर टैब सिंक करने की सुविधा, डिवाइसों को जोड़ने की प्रोसेस को और भी आसान बनाती है. इसकी मदद से, पीसी पर खुले पेजों को मोबाइल डिवाइस पर आसानी से खोला जा सकता है. इसके अलावा, मोबाइल डिवाइस पर खुले पेजों को पीसी पर भी खोला जा सकता है. पीसी, सर्वर से जारी किया गया कनेक्शन नंबर लेता है और history.replaceState
का इस्तेमाल करके, उसे पेज के यूआरएल में जोड़ता है.
history.replaceState(null, null, '/maze/' + connectionNumber)
अगर टैब सिंक की सुविधा चालू है, तो कुछ सेकंड के बाद यूआरएल सिंक हो जाता है. इसके बाद, उसी पेज को मोबाइल डिवाइस पर खोला जा सकता है. मोबाइल डिवाइस, खुले हुए पेज के यूआरएल की जांच करता है. अगर कोई नंबर जोड़ा जाता है, तो वह तुरंत कनेक्ट होना शुरू कर देता है. इससे, आपको मैन्युअल तरीके से नंबर डालने या कैमरे से क्यूआर कोड स्कैन करने की ज़रूरत नहीं पड़ती.
इंतज़ार का समय
रिले सर्वर अमेरिका में है. इसलिए, जापान से इसे ऐक्सेस करने पर, स्मार्टफ़ोन के झुकाव का डेटा पीसी तक पहुंचने में करीब 200 मिलीसेकंड की देरी होती है. डेवलपमेंट के दौरान इस्तेमाल किए गए लोकल एनवायरमेंट की तुलना में, रिस्पॉन्स टाइम काफ़ी धीमा था. हालांकि, लो-पास फ़िल्टर (मैंने EMA का इस्तेमाल किया) जैसा कुछ डालने से, रिस्पॉन्स टाइम काफ़ी बेहतर हो गया. (असल में, प्रज़ेंटेशन के लिए भी लो-पास फ़िल्टर की ज़रूरत थी; टिल्ट सेंसर से मिली वैल्यू में काफ़ी नॉइज़ शामिल था और स्क्रीन पर उन वैल्यू को लागू करने से काफ़ी झटके महसूस होते थे.) यह सुविधा, जंप के साथ काम नहीं करती थी. इसकी वजह से, वीडियो में रुकावट आती थी. हालांकि, इस समस्या को ठीक नहीं किया जा सका.
मुझे शुरू से ही इंतज़ार की अवधि से जुड़ी समस्याओं का अंदाज़ा था. इसलिए, मैंने दुनिया भर में रिले सर्वर सेट अप करने का फ़ैसला लिया, ताकि क्लाइंट सबसे नज़दीकी सर्वर से कनेक्ट हो सकें. इससे इंतज़ार की अवधि कम हो जाती है. हालांकि, मैंने Google Compute Engine (GCE) का इस्तेमाल किया, जो उस समय सिर्फ़ अमेरिका में उपलब्ध था. इसलिए, ऐसा नहीं किया जा सका.
नेगल एल्गोरिदम से जुड़ी समस्या
Nagle एल्गोरिदम को आम तौर पर ऑपरेटिंग सिस्टम में शामिल किया जाता है, ताकि टीसीपी लेवल पर बफ़र करके बेहतर तरीके से कम्यूनिकेट किया जा सके. हालांकि, मुझे पता चला कि इस एल्गोरिदम के चालू होने पर, रीयल टाइम में डेटा नहीं भेजा जा सकता. खास तौर पर, TCP देर से मिलने वाली पुष्टि के साथ इस्तेमाल करने पर. अगर ACK
में कोई देरी नहीं हुई है, तब भी यह समस्या तब आती है, जब सर्वर किसी दूसरे देश में होने की वजह से ACK
में कुछ देरी हो जाती है.)
Chrome for Android में वेबवॉस्क के साथ, नेगल लेटेंसी की समस्या नहीं हुई. इसमें नेगल को बंद करने के लिए TCP_NODELAY
विकल्प शामिल है. हालांकि, यह समस्या Chrome for iOS में इस्तेमाल किए जाने वाले वेबवॉस्क वेबवॉस्क के साथ हुई, जिसमें यह विकल्प चालू नहीं है. Safari में भी यही समस्या थी. यह ब्राउज़र भी WebKit का इस्तेमाल करता है. Google के ज़रिए Apple को इस समस्या की शिकायत की गई थी. ऐसा लगता है कि WebKit के डेवलपमेंट वर्शन में इस समस्या को ठीक कर दिया गया है.
यह समस्या होने पर, हर 100 मिलीसेकंड में भेजा जाने वाला टिल्ट डेटा, ऐसे चंक में जोड़ दिया जाता है जो सिर्फ़ हर 500 मिलीसेकंड में पीसी तक पहुंचते हैं. इन स्थितियों में गेम काम नहीं कर सकता. इसलिए, यह इस देरी से बचने के लिए, सर्वर साइड से कम अंतराल (हर 50 मिलीसेकंड या उससे कम) पर डेटा भेजता है. मुझे लगता है कि कम समय के अंतराल पर ACK
मिलने से, Nagle एल्गोरिदम को यह लगता है कि डेटा भेजना ठीक है.
ऊपर दिए गए ग्राफ़ में, मिले असल डेटा के इंटरवल दिखाए गए हैं. यह पैकेट के बीच के समय के अंतराल को दिखाता है. हरे रंग से आउटपुट इंटरवल और लाल रंग से इनपुट इंटरवल का पता चलता है. कम से कम 54 मिलीसेकंड, ज़्यादा से ज़्यादा 158 मिलीसेकंड, और बीच में 100 मिलीसेकंड. यहां मैंने जापान में मौजूद रिले सर्वर के साथ iPhone का इस्तेमाल किया है. आउटपुट और इनपुट, दोनों में करीब 100 मिलीसेकंड लगते हैं और काम आसानी से होता है.
इसके उलट, यह ग्राफ़ अमेरिका में सर्वर का इस्तेमाल करने के नतीजे दिखाता है. हरे रंग के आउटपुट इंटरवल 100 मि॰से॰ पर स्थिर रहते हैं, जबकि इनपुट इंटरवल 0 मि॰से॰ से 500 मि॰से॰ के बीच में उतार-चढ़ाव करते हैं. इससे पता चलता है कि पीसी को डेटा, अलग-अलग हिस्सों में मिल रहा है.
आखिर में, यह ग्राफ़ दिखाता है कि सर्वर से प्लेसहोल्डर डेटा भेजने पर, लैटेंसी से बचने के नतीजे क्या होते हैं. यह जापानी सर्वर के मुकाबले उतना अच्छा परफ़ॉर्म नहीं करता, लेकिन यह साफ़ तौर पर पता चलता है कि इनपुट इंटरवल करीब 100 मिलीसेकंड पर अपेक्षाकृत स्थिर रहता है.
क्या कोई गड़बड़ी है?
Android 4 (ICS) के डिफ़ॉल्ट ब्राउज़र में WebSocket API होने के बावजूद, यह कनेक्ट नहीं हो पाता. इस वजह से, Socket.IO connect_failed इवेंट जनरेट होता है. अंदरूनी तौर पर, यह समयसीमा खत्म हो जाती है और सर्वर साइड भी कनेक्शन की पुष्टि नहीं कर पाता. (मैंने इसकी जांच सिर्फ़ WebSocket के साथ नहीं की है, इसलिए यह Socket.IO की समस्या हो सकती है.)
रिले सर्वर को स्केल करना
रिले सर्वर की भूमिका इतनी मुश्किल नहीं है. इसलिए, सर्वर की संख्या बढ़ाना और उन्हें स्केल करना मुश्किल नहीं होगा. हालांकि, यह पक्का करना ज़रूरी है कि एक ही पीसी और मोबाइल डिवाइस हमेशा एक ही सर्वर से कनेक्ट रहे.
भौतिक विज्ञान
गेम में बॉल की हर गतिविधि, जैसे कि पहाड़ी से नीचे लुढ़कना, ज़मीन से टकराना, दीवार से टकराना, आइटम इकट्ठा करना वगैरह, 3D फ़िज़िक्स सिम्युलेटर की मदद से की जाती है. मैंने Ammo.js का इस्तेमाल किया. यह Emscripten का इस्तेमाल करके, Bullet फ़िज़िक्स इंजन को JavaScript में पोर्ट करता है. साथ ही, इसे "वेब वर्कर" के तौर पर इस्तेमाल करने के लिए, Physijs का भी इस्तेमाल किया जाता है.
वेब वर्कर
वेब वर्कर्स, अलग-अलग थ्रेड में JavaScript चलाने के लिए एक एपीआई है. वेब वर्कर्स के तौर पर लॉन्च किया गया JavaScript, उस थ्रेड से अलग थ्रेड के तौर पर चलता है जिसने इसे मूल रूप से कॉल किया था. इसलिए, पेज को रिस्पॉन्सिव बनाए रखते हुए ज़्यादा काम किए जा सकते हैं. Physijs, वेब वर्कर्स का बेहतर तरीके से इस्तेमाल करता है, ताकि आम तौर पर ज़्यादा काम करने वाले 3D फ़िज़िक्स इंजन को आसानी से चलाया जा सके. World Wide Maze, फ़िज़िक्स इंजन और WebGL इमेज रेंडरिंग को अलग-अलग फ़्रेम रेट पर हैंडल करता है. इसलिए, अगर WebGL रेंडरिंग के ज़्यादा लोड की वजह से, कम स्पेसिफ़िकेशन वाली मशीन पर फ़्रेम रेट कम हो जाता है, तब भी फ़िज़िक्स इंजन कम या ज़्यादा 60 FPS बनाए रखेगा. इससे गेम के कंट्रोल पर कोई असर नहीं पड़ेगा.
इस इमेज में, Lenovo G570 पर फ़्रेम रेट दिखाए गए हैं. ऊपरी बॉक्स में, WebGL (इमेज रेंडरिंग) का फ़्रेम रेट दिखता है. वहीं, निचले बॉक्स में फ़िज़िक्स इंजन का फ़्रेम रेट दिखता है. जीपीयू, इंटिग्रेटेड Intel HD Graphics 3000 चिप है. इसलिए, इमेज रेंडरिंग फ़्रेम रेट, उम्मीद के मुताबिक 60 fps तक नहीं पहुंचा. हालांकि, फ़िज़िक्स इंजन ने उम्मीद के मुताबिक फ़्रेम रेट हासिल किया है. इसलिए, गेमप्ले की परफ़ॉर्मेंस, बेहतर स्पेसिफ़िकेशन वाली मशीन पर मिलने वाली परफ़ॉर्मेंस से काफ़ी अलग नहीं है.
चालू वेब वर्कर्स वाली थ्रेड में कंसोल ऑब्जेक्ट नहीं होते. इसलिए, डीबगिंग लॉग जनरेट करने के लिए, डेटा को postMessage के ज़रिए मुख्य थ्रेड में भेजा जाना चाहिए. console4Worker का इस्तेमाल करने से, Worker में console ऑब्जेक्ट के बराबर का ऑब्जेक्ट बन जाता है. इससे, डीबग करने की प्रोसेस काफ़ी आसान हो जाती है.
Chrome के नए वर्शन में, वेब वर्कर्स लॉन्च करते समय ब्रेकपॉइंट सेट किए जा सकते हैं. यह सुविधा, डीबग करने के लिए भी काम की है. इसे डेवलपर टूल के "वर्कर्स" पैनल में देखा जा सकता है.
परफ़ॉर्मेंस
ज़्यादा पॉलीगॉन वाले स्टेज में कभी-कभी 1,00,000 से ज़्यादा पॉलीगॉन हो जाते हैं. हालांकि, पूरी तरह से Physijs.ConcaveMesh
(बुललेट में btBvhTriangleMeshShape
) के तौर पर जनरेट होने पर भी, परफ़ॉर्मेंस पर कोई खास असर नहीं पड़ा.
शुरू में, फ़्रेम रेट कम हो गया था, क्योंकि जिन ऑब्जेक्ट के लिए टक्कर का पता लगाना ज़रूरी था उनकी संख्या बढ़ गई थी. हालांकि, Physijs में ग़ैर-ज़रूरी प्रोसेसिंग को हटाने से परफ़ॉर्मेंस बेहतर हुई. यह सुधार, मूल Physijs के फ़ॉर्क में किया गया था.
गॉस्ट ऑब्जेक्ट
जिन ऑब्जेक्ट में टक्कर का पता चलता है, लेकिन टक्कर होने पर उन पर कोई असर नहीं पड़ता और दूसरे ऑब्जेक्ट पर भी कोई असर नहीं पड़ता उन्हें Bullet में "घोस्ट ऑब्जेक्ट" कहा जाता है. हालांकि, Physijs में आधिकारिक तौर पर घोस्ट ऑब्जेक्ट काम नहीं करते, लेकिन Physijs.Mesh
जनरेट करने के बाद, फ़्लैग में बदलाव करके उन्हें वहां बनाया जा सकता है. World Wide Maze, आइटम और लक्ष्य पॉइंट के बीच होने वाली टक्कर का पता लगाने के लिए, गॉस्ट ऑब्जेक्ट का इस्तेमाल करता है.
hit = new Physijs.SphereMesh(geometry, material, 0)
hit._physijs.collision_flags = 1 | 4
scene.add(hit)
collision_flags
के लिए, 1 को CF_STATIC_OBJECT
और 4 को CF_NO_CONTACT_RESPONSE
लिखा जाएगा. ज़्यादा जानकारी के लिए, Bullet फ़ोरम, Stack Overflow या Bullet दस्तावेज़ खोजें. Physijs, Ammo.js का रैपर है और Ammo.js, Bullet से मिलता-जुलता है. इसलिए, Bullet में जो काम किए जा सकते हैं वे Physijs में भी किए जा सकते हैं.
Firefox 18 से जुड़ी समस्या
Firefox के 17 से 18 वर्शन में हुए अपडेट की वजह से, वेब वर्कर्स के डेटा एक्सचेंज करने का तरीका बदल गया. इस वजह से, Physijs काम करना बंद कर दिया. समस्या की शिकायत GitHub पर की गई थी और कुछ दिनों बाद उसे ठीक कर दिया गया था. ओपन सोर्स की इस सुविधा से मुझे काफ़ी खुशी हुई. साथ ही, इस घटना से मुझे यह भी याद आया कि World Wide Maze में कई अलग-अलग ओपन सोर्स फ़्रेमवर्क शामिल हैं. मैंने यह लेख इसलिए लिखा है, ताकि मैं आपको कुछ सुझाव/राय दे सकूं.
asm.js
हालांकि, इसका सीधे तौर पर World Wide Maze पर कोई असर नहीं पड़ता, लेकिन Ammo.js पहले से ही Mozilla के हाल ही में लॉन्च किए गए asm.js के साथ काम करता है. इसमें कोई हैरानी नहीं है, क्योंकि asm.js को मूल रूप से Emscripten से जनरेट किए गए JavaScript को तेज़ करने के लिए बनाया गया था. साथ ही, Emscripten के क्रिएटर ने ही Ammo.js को भी बनाया है. अगर Chrome में asm.js भी काम करता है, तो फ़िज़िक्स इंजन के कंप्यूटिंग लोड में काफ़ी कमी आनी चाहिए. Firefox Nightly के साथ टेस्ट करने पर, हमें काफ़ी तेज़ रफ़्तार मिली. शायद, C/C++ में उन सेक्शन को लिखना सबसे अच्छा होगा जिनमें ज़्यादा स्पीड की ज़रूरत होती है. इसके बाद, Emscripten का इस्तेमाल करके उन्हें JavaScript में पोर्ट करें?
WebGL
WebGL को लागू करने के लिए, मैंने सबसे ज़्यादा इस्तेमाल की जाने वाली लाइब्रेरी, three.js (r53) का इस्तेमाल किया. हालांकि, डेवलपमेंट के आखिरी चरणों में ही रिविज़न 57 रिलीज़ हो चुका था, लेकिन एपीआई में बड़े बदलाव किए गए थे. इसलिए, मैंने रिलीज़ के लिए ओरिजनल रिविज़न का ही इस्तेमाल किया.
चमकदार इफ़ेक्ट
गेंद के कोर और आइटम में जोड़ा गया चमकदार इफ़ेक्ट, "कवासे मेथड एमजीएफ़" के आसान वर्शन का इस्तेमाल करके लागू किया जाता है. हालांकि, कावासे मेथड से सभी चमकीले हिस्सों को बेहतर बनाया जाता है, जबकि वर्ल्ड वाइड मेज़ उन हिस्सों के लिए अलग-अलग रेंडर टारगेट बनाता है जिन्हें चमकाना है. इसकी वजह यह है कि स्टेज के टेक्सचर के लिए, वेबसाइट के स्क्रीनशॉट का इस्तेमाल किया जाना चाहिए. अगर सभी चमकदार हिस्सों को निकाला जाता है, तो पूरी वेबसाइट चमकने लगेगी. उदाहरण के लिए, अगर वेबसाइट का बैकग्राउंड सफ़ेद है, तो ऐसा होगा. मैंने सभी चीज़ों को HDR में प्रोसेस करने का भी विचार किया था, लेकिन इस बार मैंने ऐसा नहीं किया, क्योंकि इसे लागू करना काफ़ी मुश्किल होता.
सबसे ऊपर बाईं ओर पहला पास दिखता है, जहां चमक वाले हिस्सों को अलग से रेंडर किया गया था और फिर धुंधला किया गया था. सबसे नीचे दाईं ओर दूसरा पास दिखाया गया है. इसमें इमेज का साइज़ 50% कम किया गया है और फिर उस पर धुंधलापन लागू किया गया है. सबसे ऊपर दाईं ओर तीसरा पास दिखाया गया है. इसमें इमेज को फिर से 50% कम किया गया और फिर धुंधला किया गया. इसके बाद, तीनों इमेज को ओवरले करके, सबसे नीचे बाईं ओर दिखाई गई फ़ाइनल कंपोजिट इमेज बनाई गई. धुंधला करने के लिए, मैंने three.js में शामिल VerticalBlurShader
और HorizontalBlurShader
का इस्तेमाल किया है. इसलिए, इसमें अब भी ऑप्टिमाइज़ेशन की गुंजाइश है.
रिफ़्लेक्टिव बॉल
गेंद पर दिखने वाला अक्स, three.js के सैंपल पर आधारित है. सभी दिशाओं को गेंद की पोज़िशन से रेंडर किया जाता है और इन्हें एनवायरमेंट मैप के तौर पर इस्तेमाल किया जाता है. हर बार गेंद के हिलने पर, एनवायरमेंट मैप को अपडेट करना ज़रूरी होता है. हालांकि, 60 fps पर अपडेट करना मुश्किल होता है, इसलिए इन्हें हर तीन फ़्रेम में अपडेट किया जाता है. इसकी वजह से, हर फ़्रेम को अपडेट करने के मुकाबले वीडियो में थोड़ा फ़र्क़ दिखता है. हालांकि, जब तक इस फ़र्क़ की ओर ध्यान नहीं दिलाया जाता, तब तक यह साफ़ तौर पर नहीं दिखता.
शेडर, शेडर, शेडर…
WebGL को सभी रेंडरिंग के लिए शेडर (वर्टिक्स शेडर, फ़्रैगमेंट शेडर) की ज़रूरत होती है. three.js में पहले से ही कई तरह के इफ़ेक्ट वाले शेडर मौजूद हैं. हालांकि, बेहतर शेडिंग और ऑप्टिमाइज़ेशन के लिए, आपको खुद शेडर लिखने होंगे. World Wide Maze, अपने फ़िज़िक्स इंजन की मदद से सीपीयू को व्यस्त रखता है. इसलिए, मैंने शेडिंग लैंग्वेज (GLSL) में ज़्यादा से ज़्यादा कोड लिखकर, जीपीयू का इस्तेमाल करने की कोशिश की. भले ही, सीपीयू प्रोसेसिंग (JavaScript के ज़रिए) आसान होती. समुद्र की लहरों के इफ़ेक्ट, शेडर पर निर्भर करते हैं. इसी तरह, गोल करने पर होने वाले पटाखे और गेंद के दिखने पर इस्तेमाल होने वाले मेश इफ़ेक्ट भी शेडर पर निर्भर करते हैं.
ऊपर दिए गए इमेज, गेंद दिखने पर इस्तेमाल किए जाने वाले मेश इफ़ेक्ट के टेस्ट से लिए गए हैं. बाईं ओर मौजूद मॉडल, गेम में इस्तेमाल किया जाता है. इसमें 320 पॉलीगॉन हैं. बीच में मौजूद इमेज में करीब 5,000 पॉलीगॉन का इस्तेमाल किया गया है. वहीं, दाईं ओर मौजूद इमेज में करीब 3,00,000 पॉलीगॉन का इस्तेमाल किया गया है. इतने पॉलीगॉन होने के बावजूद, शेडर की मदद से प्रोसेस करने पर, फ़्रेम रेट 30 fps पर स्थिर रह सकता है.
पूरे स्टेज पर बिखरे हुए छोटे आइटम, एक ही मेश में इंटिग्रेट किए जाते हैं. साथ ही, हर आइटम का मूवमेंट, शेडर पर निर्भर करता है, जो पॉलीगॉन के हर टिप को मूव करता है. यह एक टेस्ट से लिया गया है. इससे यह पता चलता है कि ज़्यादा ऑब्जेक्ट मौजूद होने पर, परफ़ॉर्मेंस पर असर पड़ेगा या नहीं. यहां करीब 5,000 ऑब्जेक्ट हैं, जो करीब 20,000 पॉलीगॉन से बने हैं. परफ़ॉर्मेंस पर कोई असर नहीं पड़ा.
poly2tri
सर्वर से मिली आउटलाइन की जानकारी के आधार पर स्टेज बनाए जाते हैं. इसके बाद, JavaScript की मदद से उन्हें पॉलीगॉन में बदला जाता है. इस प्रोसेस का एक अहम हिस्सा ट्रायऐंगलेशन है. three.js इसे ठीक से लागू नहीं करता और आम तौर पर यह काम नहीं करता. इसलिए, मैंने खुद ही poly2tri नाम की एक अलग ट्रायंगलेशन लाइब्रेरी को इंटिग्रेट करने का फ़ैसला किया. ऐसा लगता है कि three.js ने पहले भी ऐसा ही करने की कोशिश की थी. इसलिए, मैंने इसके कुछ हिस्से को कॉमेंट करके, इसे काम कर लिया. इस वजह से, गड़बड़ियों की संख्या काफ़ी कम हो गई. साथ ही, गेम में ज़्यादा से ज़्यादा स्टेज खेले जा सकेंगे. कभी-कभी गड़बड़ी बनी रहती है और किसी वजह से poly2tri, सूचनाएं जारी करके गड़बड़ियों को मैनेज करता है. इसलिए, मैंने इसे अपवादों को फेंकने के लिए बदल दिया है.
ऊपर दिए गए उदाहरण में दिखाया गया है कि नीली आउटलाइन को त्रिकोण में कैसे बांटा जाता है और लाल पॉलीगॉन कैसे जनरेट होते हैं.
एनिसोट्रोपिक फ़िल्टरिंग
स्टैंडर्ड आइसोट्रॉपिक एमआईपी मैपिंग, हॉरिज़ॉन्टल और वर्टिकल, दोनों अक्षों पर इमेज को छोटा कर देती है. इसलिए, ऑब्लिक ऐंगल से पॉलीगॉन देखने पर, वर्ल्ड वाइड मेज़ के आखिरी हिस्से के टेक्सचर, हॉरिज़ॉन्टल तौर पर लंबे और कम रिज़ॉल्यूशन वाले टेक्सचर जैसे दिखते हैं. इस Wikipedia पेज पर सबसे ऊपर दाईं ओर मौजूद इमेज, इसका एक अच्छा उदाहरण है. असल में, ज़्यादा हॉरिज़ॉन्टल रिज़ॉल्यूशन की ज़रूरत होती है. WebGL (OpenGL), ऐनिसोट्रॉपिक फ़िल्टरिंग नाम के तरीके का इस्तेमाल करके, इस समस्या को हल करता है. three.js में, THREE.Texture.anisotropy
के लिए 1 से ज़्यादा वैल्यू सेट करने पर, ऐनिसोट्रॉपिक फ़िल्टरिंग चालू हो जाती है. हालांकि, यह सुविधा एक एक्सटेंशन है और हो सकता है कि यह सभी जीपीयू पर काम न करे.
Optimize
WebGL के सबसे सही तरीकों के बारे में बताने वाले इस लेख में भी बताया गया है कि ड्रॉ कॉल को कम करना, WebGL (OpenGL) की परफ़ॉर्मेंस को बेहतर बनाने का सबसे अहम तरीका है. World Wide Maze के शुरुआती डेवलपमेंट के दौरान, गेम में मौजूद सभी द्वीप, पुल, और गार्ड रेलिंग अलग-अलग ऑब्जेक्ट थे. इससे कभी-कभी 2,000 से ज़्यादा ड्रॉ कॉल होते थे, जिससे जटिल स्टेज को मैनेज करना मुश्किल हो जाता था. हालांकि, जब मैंने एक ही तरह के सभी ऑब्जेक्ट को एक ही मेश में पैक किया, तो ड्रॉ कॉल की संख्या घटकर पचास या इससे कम हो गई. इससे परफ़ॉर्मेंस में काफ़ी सुधार हुआ.
मैंने ज़्यादा ऑप्टिमाइज़ेशन के लिए, Chrome की ट्रैकिंग सुविधा का इस्तेमाल किया. Chrome के डेवलपर टूल में शामिल प्रोफ़ाइलर, किसी तरीके को प्रोसेस करने में लगने वाले कुल समय का कुछ हद तक पता लगा सकते हैं. हालांकि, ट्रैकिंग से आपको यह पता चल सकता है कि हर चरण में कितना समय लगता है. यह समय 1/1000 सेकंड तक का हो सकता है. ट्रैकिंग का इस्तेमाल करने का तरीका जानने के लिए, यह लेख पढ़ें.
ऊपर दिए गए नतीजे, गेंद के रिफ़्लेक्शन के लिए एनवायरमेंट मैप बनाने से मिले हैं. three.js में काम की जगहों पर console.time
और console.timeEnd
डालने से, हमें ऐसा ग्राफ़ मिलता है. समय बाईं से दाईं ओर बढ़ता है और हर लेयर कॉल स्टैक की तरह होती है. console.time
में console.time को नेस्ट करने से, ज़्यादा मेज़रमेंट किए जा सकते हैं. ऊपर वाला ग्राफ़, ऑप्टिमाइज़ेशन से पहले का है और नीचे वाला ग्राफ़, ऑप्टिमाइज़ेशन के बाद का है. जैसा कि सबसे ऊपर दिए गए ग्राफ़ में दिखाया गया है, प्री-ऑप्टिमाइज़ेशन के दौरान 0 से 5 तक के हर रेंडर के लिए, updateMatrix
(हालांकि शब्द काट दिया गया है) को कॉल किया गया था. मैंने इसमें बदलाव किया है, ताकि इसे सिर्फ़ एक बार कॉल किया जा सके. हालांकि, यह प्रोसेस सिर्फ़ तब ज़रूरी होती है, जब ऑब्जेक्ट की पोज़िशन या ओरिएंटेशन बदलता है.
ट्रैकिंग की प्रोसेस में अपने-आप रिसॉर्स खर्च होते हैं. इसलिए, console.time
को ज़्यादा डालने से असल परफ़ॉर्मेंस में काफ़ी अंतर हो सकता है. इससे, ऑप्टिमाइज़ेशन के लिए सही जगहों का पता लगाना मुश्किल हो जाता है.
परफ़ॉर्मेंस अडजस्टर
इंटरनेट की सुविधाओं की वजह से, यह गेम अलग-अलग स्पेसिफ़िकेशन वाले सिस्टम पर खेला जा सकता है. फ़रवरी की शुरुआत में रिलीज़ हुई Find Your Way to Oz, IFLAutomaticPerformanceAdjust
नाम की क्लास का इस्तेमाल करती है. इससे फ़्रेम रेट में होने वाले उतार-चढ़ाव के हिसाब से इफ़ेक्ट को कम किया जाता है. इससे वीडियो को आसानी से चलाया जा सकता है. World Wide Maze, उसी IFLAutomaticPerformanceAdjust
क्लास पर आधारित है. साथ ही, गेमप्ले को ज़्यादा से ज़्यादा आसान बनाने के लिए, इफ़ेक्ट को इस क्रम में कम किया जाता है:
- अगर फ़्रेम रेट 45 एफ़पीएस से कम हो जाता है, तो एनवायरमेंट मैप अपडेट होना बंद हो जाते हैं.
- अगर फ़्रेम रेट फिर भी 40 एफ़पीएस से कम हो जाता है, तो रेंडरिंग रिज़ॉल्यूशन को 70% (सर्फ़ेस रेशियो का 50%) तक कम कर दिया जाता है.
- अगर फ़्रेम रेट अब भी 40 FPS से कम हो जाता है, तो FXAA (ऐंटी-ऐलिऐसिंग) की सुविधा बंद कर दी जाती है.
- अगर फ़्रेम रेट अब भी 30 एफ़पीएस से कम हो जाता है, तो चमक वाले इफ़ेक्ट हटा दिए जाते हैं.
मेमोरी लीक
three.js में, ऑब्जेक्ट को आसानी से हटाना मुश्किल है. हालांकि, उन्हें छोड़ने पर मेमोरी लीक हो सकती है. इसलिए, मैंने नीचे दिया गया तरीका अपनाया है. @renderer
का मतलब THREE.WebGLRenderer
से है. (three.js के नए वर्शन में, मेमोरी को डिलीज़ करने का तरीका थोड़ा अलग है. इसलिए, हो सकता है कि यह तरीका उसमें काम न करे.)
destructObjects: (object) =>
switch true
when object instanceof THREE.Object3D
@destructObjects(child) for child in object.children
object.parent?.remove(object)
object.deallocate()
object.geometry?.deallocate()
@renderer.deallocateObject(object)
object.destruct?(this)
when object instanceof THREE.Material
object.deallocate()
@renderer.deallocateMaterial(object)
when object instanceof THREE.Texture
object.deallocate()
@renderer.deallocateTexture(object)
when object instanceof THREE.EffectComposer
@destructObjects(object.copyPass.material)
object.passes.forEach (pass) =>
@destructObjects(pass.material) if pass.material
@renderer.deallocateRenderTarget(pass.renderTarget) if pass.renderTarget
@renderer.deallocateRenderTarget(pass.renderTarget1) if pass.renderTarget1
@renderer.deallocateRenderTarget(pass.renderTarget2) if pass.renderTarget2
एचटीएमएल
मेरी राय में, WebGL ऐप्लिकेशन की सबसे अच्छी बात यह है कि इसमें एचटीएमएल में पेज लेआउट डिज़ाइन किया जा सकता है. फ़्लैश या openFrameworks (OpenGL) में स्कोर या टेक्स्ट डिसप्ले जैसे 2D इंटरफ़ेस बनाना मुश्किल है. Flash में कम से कम एक आईडीई (IDE) होता है. हालांकि, अगर आपको openFrameworks का इस्तेमाल नहीं आता है, तो यह मुश्किल हो सकता है. Cocos2D जैसे टूल का इस्तेमाल करने से, यह आसान हो सकता है. दूसरी ओर, एचटीएमएल में सीएसएस की मदद से, वेबसाइट बनाने की तरह ही फ़्रंटएंड डिज़ाइन के सभी पहलुओं को सटीक तरीके से कंट्रोल किया जा सकता है. हालांकि, किसी लोगो में कण इकट्ठा होने जैसे जटिल इफ़ेक्ट नहीं बनाए जा सकते, लेकिन सीएसएस ट्रांसफ़ॉर्म की सुविधाओं के दायरे में कुछ 3D इफ़ेक्ट बनाए जा सकते हैं. World Wide Maze के "GOAL" और "TIME IS UP" टेक्स्ट इफ़ेक्ट को सीएसएस ट्रांज़िशन में स्केल का इस्तेमाल करके ऐनिमेट किया गया है. इसे Transit की मदद से लागू किया गया है. (बैकग्राउंड ग्रेडिएशन में WebGL का इस्तेमाल होता है.)
गेम के हर पेज (टाइटल, नतीजा, रैंकिंग वगैरह) की अपनी एचटीएमएल फ़ाइल होती है. इन फ़ाइलों को टेंप्लेट के तौर पर लोड करने के बाद, सही समय पर $(document.body).append()
को सही वैल्यू के साथ कॉल किया जाता है. एक समस्या यह थी कि जोड़ने से पहले, माउस और कीबोर्ड इवेंट सेट नहीं किए जा सकते. इसलिए, जोड़ने से पहले el.click (e) -> console.log(e)
का इस्तेमाल करने पर काम नहीं करता.
इंटरनेशनलाइजे़शन (i18n)
अंग्रेज़ी भाषा का वर्शन बनाने के लिए भी, एचटीएमएल में काम करना आसान था. मैंने अंतरराष्ट्रीय स्तर पर उपलब्ध कराने से जुड़ी अपनी ज़रूरतों के लिए, वेब i18n लाइब्रेरी i18next का इस्तेमाल किया. इस लाइब्रेरी को बिना किसी बदलाव के इस्तेमाल किया जा सकता है.
गेम में मौजूद टेक्स्ट में बदलाव करने और उसका अनुवाद करने के लिए, Google Docs स्प्रेडशीट का इस्तेमाल किया गया. i18next को JSON फ़ाइल की ज़रूरत होती है. इसलिए, मैंने स्प्रेडशीट को TSV में एक्सपोर्ट किया और फिर उन्हें कस्टम कन्वर्टर की मदद से बदला. मैंने रिलीज़ होने से ठीक पहले कई अपडेट किए थे. इसलिए, Google Docs स्प्रेडशीट से एक्सपोर्ट की प्रोसेस को ऑटोमेट करने से, काम बहुत आसान हो जाता.
Chrome की अपने-आप अनुवाद होने की सुविधा भी सामान्य रूप से काम करती है, क्योंकि पेज एचटीएमएल के साथ बनाए जाते हैं. हालांकि, कभी-कभी यह भाषा की सही पहचान नहीं कर पाता. इसके बजाय, इसे किसी दूसरी भाषा के तौर पर समझ लेता है (उदाहरण के लिए, वियतनामीज़) में उपलब्ध नहीं है. इसलिए, फ़िलहाल यह सुविधा बंद है. (इसे मेटा टैग का इस्तेमाल करके बंद किया जा सकता है.)
RequireJS
मैंने अपने JavaScript मॉड्यूल सिस्टम के तौर पर RequireJS को चुना है. गेम के सोर्स कोड की 10,000 लाइनों को करीब 60 क्लास (= कॉफ़ी फ़ाइलें) में बांटा गया है और उन्हें अलग-अलग js फ़ाइलों में कंपाइल किया गया है. RequireJS, इन अलग-अलग फ़ाइलों को डिपेंडेंसी के आधार पर सही क्रम में लोड करता है.
define ->
class Hoge
hogeMethod: ->
ऊपर बताई गई क्लास (hoge.coffee) का इस्तेमाल इस तरह किया जा सकता है:
define ['hoge'], (Hoge) ->
class Moge
constructor: ->
@hoge = new Hoge()
@hoge.hogeMethod()
काम करने के लिए, hoge.js को moge.js से पहले लोड किया जाना चाहिए. साथ ही, "hoge" को "define" के पहले आर्ग्युमेंट के तौर पर तय किया गया है. इसलिए, hoge.js हमेशा पहले लोड होता है. इसके लोड होने के बाद, इसे वापस बुलाया जाता है. इस प्रोसेस को AMD कहा जाता है. इस तरह के कॉलबैक के लिए, तीसरे पक्ष की किसी भी लाइब्रेरी का इस्तेमाल तब तक किया जा सकता है, जब तक वह AMD के साथ काम करती है. तीन.js जैसे ऐसे लाइब्रेरी भी इसी तरह काम करेंगे, बशर्ते उनकी डिपेंडेंसी पहले से तय की गई हों.
यह AS3 इंपोर्ट करने जैसा ही है, इसलिए आपको इसमें कोई परेशानी नहीं होनी चाहिए. अगर आपको ज़्यादा डिपेंडेंट फ़ाइलें मिलती हैं, तो यह एक संभावित समाधान है.
r.js
RequireJS में r.js नाम का ऑप्टिमाइज़र शामिल होता है. यह मुख्य js को, उससे जुड़ी सभी js फ़ाइलों के साथ एक में बंडल करता है. इसके बाद, UglifyJS (या Closure Compiler) का इस्तेमाल करके इसे छोटा कर देता है. इससे ब्राउज़र को लोड करने के लिए, फ़ाइलों और डेटा की कुल संख्या कम हो जाती है. World Wide Maze के लिए, JavaScript फ़ाइल का कुल साइज़ करीब 2 एमबी है. इसे r.js ऑप्टिमाइज़ेशन की मदद से, करीब 1 एमबी तक कम किया जा सकता है. अगर गेम को gzip का इस्तेमाल करके डिस्ट्रिब्यूट किया जा सकता है, तो यह साइज़ और भी कम होकर 250 केबी हो जाएगा. (GAE में एक समस्या है, जिसकी वजह से 1 एमबी या उससे बड़ी gzip फ़ाइलों को ट्रांसमिट नहीं किया जा सकता. इसलिए, फ़िलहाल गेम को बिना कंप्रेस किए 1 एमबी के प्लैन टेक्स्ट के तौर पर डिस्ट्रिब्यूट किया जाता है.)
स्टेज बिल्डर
स्टेज डेटा इस तरह जनरेट किया जाता है. यह पूरी प्रोसेस अमेरिका में मौजूद GCE सर्वर पर की जाती है:
- जिस वेबसाइट को स्टेज में बदलना है उसका यूआरएल, WebSocket के ज़रिए भेजा जाता है.
- PhantomJS एक स्क्रीनशॉट लेता है. इसके बाद, div और img टैग की पोज़िशन को JSON फ़ॉर्मैट में वापस पाया जाता है और आउटपुट दिया जाता है.
- दूसरे चरण के स्क्रीनशॉट और एचटीएमएल एलिमेंट के पोज़िशनिंग डेटा के आधार पर, कस्टम C++ (OpenCV, Boost) प्रोग्राम ग़ैर-ज़रूरी इलाकों को मिटा देता है, आइलैंड जनरेट करता है, आइलैंड को ब्रिज से जोड़ता है, गार्ड रेल और आइटम की पोज़िशन का हिसाब लगाता है, और लक्ष्य पॉइंट सेट करता है. नतीजे, JSON फ़ॉर्मैट में आउटपुट होते हैं और ब्राउज़र पर दिखाए जाते हैं.
PhantomJS
PhantomJS एक ऐसा ब्राउज़र है जिसे स्क्रीन की ज़रूरत नहीं होती. यह विंडो खोले बिना वेब पेजों को लोड कर सकता है. इसलिए, इसका इस्तेमाल ऑटोमेटेड टेस्ट में किया जा सकता है या सर्वर साइड पर स्क्रीनशॉट कैप्चर करने के लिए किया जा सकता है. इसका ब्राउज़र इंजन WebKit है. इसका इस्तेमाल Chrome और Safari भी करते हैं. इसलिए, इसका लेआउट और JavaScript के लागू होने के नतीजे, स्टैंडर्ड ब्राउज़र के जैसे ही होते हैं.
PhantomJS में, JavaScript या CoffeeScript का इस्तेमाल करके वे प्रोसेस लिखी जाती हैं जिन्हें आपको एक्ज़ीक्यूट करना है. स्क्रीनशॉट कैप्चर करना बहुत आसान है, जैसा कि इस सैंपल में दिखाया गया है. मैं Linux सर्वर (CentOS) पर काम कर रहा था. इसलिए, मुझे जैपनीज़ (M+ FONTS) दिखाने के लिए फ़ॉन्ट इंस्टॉल करने थे. इसके बावजूद, फ़ॉन्ट रेंडरिंग को Windows या Mac OS के मुकाबले अलग तरीके से मैनेज किया जाता है. इसलिए, एक ही फ़ॉन्ट अन्य मशीनों पर अलग दिख सकता है. हालांकि, फ़र्क़ बहुत कम होता है.
img और div टैग की पोज़िशन को वापस लाने की प्रोसेस, स्टैंडर्ड पेजों पर इस्तेमाल की जाने वाली प्रोसेस से मिलती-जुलती है. jQuery का इस्तेमाल भी बिना किसी समस्या के किया जा सकता है.
stage_builder
मैंने शुरुआत में, Firefox 3D Inspector की तरह ही स्टेज जनरेट करने के लिए, डीओएम पर आधारित तरीके का इस्तेमाल करने पर विचार किया. साथ ही, PhantomJS में डीओएम विश्लेषण जैसा कुछ करने की कोशिश की. आखिर में, मैंने इमेज प्रोसेसिंग के तरीके को अपनाया. इसके लिए, मैंने "stage_builder" नाम का एक C++ प्रोग्राम लिखा है, जो OpenCV और Boost का इस्तेमाल करता है. यह ये काम करता है:
- स्क्रीनशॉट और JSON फ़ाइलें लोड करता है.
- इमेज और टेक्स्ट को "आइलैंड" में बदलता है.
- द्वीपों को जोड़ने के लिए पुल बनाता है.
- यह मैज़ बनाने के लिए, ग़ैर-ज़रूरी ब्रिज हटा देता है.
- बड़े आइटम रखता है.
- छोटे आइटम रखने के लिए.
- गार्ड रेलिंग जोड़ता है.
- पोज़िशनिंग डेटा को JSON फ़ॉर्मैट में दिखाता है.
हर चरण के बारे में यहां जानकारी दी गई है.
स्क्रीनशॉट और JSON फ़ाइलें लोड हो रही हैं
स्क्रीनशॉट लोड करने के लिए, आम तौर पर cv::imread
का इस्तेमाल किया जाता है. मैंने JSON फ़ाइलों के लिए कई लाइब्रेरी आज़माईं, लेकिन picojson का इस्तेमाल करना सबसे आसान लगा.
इमेज और टेक्स्ट को "आइलैंड" में बदलना
ऊपर aid-dcc.com के खबरों वाले सेक्शन का स्क्रीनशॉट दिया गया है. असल साइज़ में देखने के लिए क्लिक करें. इमेज और टेक्स्ट एलिमेंट को आइलैंड में बदलना ज़रूरी है. इन सेक्शन को अलग करने के लिए, हमें स्क्रीनशॉट में सबसे ज़्यादा इस्तेमाल हुए रंग को मिटाना होगा. ऐसा करने के बाद, यह इस तरह दिखेगा:
सफ़ेद सेक्शन, संभावित आइलैंड हैं.
टेक्स्ट बहुत छोटा और शार्प है, इसलिए हम इसे cv::dilate
, cv::GaussianBlur
, और cv::threshold
की मदद से मोटा करेंगे. इमेज का कॉन्टेंट भी मौजूद नहीं है. इसलिए, हम PhantomJS से मिले img टैग के डेटा आउटपुट के आधार पर, उन जगहों को सफ़ेद रंग से भर देंगे. इससे मिलने वाली इमेज कुछ ऐसी दिखती है:
अब टेक्स्ट सही क्लंप बनाता है और हर इमेज एक सही आइलैंड होती है.
द्वीपों को जोड़ने के लिए पुल बनाना
द्वीप तैयार होने के बाद, उन्हें पुलों से जोड़ दिया जाता है. हर द्वीप, बगल में मौजूद द्वीपों को बाईं ओर, दाईं ओर, ऊपर, और नीचे खोजता है. इसके बाद, सबसे नज़दीक द्वीप के सबसे नज़दीक पॉइंट से ब्रिज जोड़ता है. इससे कुछ ऐसा नतीजा मिलता है:
मैज़ बनाने के लिए, ग़ैर-ज़रूरी ब्रिज हटाना
सभी ब्रिज को शामिल करने पर, स्टेज पर नेविगेट करना बहुत आसान हो जाएगा. इसलिए, कुछ ब्रिज हटाकर, मैज़ बनाना होगा. शुरुआती पॉइंट के तौर पर एक द्वीप (उदाहरण के लिए, सबसे ऊपर बाईं ओर मौजूद द्वीप) चुना जाता है. साथ ही, उस द्वीप से जुड़े सभी ब्रिज को मिटा दिया जाता है. हालांकि, एक ब्रिज को नहीं मिटाया जाता. यह ब्रिज, द्वीप से रैंडम तौर पर चुना जाता है. इसके बाद, बाकी बचे ब्रिज से जुड़े अगले द्वीप के लिए भी यही तरीका अपनाया जाता है. जब रास्ता किसी आखिरी पॉइंट पर पहुंच जाता है या पहले से विज़िट किए गए द्वीप पर वापस ले जाता है, तो यह उस पॉइंट पर वापस आ जाता है जहां से किसी नए द्वीप का ऐक्सेस मिलता है. इस तरह से सभी द्वीपों को प्रोसेस करने के बाद, भूलभुलैया पूरी हो जाती है.
बड़े आइटम दिखाना
हर द्वीप के डाइमेंशन के हिसाब से, उस पर एक या उससे ज़्यादा बड़े आइटम रखे जाते हैं. ये आइटम, द्वीप के किनारों से सबसे दूर के पॉइंट पर रखे जाते हैं. हालांकि, ये पॉइंट साफ़ तौर पर नहीं दिखते, लेकिन इन्हें यहां लाल रंग में दिखाया गया है:
इन सभी संभावित पॉइंट में से, सबसे ऊपर बाईं ओर मौजूद पॉइंट को शुरुआती पॉइंट (लाल रंग का सर्कल) के तौर पर सेट किया जाता है. सबसे नीचे दाईं ओर मौजूद पॉइंट को लक्ष्य (हरा रंग का सर्कल) के तौर पर सेट किया जाता है. साथ ही, बाकी बचे पॉइंट में से ज़्यादा से ज़्यादा छह पॉइंट को बड़े आइटम के प्लेसमेंट (बैंगनी रंग का सर्कल) के लिए चुना जाता है.
छोटे आइटम डालना
टापू के किनारों से तय दूरी पर, लाइनों के साथ छोटे आइटम की सही संख्या रखी जाती है. ऊपर दी गई इमेज (aid-dcc.com से नहीं) में, अनुमानित प्लेसमेंट लाइनें स्लेटी रंग में दिखाई गई हैं. ये लाइनें, द्वीप के किनारों से नियमित अंतराल पर ऑफ़सेट और प्लेस की गई हैं. लाल बिंदु से पता चलता है कि छोटे आइटम कहां रखे गए हैं. यह इमेज, डेवलपमेंट के बीच के वर्शन की है. इसलिए, इसमें आइटम सीधी लाइनों में दिखाए गए हैं. हालांकि, फ़ाइनल वर्शन में आइटम, स्लेटी लाइनों के दोनों ओर थोड़े अनियमित तरीके से दिखाए गए हैं.
गार्ड रेलिंग लगाना
गार्ड रेलिंग, मुख्य रूप से द्वीपों की बाहरी सीमाओं के साथ लगाई जाती हैं. हालांकि, ऐक्सेस देने के लिए इन्हें पुलों पर काट दिया जाना चाहिए. Boost ज्यामिति लाइब्रेरी इस काम के लिए मददगार साबित हुई. इसकी मदद से, ज्यामितीय कैलकुलेशन को आसान बनाया जा सकता है. जैसे, यह पता लगाना कि द्वीप की सीमा का डेटा, किसी पुल के दोनों ओर की लाइनों से कहां मिलता है.
द्वीपों की आउटलाइन वाली हरी लाइनें, गार्ड रेल हैं. इस इमेज में यह देखना मुश्किल हो सकता है, लेकिन जहां पुल हैं वहां कोई हरी लाइन नहीं है. यह डीबग करने के लिए इस्तेमाल की जाने वाली आखिरी इमेज है. इसमें वे सभी ऑब्जेक्ट शामिल होते हैं जिन्हें JSON में आउटपुट करना होता है. हल्के नीले रंग के बिंदु, छोटे आइटम होते हैं. वहीं, धूसर रंग के बिंदु, वीडियो को फिर से शुरू करने के सुझाए गए पॉइंट होते हैं. गेंद के समुद्र में गिरने पर, गेम को सबसे नज़दीक के उस पॉइंट से फिर से शुरू किया जाता है जहां से गेम को रोका गया था. रीस्टार्ट करने के पॉइंट, छोटे आइटम की तरह ही व्यवस्थित किए जाते हैं. ये आइलैंड के किनारे से तय दूरी पर, नियमित अंतराल पर होते हैं.
पोज़िशनिंग डेटा को JSON फ़ॉर्मैट में आउटपुट करना
मैंने आउटपुट के लिए भी picojson का इस्तेमाल किया है. यह डेटा को स्टैंडर्ड आउटपुट में लिखता है. इसके बाद, कॉलर (Node.js) को यह डेटा मिलता है.
Linux पर चलाने के लिए, Mac पर C++ प्रोग्राम बनाना
इस गेम को Mac पर डेवलप किया गया था और Linux में डिप्लॉय किया गया था. हालांकि, दोनों ऑपरेटिंग सिस्टम के लिए OpenCV और Boost मौजूद थे. इसलिए, कंपाइल एनवायरमेंट सेट अप करने के बाद, डेवलपमेंट करना मुश्किल नहीं था. मैंने Mac पर बिल्ड को डीबग करने के लिए, Xcode में कमांड लाइन टूल का इस्तेमाल किया. इसके बाद, automake/autoconf का इस्तेमाल करके कॉन्फ़िगर फ़ाइल बनाई, ताकि बिल्ड को Linux में कंपाइल किया जा सके. इसके बाद, मुझे Linux में "configure && make" का इस्तेमाल करके, एक्ज़ीक्यूटेबल फ़ाइल बनानी थी. कंपाइलर के वर्शन में अंतर की वजह से, मुझे Linux से जुड़ी कुछ गड़बड़ियां मिलीं. हालांकि, gdb का इस्तेमाल करके उन्हें आसानी से ठीक किया जा सका.
नतीजा
इस तरह का गेम, फ़्लैश या Unity की मदद से बनाया जा सकता है. इससे कई फ़ायदे मिलेंगे. हालांकि, इस वर्शन के लिए किसी प्लग इन की ज़रूरत नहीं होती. साथ ही, HTML5 + CSS3 के लेआउट की सुविधाएं काफ़ी असरदार साबित हुई हैं. हर टास्क के लिए सही टूल का होना ज़रूरी है. मुझे यह देखकर हैरानी हुई कि पूरी तरह से HTML5 में बनाया गया यह गेम कितना अच्छा है. हालांकि, इसमें अब भी कई चीज़ों की कमी है, लेकिन मुझे यह देखने में दिलचस्पी है कि आने वाले समय में यह गेम कैसा बनेगा.