प्रत्येक गेम डेव्हलपमेंट ट्युटोरियल एकाच पद्धतीने सुरू होते: एका राखाडी कॅनव्हासवर सरकणारे रंगीत आयत (rectangles). सिंटॅक्स शिकण्यासाठी हे ठीक आहे, पण गेम इंजिन प्रत्यक्षात कसे कार्य करते याबद्दल ते तुम्हाला काहीही शिकवत नाही. मला या आयतांच्या खेळातून बाहेर पडायचे होते. मला काहीतरी खऱ्या गेमसारखे बनवायचे होते—हालचाल, युद्ध (combat), HUD आणि ध्वनीसह एक 'top-down dungeon crawler'.
मी Phaser v4 वापरण्याचा निर्णय घेतला आणि स्वतःसाठी एक कडक नियम ठरवला. शून्य बाह्य मालमत्ता (zero external assets). कोणतेही इमेज फाइल्स नाहीत, कोणतेही ऑडिओ क्लिप्स नाहीत आणि Webpack किंवा Vite सारखी कोणतीही बिल्ड टूल्स नाहीत. संपूर्ण प्रोजेक्ट साध्या JavaScript ने लिहिलेल्या एकाच HTML फाईलमध्ये असायला हवा होता. ही मर्यादा केवळ मिनिमलिझमसाठी नव्हती. ती प्रत्येक सबब आणि प्रत्येक 'ब्लॅक बॉक्स' काढून टाकण्यासाठी होती. जेव्हा तुम्ही तुमच्या ज्ञानातील त्रुटी भरून काढण्यासाठी 'sprite pack' डाउनलोड करू शकत नाही, तेव्हा तुम्हाला इंजिन अंतर्गत (under the hood) टेक्सचर्स, ॲनिमेशन, ऑडिओ आणि स्टेट कसे व्यवस्थापित करते हे शिकण्यास भाग पाडले जाते.
कोडमधून जग रेखाटणे
एका सामान्य Phaser प्रोजेक्टमध्ये, तुम्ही preload फंक्शनमध्ये this.load.image() कॉल करता आणि इंजिनला एका PNG फाईलकडे निर्देशित करता. त्या पर्यायाशिवाय, तुम्हाला Graphics ऑब्जेक्टचा वापर करावा लागतो. तुम्ही त्याचे इन्स्टन्सिएट (instantiate) करता, मूलभूत आकार (primitive shapes) काढता—जसे की फ्लोअर टाइल्ससाठी आयत, भिंतींसाठी जाड स्ट्रोक्स, कदाचित प्लेयर टोकनसाठी एक वर्तुळ—आणि नंतर generateTexture कॉल करता. ही पद्धत ग्राफिक्स बफर कॅप्चर करते आणि तुम्ही निवडलेल्या की (key) अंतर्गत Phaser च्या टेक्सचर मॅनेजरमध्ये त्याची नोंदणी करते.
त्या क्षणापासून, इंजिन त्या तयार केलेल्या बिटमॅपला (bitmap) अगदी लोड केलेल्या इमेज फाईलप्रमाणेच वागवते. तुम्ही ते tilemaps ला देऊ शकता, त्याचे स्प्राइट्समध्ये तुकडे करू शकता किंवा त्याला टिंट (tint) करू शकता. डंजियन क्रॉलरसाठी, याचा अर्थ असा होता की मी माझा कोड एडिटर न सोडता फ्लोअर ग्रिड प्रोसिजरली (procedurally) तयार करू शकत होतो, भिंतींचे भाग बसवू शकत होतो आणि कलर पॅलेटमध्ये बदल करू शकत होतो. याचा व्यावहारिक धडा असा आहे की टेक्सचर म्हणजे मेमरीमध्ये असलेला बिटमॅप डेटाचा एक तुकडा आहे. ते HTTP रिक्वेस्टद्वारे आले आहे की हाताने कोड केलेल्या Graphics कॉलद्वारे, याने Phaser ला काहीही फरक पडत नाही.
हा दृष्टिकोन तुम्हाला 'draw order' आणि 'batching' बद्दल विचार करण्यास प्रवृत्त करतो. जेव्हा प्रत्येक भिंत आणि फ्लोअर टाइल एकाच प्रकारच्या तयार केलेल्या टेक्सचर्समधून येते, तेव्हा तुम्ही Phaser कशा प्रकारे रेंडर कॉल्स (render calls) ग्रुप करते याकडे लक्ष देऊ लागता. तुम्हाला एक स्टॅटिक tilemap लेयर आणि वैयक्तिक ऑब्जेक्ट्सचा spritemap मधील फरक जाणवू लागतो, कारण तुम्ही स्वतः ठरवत असता की कोणता घटक टेक्सचर इन्स्टन्स म्हणून अस्तित्वात असणे आवश्यक आहे.
स्प्राइट्सheets शिवाय ॲनिमेशन
स्थिर चौरस लवकर कंटाळवाणे होतात, परंतु Phaser ची ॲनिमेशन प्रणाली एका spritesheet ची अपेक्षा करते—जे सहसा ग्रिडमध्ये मांडलेल्या फ्रेम्स असलेली एक सिंगल PNG असते. मी एका ऑफस्क्रीन HTML canvas एलिमेंटचा वापर करून ती स्ट्रिप तयार केली. ॲनिमेशनच्या प्रत्येक फ्रेमसाठी, मी कॅनव्हास क्लिअर केला आणि एक नवीन पोझ (pose) काढली: तलवारीचा साधा वार, दोन पावले चालण्याचे सायकल, किंवा शत्रूची हालचाल (idle bob). एकदा स्ट्रिप पूर्ण झाली की, मी ती Phaser मध्ये spritesheet म्हणून नोंदवली, फ्रेमची रुंदी आणि उंची परिभाषित केली जेणेकरून इंजिनला समजेल की एक पोझ कुठे संपते आणि दुसरी कुठे सुरू होते.
हे मॅन्युअली केल्यामुळे ॲनिमेशन प्रत्यक्षात काय आहे हे स्पष्ट होते: एका सामायिक टेक्सचरवरील फ्रेम सीमांचा संच. Phaser चे ॲनिमेशन घटक स्टार्ट फ्रेम, एंड फ्रेम आणि फ्रेम रेटची मागणी करते. त्यानंतर ते गेम क्लॉकच्या प्रत्येक टिकवर त्या आयताकृती तुकड्यांमधून पॉइंटर पुढे सरकवते. तुम्ही स्प्राइट्सheets ककडे कलाकारांनी तयार केलेली जादूई मालमत्ता म्हणून पाहणे थांबवता आणि त्यांना 'coordinate math' म्हणून पाहू लागता. जेव्हा तुम्ही प्रत्यक्ष आर्टचे तुकडे करत असता किंवा ॲनिमेशनचे फ्रेम्स चुकीच्या क्रमाने का चालतात याचे डीबगिंग करत असता, तेव्हा हा दृष्टिकोन अत्यंत मोलाचा ठरतो.
शून्यातून ध्वनी
ऑडिओ फाइल्सवर बंदी होती, म्हणून मी थेट Web Audio API वापरला. JavaScript च्या काही ओळी वापरून तुम्ही एक oscillator node तयार करू शकता, त्याला square किंवा sine wave वर सेट करू शकता, त्याला gain node मधून पाठवू शकता आणि ध्वनीचा एक छोटा तुकडा (burst) शेड्यूल करू शकता. मी सामान्य घटनांसाठी लहान हेल्पर्स लिहिले: पावलांच्या आवाजासाठी कमी वारंवारतेचा (low-frequency) बीप, लूट उचलण्यासाठी वाढणारा चर्प (chirp), आणि इजा झाल्यावर (damage) कर्कश square-wave टोन.
हे सिंथेसाईज्ड (synthesized) आवाज डिझाइननुसार केवळ प्लेसहोल्डर्स आहेत, परंतु ते गेमला त्वरित मेकॅनिकल फीडबॅक देतात. प्रत्यक्ष ऑडिओ रेकॉर्ड करण्यापूर्वी किंवा शोधण्यापूर्वी टायमिंग योग्य आहे की नाही हे तुम्हाला जाणवू शकते. याचा खरा फायदा आर्किटेक्चरमध्ये मिळतो. कारण तुम्ही साऊंड ट्रिगर एका साध्या फंक्शनमध्ये गुंडाळला (wrap) आहे, त्यामुळे नंतर सिंथेसाईज्ड बीपच्या जागी लोड केलेला साऊंड बफर वापरणे म्हणजे केवळ एका ओळीचा बदल आहे. गेमचा बाकी भाग—collision event, UI flash, score increment—तसाच राहतो. तुम्ही प्रथम अनुभवाचा प्रोटोटाइप तयार करता आणि नंतर त्याची गुणवत्ता (fidelity) वाढवता.
अनेक जग व्यवस्थापित करणे
एका खऱ्या गेमसाठी एकापेक्षा जास्त स्क्रीनची आवश्यकता असते, म्हणून मी प्रोजेक्टला वेगळ्या Phaser scenes मध्ये विभागले: Menu, Game, UI, आणि Pause. UI scene ही Game scene सोबत समांतर चालते, जी एकाच वेळी सुरू केली जाते जेणेकरून हेल्थ बार आणि स्कोअर काउंटर त्यांच्या स्वतःच्या sandbox मध्ये राहतील, तर खाली dungeon crawls सुरू राहतील. ते केवळ events द्वारे संवाद साधतात. जेव्हा खेळाडूला इजा होते, तेव्हा Game scene एक बदल (change) सूचित करते. UI scene ते ऐकते आणि तिचे text objects अपडेट करते. Game scene UI ला import करत नाही, त्याच्या methods ला कॉल करत नाही किंवा ती अस्तित्वात आहे की नाही हे देखील तपासत नाही. ती फक्त डेटा हवेत (void मध्ये) पाठवते. या decoupling मुळे तुम्ही मुख्य game loop ला स्पर्श न करता, टेस्टिंगसाठी HUD काढून टाकू शकता किंवा तो पूर्णपणे बदलू शकता.
पॉझ (pause) करण्यासाठी, मी Game scene च्या वर एक stacked Pause scene वापरली. महत्त्वाचे म्हणजे, Game scene वर scene.pause() कॉल केल्यामुळे प्रत्यक्षात physics world गोठते आणि timers थांबतात. Game scene अपडेट होणे थांबवते, परंतु Pause scene मेनू रेंडर करण्यासाठी आणि unpause सिग्नलची प्रतीक्षा करण्यासाठी सक्रिय राहते. जर तुम्ही आतापर्यंत एका मोठ्या update loop मध्ये केवळ boolean flag वापरून pause states हाताळल्या असतील, तर हे एखाद्या लाइट स्विचचा शोध लावल्यासारखे वाटते. इंजिन तुम्हाला तुमच्या कोडमध्ये वारंवार if (isPaused) return सारखे guards वापरण्यास भाग पाडण्याऐवजी एक खरा pause lifecycle प्रदान करते.
खऱ्या Bugs मधून मिळालेले कठीण धडे
इंजिनच्या इतक्या जवळ काम केल्यामुळे मला बदलण्याची गरज असलेल्या दोन सवयी समजल्या.
पहिले म्हणजे, मी Phaser v4 मधील अशा एका method चा वापर करण्याचा प्रयत्न केला जो public वाटत होता पण तो documented API चा भाग नव्हता. आवृत्त्यांमध्ये (versions) तो बदलला आणि त्यामुळे माझे build बिघडले. मी getChildren() वापरण्यासाठी refactor केले, जे एक stable आणि documented public method आहे, आणि ती अस्थिरता निघून गेली. धडा स्पष्ट आहे: जर एखादी method अधिकृत documentation मध्ये नसेल, तर त्यावर तुमचा गेम तयार करू नका. Internal APIs काही कारणास्तव internal ठेवल्या जातात. Public surface area ला धरून राहा आणि तुमचा प्रोजेक्ट engine updates मध्ये टिकून राहील.
दुसरे म्हणजे, मी शिकलो की प्रत्येक गोष्टीसाठी events वर कधीही अवलंबून राहू नका. कॉइन उचलणे किंवा चेस्ट उघडणे यांसारख्या कमी महत्त्वाच्या (low-stakes) संवादांसाठी event callbacks उत्तम काम करतात. परंतु महत्त्वाच्या state transitions साठी—विशेषतः Game Over साठी—मी मुख्य update loop मध्ये एक redundant check जोडला. जर एखादा listener काढून टाकला असेल, एखादी scene चुकीच्या microsecond मध्ये pause झाली असेल, किंवा emission आणि handling च्या दरम्यान race condition निर्माण झाली असेल, तर events चुकण्याची शक्यता असते. update loop मध्ये थेट खेळाडूचे आरोग्य (health) तपासून आणि ते शून्य झाल्यास game-over state लागू करून, मी हे सुनिश्चित केले की जर एखादा event फायर होण्यास अपयशी ठरला, तरी गेम कधीही limbo state मध्ये अडकून पडणार नाही. events अजूनही दुय्यम परिणाम हाताळतात—जसे की screen shake, sound cues, score submission—परंतु मुख्य (authoritative) logic तिथेच असते जिथे game clock असते.
तुम्ही हे का प्रयत्न केले पाहिजे
जर तुम्ही game development शिकत असाल, तर तुमच्या पुढच्या प्रोजेक्टवर हीच अट घाला: कोणतेही external assets नको, फक्त एक HTML file. हे मर्यादित वाटते, पण ते प्रत्येक सबब (excuse) काढून टाकते. तुम्हाला bundler कॉन्फिगर करण्याची, local audio files वरील CORS errors शी झुंज देण्याची किंवा मोफत asset packs गोळा करण्यासाठी पूर्ण दुपार खर्च करण्याची गरज नाही. तुम्ही कोड लिहिता, ब्राउझर रिफ्रेश करता आणि तुम्हाला निकाल दिसतात.
अधिक महत्त्वाचे म्हणजे, इंजिन का आणि कसे वागते हे तुम्हाला समजेल. generateTexture कॉल केल्यामुळे texture GPU मध्ये कसे प्रवेश करते हे तुम्हाला समजेल. animation frames कसे index केले जातात हे तुम्हाला समजेल कारण तुम्ही स्वतः सीमा (boundaries) नोंदवल्या असतील. audio स्पीकर्सपर्यंत कसे पोहोचते हे तुम्हाला समजेल कारण तुम्ही oscillator जोडला असेल. हे ज्ञान थेट मोठ्या प्रोजेक्ट्समध्ये वापरता येते जे external assets वापरतात, कारण त्यातील मूळ यंत्रणा (underlying mechanics) कधीही बदलत नाही—इंजिन
