ब्राउझर टॅब बंद केल्यामुळे चार तासांची प्रगती (progress) पुसली जाऊ नये. हे ऐकायला साधे वाटते, तरीही अनेक ब्राउझर गेम्समध्ये localStorage कडे दुर्लक्ष केले जाते. खेळाडू एक हाय स्कोर अनलॉक करतो, आपली सेटिंग्ज बदलतो, दुसऱ्या दिवशी परत येतो आणि त्याला काहीच सापडत नाही. यापेक्षा वाईट म्हणजे, पॅच (patch) नंतर जेव्हा ते परत येतात, तेव्हा गेम एरर दाखवतो कारण त्यांच्या मशीनवरील सेव्ह फाईल तुमच्या नवीन कोडशी जुळत नाही. Phaser 4 मध्ये 'survivor-style shooter' गेम बनवताना शत्रूंच्या सततच्या लाटांचा सामना करावा लागतो, पण खरा दीर्घकालीन धोका तुमच्या स्वतःच्या भविष्यातील अपडेट्समधून येतो.

बहुतेक डेव्हलपर्स त्यांचे पहिले सेव्ह सिस्टम तयार करताना एक ऑब्जेक्ट घेतात, त्याला JSON.stringify मधून पाठवतात आणि localStorage मध्ये टाकून देतात. लोड करताना, ते त्याला 'parse' करतात आणि गेमला थेट (raw) परत देतात. हे पहिल्या दिवशी काम करते. पण ज्या क्षणी तुम्ही एखादी नवीन सेटिंग, नवीन 'unlock flag' किंवा कॉन्फिगरेशनचा तिसरा स्तर (layer) जोडता, त्याच क्षणी हे तुटते. जर एखाद्या जुन्या खेळाडूच्याकडे अशी सेव्ह फाईल असेल ज्यामध्ये vignette प्रॉपर्टी नाही आणि तुमचा नवीन कोड ती अस्तित्वात आहे अशी अपेक्षा करत असेल, तर तुम्हाला 'boolean' ऐवजी undefined मिळेल. अशा डझनभर नवीन फीचर्सचा विचार केला, तर तुम्हाला एक 'debugging nightmare' पाहायला मिळेल, ज्याचा फटका सर्वात आधी तुमच्या सर्वात निष्ठावान खेळाडूंना बसतो.

कच्च्या ऑब्जेक्टऐवजी (Raw Object), एका करारापासून (Contract) सुरुवात करा

localStorage ला स्पर्श करण्यापूर्वी, तुमच्या कोडबेसमध्ये एक 'default save schema' परिभाषित करा. याला एका करारासारखे समजा ज्याचे पालन प्रत्येक सेव्ह फाईलला करावे लागेल, मग ती पाच मिनिटांपूर्वी तयार झाली असो किंवा पाच महिन्यांपूर्वी. एक स्पष्ट सुरुवात अशी असू शकते:

const defaultSave = {
  highScore: 0,
  settings: {
    screenShake: true,
    vignette: true
  }
};

हा ऑब्जेक्ट तुमच्या सोर्स कोडमध्ये असतो. जेव्हा गेम सुरू होतो, तेव्हा हा स्ट्रक्चर (shape) नेहमी उपलब्ध असतो. हे तुम्हाला एक बेसलाईन देते. तसेच, काहीही 'serialize' करण्यापूर्वी तुम्हाला स्ट्रक्चरचा विचार करण्यास भाग पाडते. जर तुम्ही ही पायरी वगळली आणि त्या वेळी सोयीस्कर असलेला कोणताही 'state object' साठवला, तर तुम्हाला विसंगत कीज (inconsistent keys), गहाळ फील्ड्स (missing fields) आणि जुन्या सेव्ह फाईल्स तुमच्या अपेक्षेनुसार नसल्यामुळे होणारे 'silent failures' यांचा सामना करावा लागेल.

try/catch वापरून सुरक्षित लोडिंग (Defensive Loading)

'Local storage' हा डेटाबेस नाही. तो ब्राउझरमधील एक 'string closet' आहे आणि त्यात काहीही असू शकते. वापरकर्त्याने एखादी व्हॅल्यू मॅन्युअली बदलली असू शकते, एखादी 'write operation' अर्धवट राहिली असू शकते किंवा एखाद्या ब्राउझर एक्सटेंशनने तुमच्या की (key) मध्ये कचरा (garbage) टाकला असू शकतो. जेव्हा तुम्ही ती स्ट्रिंग बाहेर काढता आणि JSON.parse ला देता, तेव्हा एकही चुकीचा कॅरेक्टर 'hard exception' निर्माण करू शकतो. Phaser गेममध्ये, तो अनहँडल्ड एरर (unhandled error) तुमचा 'boot sequence' फ्रीज करू शकतो किंवा खेळाडूला रिकाम्या स्क्रीनवर नेऊन टाकू शकतो.

तुमची 'read' आणि 'parse' लॉजिक नेहमी try/catch ब्लॉक मध्ये ठेवा. अयशस्वी झाल्यास, तुमच्या 'default schema' कडे वळा. उद्दिष्ट साधे आहे: जर सेव्ह फाईल वाचता येत नसेल, तर संपूर्ण सेशन क्रॅश करण्याऐवजी खेळाडूला नवीन वापरकर्ता म्हणून समजा. ही एक सवय 'hobby projects' आणि 'production-grade builds' मधील फरक स्पष्ट करते. हे लागू करण्यासाठी फारसा खर्च येत नाही आणि यामुळे तुम्हाला अशा अनाकलनीय बग रिपोर्ट्सपासून वाचते जे पुन्हा तयार करणे (reproduce) अशक्य असते.

जुना डेटा 'Defaults' सोबत मर्ज करा

यशस्वी 'parse' झाला म्हणजे तुम्ही सुरक्षित आहात असा होत नाही. 'Parsed result' ने तुमचा 'default object' पूर्णपणे कधीही बदलू नका. त्या जुन्या सेव्ह फाईलमध्ये तुमच्या नवीन सेटिंग्ज नसतील. त्यात screenShake असू शकतो पण vignette नसू शकतो. जर तुमच्या गेम लॉजिकमध्ये vignette अस्तित्वात आहे असे मानले गेले (कारण ते लेटेस्ट अपडेटसोबत आले आहे), तर तुम्ही पुन्हा एकदा undefined एरर्सच्या मागे लागता.

त्याऐवजी, लोड केलेला डेटा तुमच्या 'defaults' सोबत मर्ज करा. सेव्ह केलेल्या व्हॅल्यूज बेसलाईन स्कीमावर लेअर करण्यासाठी Object.assign वापरा. 'Defaults' आपोआप सर्व गहाळ जागा भरतात. व्हर्जन दोनमध्ये तुम्ही जोडलेले नवीन प्रॉपर्टीज 'default object' मधून त्यांची सुरुवातीची व्हॅल्यूज घेतात. खेळाडूने प्रत्यक्षात बदललेल्या अस्तित्वात असलेल्या प्रॉपर्टीज त्यांच्या स्टोअर केलेल्या पसंतीनुसार (preferences) ओव्हरराईट केल्या जातात. यात सर्वांचाच फायदा होतो. परत येणारा खेळाडू त्याचा हाय स्कोर कायम ठेवतो आणि गेम क्रॅश न होता तुम्ही काल जोडलेल्या नवीन टॉगलचा (toggle) वापर करू शकतो.

लक्षात ठेवा की Object.assign हे 'shallow merge' करते. जर तुमचा सेटिंग्ज ऑब्जेक्ट कालांतराने 'deeply nested' झाला, तर तुम्हाला त्या आतील ऑब्जेक्ट्स हाताळताना थोडी अधिक काळजी घ्यावी लागेल. तरीही, तत्व तेच आहे: खेळाडूचा डेटा तुमच्या 'defaults' ला सजवला पाहिजे, त्यांना पूर्णपणे बदलू नये.

तुमच्या कीज (Keys) ला व्हर्जन द्या

ब्राउझर जुन्या 'local storage' एन्ट्रीज आपोआप डिलीट करत नाहीत. जर तुम्ही तुमच्या डेटा स्ट्रक्चरमध्ये मोठा बदल केला, तर जुना फॉरमॅट सोडून देण्यासाठी तुम्हाला एका स्वच्छ मार्गाची गरज आहे. तुमच्या स्टोरेज कीला व्हर्जन सफिक्स (version suffix) द्या. bitSurvivorsSave_v1 हे स्पष्ट आहे. त्या फाईलमध्ये कोणता स्कीमा वापरला होता हे ते तुम्हाला अचूक सांगते. नंतर, जेव्हा तुम्ही प्रोग्रेशनमध्ये मोठा बदल कराल किंवा पूर्ण इन्व्हेंटरी सिस्टम जोडाल, तेव्हा bitSurvivorsSave_v2 कडे वळा.

यामुळे तुम्हाला दोन व्यावहारिक फायदे मिळतात. पहिले म्हणजे, तुम्ही चुकून v1 blob ला v2 लॉजिकने parse करणार नाही. दुसरे म्हणजे, तुम्हाला हवे असल्यास तुम्ही मायग्रेशन कोड लिहू शकता. बूट होताना, v1 तपासा. जर तो अस्तित्वात असेल आणि v2 नसेल, तर जुना डेटा नवीन स्ट्रक्चरमध्ये मायग्रेट करा, तो नवीन की मध्ये लिहा आणि पुढे जा. जर तुम्हाला मायग्रेट करायचे नसेल, तर किमान जुनी की स्टोरेजमध्ये सुरक्षित राहील आणि तुमचा नवीन कोड त्याकडे दुर्लक्ष करेल. कोणत्याही परिस्थितीत, वर्जनिंगमुळे सायलेंट करप्शन (silent corruption) टाळता येते.

सेव्हिंग प्रक्रिया अदृश्य करा

पर्सिस्टन्स (Persistence) ही श्वासासारखी नैसर्गिक वाटली पाहिजे. खेळाडूला त्याबद्दल कधीही विचार करावा लागू नये. तुमच्या सेटिंग्स मेनूमध्ये 'Apply' बटण देऊ नका. 'Apply' बटणे वापराकार्यांमध्ये अडथळा निर्माण करतात आणि वापरकर्त्यांना त्यांच्या निवडी खरोखच लागू झाल्या आहेत की नाही, याची काळजी करण्यास प्रवृत्त करतात. जेव्हा एखादा खेळाडू तीन पर्याय बदलतो, 'Apply' करायला विसरतो आणि टॅब बंद करतो, तेव्हा यामुळे डेटा गमावण्याची शक्यताही वाढते.

क्रिया घडताच लगेच सेव्ह करा. जेव्हा खेळाडू स्क्रीन शेक (screen shake) बंद करण्यासाठी चेकबॉक्सवर क्लिक करतो, तेव्हा लगेच तुमची write फंक्शन कॉल करा. जेव्हा गेम संपतो आणि अंतिम स्कोअरची बेरीज होते, तेव्हा 'game over' स्क्रीनचे ॲनिमेशन पूर्ण होण्यापूर्वीच नवीन हाय स्कोअर लिहा. इव्हेंट-ड्रिव्हन सेव्हिंगमुळे तुमचे आर्किटेक्चर अधिक निश्चित राहते, कारण सेव्हिंग ही क्रिया डेटा बदलणाऱ्या कृतीच्या अगदी जवळ असते. तुम्हाला कधीही एखादे मध्यवर्ती बॅचिंग फंक्शन शोधण्याची किंवा जुन्या (stale) स्टेटची काळजी करण्याची गरज पडणार नाही.

हा दृष्टिकोन तुमच्या मेंटल मॉडेलला देखील सोपे करतो. पर्सिस्टन्स नेमके कुठे घडते हे तुम्हाला माहित असेल: टॉगल हाताळणाऱ्या कॉलबॅक मध्ये आणि मृत्यू (death) हाताळणाऱ्या फंक्शन मध्ये. कोडबेसमध्ये कुठेही अनाकलनीय 'writes' विखुरलेले नसतील.

स्वतःसाठी एक रिसेट बटण तयार करा

डेव्हलपमेंट दरम्यान तुम्ही स्वतःचे सेव्ह केलेले डेटा करप्ट कराल. तुम्ही चुकीचा डेटा लिहाल, एज केसेस (edge cases) टेस्ट कराल आणि तुम्हाला पटकन स्वच्छ स्थितीत (clean state) परत येण्याची गरज पडेल. डीबग मेनूमध्ये किंवा एखाद्या लपविलेल्या की कॉम्बिनेशनमध्ये रिसेट बटण तयार करा. त्या रिसेट बटणाने नेमक्या याच क्रमाने दोन गोष्टी केल्या पाहिजेत: तुमची इन-मेमरी स्टेट (in-memory state) डिफॉल्ट स्कीमावर रिसेट करा आणि त्यानंतर लगेच तीच सेव्ह फंक्शन कॉल करा जी लोकल स्टोरेजमध्ये डेटा लिहिते.

जर तुम्ही फक्त लोकल व्हेरिएबल क्लिअर केले आणि 'write' स्टेप वगळली, तर तुम्ही काहीच साध्य केलेले नाही. पुढच्या वेळी पेज रिफ्रेश केल्यावर ब्राउझरमधून जुना डेटा पुन्हा बाहेर येईल आणि तो पुन्हा सक्रिय होईल. पर्सिस्ट करायला विसरलेले रिसेट हे असे बग आहे जे तुमचा बराच वेळ वाया घालवू शकते. एकदा ही सिक्वेन्स (sequence) नीट जमली की, तुमच्या प्रोजेक्टच्या उर्वरित काळात तुमची टेस्टिंग लूप वेगवान राहील.

मुख्य निष्कर्ष

सेव्हिंग ही शेवटी जोडली जाणारी एखादी फिचर नाही. हे असे इन्फ्रास्ट्रक्चर आहे जे ठरवते की तुमचा गेम किती टिकाऊ आहे आणि तो खेळाडूच्या वेळेचा आदर करतो की नाही. एक Phaser 4 survivor shooter गेम वारंवार खेळल्या जाण्यावर अवलंबून असतो. जर ब्राउझर टॅबमुळे खेळाडूच्या प्रगतीला धोका निर्माण होत असेल, तर ते शेवटी खेळणे थांबवतील. एक स्कीमा लिहा, चुकीच्या डेटापासून संरक्षण करा, डेटा रिप्लेस करण्याऐवजी मर्ज करा, तुमच्या कीजचे वर्जनिंग करा आणि प्रत्येक महत्त्वाच्या इव्हेंटवर सेव्ह करा. तुमचे भविष्यातील रूप आणि तुमच्या पुढच्या अपडेटनंतर परत येणारा प्रत्येक खेळाडू तुमचे आभार मानतील.