Player आर्किटेक्चर

Romergo Player सामान्य DOM पर क्यों चलता है

आम तौर पर किसी विज़ुअल नॉवेल का इंजन 3D शूटर या रीयल-टाइम स्ट्रैटेजी गेम के इंजन जितना जटिल नहीं होता। उसे हर सेकंड एक विशाल दुनिया की भौतिकी, सैकड़ों ऑब्जेक्ट का व्यवहार और जटिल प्रकाश व्यवस्था की गणना नहीं करनी पड़ती। लेकिन इसका अर्थ यह नहीं कि क्लिक पर केवल तस्वीरें बदल देना पर्याप्त है।

विज़ुअल नॉवेल का सामान्य दृश्य कुछ स्पष्ट तत्वों से बनता है:

सारी जादुई अनुभूति इन मानकों को समय के साथ बदलने से बनती है। केवल यह महत्वपूर्ण नहीं कि क्या दिखाना है, बल्कि यह भी कि कब दिखाना है, क्या अपरिवर्तित रखना है, दृश्यों के बीच संगीत कैसे जारी रखना है, कौन-सी भाषा चुननी है और खिलाड़ी की कार्रवाई के बाद कहाँ जाना है।

मैंने लंबे समय तक इंजन खोजा और ब्राउज़र चुना

मैंने Godot, PixiJS और Three.js पर दृश्य बनाने की कोशिश की। अलग-अलग स्तर की सफलता के साथ वे सभी काम करते थे, और इसमें इंजनों की कोई गलती नहीं थी। लेकिन quests बनाने के मेरे तरीके में वे एक अतिरिक्त परत जोड़ते थे, जिसके साथ मुझे अलग से तालमेल बैठाना पड़ता था।

मुझे केवल तैयार गेम स्क्रीन नहीं चाहिए थी। मेरे लिए Builder में हर बदलाव तुरंत देखना, पूरा गेम बनाए बिना एक दृश्य चलाना, preview में ही तत्वों को चुनना और खिसकाना, और फिर प्रकाशित Player में वही व्यवहार पाना महत्वपूर्ण था।

अंत में सबसे प्रभावी समाधान सबसे सरल निकला: ब्राउज़र का सामान्य DOM।

बैकग्राउंड, पात्र और संवाद ब्राउज़र की परिचित लेयर बने रहते हैं। अधिकांश संरचना और इफ़ेक्ट CSS से बनाए जाते हैं। Canvas 2D का उपयोग अलग-अलग विज़ुअल तत्वों के लिए होता है, और WebGL canvas केवल वहाँ जोड़ा जाता है जहाँ तस्वीरों के बीच shader ट्रांज़िशन वास्तव में आवश्यक हों।

ध्वनि ब्राउज़र के HTMLAudioElement से काम करती है। Runtime सक्रिय क्लिप के लिए ऑडियो बनाता है, आवाज़ और लूपिंग नियंत्रित करता है, और यदि asset नहीं बदला है तो ट्रांज़िशन के दौरान वही संगीत जारी रखता है।

यह हर गेम के लिए सार्वभौमिक नुस्खा नहीं है। लेकिन विज़ुअल नॉवेल के लिए DOM कोई समझौता नहीं, बल्कि बहुत सटीक औज़ार साबित हुआ।

सरल दृश्य और समझदार runtime

मैं Player को मन में दो हिस्सों में बाँटता हूँ:

दृश्य को यह जानने की आवश्यकता नहीं कि कोई विशेष पात्र क्यों आया, किस शर्त से उत्तर खुला या अगला अध्याय कौन-सा है। उसे क्लिप, वर्तमान समय, भाषा, स्क्रीन मोड और मीडिया संदर्भ मिलते हैं। फिर वह परिणाम प्रस्तुत करता है और खिलाड़ी की कार्रवाइयाँ वापस भेजता है।

Runtime को quest का विवरण और playthrough की स्थिति मिलती है। वह वर्तमान अध्याय, नोड और दृश्य, वेरिएबल के मान, किए गए चुनाव, checkpoints और पूरे हो चुके अध्याय जानता है। जब खिलाड़ी दृश्य पर क्लिक करता है या उत्तर चुनता है, runtime इफ़ेक्ट लागू करता है, अगला चरण खोजता है और दृश्य को फिर तैयार स्थिति देता है।

बहुत सरल रूप में चक्र ऐसा दिखता है:

Quest JSON + सहेजी गई प्रगति
          ↓
runtime वर्तमान चरण तय करता है
          ↓
दृश्य क्लिप और मीडिया दिखाता है
          ↓
खिलाड़ी की कार्रवाई runtime में लौटती है
          ↓
अगला चरण — और चक्र अंत तक दोहरता है

प्रकाशित गेम में Player पूरे quest का स्थिर runtime snapshot लोड करता है। Builder में preview मौजूदा draft से संगत runtime बनाता है। इसलिए ये दो मिलते-जुलते player नहीं हैं जो समय के साथ अलग व्यवहार करने लगें, बल्कि अलग-अलग आवरणों में एक साझा RuntimePlayer, SceneStage और viewport हैं।

दृश्यों के बीच निरंतरता कैसे बनी रहती है

ट्रांज़िशन के समय runtime नए दृश्य की स्थिति फिर से गणना करता है। दोहराया गया संगीत asset ID से पहचाना जाता है और नए आरंभ या दोबारा fade-in के बिना चलता रहता है। तस्वीरें ब्राउज़र लोड और cache करता है, इसलिए बैकग्राउंड दोबारा उपयोग करने का अर्थ नेटवर्क से फ़ाइल फिर डाउनलोड करना नहीं है।

इसलिए optimization किसी एक बड़े “कुछ न भेजें” नियम में नहीं, बल्कि सही स्तरों पर रहती है: runtime playback की निरंतरता बनाए रखता है, resolver स्थिर संसाधन लौटाता है और ब्राउज़र cache ज्ञात मीडिया को फिर डाउनलोड नहीं करता।

एक दृश्य के लिए दो स्क्रीन

मेरे लिए सबसे कठिन काम rendering नहीं, बल्कि फ़ोन के लिए अनुकूलन था। किसी horizontal दृश्य को केवल छोटा करने से पात्र बहुत छोटे हो जाते हैं, संवाद की जगह तंग हो जाती है और बैकग्राउंड के महत्वपूर्ण विवरण आसानी से किनारे से बाहर चले जाते हैं।

Romergo में एक दृश्य के दो स्थिर virtual viewport हैं: horizontal 1280 × 720 और vertical 390 × 693। Player डिवाइस और orientation के अनुसार उचित मोड चुनता है और फिर तैयार दृश्य को पूरा का पूरा scale करता है।

Desktop layout मुख्य बना रहता है। फ़ोन के लिए पात्रों की अलग स्थिति और scale तय किए जा सकते हैं; यदि override नहीं है तो desktop रूप को और छोटा करके इस्तेमाल किया जाता है। इस तरह लेखक दो स्वतंत्र दृश्य नहीं, बल्कि मोबाइल के लिए चुनिंदा सुधारों वाला एक दृश्य संपादित करता है।

बैकग्राउंड के लिए bgX, bgY और scale दोनों मोड में साझा हैं, जबकि vertical viewport उसी तस्वीर को अपने तरीके से crop करता है। इसलिए रिलीज़ से पहले दोनों format में framing जाँचनी और एक साझा focus चुनना आवश्यक है। अलग मोबाइल बैकग्राउंड settings runtime contract का हिस्सा नहीं हैं।

पात्रों के एक जैसे सुधार MCP के माध्यम से AI agent को दिए जा सकते हैं और फिर दोनों viewport के वास्तविक preview में जाँचे जा सकते हैं। इसलिए मैं इस अनुकूलन को पूरी तरह automatic नहीं, बल्कि semi-automatic कहता हूँ।

खिलाड़ी को काली स्क्रीन दिखने से कैसे बचाएँ

यदि आवश्यक तस्वीर अभी नेटवर्क से नहीं आई है तो सरल दृश्य भी मदद नहीं करता। इसलिए loading भी runtime contract का हिस्सा बन गई।

पहले render से पहले Player शुरुआती दृश्य का सक्रिय मीडिया इकट्ठा करता है और उसके लोड होने की प्रतीक्षा करता है। इस दौरान खिलाड़ी खाली बैकग्राउंड के बजाय उचित progress indicator देखता है। शुरू होने के बाद runtime थोड़ी देर रुककर वर्तमान दृश्य और graph के अगले दो चरणों में पहुँच योग्य दृश्यों के संसाधन पहले से तैयार करता है। Default depth दो transitions आगे है, जिसमें दोनों चरणों की सभी संभावित branches शामिल हैं।

Preloading ट्रांज़िशन graph से जुड़ी है और उसकी depth अलग से बदली जा सकती है। यहाँ दृश्यों की निश्चित संख्या इस्तेमाल नहीं होती: linear sequence और branch अलग load बनाते हैं, भले ही औपचारिक रूप से आगे चरणों की संख्या समान हो।

Offline खेलने के लिए दूसरा मोड है: उपयोगकर्ता पूरा quest पहले से डाउनलोड कर सकता है। तब runtime snapshot, media और PWA shell ब्राउज़र cache में रहते हैं और नेटवर्क के बिना खुलते हैं। अगले दृश्य की preloading सहज online play देती है, जबकि पूरा download वास्तविक offline play देता है।

Telegram एक shell, Discord एक अलग system

Player को Telegram और Discord में embed करते समय ब्राउज़र के प्रति मेरा लगाव विशेष रूप से काम आया। दोनों जगह platform के भीतर वही web runtime खुलता है, इसलिए दृश्य और playthrough के नियमों को नए game engine के लिए दोबारा लिखना नहीं पड़ा।

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 साझा है, लेकिन भाषा और स्क्रीन का आकार local रहते हैं। एक participant फ़ोन पर हिंदी में दृश्य देख सकता है, दूसरा बड़े स्क्रीन पर अंग्रेज़ी में, और दोनों host के साथ sync में रहते हैं। दर्शक दूसरा host बने बिना उत्तर विकल्पों पर vote भी कर सकते हैं।

सभी मोड के लिए एक आधार

Player बहुत पहले “बैकग्राउंड, पात्र और संवाद” के मूल समूह से आगे बढ़ चुका है। अब इसमें branches, variables, checkpoints, rewind, interactive interface scenes, translations, offline और synchronized playthrough शामिल हैं।

लेकिन मूल निर्णय ने इस वृद्धि को सह लिया। दृश्य अभी भी चित्र और ध्वनि संभालता है। Runtime अभी भी state और transitions संभालता है। Browser API rendering, media, cache और embedding को पूरा करती हैं, और विशेष layers केवल वहाँ जोड़ी जाती हैं जहाँ वे सच में आवश्यक हों।

इसी कारण एक ही Player को Builder preview, tests, सामान्य browser play, Telegram और Discord में बनाए रखा जा सकता है। मेरे लिए यह अच्छा उदाहरण है कि यदि जिम्मेदारियों की सीमा सही खींची जाए तो सबसे सरल और थोड़ा रूखा समाधान ही सबसे लचीला बन सकता है।

डेव ब्लॉग पर वापस जाएँ