Player আর্কিটেকচার

কেন Romergo Player সাধারণ DOM-এ চলে

সাধারণভাবে একটি ভিজ্যুয়াল নভেল ইঞ্জিন 3D শুটার বা রিয়েল-টাইম স্ট্র্যাটেজি গেমের ইঞ্জিনের মতো জটিল নয়। প্রতি সেকেন্ডে বিশাল জগতের পদার্থবিদ্যা, শত শত অবজেক্টের আচরণ এবং জটিল আলো হিসাব করতে হয় না। তবে তার মানে এই নয় যে ক্লিক করলে শুধু ছবি বদলালেই যথেষ্ট।

একটি সাধারণ ভিজ্যুয়াল নভেল দৃশ্য বেশ কিছু সহজবোধ্য উপাদান নিয়ে তৈরি:

সময়ের সঙ্গে এই প্যারামিটারগুলো বদলানোর মধ্যেই সব জাদু তৈরি হয়। শুধু কী দেখানো হবে তা নয়, কখন দেখানো হবে, কী অপরিবর্তিত থাকবে, দৃশ্যের মধ্যে সঙ্গীত কীভাবে চলতে থাকবে, কোন ভাষা বেছে নেওয়া হবে এবং খেলোয়াড়ের কাজের পর কোথায় যাওয়া হবে—সবই গুরুত্বপূর্ণ।

অনেক দিন ইঞ্জিন খুঁজে শেষে ব্রাউজার বেছে নিয়েছি

আমি Godot, PixiJS এবং Three.js দিয়ে দৃশ্য তৈরির চেষ্টা করেছি। বিভিন্ন মাত্রার সাফল্যে সবই কাজ করেছে, এবং ইঞ্জিনগুলোর নিজস্ব কোনো দোষ ছিল না। কিন্তু আমি যেভাবে quest তৈরি করি, সেখানে এগুলো আলাদাভাবে সামলাতে হয় এমন আরেকটি স্তর যোগ করেছিল।

আমার শুধু একটি তৈরি গেম স্ক্রিন দরকার ছিল না। Builder-এ প্রতিটি পরিবর্তন সঙ্গে সঙ্গে দেখা, পুরো গেম build না করে একটি দৃশ্য চালানো, preview-তেই উপাদান বেছে সরানো এবং তারপর প্রকাশিত Player-এ একই আচরণ পাওয়া আমার জন্য গুরুত্বপূর্ণ ছিল।

শেষ পর্যন্ত সবচেয়ে কার্যকর সমাধানটি ছিল সবচেয়ে সহজ: ব্রাউজারের সাধারণ DOM।

ব্যাকগ্রাউন্ড, চরিত্র এবং সংলাপ পরিচিত ব্রাউজার layer হিসেবেই থাকে। বেশির ভাগ composition ও effect CSS দিয়ে তৈরি হয়। Canvas 2D আলাদা ভিজ্যুয়াল উপাদানের জন্য ব্যবহৃত হয়, আর ছবির মধ্যে shader transition সত্যিই দরকার হলে WebGL canvas যুক্ত হয়।

শব্দ ব্রাউজারের HTMLAudioElement দিয়ে কাজ করে। Runtime সক্রিয় clip-এর জন্য audio তৈরি করে, volume ও looping নিয়ন্ত্রণ করে এবং asset না বদলালে transition-এর সময় একই সঙ্গীত চালু রাখে।

এটি সব ধরনের গেমের জন্য সার্বজনীন নিয়ম নয়। কিন্তু ভিজ্যুয়াল নভেলের জন্য DOM কোনো আপস নয়, বরং খুব নির্ভুল একটি হাতিয়ার হয়েছে।

সহজ দৃশ্য এবং বুদ্ধিমান runtime

আমি Player-কে মানসিকভাবে দুই ভাগে দেখি:

কেন কোনো নির্দিষ্ট চরিত্র এসেছে, কোন condition একটি উত্তর খুলেছে বা পরের chapter কোনটি—দৃশ্যের তা জানার দরকার নেই। এটি clip, বর্তমান সময়, ভাষা, screen mode এবং media reference পায়। তারপর ফলাফল render করে এবং খেলোয়াড়ের কাজ ফিরিয়ে দেয়।

Runtime quest-এর বিবরণ এবং playthrough state পায়। এটি বর্তমান chapter, node ও scene, variable-এর মান, করা choice, checkpoint এবং শেষ হওয়া chapter জানে। খেলোয়াড় দৃশ্যে ক্লিক করলে বা উত্তর বাছলে runtime effect প্রয়োগ করে, পরের ধাপ খুঁজে বের করে এবং দৃশ্যকে আবার প্রস্তুত state দেয়।

অনেক সহজ করে বললে চক্রটি এমন:

Quest JSON + সংরক্ষিত অগ্রগতি
          ↓
runtime বর্তমান ধাপ নির্ধারণ করে
          ↓
দৃশ্য clip ও media দেখায়
          ↓
খেলোয়াড়ের কাজ runtime-এ ফিরে আসে
          ↓
পরের ধাপ — শেষ পর্যন্ত চক্রটি চলতে থাকে

প্রকাশিত গেমে Player পুরো quest-এর স্থির runtime snapshot লোড করে। Builder-এ preview বর্তমান draft থেকে সামঞ্জস্যপূর্ণ runtime তৈরি করে। তাই এগুলো এমন দুইটি একই রকম player নয় যেগুলো সময়ের সঙ্গে ভিন্ন আচরণ শুরু করে, বরং ভিন্ন shell-এর ভেতরে একই RuntimePlayer, SceneStage এবং viewport।

দৃশ্যের মধ্যে ধারাবাহিকতা যেভাবে থাকে

Transition-এর সময় runtime নতুন দৃশ্যের state আবার হিসাব করে। পুনরাবৃত্ত সঙ্গীত asset ID দিয়ে শনাক্ত হয় এবং নতুন করে শুরু বা আবার fade-in ছাড়াই চলতে থাকে। ব্রাউজার ছবি load ও cache করে, তাই একই ব্যাকগ্রাউন্ড ব্যবহার মানে network থেকে file আবার download করা নয়।

অর্থাৎ optimization একটি বড় “কিছুই পাঠাব না” condition-এর মধ্যে নেই; এটি সঠিক layer-এ থাকে: runtime playback-এর ধারাবাহিকতা রাখে, resolver স্থিতিশীল resource ফেরায় এবং browser cache পরিচিত media আবার download করে না।

একটি দৃশ্যের জন্য দুইটি স্ক্রিন

আমার জন্য সবচেয়ে বিরক্তিকর কাজ rendering নয়, ফোনে মানিয়ে নেওয়া ছিল। একটি horizontal দৃশ্য শুধু ছোট করলে চরিত্রগুলো খুব ছোট হয়, সংলাপের জায়গা সংকুচিত হয় এবং ব্যাকগ্রাউন্ডের গুরুত্বপূর্ণ অংশ সহজেই কিনারার বাইরে চলে যায়।

Romergo-তে একটি দৃশ্যের দুইটি fixed virtual viewport আছে: horizontal 1280 × 720 এবং vertical 390 × 693। Player device ও orientation অনুযায়ী উপযুক্ত mode বেছে নেয়, তারপর সম্পূর্ণ তৈরি দৃশ্যটি একসঙ্গে scale করে।

Desktop layout-ই প্রধান থাকে। ফোনের জন্য চরিত্রের আলাদা position ও scale নির্ধারণ করা যায়; override না থাকলে আরও ছোট করা desktop version ব্যবহার হয়। তাই লেখক দুইটি স্বাধীন দৃশ্য নয়, নির্দিষ্ট mobile adjustment-সহ একটি দৃশ্য সম্পাদনা করেন।

ব্যাকগ্রাউন্ডের bgX, bgY ও scale দুই mode-এ একই থাকে, আর vertical viewport একই ছবিকে নিজস্বভাবে crop করে। তাই প্রকাশের আগে দুই format-এ framing পরীক্ষা করে একটি সাধারণ focus বেছে নিতে হয়। আলাদা mobile background setting runtime contract-এর অংশ নয়।

একই ধরনের character adjustment MCP-এর মাধ্যমে AI agent-কে দেওয়া যায় এবং তারপর দুই viewport-এর বাস্তব preview-তে পরীক্ষা করা যায়। তাই আমি adaptation-কে সম্পূর্ণ automatic নয়, semi-automatic বলি।

খেলোয়াড়কে কালো স্ক্রিন না দেখানোর উপায়

প্রয়োজনীয় ছবি network থেকে এখনও না এলে সহজ দৃশ্য কোনো সাহায্য করে না। তাই loading-ও runtime contract-এর অংশ হয়ে গেছে।

প্রথম render-এর আগে Player শুরুর দৃশ্যের active media সংগ্রহ করে এবং সেগুলো load হওয়া পর্যন্ত অপেক্ষা করে। এ সময় খেলোয়াড় খালি ব্যাকগ্রাউন্ডের বদলে স্বাভাবিক progress indicator দেখে। শুরু হওয়ার পর runtime অল্প অপেক্ষা করে বর্তমান দৃশ্য এবং graph-এর পরবর্তী দুই ধাপে পৌঁছানো যায় এমন দৃশ্যগুলোর resource আগে থেকে প্রস্তুত করে। Default depth দুই transition সামনে, যার মধ্যে উভয় ধাপের সব সম্ভাব্য branch থাকে।

Preloading transition graph-এর সঙ্গে যুক্ত এবং এর depth আলাদাভাবে বদলানো যায়। এখানে fixed সংখ্যক দৃশ্য ব্যবহার হয় না: একটি linear sequence এবং একটি branch সামনে একই সংখ্যক ধাপ থাকলেও ভিন্ন load তৈরি করে।

Offline খেলার জন্য আরেকটি mode আছে: ব্যবহারকারী আগে থেকেই পুরো quest download করতে পারেন। তখন runtime snapshot, media এবং PWA shell browser cache-এ থাকে এবং network ছাড়াই খোলে। পরের দৃশ্য preloading মসৃণ online play নিশ্চিত করে, আর পূর্ণ download সত্যিকারের offline play দেয়।

Telegram একটি shell, Discord একটি আলাদা system

Player-কে Telegram ও Discord-এ embed করার সময় ব্রাউজারের প্রতি আমার ভালোবাসা বিশেষভাবে কাজে দিয়েছে। দুই ক্ষেত্রেই platform-এর ভেতরে একই web runtime খোলে, তাই নতুন game engine-এর জন্য দৃশ্য ও playthrough-এর নিয়ম আবার লিখতে হয়নি।

Telegram-এর জন্য প্রধানত platform session, quest launch, local progress এবং Mini App ও সাধারণ Player-এর মধ্যে navigation দরকার হয়েছে। গেমটি নিজে একই থেকেছে।

Discord বেশি জটিল, কারণ একাধিক মানুষকে গল্পের একই অবস্থান দেখতে হয়। প্রত্যেক participant সত্যিই নিজের local runtime চালান, কিন্তু host সত্যের উৎস। Host-এর Player পরিবর্তন হলে এবং প্রায় প্রতি 750 milliseconds-এ state পাঠায়। Realtime room WebSocket-এ command পায়, snapshot সংরক্ষণ করে এবং participants-দের পাঠায়। দর্শকদের runtime remote state প্রয়োগ করে, দৃশ্য পুনরুদ্ধার করে এবং synchronization-এর মধ্যে local সময় চালু রাখে।

এই বিভাজন একটি সুবিধাজনক ফল দিয়েছে: গল্পের state shared, কিন্তু ভাষা ও screen size local থাকে। একজন participant ফোনে বাংলায় দৃশ্য দেখতে পারেন, আরেকজন বড় screen-এ ইংরেজিতে, এবং দুজনই host-এর সঙ্গে sync-এ থাকেন। দর্শকেরা দ্বিতীয় host না হয়েও উত্তরগুলোর জন্য vote করতে পারেন।

সব mode-এর জন্য একটি ভিত্তি

Player অনেক আগেই “ব্যাকগ্রাউন্ড, চরিত্র এবং সংলাপ”-এর মৌলিক সেট ছাড়িয়ে গেছে। এখন এতে branch, variable, checkpoint, rewind, interactive interface scene, translation, offline এবং synchronized playthrough রয়েছে।

তবু মূল সিদ্ধান্তটি এই বৃদ্ধি সামলেছে। দৃশ্য এখনও ছবি ও শব্দের দায়িত্বে। Runtime এখনও state ও transition-এর দায়িত্বে। Browser API rendering, media, cache ও embedding সামলায়, আর বিশেষ layer শুধু যেখানে সত্যিই প্রয়োজন সেখানে যোগ হয়।

এ কারণে একই Player-কে Builder preview, test, সাধারণ browser play, Telegram ও Discord-এ বজায় রাখা যায়। আমার কাছে এটি একটি ভালো উদাহরণ: দায়িত্বের সীমানা সঠিকভাবে আঁকা হলে সবচেয়ে সহজ ও কিছুটা রুক্ষ সমাধানই সবচেয়ে নমনীয় হতে পারে।

ডেভ ব্লগে ফিরে যান