Every game development tutorial starts the same way: colored rectangles sliding across a gray canvas. That is fine for learning syntax, but it teaches you nothing about how a game engine actually breathes. I wanted out of the rectangle business. I wanted to build something that felt like a real game—a top-down dungeon crawler with movement, combat, a HUD, and sound.
I committed to Phaser v4 and gave myself one hard rule. Zero external assets. No image files, no audio clips, and no build tools like Webpack or Vite. The entire project had to live inside a single HTML file written with plain JavaScript. That constraint was not about minimalism for its own sake. It was about removing every excuse and every black box. When you cannot download a sprite pack to cover up a gap in your knowledge, you are forced to learn how the engine manages textures, animations, audio, and state under the hood.
কোড থেকে বিশ্ব তৈরি করা
একটি সাধারণ Phaser প্রজেক্টে, আপনি একটি preload ফাংশনের ভেতরে this.load.image() কল করেন এবং ইঞ্জিনকে একটি PNG ফাইলের দিকে নির্দেশ করেন। সেই বিকল্পটি ছাড়া, আপনি Graphics অবজেক্টের সাহায্য নেন। আপনি এটি ইনস্ট্যানশিয়েট (instantiate) করেন, মৌলিক আকৃতিগুলো আঁকেন—ফ্লোর টাইলসের জন্য আয়তক্ষেত্র, দেয়ালের জন্য মোটা স্ট্রোক, সম্ভবত প্লেয়ার টোকেনের জন্য একটি বৃত্ত—এবং তারপর generateTexture কল করেন। এই মেথডটি গ্রাফিক্স বাফারটি ক্যাপচার করে এবং আপনার পছন্দ করা একটি কী (key)-এর অধীনে Phaser-এর টেক্সচার ম্যানেজারের সাথে এটি রেজিস্টার করে।
সেই মুহূর্ত থেকে, ইঞ্জিন সেই জেনারেট করা বিটম্যাপটিকে (bitmap) ঠিক একটি লোড করা ইমেজ ফাইলের মতো ট্রিট করে। আপনি এটিকে tilemaps-এ অ্যাসাইন করতে পারেন, স্প্রাইটে স্লাইস করতে পারেন বা টিন্ট (tint) করতে পারেন। ডানজন ক্রলারের জন্য এর মানে ছিল আমি আমার কোড এডিটর না ছেড়েই প্রসিডিউরালভাবে (procedurally) একটি ফ্লোর গ্রিড তৈরি করতে পারি, দেয়ালের অংশগুলো বসাতে পারি এবং কালার প্যালেটে পরিবর্তন আনতে পারি। এর ব্যবহারিক শিক্ষা হলো একটি টেক্সচার হলো মেমরিতে থাকা বিটম্যাপ ডেটার একটি অংশ মাত্র। এটি HTTP রিকোয়েস্টের মাধ্যমে এসেছে নাকি হাতে লেখা Graphics কল-এর মাধ্যমে, Phaser তা নিয়ে মাথা ঘামায় না।
এই পদ্ধতিটি আপনাকে ড্র অর্ডার (draw order) এবং ব্যাচিং (batching) সম্পর্কে চিন্তাশীল হতে সাহায্য করে। যখন প্রতিটি দেয়াল এবং ফ্লোর টাইল একই ধরণের জেনারেট করা টেক্সচার থেকে আসে, তখন আপনি লক্ষ্য করতে শুরু করেন যে Phaser কীভাবে রেন্ডার কলগুলোকে গ্রুপ করে। আপনি একটি স্ট্যাটিক tilemap লেয়ার এবং স্বতন্ত্র অবজেক্টের একটি স্প্রাইটম্যাপের (spritemap) মধ্যে পার্থক্য বুঝতে পারেন, কারণ আপনি ম্যানুয়ালি সিদ্ধান্ত নিচ্ছেন কোনটি একটি টেক্সচার ইনস্ট্যান্স হিসেবে থাকা উচিত।
স্প্রাইটশিট ছাড়া অ্যানিমেশন
স্থির বর্গক্ষেত্রগুলো দ্রুত একঘেয়ে হয়ে যায়, কিন্তু Phaser-এর অ্যানিমেশন সিস্টেম একটি স্প্রাইটশিট (spritesheet) আশা করে—সাধারণত একটি গ্রিডে সাজানো ফ্রেমসহ একটি একক PNG। আমি একটি অফস্ক্রিন HTML canvas এলিমেন্ট ব্যবহার করে সেই স্ট্রিপটি তৈরি করি। অ্যানিমেশনের প্রতিটি ফ্রেমের জন্য, আমি ক্যানভাসটি পরিষ্কার করি এবং একটি নতুন পোজ (pose) আঁকি: একটি সাধারণ তলোয়ারের আঘাত, দুই ধাপের হাঁটার সাইকেল, বা শত্রুর স্থির থাকা (idle bob)। স্ট্রিপটি সম্পন্ন হওয়ার পর, আমি ফ্রেমের প্রস্থ এবং উচ্চতা নির্ধারণ করে এটিকে Phaser-এর সাথে একটি স্প্রাইটশিট হিসেবে রেজিস্টার করি, যাতে ইঞ্জিন বুঝতে পারে একটি পোজ কোথায় শেষ হয়েছে এবং পরবর্তীটি কোথায় শুরু হয়েছে।
এটি ম্যানুয়ালি করার মাধ্যমে অ্যানিমেশন আসলে কী তা স্পষ্টভাবে বোঝা যায়: একটি শেয়ার্ড টেক্সচারের ওপর ফ্রেমের সীমানার একটি সেট। Phaser-এর অ্যানিমেশন কম্পোনেন্ট একটি স্টার্ট ফ্রেম, একটি এন্ড ফ্রেম এবং একটি ফ্রেম রেট চায়। তারপর এটি গেম ক্লকের প্রতিটি টিক-এ সেই আয়তাকার স্লাইসগুলোর মধ্য দিয়ে একটি পয়েন্টারকে এগিয়ে নিয়ে যায়। আপনি স্প্রাইটশিটগুলোকে শিল্পীদের তৈরি করা কোনো জাদুকরী অ্যাসেট হিসেবে দেখা বন্ধ করেন এবং সেগুলোকে কোঅর্ডিনেট ম্যাথ (coordinate math) হিসেবে দেখতে শুরু করেন। এই দৃষ্টিভঙ্গিটি পরবর্তীতে অত্যন্ত মূল্যবান যখন আপনি আসল আর্ট স্লাইস করছেন বা কেন একটি অ্যানিমেশন ভুল ক্রমে ফ্রেম দেখাচ্ছে তা ডিবাগ করছেন।
শূন্য থেকে শব্দ তৈরি
অডিও ফাইল ব্যবহার করা নিষিদ্ধ ছিল, তাই আমি সরাসরি Web Audio API ব্যবহার করেছি। মাত্র কয়েক লাইন JavaScript দিয়ে একটি oscillator node তৈরি করা যায়, সেটিকে একটি square বা sine wave-এ সেট করা যায়, একটি gain node-এর মধ্য দিয়ে চালনা করা যায় এবং শব্দের একটি ছোট বিস্ফোরণ (burst) শিডিউল করা যায়। আমি সাধারণ ইভেন্টগুলোর জন্য ছোট ছোট হেল্পার ফাংশন লিখেছি: পায়ের শব্দের জন্য একটি লো-ফ্রিকোয়েন্সি বিপ (beep), লুট সংগ্রহের জন্য একটি ক্রমবর্ধমান চিপ (chirp), এবং ড্যামেজ খাওয়ার জন্য একটি কর্কশ square-wave টোন।
এই সিন্থেসাইজড শব্দগুলো মূলত প্লেসহোল্ডার (placeholder), কিন্তু এগুলো গেমটিকে তাৎক্ষণিক মেকানিক্যাল ফিডব্যাক দেয়। আসল অডিও রেকর্ড বা সংগ্রহ করার আগে আপনি বুঝতে পারবেন টাইমিং ঠিক আছে কিনা। আসল সুফলটি আসে আর্কিটেকচারে। যেহেতু আপনি সাউন্ড ট্রিগারটিকে একটি সাধারণ ফাংশনের মধ্যে রেখেছেন, তাই পরবর্তীতে সিন্থেসাইজড বিপ-এর পরিবর্তে একটি লোড করা সাউন্ড বাফার ব্যবহার করতে মাত্র এক লাইনের পরিবর্তন প্রয়োজন। গেমের বাকি অংশ—কলিশন ইভেন্ট, UI ফ্ল্যাশ, স্কোর বৃদ্ধি—সবকিছু অপরিবর্তিত থাকে। আপনি প্রথমে অনুভূতির (feel) প্রোটোটাইপ তৈরি করেন, তারপর সেটির মান (fidelity) উন্নত করেন।
একাধিক ওয়ার্ল্ড পরিচালনা করা
একটি সত্যিকারের গেমের জন্য একের অধিক স্ক্রিনের প্রয়োজন, তাই আমি প্রজেক্টটিকে আলাদা আলাদা Phaser scene-এ বিভক্ত করেছি: Menu, Game, UI, এবং Pause। UI scene-টি Game scene-এর সাথে সমান্তরালভাবে চলে এবং এটি একই সাথে চালু করা হয় যাতে হেলথ বার (health bar) এবং স্কোর কাউন্টার (score counter) তাদের নিজস্ব পরিবেশে (sandbox) কাজ করতে পারে, যখন নিচে ডানজন ক্রল (dungeon crawls) চলতে থাকে। তারা কঠোরভাবে ইভেন্টের (events) মাধ্যমে যোগাযোগ করে। যখন প্লেয়ার আঘাত পায়, Game scene একটি পরিবর্তন নির্গত (emit) করে। UI scene সেটি শোনে এবং তার টেক্সট অবজেক্টগুলো আপডেট করে। Game scene UI-কে ইমপোর্ট করে না, এর মেথডগুলো কল করে না, এমনকি এটি আছে কি না তাও পরীক্ষা করে না। এটি কেবল শূন্যে (void) ডেটা পাঠিয়ে দেয়। এই ডিকাপলিং (decoupling)-এর মানে হলো আপনি কোর গেম লুপে (core game loop) কোনো পরিবর্তন না করেই টেস্টিংয়ের জন্য HUD সরিয়ে ফেলতে পারেন বা সম্পূর্ণভাবে বদলে দিতে পারেন।
পজ (pause) করার জন্য, আমি একটি স্ট্যাকড (stacked) Pause scene ব্যবহার করেছি যা Game scene-এর উপরে অবস্থান করে। গুরুত্বপূর্ণ বিষয় হলো, Game scene-এ scene.pause() কল করলে এটি আসলে ফিজিক্স ওয়ার্ল্ডকে (physics world) ফ্রিজ করে দেয় এবং টাইমারগুলো থামিয়ে দেয়। Game scene আপডেট হওয়া বন্ধ করে দেয়, কিন্তু Pause scene একটি মেনু রেন্ডার করার জন্য এবং আনপজ (unpause) সিগন্যালের জন্য অপেক্ষা করার জন্য সচল থাকে। আপনি যদি আগে কখনও একটি বিশাল আপডেট লুপের ভেতরে একটি বুলিয়ান ফ্ল্যাগ (boolean flag) দিয়ে পজ স্টেট ম্যানেজ করে থাকেন, তবে এটি আপনার কাছে একটি লাইট সুইচ আবিষ্কার করার মতো মনে হবে। ইঞ্জিন আপনাকে কোডে বারবার if (isPaused) return গার্ড ব্যবহার করতে বাধ্য না করে একটি প্রকৃত পজ লাইফসাইকেল (pause lifecycle) প্রদান করে।
বাস্তব বাগ (bugs) থেকে পাওয়া কঠিন শিক্ষা
সিস্টেমের একদম গভীরে কাজ করা আমাকে এমন দুটি অভ্যাসের মুখোমুখি করেছে যা পরিবর্তন করা আমার জন্য প্রয়োজন ছিল।
প্রথমত, আমি Phaser v4-এ এমন একটি মেথড ব্যবহার করার চেষ্টা করেছিলাম যা দেখতে পাবলিক মনে হলেও ডকুমেন্ট করা API-এর অংশ ছিল না। এটি ভার্সন পরিবর্তনের সাথে সাথে পরিবর্তিত হয়ে যায় এবং আমার বিল্ড (build) নষ্ট করে দেয়। আমি getChildren() ব্যবহার করার জন্য কোড রিফ্যাক্টর (refactor) করেছি, যা একটি স্থিতিশীল এবং ডকুমেন্ট করা পাবলিক মেথড, এবং এর ফলে অস্থিরতা দূর হয়ে যায়। শিক্ষাটি খুব স্পষ্ট: যদি কোনো মেথড অফিসিয়াল ডকুমেন্টেশনে না থাকে, তবে তার ওপর ভিত্তি করে আপনার গেম তৈরি করবেন না। ইন্টারনাল API-গুলো কোনো কারণ ছাড়াই ইন্টারনাল নয়। পাবলিক সারফেস এরিয়া (public surface area) অনুসরণ করুন এবং আপনার প্রজেক্ট ইঞ্জিন আপডেটের পরেও টিকে থাকবে।
দ্বিতীয়ত, আমি শিখেছি যে সবকিছুর জন্য ইভেন্টের ওপর নির্ভর করা উচিত নয়। কয়েন সংগ্রহ করা বা চেস্ট (chest) খোলার মতো কম গুরুত্বপূর্ণ ইন্টারঅ্যাকশনের জন্য ইভেন্ট কলব্যাক (event callbacks) চমৎকারভাবে কাজ করে। কিন্তু গুরুত্বপূর্ণ স্টেট ট্রানজিশনের (state transitions) ক্ষেত্রে—বিশেষ করে Game Over-এর জন্য—আমি মেইন আপডেট লুপের ভেতরে একটি অতিরিক্ত চেক (redundant check) যোগ করেছি। যদি কোনো লিসেনার (listener) সরিয়ে ফেলা হয়, কোনো সিন একটি অস্বস্তিকর মাইক্রোসেকেন্ডে পজ হয়ে যায়, অথবা ইমিশন (emission) এবং হ্যান্ডলিংয়ের মধ্যে কোনো রেস কন্ডিশন (race condition) তৈরি হয়, তবে ইভেন্ট ভুলভাবে কাজ করতে পারে। আপডেট লুপে সরাসরি প্লেয়ারের হেলথ পরীক্ষা করে এবং এটি শূন্য হয়ে গেলে গেম-ওভার স্টেট কার্যকর করার মাধ্যমে, আমি নিশ্চিত করেছি যে কোনো ইভেন্ট কাজ করতে ব্যর্থ হলেও গেমটি যেন কোনো অস্পষ্ট বা 'লিম্বো' (limbo) অবস্থায় আটকে না থাকে। ইভেন্টগুলো এখনও সেকেন্ডারি ইফেক্টগুলো—যেমন স্ক্রিন শেক (screen shake), সাউন্ড কিউ (sound cues), স্কোর সাবমিশন—হ্যান্ডেল করে, কিন্তু মূল বা অথরিটেটিভ লজিক (authoritative logic) সেখানেই থাকে যেখানে গেম ক্লক (game clock) থাকে।
কেন আপনার এটি চেষ্টা করা উচিত
আপনি যদি গেম ডেভেলপমেন্ট শিখছেন হন, তবে আপনার পরবর্তী প্রজেক্টে এই নির্দিষ্ট সীমাবদ্ধতা আরোপ করুন: কোনো এক্সটারনাল অ্যাসেট (external assets) থাকবে না, শুধুমাত্র একটি HTML ফাইল। এটি শুনতে সীমাবদ্ধ মনে হতে পারে, কিন্তু এটি সব অজুহাত দূর করে দেয়। আপনাকে কোনো বান্ডলার (bundler) কনফিগার করতে হবে না, লোকাল অডিও ফাইলের CORS এরর নিয়ে লড়াই করতে হবে না, বা ফ্রি অ্যাসেট প্যাক কিউরেট করতে পুরো বিকেল ব্যয় করতে হবে না। আপনি কোড লিখবেন, ব্রাউজার রিফ্রেশ করবেন এবং ফলাফল দেখতে পাবেন।
আরও গুরুত্বপূর্ণ বিষয় হলো, ইঞ্জিন কেন এভাবে কাজ করে তা আপনি বুঝতে পারবেন। আপনি জানবেন কীভাবে একটি টেক্সচার GPU-তে প্রবেশ করে কারণ আপনি generateTexture কল করেছেন। আপনি জানবেন কীভাবে অ্যানিমেশন ফ্রেমগুলো ইনডেক্স করা হয় কারণ আপনি নিজে সীমানাগুলো রেজিস্টার করেছেন। আপনি জানবেন কীভাবে অডিও স্পিকার পর্যন্ত পৌঁছায় কারণ আপনি অসিলেটরটি (oscillator) ওয়্যার করেছেন। এই জ্ঞান সরাসরি বড় প্রজেক্টগুলোতেও কাজে লাগবে যেখানে এক্সটারনাল অ্যাসেট ব্যবহার করা হয়, কারণ এর অন্তর্নিহিত মেকানিক্স কখনো পরিবর্তন হয় না—ইঞ্জিন
