केस स्टडी - बाउंसी माउस

परिचय

बाउंसी माउस

पिछले साल के आखिर में, iOS और Android पर Bouncy Mouse पब्लिश करने के बाद, मुझे कुछ अहम बातें पता चलीं. इनमें से सबसे अहम बात यह थी कि किसी मशहूर बाज़ार में अपनी जगह बनाना मुश्किल है. iPhone के बाज़ार में, ऐप्लिकेशन को लोकप्रिय बनाना बहुत मुश्किल था. वहीं, Android Marketplace में, ऐप्लिकेशन को लोकप्रिय बनाना ज़्यादा आसान था, लेकिन फिर भी आसान नहीं था. इस अनुभव के बाद, मुझे Chrome Web Store पर एक दिलचस्प अवसर मिला. वेब स्टोर में कई गेम मौजूद हैं, लेकिन अच्छी क्वालिटी वाले HTML5 गेम का कैटलॉग अभी शुरुआती दौर में है. इसका मतलब है कि नए ऐप्लिकेशन डेवलपर के लिए, रैंकिंग चार्ट बनाना और अपने ऐप्लिकेशन को लोगों तक पहुंचाना ज़्यादा आसान हो गया है. इस अवसर को ध्यान में रखते हुए, मैंने Bouncy Mouse को HTML5 में पोर्ट करने की कोशिश की. मुझे उम्मीद है कि इससे नए उपयोगकर्ताओं को गेमप्ले का बेहतर अनुभव मिलेगा. इस केस स्टडी में, हम Bouncy Mouse को HTML5 में पोर्ट करने की सामान्य प्रोसेस के बारे में थोड़ी बात करेंगे. इसके बाद, हम तीन ऐसे विषयों पर ज़्यादा जानकारी देंगे जो हमारे लिए दिलचस्प रहे: ऑडियो, परफ़ॉर्मेंस, और कमाई करना.

C++ गेम को HTML5 में पोर्ट करना

फ़िलहाल, Bouncy Mouse Android(C++), iOS (C++), Windows Phone 7 (C#), और Chrome (Javascript) पर उपलब्ध है. इससे कभी-कभी यह सवाल उठता है: ऐसा गेम कैसे लिखें जिसे आसानी से कई प्लैटफ़ॉर्म पर पोर्ट किया जा सके? मुझे लगता है कि लोग किसी ऐसे जादुई तरीके की उम्मीद कर रहे हैं जिससे वे हाथ से पोर्ट किए बिना, इस लेवल की पोर्टेबिलिटी हासिल कर सकें. माफ़ करें, मुझे नहीं पता कि ऐसा कोई समाधान अब तक मौजूद है या नहीं. Google का PlayN फ़्रेमवर्क या Unity इंजन, शायद इनमें से कोई एक ऐसा समाधान हो सकता है. हालांकि, इनमें से कोई भी समाधान, मेरे मकसद के मुताबिक नहीं है. असल में, मेरा तरीका एक हैंड पोर्ट था. मैंने सबसे पहले iOS/Android वर्शन को C++ में लिखा. इसके बाद, इस कोड को हर नए प्लैटफ़ॉर्म पर पोर्ट किया. ऐसा लग सकता है कि इसमें काफ़ी काम है, लेकिन WP7 और Chrome के वर्शन को बनाने में, दोनों को दो हफ़्ते से ज़्यादा नहीं लगे. इसलिए, अब सवाल यह है कि क्या कोडबेस को आसानी से एक जगह से दूसरी जगह ले जाने के लिए कुछ किया जा सकता है? मैंने कुछ ऐसी चीज़ें की हैं जिनसे मुझे इस काम में मदद मिली:

कोडबेस को छोटा रखना

यह बात शायद आपको साफ़ तौर पर पता हो, लेकिन इसकी वजह से ही गेम को इतनी जल्दी पोर्ट किया जा सका. Bouncy Mouse का क्लाइंट कोड, C++ की सिर्फ़ 7,000 लाइनें है. 7,000 लाइनें कोड कम नहीं है, लेकिन इसे मैनेज किया जा सकता है. क्लाइंट कोड के C# और JavaScript, दोनों वर्शन का साइज़ करीब-करीब एक जैसा था. अपने कोडबेस को छोटा रखने के लिए, मैंने दो मुख्य तरीके अपनाए: ज़रूरत से ज़्यादा कोड न लिखें और ज़्यादा से ज़्यादा कोड को प्री-प्रोसेसिंग (नॉन-रनटाइम) कोड में डालें. ज़रूरत से ज़्यादा कोड न लिखना एक सामान्य बात लग सकती है, लेकिन यह एक ऐसी चीज़ है जिसे लेकर मैं हमेशा खुद से लड़ता रहता हूं. मुझे अक्सर किसी भी ऐसी चीज़ के लिए हेल्पर क्लास/फ़ंक्शन लिखने का मन करता है जिसे हेल्पर में शामिल किया जा सकता है. हालांकि, अगर आपको हेल्पर फ़ंक्शन का कई बार इस्तेमाल नहीं करना है, तो आम तौर पर इससे आपके कोड का साइज़ बढ़ जाता है. Bouncy Mouse के साथ, मैंने इस बात का ध्यान रखा कि जब तक मुझे किसी हेल्पर का इस्तेमाल कम से कम तीन बार नहीं करना है, तब तक उसे न लिखूं. जब मैंने हेल्पर क्लास लिखी, तो मैंने उसे अपने आने वाले प्रोजेक्ट के लिए आसान, पोर्टेबल, और फिर से इस्तेमाल किया जा सकने वाला बनाने की कोशिश की. दूसरी ओर, सिर्फ़ Bouncy Mouse के लिए कोड लिखते समय, मेरा ध्यान इस बात पर था कि कोडिंग के काम को आसानी से और जल्द से जल्द पूरा किया जाए. भले ही, यह कोड लिखने का “सबसे अच्छा” तरीका न हो. कोडबेस को छोटा रखने का दूसरा और ज़्यादा अहम हिस्सा, प्रोसेस करने से पहले के चरणों में ज़्यादा से ज़्यादा काम करना था. अगर किसी रनटाइम टास्क को प्रीप्रोसेसिंग टास्क में बदला जा सकता है, तो आपका गेम तेज़ी से चलेगा. साथ ही, आपको कोड को हर नए प्लैटफ़ॉर्म पर पोर्ट नहीं करना पड़ेगा. उदाहरण के लिए, मैंने अपने लेवल की ज्यामिति का डेटा, बिना प्रोसेस किए गए फ़ॉर्मैट में सेव किया था. साथ ही, रन टाइम पर असल OpenGL/WebGL वर्टिक्स बफ़र को इकट्ठा किया था. इसके लिए, कुछ सेटअप और रनटाइम कोड की कुछ सौ लाइनें लिखनी पड़ीं. बाद में, मैंने इस कोड को प्रीप्रोसेसिंग के चरण में ले जाकर, कंपाइल के समय पूरी तरह से पैक किए गए OpenGL/WebGL वर्टिक्स बफ़र को लिखा. कोड की कुल संख्या पहले जैसी ही थी, लेकिन उन कुछ सौ लाइनों को प्रोसेस करने से पहले की प्रक्रिया में ले जाया गया था. इसका मतलब है कि मुझे उन्हें किसी नए प्लैटफ़ॉर्म पर पोर्ट नहीं करना पड़ा. Bouncy Mouse में इस तरह के कई उदाहरण हैं. हर गेम में अलग-अलग चीज़ें हो सकती हैं. हालांकि, इस बात का ध्यान रखें कि रनटाइम के दौरान ऐसी कोई भी चीज़ न हो जिसकी ज़रूरत न हो.

ऐसी डिपेंडेंसी न जोड़ें जिनकी आपको ज़रूरत नहीं है

Bouncy Mouse को आसानी से पोर्ट करने की एक और वजह यह है कि इसमें किसी दूसरे सोर्स पर निर्भरता नहीं होती. इस चार्ट में, हर प्लैटफ़ॉर्म के हिसाब से Bouncy Mouse की मुख्य लाइब्रेरी डिपेंडेंसी के बारे में बताया गया है:

Android iOS HTML5 WP7
ग्राफ़िक OpenGL ES OpenGL ES WebGL XNA
आवाज़ OpenSL ES OpenAL Web Audio XNA
भौतिक विज्ञान Box2D Box2D Box2D.js Box2D.xna

बस इतना ही. Box2D के अलावा, तीसरे पक्ष की किसी भी बड़ी लाइब्रेरी का इस्तेमाल नहीं किया गया है. Box2D सभी प्लैटफ़ॉर्म पर काम करती है. ग्राफ़िक के लिए, WebGL और XNA, दोनों OpenGL के साथ लगभग 1:1 मैप करते हैं. इसलिए, यह कोई बड़ी समस्या नहीं थी. सिर्फ़ साउंड की लाइब्रेरी अलग थीं. हालांकि, Bouncy Mouse में साउंड कोड छोटा है (प्लैटफ़ॉर्म के हिसाब से कोड की करीब सौ लाइनें), इसलिए यह कोई बड़ी समस्या नहीं थी. Bouncy Mouse को बड़ी और पोर्टेबल लाइब्रेरी से मुक्त रखने का मतलब है कि भाषा में बदलाव होने के बावजूद, रनटाइम कोड का लॉजिक, वर्शन के बीच लगभग एक जैसा हो सकता है. साथ ही, इससे हमें ऐसे टूल चेन का इस्तेमाल करने से बचने में मदद मिलती है जो पोर्टेबल नहीं होते. मुझसे पूछा गया है कि क्या Cocos2D या Unity जैसी लाइब्रेरी का इस्तेमाल करने की तुलना में, सीधे तौर पर OpenGL/WebGL के लिए कोडिंग करने से, प्रोग्राम कोडिंग करना ज़्यादा मुश्किल हो जाता है. इसके अलावा, WebGL के लिए कुछ सहायक टूल भी उपलब्ध हैं. असल में, मेरा मानना है कि ऐसा नहीं है. ज़्यादातर मोबाइल फ़ोन / HTML5 गेम (कम से कम Bouncy Mouse जैसे गेम) बहुत आसान होते हैं. ज़्यादातर मामलों में, गेम में सिर्फ़ कुछ स्प्राइट और शायद कुछ टेक्सचर वाली ज्यामिति होती है. Bouncy Mouse में OpenGL के लिए खास तौर पर लिखे गए कोड की कुल संख्या शायद 1,000 लाइनों से कम है. अगर हेल्पर लाइब्रेरी का इस्तेमाल करने से, इस संख्या में असल में कमी आएगी, तो मुझे हैरानी होगी. भले ही, इस संख्या को आधा कर दिया जाए, लेकिन मुझे सिर्फ़ 500 लाइनों के कोड को सेव करने के लिए, नई लाइब्रेरी/टूल सीखने में काफ़ी समय देना होगा. इसके अलावा, मुझे अब तक ऐसी कोई हेल्पर लाइब्रेरी नहीं मिली है जो मेरे पसंदीदा सभी प्लैटफ़ॉर्म पर काम करती हो. इसलिए, इस तरह की डिपेंडेंसी का इस्तेमाल करने से, लाइब्रेरी को एक प्लैटफ़ॉर्म से दूसरे प्लैटफ़ॉर्म पर ले जाने में काफ़ी परेशानी होगी. अगर मुझे कोई ऐसा 3D गेम बनाना होता है जिसमें लाइटमैप, डाइनैमिक एलओडी, स्किन वाला ऐनिमेशन वगैरह की ज़रूरत होती है, तो मेरा जवाब निश्चित रूप से बदल जाएगा. इस मामले में, मुझे OpenGL के बजाय अपने पूरे इंजन को मैन्युअल तरीके से कोड करने के लिए, फिर से पहिया बनाना होगा. मेरा कहना है कि ज़्यादातर मोबाइल/HTML5 गेम अभी इस कैटगरी में नहीं हैं. इसलिए, ज़रूरत से पहले चीज़ों को मुश्किल बनाने की ज़रूरत नहीं है.

भाषाओं के बीच समानताओं को कम न आंकें

एक और अहम बात यह है कि C++ कोडबेस को किसी नई भाषा में पोर्ट करने में काफ़ी समय बचता है. ऐसा इसलिए होता है, क्योंकि ज़्यादातर कोड हर भाषा में एक जैसा होता है. हालांकि, कुछ मुख्य एलिमेंट बदल सकते हैं, लेकिन ये उन चीज़ों की तुलना में काफ़ी कम हैं जो नहीं बदलती हैं. असल में, कई फ़ंक्शन के लिए, C++ से JavaScript पर जाने के लिए, मेरे C++ कोडबेस पर कुछ रेगुलर एक्सप्रेशन बदलने की ज़रूरत पड़ी.

पोर्ट करने से जुड़े नतीजे

नंबर पोर्ट करने की प्रोसेस यहीं तक पूरी हो जाती है. हम अगले कुछ सेक्शन में, एचटीएमएल5 से जुड़ी कुछ चुनौतियों के बारे में बताएंगे. हालांकि, हमारा मुख्य मकसद यह बताना है कि अगर आपने अपना कोड आसान रखा है, तो पोर्टिंग करना आपके लिए मुश्किल नहीं होगा.

ऑडियो

मुझे और शायद सभी को ऑडियो से जुड़ी समस्या हुई. iOS और Android पर, ऑडियो के लिए कई बेहतर विकल्प उपलब्ध हैं (OpenSL, OpenAL), लेकिन HTML5 की दुनिया में, चीज़ें काफ़ी खराब थीं. HTML5 ऑडियो उपलब्ध है, लेकिन मुझे पता चला है कि गेम में इसका इस्तेमाल करने पर, कुछ समस्याएं आती हैं. मुझे नए ब्राउज़र पर भी, अक्सर अजीब समस्याएं आती थीं. उदाहरण के लिए, Chrome में एक साथ कितने ऑडियो एलिमेंट (सोर्स) बनाए जा सकते हैं, इसकी कोई सीमा नहीं है. इसके अलावा, कभी-कभी आवाज़ चलने के बावजूद, वह अचानक खराब हो जाती थी. कुल मिलाकर, मुझे थोड़ी चिंता थी. ऑनलाइन खोजने पर पता चला कि लगभग सभी को एक जैसी समस्या आ रही है. शुरुआत में, मुझे SoundManager2 नाम का एक एपीआई मिला. यह एपीआई, उपलब्ध होने पर HTML5 ऑडियो का इस्तेमाल करता है. हालांकि, मुश्किल स्थितियों में यह Flash का इस्तेमाल करता है. हालांकि, यह तरीका काम करता था, लेकिन इसमें अब भी गड़बड़ियां थीं और यह पूरी तरह से भरोसेमंद नहीं था. हालांकि, यह पूरी तरह से HTML5 ऑडियो के मुकाबले कम गड़बड़ियों वाला था. लॉन्च होने के एक हफ़्ते बाद, मैंने Google के कुछ सहायक लोगों से बात की. उन्होंने मुझे Webkit के Web Audio API के बारे में बताया. मैंने शुरुआत में इस एपीआई का इस्तेमाल करने के बारे में सोचा था, लेकिन मुझे लगा कि यह एपीआई मेरे लिए ज़रूरत से ज़्यादा जटिल है. इसलिए, मैंने इसका इस्तेमाल नहीं किया. मुझे सिर्फ़ कुछ आवाज़ें चलानी थीं: HTML5 ऑडियो के साथ, इसके लिए JavaScript की कुछ लाइनें जोड़ी जा सकती हैं. हालांकि, वेब ऑडियो को थोड़ी देर के लिए इस्तेमाल करने पर, मुझे इसकी स्पेसिफ़िकेशन (70 पेज) काफ़ी बड़ी लगी. साथ ही, वेब पर सैंपल की संख्या कम थी (यह किसी नए एपीआई के लिए आम बात है) और स्पेसिफ़िकेशन में कहीं भी “चलाएं”, “रोकें” या “बंद करें” फ़ंक्शन नहीं था. Google ने मुझे भरोसा दिलाया कि मेरी चिंताएं सही नहीं हैं. इसलिए, मैंने एपीआई को फिर से इस्तेमाल किया. कुछ और उदाहरणों को देखने और थोड़ी और रिसर्च करने के बाद, मुझे पता चला कि Google सही था–एपीआई निश्चित रूप से मेरी ज़रूरतों को पूरा कर सकता है. साथ ही, यह अन्य एपीआई में होने वाली गड़बड़ियों के बिना ऐसा कर सकता है. Web Audio API का इस्तेमाल शुरू करना लेख खास तौर पर मददगार है. इससे, एपीआई के बारे में बेहतर तरीके से समझने में मदद मिलती है. मेरी असली समस्या यह है कि एपीआई को समझने और इस्तेमाल करने के बाद भी, मुझे यह ऐसा एपीआई लगता है जिसे “सिर्फ़ कुछ साउंड चलाने” के लिए डिज़ाइन नहीं किया गया है. इस गलतफ़हमी को दूर करने के लिए, मैंने एक छोटी हेल्पर क्लास लिखी है. इसकी मदद से, एपीआई को अपनी पसंद के मुताबिक इस्तेमाल किया जा सकता है. जैसे, साउंड चलाना, रोकना, बंद करना, और उसकी स्थिति के बारे में क्वेरी करना. मैंने इस हेल्पर क्लास को AudioClip नाम दिया है. पूरा सोर्स, Apache 2.0 लाइसेंस के तहत GitHub पर उपलब्ध है. हम इस क्लास के बारे में यहां ज़्यादा जानकारी देंगे. हालांकि, सबसे पहले Web Audio API के बारे में कुछ जानकारी:

वेब ऑडियो ग्राफ़

Web Audio API को HTML5 Audio एलिमेंट से ज़्यादा मुश्किल (और ज़्यादा बेहतर) बनाने वाली पहली बात यह है कि यह उपयोगकर्ता को ऑडियो आउटपुट करने से पहले, उसे प्रोसेस / मिक्स कर सकता है. यह सुविधा काफ़ी असरदार है. हालांकि, किसी भी ऑडियो प्लेबैक में ग्राफ़ शामिल होने की वजह से, सामान्य स्थितियों में चीज़ें थोड़ी मुश्किल हो जाती हैं. Web Audio API की क्षमता को दिखाने के लिए, यह ग्राफ़ देखें:

वेब ऑडियो का बुनियादी ग्राफ़
वेब ऑडियो का बुनियादी ग्राफ़

ऊपर दिए गए उदाहरण में, Web Audio API की सुविधाओं के बारे में बताया गया है. हालांकि, मुझे अपनी परिस्थिति में इनमें से ज़्यादातर सुविधाओं की ज़रूरत नहीं थी. मुझे सिर्फ़ कोई आवाज़ चलानी थी. इसके लिए अब भी ग्राफ़ की ज़रूरत होती है, लेकिन यह ग्राफ़ बहुत आसान होता है.

ग्राफ़ आसान हो सकते हैं

Web Audio API को HTML5 Audio एलिमेंट से ज़्यादा मुश्किल (और ज़्यादा बेहतर) बनाने वाली पहली बात यह है कि यह उपयोगकर्ता को ऑडियो आउटपुट करने से पहले, उसे प्रोसेस / मिक्स कर सकता है. यह सुविधा काफ़ी असरदार है. हालांकि, किसी भी ऑडियो प्लेबैक में ग्राफ़ शामिल होने की वजह से, सामान्य स्थितियों में चीज़ें थोड़ी मुश्किल हो जाती हैं. Web Audio API की क्षमता को दिखाने के लिए, यह ग्राफ़ देखें:

ट्रिविअल वेब ऑडियो ग्राफ़
सामान्य वेब ऑडियो ग्राफ़

ऊपर दिखाया गया छोटा ग्राफ़, किसी साउंड को चलाने, रोकने या बंद करने के लिए ज़रूरी सभी काम कर सकता है.

लेकिन, ग्राफ़ की चिंता भी न करें

ग्राफ़ को समझना अच्छा है, लेकिन हर बार कोई साउंड चलाते समय, मुझे इसकी ज़रूरत नहीं है. इसलिए, मैंने एक आसान रैपर क्लास “AudioClip” लिखी. यह क्लास, इस ग्राफ़ को अंदरूनी तौर पर मैनेज करती है, लेकिन उपयोगकर्ता के लिए एक बहुत आसान एपीआई उपलब्ध कराती है.

AudioClip
AudioClip

यह क्लास, वेब ऑडियो ग्राफ़ और कुछ हेल्पर स्टेटस के अलावा और कुछ नहीं है. हालांकि, इससे मुझे हर साउंड चलाने के लिए वेब ऑडियो ग्राफ़ बनाने के बजाय, बहुत आसान कोड का इस्तेमाल करने में मदद मिलती है.

// At startup time
var sound = new AudioClip("ping.wav");

// Later
sound.play();

लागू करने से जुड़ी जानकारी

आइए, हेल्पर क्लास के कोड पर एक नज़र डालें: कन्स्ट्रक्टर – कन्स्ट्रक्टर, XHR का इस्तेमाल करके साउंड डेटा को लोड करता है. उदाहरण को आसान बनाने के लिए, यहां HTML5 ऑडियो एलिमेंट को सोर्स नोड के तौर पर इस्तेमाल नहीं किया गया है. हालांकि, इसका इस्तेमाल सोर्स नोड के तौर पर भी किया जा सकता है. यह खास तौर पर बड़े सैंपल के लिए मददगार होता है. ध्यान दें कि Web Audio API के लिए ज़रूरी है कि हम इस डेटा को “arraybuffer” के तौर पर फ़ेच करें. डेटा मिलने के बाद, हम इस डेटा से एक वेब ऑडियो बफ़र बनाते हैं. इसके लिए, हम इसे उसके ओरिजनल फ़ॉर्मैट से रनटाइम PCM फ़ॉर्मैट में डिकोड करते हैं.

/**
* Create a new AudioClip object from a source URL. This object can be played,
* paused, stopped, and resumed, like the HTML5 Audio element.
*
* @constructor
* @param {DOMString} src
* @param {boolean=} opt_autoplay
* @param {boolean=} opt_loop
*/
AudioClip = function(src, opt_autoplay, opt_loop) {
// At construction time, the AudioClip is not playing (stopped),
// and has no offset recorded.
this.playing_ = false;
this.startTime_ = 0;
this.loop_ = opt_loop ? true : false;

// State to handle pause/resume, and some of the intricacies of looping.
this.resetTimout_ = null;
this.pauseTime_ = 0;

// Create an XHR to load the audio data.
var request = new XMLHttpRequest();
request.open("GET", src, true);
request.responseType = "arraybuffer";

var sfx = this;
request.onload = function() {
// When audio data is ready, we create a WebAudio buffer from the data.
// Using decodeAudioData allows for async audio loading, which is useful
// when loading longer audio tracks (music).
AudioClip.context.decodeAudioData(request.response, function(buffer) {
    sfx.buffer_ = buffer;
    
    if (opt_autoplay) {
    sfx.play();
    }
});
}

request.send();
}

चलाना – हमारे साउंड को चलाने के लिए दो चरण पूरे करने होते हैं: प्लेलिस्ट ग्राफ़ सेट अप करना और ग्राफ़ के सोर्स पर “noteOn” का कोई वर्शन कॉल करना. किसी सोर्स को सिर्फ़ एक बार चलाया जा सकता है. इसलिए, हर बार चलाने के लिए, हमें सोर्स/ग्राफ़ को फिर से बनाना होगा. इस फ़ंक्शन में सबसे ज़्यादा मुश्किल, रोकी गई क्लिप (this.pauseTime_ > 0) को फिर से चलाने की ज़रूरी शर्तों की वजह से आती है. रोकी गई क्लिप को फिर से चलाने के लिए, हम noteGrainOn का इस्तेमाल करते हैं. इससे बफ़र के किसी सब-क्षेत्र को चलाया जा सकता है. माफ़ करें, इस स्थिति में noteGrainOn, लूपिंग के साथ सही तरीके से काम नहीं करता. यह पूरे बफ़र के बजाय, सब-क्षेत्र को लूप करेगा. इसलिए, हमें noteGrainOn का इस्तेमाल करके, क्लिप के बाकी हिस्से को चलाना होगा. इसके बाद, क्लिप को शुरुआत से फिर से चलाना होगा और लूप करने की सुविधा चालू करनी होगी.

/**
* Recreates the audio graph. Each source can only be played once, so
* we must recreate the source each time we want to play.
* @return {BufferSource}
* @param {boolean=} loop
*/
AudioClip.prototype.createGraph = function(loop) {
var source = AudioClip.context.createBufferSource();
source.buffer = this.buffer_;
source.connect(AudioClip.context.destination);

// Looping is handled by the Web Audio API.
source.loop = loop;

return source;
}

/**
* Plays the given AudioClip. Clips played in this manner can be stopped
* or paused/resumed.
*/
AudioClip.prototype.play = function() {
if (this.buffer_ && !this.isPlaying()) {
// Record the start time so we know how long we've been playing.
this.startTime_ = AudioClip.context.currentTime;
this.playing_ = true;
this.resetTimeout_ = null;

// If the clip is paused, we need to resume it.
if (this.pauseTime_ > 0) {
    // We are resuming a clip, so it's current playback time is not correctly
    // indicated by startTime_. Correct this by subtracting pauseTime_.
    this.startTime_ -= this.pauseTime_;
    var remainingTime = this.buffer_.duration - this.pauseTime_;

    if (this.loop_) {
    // If the clip is paused and looping, we need to resume the clip
    // with looping disabled. Once the clip has finished, we will re-start
    // the clip from the beginning with looping enabled
    this.source_ = this.createGraph(false);
    this.source_.noteGrainOn(0, this.pauseTime_, remainingTime)

    // Handle restarting the playback once the resumed clip has completed.
    // *Note that setTimeout is not the ideal method to use here. A better 
    // option would be to handle timing in a more predictable manner,
    // such as tying the update to the game loop.
    var clip = this;
    this.resetTimeout_ = setTimeout(function() { clip.stop(); clip.play() },
                                    remainingTime * 1000);
    } else {
    // Paused non-looping case, just create the graph and play the sub-
    // region using noteGrainOn.
    this.source_ = this.createGraph(this.loop_);
    this.source_.noteGrainOn(0, this.pauseTime_, remainingTime);
    }

    this.pauseTime_ = 0;
} else {
    // Normal case, just creat the graph and play.
    this.source_ = this.createGraph(this.loop_);
    this.source_.noteOn(0);
}
}
}

साउंड इफ़ेक्ट के तौर पर चलाएं - ऊपर दिए गए 'चलाएं' फ़ंक्शन की मदद से, ऑडियो क्लिप को ओवरलैप करके कई बार नहीं चलाया जा सकता. क्लिप को दूसरी बार सिर्फ़ तब चलाया जा सकता है, जब वह खत्म हो गई हो या उसे रोक दिया गया हो. कभी-कभी, गेम में किसी साउंड को कई बार चलाया जा सकता है. इसके लिए, उसे हर बार पूरा चलने का इंतज़ार नहीं करना पड़ता. जैसे, गेम में सिक्के इकट्ठा करना वगैरह. इसे चालू करने के लिए, AudioClip क्लास में playAsSFX() तरीका है. एक साथ कई वीडियो चलाए जा सकते हैं. इसलिए, playAsSFX() से चलाया जाने वाला वीडियो, AudioClip के साथ 1:1 में बाउंड नहीं होता. इसलिए, वीडियो चलाने की प्रोसेस को रोका, रोका नहीं जा सकता या उसकी स्थिति के बारे में क्वेरी नहीं की जा सकती. लूप करने की सुविधा भी बंद कर दी जाती है, क्योंकि इस तरह से लूप की गई आवाज़ को रोकने का कोई तरीका नहीं होता.

/**
* Plays the given AudioClip as a sound effect. Sound Effects cannot be stopped
* or paused/resumed, but can be played multiple times with overlap.
* Additionally, sound effects cannot be looped, as there is no way to stop
* them. This method of playback is best suited to very short, one-off sounds.
*/
AudioClip.prototype.playAsSFX = function() {
if (this.buffer_) {
var source = this.createGraph(false);
source.noteOn(0);
}
}

रोकें, रोकें, और क्वेरी करने की स्थिति – बाकी फ़ंक्शन बहुत आसान हैं और इनके बारे में ज़्यादा जानकारी देने की ज़रूरत नहीं है:

/**
* Stops an AudioClip , resetting its seek position to 0.
*/
AudioClip.prototype.stop = function() {
if (this.playing_) {
this.source_.noteOff(0);
this.playing_ = false;
this.startTime_ = 0;
this.pauseTime_ = 0;
if (this.resetTimeout_ != null) {
    clearTimeout(this.resetTimeout_);
}
}
}

/**
* Pauses an AudioClip. The offset into the stream is recorded to allow the
* clip to be resumed later.
*/
AudioClip.prototype.pause = function() {
if (this.playing_) {
this.source_.noteOff(0);
this.playing_ = false;
this.pauseTime_ = AudioClip.context.currentTime - this.startTime_;
this.pauseTime_ = this.pauseTime_ % this.buffer_.duration;
this.startTime_ = 0;
if (this.resetTimeout_ != null) {
    clearTimeout(this.resetTimeout_);
}
}
}

/**
* Indicates whether the sound is playing.
* @return {boolean}
*/
AudioClip.prototype.isPlaying = function() {
var playTime = this.pauseTime_ +
            (AudioClip.context.currentTime - this.startTime_);

return this.playing_ && (this.loop_ || (playTime < this.buffer_.duration));
}

ऑडियो का खत्म होना

मुझे उम्मीद है कि यह हेल्पर क्लास, उन डेवलपर के लिए मददगार होगी जो ऑडियो से जुड़ी उन ही समस्याओं से जूझ रहे हैं जिनसे मुझे जूझना पड़ा. साथ ही, अगर आपको Web Audio API की कुछ बेहतर सुविधाओं को जोड़ना है, तब भी इस तरह की क्लास से शुरुआत करना सही रहेगा. दोनों ही मामलों में, इस समाधान से Bouncy Mouse की ज़रूरतें पूरी हुईं. साथ ही, इस गेम को बिना किसी रुकावट के HTML5 गेम बनाया जा सका!

परफ़ॉर्मेंस

JavaScript पोर्ट के लिए, मुझे परफ़ॉर्मेंस की भी चिंता थी. पोर्ट के वर्शन 1 को पूरा करने के बाद, मुझे पता चला कि मेरे क्वाड-कोर डेस्कटॉप पर सब कुछ ठीक से काम कर रहा था. माफ़ करें, नेटबुक या Chromebook पर यह सुविधा उतनी अच्छी नहीं थी. इस मामले में, Chrome की प्रोफ़ाइलर सुविधा ने मुझे यह बताकर बचाया कि मेरे सभी प्रोग्राम में मेरा समय कहां खर्च हो रहा है. मेरे अनुभव से पता चलता है कि कोई भी ऑप्टिमाइज़ेशन करने से पहले, प्रोफ़ाइलिंग की अहमियत को समझना ज़रूरी है. मुझे उम्मीद थी कि Box2D फ़िज़िक्स या रेंडरिंग कोड, गेम के धीमे होने की मुख्य वजह है. हालांकि, असल में ज़्यादातर समय Matrix.clone() फ़ंक्शन पर खर्च हो रहा था. मेरे गेम में गणित का ज़्यादा इस्तेमाल होता है. इसलिए, मुझे पता था कि मैंने मैट्रिक बनाने/क्लोन करने के लिए काफ़ी काम किया है. हालांकि, मुझे कभी नहीं लगा कि यह समस्या बन सकती है. आखिर में, यह पता चला कि एक बहुत ही आसान बदलाव की मदद से, गेम में सीपीयू के इस्तेमाल को तीन गुना से ज़्यादा कम किया जा सकता है. मेरे डेस्कटॉप पर, सीपीयू का इस्तेमाल 6-7% से घटकर 2% हो गया. शायद यह जानकारी JavaScript डेवलपर के लिए आम हो, लेकिन C++ डेवलपर के तौर पर मुझे इस समस्या से हैरानी हुई. इसलिए, मैं इस बारे में ज़्यादा जानकारी दूंगा. मूल रूप से, मेरी मूल मैट्रिक क्लास 3x3 मैट्रिक थी: तीन एलिमेंट वाला कलेक्शन, जिसमें हर एलिमेंट में तीन एलिमेंट वाला कलेक्शन होता है. माफ़ करें, इसका मतलब है कि मैट्रिक को क्लोन करते समय, मुझे चार नए ऐरे बनाने पड़े. मुझे सिर्फ़ इस डेटा को नौ एलिमेंट वाले एक ऐरे में ले जाना था और अपने हिसाब से अपडेट करना था. सीपीयू के इस्तेमाल में तीन गुना की कमी, सिर्फ़ इस एक बदलाव की वजह से हुई. इस बदलाव के बाद, जांच के लिए इस्तेमाल किए गए सभी डिवाइसों पर मेरी ऐप्लिकेशन की परफ़ॉर्मेंस अच्छी रही.

ज़्यादा ऑप्टिमाइज़ेशन

मेरी परफ़ॉर्मेंस ठीक थी, लेकिन मुझे अब भी कुछ समस्याएं आ रही थीं. थोड़ी और प्रोफ़ाइलिंग करने के बाद, मुझे पता चला कि ऐसा JavaScript की गै़रबेज कलेक्शन की वजह से हो रहा था. मेरा ऐप्लिकेशन 60 एफ़पीएस (फ़्रेम प्रति सेकंड) पर चल रहा था. इसका मतलब है कि हर फ़्रेम को ड्रॉ करने में सिर्फ़ 16 मि॰से॰ लगते थे. माफ़ करें, जब धीमी मशीन पर ग़ैर-ज़रूरी डेटा हटाने की प्रोसेस शुरू होती है, तो कभी-कभी इसमें 10 मिलीसेकंड लग सकते हैं. इस वजह से, हर कुछ सेकंड में गेम में रुकावट आती थी, क्योंकि गेम को एक फ़्रेम को ड्रॉ करने में करीब 16 मि॰से॰ लगते थे. इस बारे में बेहतर तरीके से जानने के लिए कि मेरे ब्राउज़र में इतना गै़र-ज़रूरी डेटा क्यों जनरेट हो रहा था, मैंने Chrome के 'हीप प्रोफ़ाइलर' का इस्तेमाल किया. मुझे यह जानकर बहुत अफ़सोस हुआ कि ज़्यादातर ग़ैर-ज़रूरी डेटा (70% से ज़्यादा) Box2D से जनरेट हो रहा था. JavaScript में गै़र-ज़रूरी कोड हटाना मुश्किल काम है. साथ ही, Box2D को फिर से लिखना मुमकिन नहीं था. इसलिए, मुझे एहसास हुआ कि मैं मुश्किल में फंस गया हूं. सौभाग्य से, मेरे पास अब भी एक पुरानी ट्रिक थी: जब 60fps पर गेम नहीं चल पा रहा है, तो 30fps पर चलाएं. आम तौर पर, यह माना जाता है कि वीडियो को 30fps पर चलाना, 60fps पर चलाने से बेहतर होता है. असल में, मुझे अब तक इस बारे में कोई शिकायत या टिप्पणी नहीं मिली है कि गेम 30fps पर चलता है. यह बताना बहुत मुश्किल है, जब तक कि दोनों वर्शन की तुलना एक साथ न की जाए. हर फ़्रेम के लिए 16 मि॰से॰ का अतिरिक्त समय होने का मतलब है कि गड़बड़ी वाली गै़रबेज कलेक्शन की स्थिति में भी, मेरे पास फ़्रेम को रेंडर करने के लिए काफ़ी समय था. टाइमिंग एपीआई (WebKit का बेहतरीन requestAnimationFrame) का इस्तेमाल करने पर, 30fps पर चलने की सुविधा साफ़ तौर पर चालू नहीं होती. हालांकि, इसे आसानी से चालू किया जा सकता है. यह तरीका, एपीआई के मुकाबले उतना आसान नहीं है. हालांकि, RequestAnimationFrame के इंटरवल को मॉनिटर के VSYNC (आम तौर पर 60fps) के साथ अलाइन करके, 30fps हासिल किया जा सकता है. इसका मतलब है कि हमें हर दूसरे कॉलबैक को अनदेखा करना होगा. अगर आपके पास कोई कॉलबैक “टिक” है, जो हर बार “RequestAnimationFrame” ट्रिगर होने पर कॉल किया जाता है, तो इसे इस तरह पूरा किया जा सकता है:

var skip = false;

function Tick() {
skip = !skip;
if (skip) {
return;
}

// OTHER CODE
}

अगर आपको ज़्यादा सावधानी बरतनी है, तो आपको यह देखना चाहिए कि कंप्यूटर का VSYNC, स्टार्टअप के समय 30fps या उससे कम पर न हो. साथ ही, इस मामले में स्किप करने की सुविधा बंद कर दें. हालांकि, मैंने जिन डेस्कटॉप/लैपटॉप कॉन्फ़िगरेशन की जांच की है उनमें मुझे अब तक यह समस्या नहीं मिली है.

डिस्ट्रिब्यूशन और कमाई करना

Bouncy Mouse के Chrome वर्शन में कमाई करने की सुविधा देखकर मुझे हैरानी हुई. इस प्रोजेक्ट में शामिल होने से पहले, मैंने HTML5 गेम को एक दिलचस्प प्रयोग के तौर पर देखा था. इससे मुझे नई टेक्नोलॉजी के बारे में जानने में मदद मिली. मुझे नहीं पता था कि यह पोर्ट बहुत बड़ी ऑडियंस तक पहुंचेगा और इससे कमाई करने की काफ़ी संभावनाएं हैं.

Bouncy Mouse को अक्टूबर के आखिर में, Chrome Web Store पर लॉन्च किया गया था. Chrome Web Store पर रिलीज़ करने से, मुझे खोजे जाने, कम्यूनिटी से जुड़ाव, रैंकिंग, और अन्य सुविधाओं के लिए मौजूदा सिस्टम का फ़ायदा मिल सका. इन सुविधाओं का इस्तेमाल मैंने मोबाइल प्लैटफ़ॉर्म पर किया था. मुझे इस बात से हैरानी हुई कि स्टोर की पहुंच कितनी व्यापक थी. रिलीज़ होने के एक महीने के अंदर, मेरे ऐप्लिकेशन को करीब चार लाख इंस्टॉल मिल गए थे. साथ ही, कम्यूनिटी से जुड़ने (गड़बड़ी की शिकायत करना, सुझाव/राय देना) से मुझे पहले से ही फ़ायदा मिल रहा था. मुझे एक और बात हैरान कर गई. वह यह कि वेब-ऐप्लिकेशन से कमाई की जा सकती है.

Bouncy Mouse में कमाई करने का एक आसान तरीका है - गेम कॉन्टेंट के बगल में बैनर विज्ञापन. हालांकि, गेम की व्यापक पहुंच को देखते हुए, मुझे पता चला कि इस बैनर विज्ञापन से काफ़ी कमाई हुई. साथ ही, गेम के सबसे व्यस्त समय के दौरान, ऐप्लिकेशन से हुई कमाई, मेरे सबसे सफल प्लैटफ़ॉर्म Android से हुई कमाई के बराबर थी. इसकी एक वजह यह है कि HTML5 वर्शन पर दिखाए जाने वाले बड़े AdSense विज्ञापन, Android पर दिखाए जाने वाले छोटे AdMob विज्ञापनों के मुकाबले हर इंप्रेशन से ज़्यादा रेवेन्यू जनरेट करते हैं. इतना ही नहीं, HTML5 वर्शन पर बैनर विज्ञापन, Android वर्शन के मुकाबले बहुत कम परेशानी पहुंचाता है. इससे गेमप्ले का बेहतर अनुभव मिलता है. कुल मिलाकर, मुझे इस नतीजे से काफ़ी खुशी हुई.

समय के साथ सामान्य तौर पर होने वाली आय.
समय के साथ आमदनी में हुई बढ़ोतरी

गेम से होने वाली आय, उम्मीद से काफ़ी ज़्यादा थी. हालांकि, यह ध्यान देने वाली बात है कि Chrome Web Store की पहुंच, Android Market जैसे बेहतर प्लैटफ़ॉर्म की तुलना में अब भी कम है. Bouncy Mouse, Chrome Web Store पर सबसे लोकप्रिय गेम की सूची में तेज़ी से #9 पर पहुंच गया. हालांकि, शुरुआती रिलीज़ के बाद से साइट पर आने वाले नए उपयोगकर्ताओं की संख्या काफ़ी कम हो गई. हालांकि, इस गेम में अब भी लगातार बढ़ोतरी हो रही है. मुझे यह देखकर खुशी हो रही है कि यह प्लैटफ़ॉर्म आगे किस तरह से डेवलप होगा!

नतीजा

मुझे लगता है कि Bouncy Mouse को Chrome पर पोर्ट करना, मेरी उम्मीद से काफ़ी आसान रहा. ऑडियो और परफ़ॉर्मेंस से जुड़ी कुछ छोटी समस्याओं के अलावा, मुझे पता चला कि Chrome, स्मार्टफ़ोन पर मौजूद किसी गेम को चलाने के लिए एक बेहतरीन प्लैटफ़ॉर्म है. हम उन सभी डेवलपर को इसे आज़माने का सुझाव देते हैं जो इस सुविधा को इस्तेमाल करने से डरते हैं. मुझे पोर्ट करने की प्रोसेस और HTML5 गेम की वजह से, गेमिंग की नई ऑडियंस से जुड़ने में बहुत खुशी हो रही है. अगर आपका कोई सवाल है, तो बेझिझक मुझे ईमेल करें. इसके अलावा, यहां टिप्पणी करें. हम समय-समय पर इन टिप्पणियों की समीक्षा करते रहेंगे.