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

शुरुआती जानकारी

बाउंसी माउस

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

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

फ़िलहाल, बाउंसी माउस 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 एक्सएनए
साउंड OpenSL ES OpenAL वेब ऑडियो एक्सएनए
भौतिक विज्ञान बॉक्स 2D बॉक्स 2D Box2D.js Box2D.xna

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

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

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

पोर्ट करने के नतीजे

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

ऑडियो

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

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

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

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

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

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

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

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

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

लेकिन आइए ग्राफ़ के बारे में चिंता न करें

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

AudioClip
AudioClip

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

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

// Later
sound.play();

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

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

/**
* 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();
}

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

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

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

var skip = false;

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

// OTHER CODE
}

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

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

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

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

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

समय के साथ सामान्य कमाई.
समय के साथ सामान्य कमाई

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

नतीजा

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