सबसे बेहतरीन roguelikes आपको सिर्फ मारते नहीं हैं। वे आपको बार-बार मरने की इच्छा जगाते हैं।
यह सुनने में अजीब लग सकता है, लेकिन जिसने भी आधी रात को एक रन (run) हारा हो और 12:03 पर दूसरा शुरू किया हो, वह इस अहसास को जानता है। मौत चुभती है। आपकी हेल्थ शून्य हो जाती है। स्क्रीन विफलता (failure) से भर जाती है। फिर भी आपकी उंगली पहले से ही प्ले बटन पर टिकी होती है। पिछले बीस मिनटों में कुछ ऐसा था जो इतना महत्वपूर्ण था कि आप गेम को वहीं नहीं छोड़ सकते जहाँ वह समाप्त हुआ था।
यह 'एक और रन' (one more run) लूप है, और यह कोई इत्तेफाक नहीं है। इसे डिज़ाइन किया गया है। यदि आप कोई roguelike या permadeath वाला कोई भी गेम बना रहे हैं, तो आपका पूरा काम दो विपरीत ताकतों के बीच संतुलन बनाना है। खिलाड़ी को तनाव महसूस करने के लिए पर्याप्त हारना चाहिए। उन्हें उम्मीद बनाए रखने के लिए पर्याप्त जीत हासिल करनी चाहिए।
असफलता की मुद्रा
अपने नवीनतम प्रोजेक्ट, Neon Survivor में, मैं ठीक वैसा ही तनाव चाहता था। जब खिलाड़ी मरता है, तो रन के दौरान इकट्ठा की गई हर चीज़ गायब हो जाती है सिवाय एक चीज़ के: गोल्ड (gold)। वह गोल्ड अपने आप बैंक में जमा हो जाता है। मेनू पर वापस आकर, वे इसे स्थायी अपग्रेड (permanent upgrades) पर खर्च करते हैं। फिर वे फिर से कूदते हैं, पहले की तुलना में थोड़े अधिक शक्तिशाली होकर।
यह सरल लूप पूरे गेम को चलाता है। इसके बिना, मौत एक पूर्ण विराम (full stop) है। खिलाड़ी गेम छोड़ देता है क्योंकि पिछले रन से उसे कुछ नहीं मिला। इसके साथ, मौत एक अल्पविराम (comma) है। वह रन एक फार्मिंग ट्रिप (farming trip) बन जाता है। हार दुखद होती है, लेकिन यह कल के लिए भुगतान भी करती है।
नुस्खा सरल है। आप कुछ चीज़ें अपने पास रखते हैं, भले ही आप बाकी सब कुछ खो दें। चुनौती यह सुनिश्चित करना है कि जो चीज़ आप रखते हैं वह चुनौती को खत्म किए बिना महत्वपूर्ण बनी रहे। यदि अपग्रेड बहुत कमजोर हैं, तो खिलाड़ी परवाह करना बंद कर देता है। यदि वे बहुत शक्तिशाली हैं, तो गेम अपने आप चलने लगता है। दोनों ही स्थितियों में लूप टूट जाता है।
दो घड़ियाँ, शून्य भ्रम
इसे ठीक से बनाने के लिए, मुझे दो अलग-अलग टाइमलाइन को मैनेज करना पड़ा।
Run Clock हर बार प्ले करने पर रीसेट हो जाता है। यह हेल्थ, वर्तमान स्कोर, दुश्मन की वेव काउंट (enemy wave count), और सत्र के दौरान लिए गए किसी भी अस्थायी पावर-अप्स को ट्रैक करता है। जब कैरेक्टर मरता है, तो यह घड़ी वापस शून्य पर आ जाती है।
Meta Clock कभी रीसेट नहीं होता। इसमें हर प्रयास में अर्जित कुल गोल्ड, अब तक पहुँची उच्चतम वेव, और खरीदे गए प्रत्येक स्थायी अपग्रेड का डेटा रहता है। ब्राउज़र कितनी भी बार रिफ्रेश हो जाए, यह घड़ी चलती रहती है।
इन दोनों को मिलाने से ऐसे बग्स पैदा होते हैं जिन्हें ट्रैक करना कठिन और ठीक करना दर्दनाक होता है। मैंने डेवलपर्स को रूटीन सीन रीसेट के दौरान गलती से खिलाड़ी की प्रगति (progress) मिटाते हुए देखा है क्योंकि एक क्लीनअप फंक्शन ने गलत डेटा स्टोर को छू लिया था। Meta Clock का डेटा गायब हो जाता है। खिलाड़ी शून्य गोल्ड और शून्य अपग्रेड के साथ वापस आता है। उस बिंदु पर, आपके और आपके खिलाड़ी के बीच का रिश्ता टूट जाता है। वे एक नया रन शुरू नहीं कर रहे होते; वे एक नया गुस्सा (grudge) शुरू कर रहे होते हैं।
घड़ियों को अलग रखना केवल एक स्टाइल चॉइस नहीं है। यह एक सर्वाइवल स्ट्रेटजी है।
Phaser v4 इस विभाजन को कैसे संभालता है
मैंने Neon Survivor को Phaser v4 में बनाया है, जो इस समस्या के लिए दो विशिष्ट टूल प्रदान करता है।
Registry वर्तमान सत्र के लिए मेमोरी में लाइव डेटा रखती है। यह तेज़ है। यह सरल है। यह उस क्षण भी गायब हो जाती है जब खिलाड़ी पेज रिफ्रेश करता है।
LocalStorage डेटा को ब्राउज़र में ही सहेजता है। यह टैब बंद होने, ब्राउज़र रीस्टार्ट होने और बिजली कटने के बाद भी सुरक्षित रहता है। यह धीमा और कम विश्वसनीय भी है। ब्राउज़र इसे ब्लॉक कर सकते हैं, इसकी गति कम कर सकते हैं, या यदि स्टोरेज कोटा भर जाता है तो इसे मिटा सकते हैं।
मेरा डिज़ाइन विकल्प सख्त था। गेमप्ले के दौरान Registry ही सत्य का एकमात्र स्रोत (source of truth) है। गेम इससे पढ़ता है, इसमें लिखता है, और इस पर पूरी तरह भरोसा करता है। LocalStorage एक सह-लेखक (co-author) के रूप में कार्य नहीं करता है। यह एक दर्पण (mirror) के रूप में कार्य करता है।
यहाँ फ्लो (flow) इस तरह काम करता है। गेम Registry में एक अपग्रेड खरीद को लिखता है। एक सिंगल मैनेजर क्लास Registry पर नज़र रखती है। उपयुक्त होने पर, वह मैनेजर Registry डेटा को LocalStorage में मिरर करता है। यदि ब्राउज़र राइट (write) को ब्लॉक करता है, तो गेम अटकता नहीं है। यदि स्टोरेज विफल हो जाता है, तो वर्तमान सत्र फिर भी पूरी तरह से चलता है। खिलाड़ी प्रगति केवल तभी खो सकता है यदि वे ठीक उसी सेकंड में टैब बंद कर दें, लेकिन सत्र स्वयं कभी क्रैश नहीं होता है।
यह पैटर्न एक सूक्ष्म आपदा को रोकता है। यदि आप हर सिस्टम को सीधे LocalStorage में लिखने की अनुमति देते हैं, तो आप एक नाजुक API पर निर्भरता पैदा करते हैं। प्राइवेसी सेटिंग्स को बहुत अधिक बढ़ाने वाला खिलाड़ी या कम स्टोरेज वाला डिवाइस गेम को कॉम्बैट के दौरान धीमा या फ्रीज होते हुए देख सकता है क्योंकि किसी बैकग्राउंड फंक्शन ने स्टैट्स (stats) सहेजने की कोशिश की थी। Registry को सत्य का एकमात्र स्रोत बनाकर, आप एक्शन को तेज़ और जोखिम को सीमित रखते हैं।
कोड को स्वतंत्र रहने दें
मैंने सिस्टम को डिकपल (decouple) करने के लिए इवेंट्स का भी उपयोग किया। जब एक रन समाप्त होता है, तो GameScene अपने स्वयं के अंतिम संस्कार को नहीं संभालता है। यह किसी सेव फंक्शन को कॉल नहीं करता है। यह किसी स्टोरेज यूटिलिटी को इम्पोर्ट नहीं करता है। यह बस प्रासंगिक डेटा के पेलोड के साथ एक "run-ended" इवेंट उत्सर्जित (emit) करता है।
एक अलग लिसनर हिसाब-किताब संभालता है। यह इवेंट प्राप्त करता है, Meta Clock को अपडेट करता है, और मैनेजर को नए टोटल्स को LocalStorage में सिंक करने के लिए कहता है।
यह अलगाव तुरंत फायदा देता है। मैं सेव सिस्टम को छुए बिना पूरे GameScene को फिर से लिख सकता हूँ, प्लेयर कैरेक्टर को बदल सकता हूँ, कैमरा एंगल बदल सकता हूँ, या यहाँ तक कि शैली को सर्वाइवल से बुलेट हेल में भी बदल सकता हूँ। सिस्टम स्वतंत्र हैं। वे डायरेक्ट फंक्शन कॉल्स के बजाय इवेंट्स के माध्यम से बात करते हैं। इसका मतलब है कम मर्ज कॉन्फ्लिक्ट्स, कम बग्स, और एक ऐसा कोडबेस जो छह महीने बाद स्पैगेटी (उलझा हुआ) नहीं बनता।
वे अपग्रेड जो गेम बदल देते हैं
यदि रिवॉर्ड्स एक स्प्रेडशीट की तरह महसूस होते हैं, तो तकनीकी आधार होना बेकार है। मैंने इस बात पर बहुत समय बिताया कि अपग्रेड्स को खेलना वास्तव में कैसा महसूस होता है।
कुछ अपग्रेड सुरक्षित होते हैं। अतिरिक्त मूवमेंट स्पीड। बोनस हेल्थ। तेज़ रीलोड। ये खिलाड़ी को गलती करने के लिए अधिक गुंजाइश देते हैं। ये सुकून देने वाले होते हैं। ये गेम के नियमों को बदले बिना उसे आसान बना देते हैं।
अन्य अपग्रेड नियमों को पूरी तरह से बदल देते हैं। Neon Survivor में, मैंने "Piercing Rounds" जोड़ा। इस अपग्रेड से पहले, एक गोली पहले दुश्मन पर टकराकर रुक जाती थी। अपग्रेड के बाद, यह दुश्मनों के आर-पार निकल जाती है, जिससे संभावित रूप से एक ही शॉट में पूरी लाइन साफ हो सकती है।
अंतर बहुत बड़ा है। स्पीड और हेल्थ आपको लंबे समय तक जीवित रहने में मदद कर सकते हैं, लेकिन Piercing Rounds यह बदल देता है कि आप अपनी स्थिति कैसे बनाते हैं। आप दुश्मनों को लाइन में लगाना शुरू कर देते हैं। आप किनारों पर घूमना (kiting) बंद कर देते हैं और बीच से रास्ता बनाना शुरू कर देते हैं। गेम का निर्णय लेने का दायरा बढ़ जाता है।
अच्छी प्रोग्रेशन को खिलाड़ी के निर्णयों को बदलना चाहिए, न कि केवल उनके नंबरों को बढ़ाना चाहिए। यदि हर अपग्रेड केवल एक प्रतिशत की वृद्धि है, तो खिलाड़ी विवरण पढ़ना बंद कर देते हैं। वे क्लिक करते हैं, अपग्रेड करते हैं, और भूल जाते हैं। यदि कोई अपग्रेड उन्हें अपनी रणनीति पर पुनर्विचार करने के लिए मजबूर करता है, तो वे उसे याद रखते हैं। वे इसके बारे में बात करते हैं। वे यह देखने के लिए वापस आते हैं कि और क्या चीज़ गेम को पूरी तरह से पलट सकती है।
असली परिणाम
"एक और रन" (one more run) लूप कोई एकल सिस्टम नहीं है। यह हानि और लाभ के बीच का एक संबंध है, जो क्लीन आर्किटेक्चर और सार्थक रिवॉर्ड्स पर आधारित है।
दो अलग-अलग टाइमलाइन बनाएं और Meta Clock की रक्षा ऐसे करें जैसे कि इसमें आपके खिलाड़ियों का भरोसा हो, क्योंकि वास्तव में ऐसा ही है। लाइव डेटा को तेज़ और परसिस्टेंट डेटा को सुरक्षित रखने के लिए अपने इंजन के टूल्स का उपयोग करें। अपने सीन्स को स्टोरेज से अलग (decouple) करें ताकि आप बिना किसी डर के सुधार (iterate) कर सकें। और जब आप अपग्रेड डिज़ाइन करें, तो खुद से पूछें कि क्या वे खिलाड़ी को अधिक समय देते हैं, या अधिक दिलचस्प विकल्प।
इसे सही कर लें, और आपके खिलाड़ी मृत्यु को केवल सहन ही नहीं करेंगे, बल्कि वे उस पर निर्भर हो जाएंगे। हर रन अगले रन के लिए एक अग्रिम भुगतान (down payment) बन जाता है। गेम रीस्टार्ट की एक श्रृंखला नहीं रह जाता, बल्कि एक एकल, निरंतर चढ़ाई बन जाता है।
