ஒவ்வொரு கேம் டெவலப்மென்ட் டுடோரியலும் ஒரே மாதிரியாகத்தான் தொடங்கும்: ஒரு சாம்பல் நிற கேன்வாஸில் வண்ணச் சதுரங்கள் நகர்ந்து கொண்டிருப்பது போல. இது தொடக்கநிலை மொழியமைப்பை (syntax) கற்க உதவியாக இருக்கலாம், ஆனால் ஒரு கேம் என்ஜின் (game engine) உண்மையில் எவ்வாறு இயங்குகிறது என்பதைப் பற்றி இது உங்களுக்கு எதையும் கற்பிப்பதில்லை. நான் அந்தச் சதுரங்களை உருவாக்குவதிலிருந்து வெளியேற விரும்பினேன். நகர்வு (movement), போர்முறை (combat), ஒரு HUD மற்றும் ஒலி ஆகியவற்றுடன் கூடிய ஒரு உண்மையான கேம் போலத் தோற்றமளிக்கும் ஒரு top-down dungeon crawler விளையாட்டை உருவாக்க விரும்பினேன்.

நான் Phaser v4-ஐத் தேர்ந்தெடுத்துக்கொண்டு எனக்கு ஒரு கடுமையான விதியை வகுத்துக் கொண்டேன். வெளிப்படையான சொத்துக்கள் (external assets) எதுவுமே இருக்கக்கூடாது. எந்தப் படக் கோப்புகளும் (image files), ஆடியோ கிளிப்களும் இல்லை, மேலும் Webpack அல்லது Vite போன்ற பில்ட் டூல்களும் (build tools) இல்லை. முழுத் திட்டமும் சாதாரண JavaScript மூலம் எழுதப்பட்ட ஒரு ஒற்றை HTML கோப்பிற்குள்ளேயே இருக்க வேண்டும். அந்தத் தடையானது வெறும் எளிமைக்காக (minimalism) மட்டும் அல்ல. அது ஒவ்வொரு சாக்குப்போக்கையும், ஒவ்வொரு மர்மமான செயல்பாட்டையும் (black box) நீக்குவதற்காகவே இருந்தது. உங்கள் அறிவில் உள்ள இடைவெளியை மறைக்க நீங்கள் ஒரு sprite pack-ஐப் பதிவிறக்கம் செய்ய முடியாதபோது, என்ஜின் எவ்வாறு டெக்ஸ்சர்கள் (textures), அனிமேஷன்கள் (animations), ஆடியோ மற்றும் நிலைகளை (state) உள்ளுக்குள் கையாள்கிறது என்பதை நீங்கள் கற்க வேண்டிய கட்டாயத்திற்குத் தள்ளப்படுகிறீர்கள்.

குறியீட்டின் மூலம் உலகத்தை வரைதல்

ஒரு சாதாரண Phaser திட்டத்தில், நீங்கள் this.load.image() என்பதை ஒரு preload செயல்பாட்டிற்குள் அழைத்து, ஒரு PNG கோப்பை நோக்கி என்ஜினைத் திருப்புவீர்கள். அந்த விருப்பம் இல்லாமல், நீங்கள் Graphics பொருளைப் (object) பயன்படுத்துவீர்கள். அதை நீங்கள் உருவாக்கி (instantiate), அடிப்படை வடிவங்களை—தரைத் தட்டுகளுக்கு (floor tiles) சதுரங்கள், சுவர்களுக்குத் தடிமனான கோடுகள், ஒருவேளை வீரரின் அடையாளத்திற்கு ஒரு வட்டம்—வரைந்துவிட்டு, பின்னர் generateTexture-ஐ அழைப்பீர்கள். அந்த முறை (method) கிராபிக்ஸ் பஃபரை (graphics buffer) பிடித்து, நீங்கள் தேர்ந்தெடுக்கும் ஒரு திறவுகோலின் (key) கீழ் Phaser-ன் டெக்ஸ்சர் மேலாளருடன் (texture manager) பதிவு செய்கிறது.

அந்தத் தருணத்திலிருந்து, என்ஜின் அந்த உருவாக்கப்பட்ட பிட்மேப்பை (bitmap) ஒரு லோட் செய்யப்பட்ட படக் கோப்பைப் போலவே கையாளும். நீங்கள் அதை tilemaps-களுக்கு ஒதுக்கலாம், ஸ்பிரைட்களாக (sprites) துண்டாக்கலாம் அல்லது அதன் நிறத்தை மாற்றலாம் (tint). இந்த dungeon crawler-க்கு, இதன் பொருள் என்னவென்றால், எனது கோட் எடிட்டரை விட்டு வெளியேறாமலேயே, ஒரு தரைத் தளத்தை (floor grid) தானியங்கி முறையில் உருவாக்கவும் (procedurally generate), சுவரைத் துண்டுகளைத் தட்டவும், வண்ணத் தேர்வுகளை (color palette) மாற்றியமைக்கவும் என்னால் முடியும் என்பதாகும். இதன் நடைமுறைப் பாடம் என்னவென்றால், ஒரு டெக்ஸ்சர் என்பது நினைவகத்தில் (memory) இருக்கும் ஒரு பிட்மேப் தரவுத் துண்டு மட்டுமே என்பதாகும். அது HTTP கோரிக்கை மூலம் வந்ததா அல்லது கைமுறையாக எழுதப்பட்ட Graphics அழைப்பு மூலம் வந்ததா என்பதை Phaser கவலைப்படுவதில்லை.

இந்த அணுகுமுறை வரைதல் வரிசை (draw order) மற்றும் பேட்ச்சிங் (batching) பற்றி நீங்கள் நிதானமாகச் சிந்திக்கவும் வைக்கிறது. ஒவ்வொரு சுவரும் தரைத் தட்டுகளும் ஒரே மாதிரியான உருவாக்கப்பட்ட டெக்ஸ்சர்களிலிருந்து வரும்போது, Phaser எவ்வாறு ரெண்டர் அழைப்புகளை (render calls) குழுவாக்குகிறது என்பதில் நீங்கள் கவனம் செலுத்தத் தொடங்குவீர்கள். ஒரு நிலையான tilemap அடுக்குக்கும் (static tilemap layer), தனித்தனிப் பொருட்களின் ஸ்பிரைட்மேப்பிற்கும் (spritemap) இடையிலான வித்தியாசத்தை நீங்கள் கவனிப்பீர்கள், ஏனெனில் எது ஒரு டெக்ஸ்சர் இன்ஸ்டன்ஸ் (texture instance) ஆக இருக்க வேண்டும் என்பதை நீங்களே கைமுறையாகத் தீர்மானிக்கிறீர்கள்.

Spritesheets இல்லாமல் அனிமேஷன் செய்தல்

நிலையான சதுரங்கள் விரைவில் சலிப்பைத் தரும், ஆனால் Phaser-ன் அனிமேஷன் அமைப்பு ஒரு spritesheet-ஐ எதிர்பார்க்கிறது—பொதுவாக ஒரு கட்டத்தில் (grid) அமைக்கப்பட்ட பிரேம்களைக் கொண்ட ஒரு ஒற்றை PNG கோப்பு. ஒரு ஆஃப்ஸ்கிரீன் (offscreen) HTML canvas உறுப்பைப் பயன்படுத்தி அந்தத் தொடரை நான் உருவாக்கினேன். அனிமேஷனின் ஒவ்வொரு பிரேமிற்கும், நான் கேன்வாஸைத் துடைத்துவிட்டு ஒரு புதிய நிலையை (pose) வரைந்தேன்: ஒரு எளிய வாள் வீச்சு, இரண்டு படி நடக்கும் சுழற்சி அல்லது ஒரு எதிரியின் அசையும் நிலை. அந்தத் தொடர் முடிந்ததும், பிரேமின் அகலம் மற்றும் உயரத்தை வரையறுத்து, அதை Phaser-உடன் ஒரு spritesheet ஆகப் பதிவு செய்தேன், இதன் மூலம் ஒரு நிலை எங்கு முடிந்து அடுத்தது எங்கு தொடங்குகிறது என்பதை என்ஜின் அறியும்.

இதை கைமுறையாகச் செய்வது, அனிமேஷன் என்பது உண்மையில் என்ன என்பதை வெளிப்படுத்துகிறது: ஒரு பகிரப்பட்ட டெக்ஸ்சரில் உள்ள பிரேம் எல்லைகளின் தொகுப்பு. Phaser-ன் அனிமேஷன் காம்போனென்ட் (animation component) ஒரு தொடக்க பிரேம், ஒரு முடிவு பிரேம் மற்றும் ஒரு பிரேம் ரேட் (frame rate) ஆகியவற்றைத் கேட்கிறது. பின்னர் அது கேம் கடிகாரத்தின் ஒவ்வொரு திக் (tick) நேரத்திலும் அந்தச் சதுரத் துண்டுகள் வழியாக ஒரு பாயிண்டரை (pointer) நகர்த்துகிறது. நீங்கள் spritesheets-களை கலைஞர்களால் உருவாக்கப்பட்ட மாயாஜால சொத்துக்களாகக் கருதுவதை நிறுத்திவிட்டு, அவற்றை ஒரு ஆயத்தொலைவு கணிதமாக (coordinate math) பார்க்கத் தொடங்குவீர்கள். உண்மையான கலைப் படைப்புகளைத் துண்டாக்கும்போதோ அல்லது ஒரு அனிமேஷன் ஏன் வரிசை தவறி இயங்குகிறது என்பதைத் திருத்தும்போதோ (debugging) இந்தத் பார்வை மிகவும் பயனுள்ளதாக இருக்கும்.

எதுவுமின்றி ஒலி உருவாக்குதல்

ஆடியோ கோப்புகள் தடை செய்யப்பட்டிருந்ததால், நான் நேரடியாக Web Audio API-ஐப் பயன்படுத்தினேன். சில வரிகள் JavaScript மூலம் ஒரு ஆஸிலேட்டர் நோடை (oscillator node) உருவாக்கலாம், அதை ஒரு ஸ்கொயர் அல்லது சைன் அலையாக (square or sine wave) அமைக்கலாம், அதை ஒரு கெயின் நோட் (gain node) வழியாகச் செலுத்தலாம் மற்றும் ஒரு குறுகிய கால ஒலியைத் திட்டமிடலாம். பொதுவான நிகழ்வுகளுக்காக நான் சிறிய உதவியாளர்களை (helpers) எழுதினேன்: காலடிச் சத்தத்திற்கு ஒரு குறைந்த அதிர்வெண் பீப் (low-frequency beep), பொருட்களை எடுப்பதற்கு ஒரு ஏறும் சத்தம் (rising chirp), மற்றும் சேதமடைவதற்கு ஒரு கடுமையான ஸ்கொயர்-வேவ் டோன் (square-wave tone).

இந்தத் தொகுக்கப்பட்ட ஒலிகள் (synthesized sounds) வடிவமைப்பிலேயே ஒரு இடத்தைப் பிடிக்கும் placeholders மட்டுமே, ஆனால் அவை கேமிற்கு உடனடியாக இயந்திர ரீதியான பின்னூட்டத்தை (mechanical feedback) வழங்குகின்றன. உண்மையான ஆடியோவை பதிவு செய்வதற்கு அல்லது தேடுவதற்கு முன்பே, அதன் நேரம் சரியாக இருக்கிறதா என்பதை உங்களால் உணர முடியும். இதன் உண்மையான பலன் அதன் கட்டமைப்பில் (architecture) உள்ளது. நீங்கள் ஒலித் தூண்டுதலை (sound trigger) ஒரு எளிய செயல்பாட்டில் (function) சுற்றியுள்ளதால், பின்னர் அந்தத் தொகுக்கப்பட்ட பீப்பிற்குப் பதிலாக ஒரு லோட் செய்யப்பட்ட சவுண்ட் பஃபரை (sound buffer) மாற்றுவதற்கு ஒரே ஒரு வரியை மாற்றினால் போதும். கேமின் மற்ற பகுதிகள்—மோதல் நிகழ்வு (collision event), UI பிளாஷ், ஸ்கோர் அதிகரிப்பு—மாற்றமின்றி அப்படியே இருக்கும். நீங்கள் முதலில் உணர்வை (feel) முன்மாதிரியாக (prototype) உருவாக்கிவிட்டு, பின்னர் அதன் தரத்தை (fidelity) மேம்படுத்தலாம்.

பல உலகங்களை நிர்வகித்தல்

ஒரு உண்மையான விளையாட்டுக்கு ஒன்றுக்கும் மேற்பட்ட திரைகள் தேவை, எனவே நான் திட்டத்தை தனித்தனி Phaser scenes ஆகப் பிரித்தேன்: Menu, Game, UI, மற்றும் Pause. UI scene, Game scene உடன் இணையாக இயங்குகிறது; health bar மற்றும் score counter ஆகியவை அவற்றிற்கெனத் தனித்தனி சூழலில் (sandbox) இயங்கும் அதே வேளையில், கீழே dungeon crawls நடைபெறுகிறது. அவை நிகழ்வுகள் (events) மூலம் மட்டுமே தொடர்பு கொள்கின்றன. வீரருக்குத் தீங்கு ஏற்படும் போது, Game scene ஒரு மாற்றத்தை (change) வெளியிடுகிறது. UI scene அதைத் தொடர்ந்து அதன் text objects-களைப் புதுப்பிக்கிறது. Game scene, UI-ஐ import செய்வதோ, அதன் முறைகளை (methods) அழைப்பதோ அல்லது அது இருக்கிறதா என்று சரிபார்ப்பதோ இல்லை. அது வெறுமனே தரவை ஒரு வெற்றிடத்திற்குள் அனுப்புகிறது. இந்த பிணைப்பற்ற நிலை (decoupling) மூலம், நீங்கள் மைய விளையாட்டுச் சுழற்சியைத் (core game loop) தொடாமல், சோதனைக்காக HUD-ஐ நீக்கவோ அல்லது அதை முழுமையாக மாற்றவோ முடியும்.

நிறுத்தப் பயன்படுத்துவதற்கு (pausing), நான் Game scene-க்கு மேலே அமர்ந்திருக்கும் ஒரு stacked Pause scene-ஐப் பயன்படுத்தினேன். முக்கியமாக, Game scene-இல் scene.pause() என்று அழைப்பது உண்மையில் physics world-ஐ உறைய வைக்கிறது மற்றும் timers-களை நிறுத்துகிறது. Game scene புதுப்பிப்பதைத் (updating) தற்காலிகமாக நிறுத்திவிடும், ஆனால் Pause scene ஒரு மெனுவை உருவாக்கவும், மீண்டும் தொடங்குவதற்கான (unpause) சிக்னலுக்காகக் காத்திருக்கவும் விழித்திருக்கும். நீங்கள் இதுவரை ஒரு பெரிய update loop-க்குள் ஒரு boolean flag மூலம் pause நிலைகளை மட்டுமே நிர்வகித்திருந்தால், இது ஒரு மின் சுவிட்சைக் கண்டறிவது போன்ற உணர்வைத் தரும். இந்த engine உங்களுக்கு if (isPaused) return போன்ற guards-களை உங்கள் குறியீட்டில் ஆங்காங்கே சேர்க்க வேண்டிய அவசியமின்றி, ஒரு உண்மையான pause lifecycle-ஐ வழங்குகிறது.

உண்மையான பிழைகளிலிருந்து கற்ற கடினமான பாடங்கள்

மென்பொருளின் ஆழமான அடுக்குகளுடன் (close to the metal) இவ்வளவு நெருக்கமாகப் பணியாற்றியது, நான் மாற்ற வேண்டிய இரண்டு பழக்கங்களை வெளிச்சம் போட்டுக் காட்டியது.

முதலாவதாக, நான் Phaser v4-இல் ஒரு முறையைப் (method) பயன்படுத்த முயன்றேன்; அது பொதுவானது (public) என்று தோன்றியது, ஆனால் ஆவணப்படுத்தப்பட்ட API-இன் ஒரு பகுதியாக இல்லை. அது பதிப்பிற்கு இடையே மாறியது மற்றும் எனது build-ஐ உடைத்தது. நான் getChildren() என்பதைப் பயன்படுத்த மறுசீரமைத்தேன் (refactored), இது ஒரு நிலையான, ஆவணப்படுத்தப்பட்ட பொதுவான முறையாகும், அதன் பிறகு அந்த நிலையற்ற தன்மை மறைந்துவிட்டது. பாடம் மிகத் தெளிவானது: ஒரு முறை அதிகாரப்பூர்வ ஆவணத்தில் இல்லை என்றால், அதன் அடிப்படையில் உங்கள் விளையாட்டை உருவாக்காதீர்கள். Internal APIs ஏன் internal ஆக இருக்கின்றன என்பதற்கு ஒரு காரணம் உண்டு. பொதுவான பயன்பாட்டுப் பரப்பிலேயே (public surface area) உறுதியாக இருங்கள், அப்போதுதான் உங்கள் திட்டம் engine updates-களையும் தாங்கி நிற்கும்.

இரண்டாவதாக, எல்லாவற்றிற்கும் நிகழ்வுகளை (events) மட்டும் நம்பக்கூடாது என்று கற்றுக்கொண்டேன். ஒரு நாணயத்தைச் சேகரிப்பது அல்லது ஒரு பெட்டியைத் திறப்பது போன்ற குறைந்த முக்கியத்துவம் வாய்ந்த தொடர்புகளுக்கு (low-stakes interactions) event callbacks சிறப்பாகச் செயல்படும். ஆனால் முக்கியமான நிலை மாற்றங்களுக்கு (critical state transitions)—குறிப்பாக Game Over-க்கு—நான் முக்கிய update loop-க்குள் ஒரு கூடுதல் சரிபார்ப்பைச் சேர்த்தேன். ஒரு listener நீக்கப்பட்டாலோ, ஒரு scene ஒரு நுணுக்கமான மைக்ரோசெகண்டில் (microsecond) நிறுத்தப்பட்டாலோ அல்லது emission மற்றும் handling ஆகியவற்றிற்கு இடையில் ஒரு race condition ஏற்பட்டாலோ நிகழ்வுகள் தவறாகச் செயல்படக்கூடும். வீரரின் ஆரோக்கியத்தை (health) நேரடியாக update loop-இல் சரிபார்த்து, அது பூஜ்ஜியத்தை எட்டினால் game-over நிலையைத் திணிக்கச் செய்வதன் மூலம், ஒரு நிகழ்வு இயங்கத் தவறினாலும் விளையாட்டு ஒரு இடைநிலை நிலையில் (limbo state) சிக்காமல் இருப்பதை நான் உறுதி செய்தேன். நிகழ்வுகள் இன்னும் இரண்டாம் நிலை விளைவுகளைக் (secondary effects)—screen shake, sound cues, score submission—கையாள்கின்றன, ஆனால் அதிகாரப்பூர்வமான தர்க்கம் (authoritative logic) விளையாட்டு கடிகாரம் இருக்கும் இடத்தில்தான் இயங்குகிறது.

நீங்கள் ஏன் இதை முயற்சி செய்ய வேண்டும்

நீங்கள் விளையாட்டு மேம்பாட்டைக் (game development) கற்றுக் கொண்டிருக்கிறீர்கள் என்றால், உங்கள் அடுத்த திட்டத்தில் இதே கட்டுப்பாட்டை விதித்துக் கொள்ளுங்கள்: வெளிப்புற சொத்துக்கள் (external assets) இல்லை, ஒரே ஒரு HTML கோப்பு மட்டும். இது கட்டுப்பாடாகத் தோன்றலாம், ஆனால் இது ஒவ்வொரு சா excuse-ஐயும் நீக்கிவிடுகிறது. நீங்கள் ஒரு bundler-ஐ உள்ளமைக்கவோ, உள்ளூர் ஆடியோ கோப்புகளில் CORS பிழைகளுடன் போராடவோ அல்லது ஒரு மதிய நேரத்தை இலவச asset packs-களைத் திரட்டுவதில் செலவிடவோ தேவையில்லை. நீங்கள் குறியீட்டை எழுதுகிறீர்கள், உலாவியை (browser) refresh செய்கிறீர்கள், முடிவுகளைப் பார்க்கிறீர்கள்.

முக்கியமாக, இந்த engine ஏன் இவ்வாறு செயல்படுகிறது என்பதை நீங்கள் புரிந்துகொள்வீர்கள். நீங்கள் generateTexture-ஐ அழைப்பதன் மூலம் ஒரு texture எவ்வாறு GPU-க்குள் நுழைகிறது என்பதைத் தெரிந்துகொள்வீர்கள். நீங்கள் எல்லைகளைக் கையால் பதிவு செய்வதால் (registered the boundaries by hand), animation frames எவ்வாறு வரிசைப்படுத்தப்படுகின்றன (indexed) என்பதைத் தெரிந்துகொள்வீர்கள். நீங்கள் oscillator-ஐ இணைப்பதன் மூலம் ஆடியோ எவ்வாறு ஸ்பீக்கர்களைச் சென்றடைகிறது என்பதைத் தெரிந்துகொள்வீர்கள். அந்த அறிவு வெளிப்புற சொத்துக்களைப் பயன்படுத்தும் பெரிய திட்டங்களுக்கும் நேரடியாகப் பயன்படும், ஏனெனில் அடிப்படையான செயல்பாடுகள் (underlying mechanics) ஒருபோதும் மாறுவதில்லை—engine...