हर गेम डेवलपमेंट ट्यूटोरियल एक ही तरह से शुरू होता है: एक ग्रे कैनवस पर फिसलते हुए रंगीन आयत (rectangles)। सिंटैक्स सीखने के लिए यह ठीक है, लेकिन यह आपको यह नहीं सिखाता कि एक गेम इंजन वास्तव में कैसे काम करता है। मैं इन आयतों के चक्कर से बाहर निकलना चाहता था। मैं कुछ ऐसा बनाना चाहता था जो एक असली गेम जैसा महसूस हो—मूवमेंट, कॉम्बैट, एक HUD और साउंड के साथ एक टॉप-डाउन डंजन क्रॉलर (top-down dungeon crawler)।

मैंने Phaser v4 का उपयोग करने का निर्णय लिया और खुद के लिए एक सख्त नियम बनाया। शून्य बाहरी एसेट्स (external assets)। कोई इमेज फाइल नहीं, कोई ऑडियो क्लिप नहीं, और Webpack या Vite जैसे कोई बिल्ड टूल्स नहीं। पूरा प्रोजेक्ट केवल प्लेन JavaScript के साथ लिखी गई एक सिंगल HTML फाइल के भीतर होना चाहिए था। वह पाबंदी केवल दिखावे के लिए मिनिमलिज्म (minimalism) नहीं थी। इसका उद्देश्य हर बहाने और हर 'ब्लैक बॉक्स' को हटाना था। जब आप अपने ज्ञान की कमी को छिपाने के लिए कोई स्प्राइट पैक (sprite pack) डाउनलोड नहीं कर सकते, तो आप यह सीखने के लिए मजबूर हो जाते हैं कि इंजन पर्दे के पीछे (under the hood) टेक्सचर्स, एनिमेशन, ऑडियो और स्टेट को कैसे मैनेज करता है।

कोड से दुनिया बनाना

एक सामान्य Phaser प्रोजेक्ट में, आप this.load.image() को preload फंक्शन के अंदर कॉल करते हैं और इंजन को एक PNG फाइल की ओर निर्देशित करते हैं। उस विकल्प के बिना, आप Graphics ऑब्जेक्ट का उपयोग करते हैं। आप इसे इंस्टेंटिएट (instantiate) करते हैं, प्रिमिटिव शेप्स (primitive shapes) बनाते हैं—फ्लोर टाइल्स के लिए आयत, दीवारों के लिए मोटे स्ट्रोक्स, शायद प्लेयर टोकन के लिए एक सर्कल—और फिर generateTexture को कॉल करते हैं। वह मेथड ग्राफिक्स बफर को कैप्चर करता है और उसे आपके द्वारा चुने गए एक की (key) के तहत Phaser के टेक्सचर मैनेजर के साथ रजिस्टर कर देता है।

उस क्षण से, इंजन उस जनरेट किए गए बिटमैप (bitmap) के साथ बिल्कुल एक लोडेड इमेज फाइल की तरह व्यवहार करता है। आप इसे tilemaps में असाइन कर सकते हैं, स्प्राइट्स में स्लाइस कर सकते हैं, या टिंट (tint) कर सकते हैं। डंजन क्रॉलर के लिए, इसका मतलब था कि मैं अपने कोड एडिटर को छोड़े बिना ही प्रोसीजरली (procedurally) एक फ्लोर ग्रिड जनरेट कर सकता था, दीवार के हिस्सों को स्टैम्प कर सकता था, और कलर पैलेट में बदलाव कर सकता था। व्यावहारिक सबक यह है कि एक टेक्सचर मेमोरी में मौजूद बिटमैप डेटा का एक हिस्सा मात्र है। Phaser को इससे कोई फर्क नहीं पड़ता कि वह HTTP रिक्वेस्ट के माध्यम से आया है या हाथ से लिखे गए Graphics कॉल के माध्यम से।

यह दृष्टिकोण आपको ड्रॉ ऑर्डर (draw order) और बैचिंग (batching) के बारे में भी सोच-समझकर काम करने पर मजबूर करता है। जब हर दीवार और फ्लोर टाइल जनरेट किए गए टेक्सचर्स के एक ही परिवार से आती है, तो आप इस बात पर ध्यान देने लगते हैं कि Phaser रेंडर कॉल्स को कैसे ग्रुप करता है। आप एक स्टैटिक tilemap लेयर और व्यक्तिगत ऑब्जेक्ट्स के spritemap के बीच का अंतर नोटिस करते हैं क्योंकि आप मैन्युअल रूप से तय कर रहे होते हैं कि किसे एक टेक्सचर इंस्टेंस के रूप में मौजूद रहना चाहिए।

स्प्राइटशीट्स के बिना एनिमेशन

स्थिर वर्ग (static squares) जल्दी ही उबाऊ हो जाते हैं, लेकिन Phaser का एनिमेशन सिस्टम एक spritesheet की अपेक्षा करता है—आमतौर पर ग्रिड में व्यवस्थित फ्रेम्स वाली एक सिंगल PNG। मैंने एक ऑफस्क्रीन HTML canvas एलिमेंट का उपयोग करके उस स्ट्रिप को रीप्लिकेट किया। एनिमेशन के प्रत्येक फ्रेम के लिए, मैंने कैनवस को क्लियर किया और एक नया पोज़ (pose) बनाया: एक साधारण तलवार का वार, दो-कदम चलने का चक्र, या दुश्मन का स्थिर होकर हिलना (idle bob)। एक बार स्ट्रिप पूरी हो जाने के बाद, मैंने इसे Phaser के साथ एक spritesheet के रूप में रजिस्टर किया, और फ्रेम की चौड़ाई और ऊंचाई को परिभाषित किया ताकि इंजन को पता चल सके कि एक पोज़ कहाँ समाप्त होता है और अगला कहाँ से शुरू होता है।

इसे मैन्युअल रूप से करने से यह स्पष्ट हो जाता है कि सतह के नीचे एनिमेशन वास्तव में क्या है: एक साझा टेक्सचर पर फ्रेम सीमाओं का एक सेट। Phaser का एनिमेशन कंपोनेंट एक स्टार्ट फ्रेम, एक एंड फ्रेम और एक फ्रेम रेट मांगता है। फिर यह गेम क्लॉक की प्रत्येक टिक पर उन आयताकार स्लाइस के माध्यम से एक पॉइंटर को आगे बढ़ाता है। आप स्प्राइटशीट्स को कलाकारों द्वारा बनाए गए जादुई एसेट्स के रूप में सोचना बंद कर देते हैं और उन्हें कोऑर्डिनेट मैथ (coordinate math) के रूप में देखना शुरू कर देते हैं। यह दृष्टिकोण बाद में तब अमूल्य होता है जब आप असली आर्ट को स्लाइस कर रहे होते हैं या यह डीबग कर रहे होते हैं कि एनिमेशन फ्रेम को गलत क्रम में क्यों चला रहा है।

शून्य से ध्वनि

ऑडियो फाइल्स पर प्रतिबंध था, इसलिए मैंने सीधे Web Audio API का उपयोग किया। JavaScript की कुछ लाइनें एक ऑसिलेटर नोड (oscillator node) बना सकती हैं, उसे स्क्वायर या साइन वेव पर सेट कर सकती हैं, उसे गेन नोड (gain node) के माध्यम से भेज सकती हैं, और ध्वनि के एक छोटे विस्फोट (burst) को शेड्यूल कर सकती हैं। मैंने सामान्य घटनाओं के लिए छोटे हेल्पर्स लिखे: कदमों के लिए एक लो-फ्रीक्वेंसी बीप, लूट उठाने के लिए एक बढ़ती हुई चहचहाहट (chirp), और डैमेज लेने के लिए एक कर्कश स्क्वायर-वेव टोन।

ये सिंथेसाइज्ड (synthesized) ध्वनियाँ डिज़ाइन के अनुसार प्लेसहोल्डर्स हैं, लेकिन वे गेम को तुरंत मैकेनिकल फीडबैक देती हैं। असली ऑडियो रिकॉर्ड करने या खोजने से पहले आप महसूस कर सकते हैं कि टाइमिंग सही है या नहीं। असली लाभ आर्किटेक्चर में मिलता है। क्योंकि आपने साउंड ट्रिगर को एक साधारण फंक्शन में लपेटा (wrap) था, इसलिए बाद में सिंथेसाइज्ड बीप को लोडेड साउंड बफर से बदलना केवल एक लाइन का काम है। गेम का बाकी हिस्सा—कोलिजन इवेंट, UI फ्लैश, स्कोर बढ़ना—अछूता रहता है। आप पहले अहसास (feel) का प्रोटोटाइप बनाते हैं, और फिर उसकी गुणवत्ता (fidelity) बढ़ाते हैं।

कई दुनियाओं का प्रबंधन

एक वास्तविक गेम के लिए एक से अधिक स्क्रीन की आवश्यकता होती है, इसलिए मैंने प्रोजेक्ट को अलग-अलग Phaser scenes में विभाजित किया: Menu, Game, UI, और Pause। UI scene, Game scene के साथ समानांतर (parallel) रूप से चलता है, जिन्हें एक साथ लॉन्च किया जाता है ताकि health bar और score counter अपने स्वयं के sandbox में रहें, जबकि नीचे dungeon crawls चलता रहे। वे पूरी तरह से events के माध्यम से संवाद करते हैं। जब खिलाड़ी को नुकसान (damage) पहुँचता है, तो Game scene एक change emit करता है। UI scene इसे सुनता है और अपने text objects को अपडेट करता है। Game scene UI को import नहीं करता, उसके methods को कॉल नहीं करता, या यह भी चेक नहीं करता कि वह मौजूद है या नहीं। यह बस डेटा को शून्य (void) में भेज देता है। इस decoupling का मतलब है कि आप core game loop को छुए बिना, टेस्टिंग के लिए HUD को निकाल सकते हैं या उसे पूरी तरह से बदल सकते हैं।

पॉज़ (pausing) के लिए, मैंने एक stacked Pause scene का उपयोग किया जो Game scene के ऊपर स्थित होता है। महत्वपूर्ण बात यह है कि Game scene पर scene.pause() कॉल करने से वास्तव में physics world फ्रीज हो जाता है और timers रुक जाते हैं। Game scene अपडेट होना बंद कर देता है, लेकिन Pause scene मेनू रेंडर करने और unpause signal का इंतज़ार करने के लिए सक्रिय रहता है। यदि आपने अब तक केवल एक विशाल update loop के भीतर एक boolean flag के साथ pause states को मैनेज किया है, तो यह एक लाइट स्विच खोजने जैसा महसूस होता है। इंजन आपको अपने कोड में if (isPaused) return जैसे guards भरने के लिए मजबूर करने के बजाय एक वास्तविक pause lifecycle प्रदान करता है।

वास्तविक बग्स (Bugs) से मिले कठिन सबक

हार्डवेयर के इतने करीब काम करने से मुझे अपनी दो ऐसी आदतें पता चलीं जिन्हें बदलने की ज़रूरत थी।

सबसे पहले, मैंने Phaser v4 में एक ऐसे method का उपयोग करने की कोशिश की जो public लग रहा था लेकिन documented API का हिस्सा नहीं था। यह वर्शन्स के बीच बदल गया और मेरे build को खराब कर दिया। मैंने getChildren() का उपयोग करने के लिए refactor किया, जो एक stable, documented public method है, और अस्थिरता समाप्त हो गई। सबक सीधा है: यदि कोई method आधिकारिक documentation में नहीं है, तो उस पर अपना गेम न बनाएं। Internal APIs किसी कारण से ही internal होती हैं। Public surface area तक ही सीमित रहें और आपका प्रोजेक्ट engine updates में सुरक्षित रहेगा।

दूसरा, मैंने सीखा कि हर चीज़ के लिए events पर कभी भरोसा न करें। Event callbacks सिक्के उठाने या संदूक खोलने जैसे कम जोखिम वाले (low-stakes) इंटरैक्शन के लिए बहुत अच्छे से काम करते हैं। लेकिन महत्वपूर्ण state transitions—विशेष रूप से Game Over—के लिए, मैंने मुख्य update loop के भीतर एक redundant check जोड़ा। यदि कोई listener हटा दिया जाता है, कोई scene किसी अजीब microsecond पर pause हो जाता है, या emission और handling के बीच कोई race condition आ जाती है, तो events गलत तरीके से काम कर सकते हैं। update loop में सीधे खिलाड़ी के health को चेक करके और शून्य होने पर game-over state को लागू करके, मैंने यह सुनिश्चित किया कि यदि कोई event fire होने में विफल रहता है, तो गेम कभी भी limbo state में नहीं फंसेगा। events अभी भी secondary effects—screen shake, sound cues, score submission—को संभालते हैं, लेकिन authoritative logic वहीं रहता है जहाँ game clock रहता है।

आपको इसे क्यों आज़माना चाहिए

यदि आप game development सीख रहे हैं, तो अपने अगले प्रोजेक्ट पर यह सटीक प्रतिबंध लगाएँ: कोई बाहरी assets नहीं, केवल एक HTML file। यह प्रतिबंधात्मक लगता है, लेकिन यह हर बहाने को खत्म कर देता है। आपको bundler configure करने, local audio files पर CORS errors से जूझने, या free asset packs इकट्ठा करने में दोपहर बिताने की ज़रूरत नहीं है। आप कोड लिखते हैं, ब्राउज़र को refresh करते हैं, और परिणाम देखते हैं।

इससे भी महत्वपूर्ण बात यह है कि आप समझेंगे कि इंजन इस तरह से व्यवहार क्यों करता है। आप जानेंगे कि texture GPU में कैसे प्रवेश करता है क्योंकि आपने generateTexture कॉल किया था। आप जानेंगे कि animation frames को कैसे index किया जाता है क्योंकि आपने सीमाओं (boundaries) को स्वयं रजिस्टर किया था। आप जानेंगे कि audio स्पीकरों तक कैसे पहुँचता है क्योंकि आपने oscillator को वायर किया था। वह ज्ञान सीधे उन बड़े प्रोजेक्ट्स में भी काम आता है जो external assets का उपयोग करते हैं, क्योंकि अंतर्निहित (underlying) मैकेनिक्स कभी नहीं बदलते—इंजन