દરેક ગેમ ડેવલપમેન્ટ ટ્યુટોરિયલ એક જ રીતે શરૂ થાય છે: ગ્રે કેનવાસ પર સરકતા રંગીન લંબચોરસ (rectangles). સિન્ટેક્સ શીખવા માટે તે ઠીક છે, પરંતુ તે તમને ગેમ એન્જિન ખરેખર કેવી રીતે કાર્ય કરે છે તે વિશે કંઈ જ શીખવતું નથી. મારે આ લંબચોરસના વ્યવસાયમાંથી બહાર નીકળવું હતું. મારે કંઈક એવું બનાવવું હતું જે ખરેખર ગેમ જેવું લાગે—હલનચલન (movement), કોમ્બેટ (combat), HUD અને સાઉન્ડ સાથેનું એક ટોપ-ડાઉન ડંજિયન ક્રોલર (top-down dungeon crawler).

મેં Phaser v4 નો ઉપયોગ કરવાનો નિર્ણય લીધો અને મારી જાતને એક કડક નિયમ આપ્યો. શૂન્ય બાહ્ય એસેટ્સ (external assets). કોઈ ઈમેજ ફાઈલ નહીં, કોઈ ઓડિયો ક્લિપ નહીં, અને Webpack અથવા Vite જેવા કોઈ બિલ્ડ ટૂલ્સ નહીં. આખું પ્રોજેક્ટ પ્લેન JavaScript સાથે લખાયેલી એક જ HTML ફાઈલમાં હોવું જોઈએ. આ મર્યાદા માત્ર મિનિમલિઝમ માટે નહોતી. તે દરેક બહાના અને દરેક 'બ્લેક બોક્સ' ને દૂર કરવા વિશે હતી. જ્યારે તમે તમારા જ્ઞાનની ખામીઓને છુપાવવા માટે સ્પ્રાઈટ પેક (sprite pack) ડાઉનલોડ કરી શકતા નથી, ત્યારે તમે એન્જિન અંદરથી ટેક્સચર (textures), એનિમેશન (animations), ઓડિયો (audio) અને સ્ટેટ (state) ને કેવી રીતે મેનેજ કરે છે તે શીખવા માટે મજબૂર બનો છો.

કોડ દ્વારા વિશ્વનું નિર્માણ

સામાન્ય Phaser પ્રોજેક્ટમાં, તમે preload ફંક્શનની અંદર this.load.image() કોલ કરો છો અને એન્જિનને PNG ફાઈલ તરફ નિર્દેશિત કરો છો. તે વિકલ્પ વિના, તમારે Graphics ઓબ્જેક્ટનો ઉપયોગ કરવો પડે છે. તમે તેને ઇન્સ્ટન્શિયેટ (instantiate) કરો છો, પ્રિમીટિવ આકારો દોરો છો—ફ્લોર ટાઇલ્સ માટે લંબચોરસ, દિવાલો માટે જાડા સ્ટ્રોક્સ, કદાચ પ્લેયર ટોકન માટે એક વર્તુળ—અને પછી generateTexture કોલ કરો છો. તે મેથડ ગ્રાફિક્સ બફરને કેપ્ચર કરે છે અને તમે પસંદ કરેલી કી (key) હેઠળ Phaser ના ટેક્સચર મેનેજર સાથે તેને રજિસ્ટર કરે છે.

તે ક્ષણથી, એન્જિન તે જનરેટ થયેલ બીટમેપ (bitmap) ને લોડ કરેલી ઈમેજ ફાઈલની જેમ જ ગણે છે. તમે તેને tilemaps માં અસાઇન કરી શકો છો, તેને સ્પ્રાઈટ્સમાં કાપી શકો છો અથવા તેને ટિન્ટ (tint) કરી શકો છો. ડંજિયન ક્રોલર માટે, આનો અર્થ એ હતો કે હું મારા કોડ એડિટરને છોડ્યા વિના જ પ્રોસિજરલી (procedurally) ફ્લોર ગ્રીડ જનરેટ કરી શકતો હતો, દિવાલના ભાગો મૂકી શકતો હતો અને કલર પેલેટમાં ફેરફાર કરી શકતો હતો. વ્યવહારુ પાઠ એ છે કે ટેક્સચર એ મેમરીમાં રહેલા બીટમેપ ડેટાનો એક ભાગ છે માત્ર. Phaser ને તેનાથી કોઈ ફરક પડતો નથી કે તે HTTP રિક્વેસ્ટ દ્વારા આવ્યું છે કે હેન્ડ-કોડેડ Graphics કોલ દ્વારા.

આ અભિગમ તમને ડ્રો ઓર્ડર (draw order) અને બેચિંગ (batching) વિશે પણ વિચારતા કરી દે છે. જ્યારે દરેક દિવાલ અને ફ્લોર ટાઇલ જનરેટ થયેલા ટેક્સચરના સમાન પરિવારમાંથી આવે છે, ત્યારે તમે Phaser કેવી રીતે રેન્ડર કોલ્સ (render calls) ને ગ્રુપ કરે છે તેના પર ધ્યાન આપવાનું શરૂ કરો છો. તમે સ્ટેટિક ટાઈલમેપ લેયર અને વ્યક્તિગત ઓબ્જેક્ટ્સના સ્પ્રાઈટમેપ વચ્ચેનો તફાવત નોંધી શકો છો કારણ કે તમે જાતે નક્કી કરી રહ્યા છો કે કયું ટેક્સચર ઇન્સ્ટન્સ તરીકે અસ્તિત્વમાં હોવું જોઈએ.

સ્પ્રાઈટશીટ્સ વગર એનિમેશન

સ્થિર ચોરસ (Static squares) ઝડપથી કંટાળાજનક બની જાય છે, પરંતુ Phaser ની એનિમેશન સિસ્ટમ સ્પ્રાઈટશીટ (spritesheet) ની અપેક્ષા રાખે છે—સામાન્ય રીતે ગ્રીડમાં ગોઠવેલા ફ્રેમ્સ સાથેની એક સિંગલ PNG. મેં એક ઓફસ્ક્રીન HTML canvas એલિમેન્ટનો ઉપયોગ કરીને તે સ્ટ્રીપની નકલ કરી. એનિમેશનની દરેક ફ્રેમ માટે, મેં કેનવાસ ક્લિયર કર્યું અને નવો પોઝ (pose) દોર્યો: એક સાદો તલવારનો ઘા, બે-સ્ટેપ વૉક સાયકલ, અથવા દુશ્મનનો આઈડલ બોબ (idle bob). એકવાર સ્ટ્રીપ પૂર્ણ થઈ જાય પછી, મેં તેને Phaser સાથે સ્પ્રાઈટશીટ તરીકે રજિસ્ટર કરી, ફ્રેમની પહોળાઈ અને ઊંચાઈ વ્યાખ્યાયિત કરી જેથી એન્જિન જાણી શકે કે એક પોઝ ક્યાં સમાપ્ત થાય છે અને બીજો ક્યાંથી શરૂ થાય છે.

આ જાતે કરવાથી એ સમજાય છે કે સપાટીની નીચે એનિમેશન ખરેખર શું છે: શેર કરેલા ટેક્સચર પર ફ્રેમની સીમાઓનો એક સેટ. Phaser નું એનિમેશન કમ્પોનન્ટ સ્ટાર્ટ ફ્રેમ, એન્ડ ફ્રેમ અને ફ્રેમ રેટ માંગે છે. તે પછી ગેમ ક્લોકના દરેક ટિક પર તે લંબચોરસ સ્લાઈસ દ્વારા પોઈન્ટરને આગળ વધારે છે. તમે સ્પ્રાઈટશીટ્સને કલાકારો દ્વારા બનાવવામાં આવેલી જાદુઈ એસેટ્સ તરીકે વિચારવાનું બંધ કરો છો અને તેને કોઓર્ડિનેટ મેથ (coordinate math) તરીકે જોવા માંડો છો. જ્યારે તમે વાસ્તવિક આર્ટને સ્લાઈસ કરી રહ્યા હોવ અથવા એનિમેશન શા માટે ક્રમ બહારના ફ્રેમ્સ ચલાવે છે તેનું ડીબગિંગ કરી રહ્યા હોવ ત્યારે આ દ્રષ્ટિકોણ અમૂલ્ય છે.

શૂન્યમાંથી અવાજ

ઓડિયો ફાઈલો પર પ્રતિબંધ હતો, તેથી મેં સીધું Web Audio API નો ઉપયોગ કર્યો. JavaScript ની થોડી લાઈનો એક ઓસિલેટર નોડ (oscillator node) બનાવી શકે છે, તેને સ્ક્વેર અથવા સાઈન વેવ પર સેટ કરી શકે છે, તેને ગેઈન નોડ (gain node) દ્વારા પાઇપ કરી શકે છે અને અવાજના ટૂંકા વિસ્ફોટનું શેડ્યૂલ કરી શકે છે. મેં સામાન્ય ઇવેન્ટ્સ માટે નાના હેલ્પર્સ લખ્યા: ડગલાં માટે લો-ફ્રીક્વન્સી બીપ, લૂટ લેવા માટે વધતો જતો ચર્પ (chirp), અને નુકસાન (damage) થવા માટે કઠોર સ્ક્વેર-વેવ ટોન.

આ સિન્થેસાઇઝ્ડ (synthesized) અવાજો ડિઝાઇન મુજબ પ્લેસહોલ્ડર્સ છે, પરંતુ તેઓ ગેમને તરત જ મિકેનિકલ ફીડબેક આપે છે. તમે વાસ્તવિક ઓડિયો રેકોર્ડ કરવા અથવા મેળવતા પહેલા સમય (timing) યોગ્ય છે કે નહીં તે અનુભવી શકો છો. સાચો ફાયદો આર્કિટેક્ચરમાં મળે છે. કારણ કે તમે સાઉન્ડ ટ્રિગરને એક સાદા ફંક્શનમાં લપેટી દીધું છે, તેથી પછીથી સિન્થેસાઇઝ્ડ બીપને લોડ કરેલા સાઉન્ડ બફર સાથે બદલવા માટે માત્ર એક લાઇનનો ફેરફાર જરૂરી છે. ગેમનો બાકીનો ભાગ—કોલિઝન ઇવેન્ટ, UI ફ્લેશ, સ્કોર ઇન્ક્રીમેન્ટ—અસ્પૃશ્ય રહે છે. તમે પહેલા અનુભવનું પ્રોટોટાઇપ બનાવો છો, પછી તેની ગુણવત્તા (fidelity) અપગ્રેડ કરો છો.

મલ્ટિપલ વર્લ્ડ્સનું મેનેજમેન્ટ

એક સાચી ગેમ માટે એકથી વધુ સ્ક્રીનની જરૂર હોય છે, તેથી મેં પ્રોજેક્ટને અલગ Phaser scenes માં વિભાજિત કર્યો: Menu, Game, UI, અને Pause. UI scene, Game scene સાથે સમાંતર ચાલે છે, જે એકસાથે શરૂ થાય છે જેથી health bar અને score counter તેમના પોતાના sandbox માં રહે અને નીચે dungeon crawls ચાલતું રહે. તેઓ ફક્ત events દ્વારા જ વાતચીત કરે છે. જ્યારે પ્લેયરને નુકસાન (damage) થાય છે, ત્યારે Game scene એક ફેરફાર (change) emit કરે છે. UI scene તેને સાંભળે છે અને તેના text objects ને અપડેટ કરે છે. Game scene UI ને import કરતું નથી, તેના methods ને call કરતું નથી, અથવા તે અસ્તિત્વમાં છે કે નહીં તેની તપાસ પણ કરતું નથી. તે ફક્ત ડેટાને 'void' માં મોકલી દે છે. આ decoupling નો અર્થ એ છે કે તમે core game loop ને અડ્યા વગર, ટેસ્ટિંગ માટે HUD ને કાઢી શકો છો અથવા તેને સંપૂર્ણપણે બદલી શકો છો.

Pause કરવા માટે, મેં એક stacked Pause scene નો ઉપયોગ કર્યો જે Game scene ની ઉપર રહે છે. મહત્વપૂર્ણ રીતે, Game scene પર scene.pause() call કરવાથી ખરેખર physics world થીજી જાય છે અને timers અટકી જાય છે. Game scene અપડેટ થવાનું બંધ કરી દે છે, પરંતુ Pause scene મેનુ રેન્ડર કરવા અને unpause signal ની રાહ જોવા માટે સક્રિય રહે છે. જો તમે અત્યાર સુધી માત્ર એક વિશાળ update loop ની અંદર boolean flag દ્વારા pause states ને મેનેજ કર્યા હોય, તો આ અનુભવ લાઈટ સ્વીચ શોધવા જેવો લાગશે. એન્જિન તમને તમારા કોડમાં if (isPaused) return જેવા guards નાખવા માટે મજબૂર કરવાને બદલે એક સાચું pause lifecycle આપે છે.

વાસ્તવિક Bugs માંથી મળેલા કઠિન પાઠ

આટલી નજીકથી (working close to the metal) કામ કરવાથી એવી બે આદતો સામે આવી જેને બદલવાની જરૂર હતી.

પ્રથમ, મેં 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) તપાસ ઉમેરી. જો listener ને દૂર કરવામાં આવે, કોઈ scene અણધારી microsecond પર pause થઈ જાય, અથવા emission અને handling વચ્ચે race condition આવી જાય, તો events ખોટા રીતે કામ કરી શકે છે. update loop માં સીધી પ્લેયરની health તપાસીને અને જો તે શૂન્ય થઈ જાય તો game-over state ને ફરજિયાત બનાવીને, મેં એ સુનિશ્ચિત કર્યું કે જો કોઈ event ફાયર કરવામાં નિષ્ફળ જાય, તો ગેમ ક્યારેય limbo state માં ફસાઈ ન જાય. events હજુ પણ secondary effects—screen shake, sound cues, score submission—ને સંભાળે છે, પરંતુ મુખ્ય (authoritative) logic ત્યાં રહે છે જ્યાં game clock રહે છે.

તમારે આ શા માટે અજમાવવું જોઈએ

જો તમે game development શીખી રહ્યા હોવ, તો તમારા આગામી પ્રોજેક્ટ પર આ જ મર્યાદા લાદો: કોઈ external assets નહીં, માત્ર એક HTML file. તે પ્રતિબંધિત લાગે છે, પરંતુ તે દરેક બહાના દૂર કરે છે. તમારે bundler configure કરવાની, local audio files પર CORS errors સાથે લડવાની, અથવા મફત asset packs શોધવામાં આખું બપોર વિતાવવાની જરૂર નથી. તમે કોડ લખો છો, બ્રાઉઝર refresh કરો છો, અને પરિણામો જુઓ છો.

વધુ મહત્વનું એ છે કે, એન્જિન કેમ આ રીતે વર્તે છે તે તમે સમજી શકશો. તમે જાણશો કે texture GPU માં કેવી રીતે પ્રવેશે છે કારણ કે તમે generateTexture call કર્યું હતું. તમે જાણશો કે animation frames કેવી રીતે index થાય છે કારણ કે તમે તેની સીમાઓ (boundaries) જાતે નોંધાવી હતી. તમે જાણશો કે audio સ્પીકરમાં કેવી રીતે પહોંચે છે કારણ કે તમે oscillator ને વાયરિંગ કર્યું હતું. તે જ્ઞાન સીધું જ એવા મોટા પ્રોજેક્ટ્સમાં ઉપયોગી થશે જે external assets નો ઉપયોગ કરે છે, કારણ કે તેની પાયાની મિકેનિક્સ ક્યારેય બદલાતી નથી—એન્જિન