একটি ব্রাউজার ট্যাব বন্ধ করার মানে এই নয় যে চার ঘণ্টার অগ্রগতি মুছে যাবে। এটি শুনতে খুব সাধারণ মনে হলেও, অনেক ব্রাউজার গেম local storage-কে গুরুত্বহীন মনে করে। একজন খেলোয়াড় একটি হাই স্কোর আনলক করেন, তার সেটিংস পরিবর্তন করেন, পরের দিন ফিরে আসেন এবং দেখেন যে কিছুই অবশিষ্ট নেই। আরও খারাপ হলো, একটি প্যাচ (patch) দেওয়ার পর তারা ফিরে এলে গেমটি একটি এরর (error) দেখায়, কারণ তাদের মেশিনে থাকা সেভ ফাইলটি আপনার নতুন কোডের সাথে আর মেলে না। Phaser 4-এ একটি সারভাইভার-স্টাইল শ্যুটার তৈরি করার অর্থ হলো শত্রুর ক্রমাগত ঢেউয়ের মোকাবিলা করা, কিন্তু আসল দীর্ঘমেয়াদী হুমকি হলো আপনার নিজের ভবিষ্যৎ আপডেটগুলো।

বেশিরভাগ ডেভেলপার তাদের প্রথম সেভ সিস্টেম তৈরি করেন একটি অবজেক্ট নিয়ে, সেটিকে JSON.stringify-এর মাধ্যমে প্রসেস করে এবং localStorage-এ জমা দিয়ে। লোড করার সময়, তারা এটিকে পার্স (parse) করে এবং সরাসরি গেমের কাছে পাঠিয়ে দেয়। এটি প্রথম দিন কাজ করলেও, আপনি যখনই একটি নতুন সেটিংস, একটি নতুন আনলক ফ্ল্যাগ বা কনফিগারেশনের তৃতীয় স্তর যোগ করবেন, তখনই এটি ভেঙে পড়বে। যদি একজন পুরনো খেলোয়াড়ের কাছে এমন একটি সেভ ফাইল থাকে যেখানে vignette প্রপার্টি নেই, আর আপনার নতুন কোড যদি সেটি থাকার আশা করে, তবে আপনি একটি বুলিয়ান (boolean)-এর পরিবর্তে undefined পাবেন। এভাবে এক ডজন নতুন ফিচারের ক্ষেত্রে এটি ঘটলে আপনি একটি ডিবাগিং দুঃস্বপ্নের সম্মুখীন হবেন, যা আপনার সবচেয়ে অনুগত খেলোয়াড়দেরকেই সবার আগে আঘাত করবে।

একটি কন্ট্রাক্ট দিয়ে শুরু করুন, র (raw) অবজেক্ট দিয়ে নয়

localStorage-এ হাত দেওয়ার আগেই আপনার কোডবেসে একটি ডিফল্ট সেভ স্কিমা (schema) সংজ্ঞায়িত করুন। এটিকে একটি কন্ট্রাক্ট হিসেবে ভাবুন যা প্রতিটি সেভ ফাইলকে মেনে চলতে হবে, তা সে পাঁচ মিনিট আগে তৈরি করা হোক বা পাঁচ মাস আগে। একটি পরিষ্কার শুরুর পয়েন্ট দেখতে অনেকটা এরকম হতে পারে:

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

এই অবজেক্টটি আপনার সোর্স কোডে থাকে। গেমটি চালু হওয়ার সময়, এই ফরম্যাটটি সবসময় আপনার কাছে থাকবে। এটি আপনাকে একটি বেসলাইন প্রদান করে। এটি আপনাকে কোনো কিছু সিরিয়ালাইজ (serialize) করার আগেই গঠন বা স্ট্রাকচার নিয়ে চিন্তা করতে বাধ্য করে। আপনি যদি এই ধাপটি বাদ দিয়ে সেই সময়ে সুবিধাজনক যেকোনো স্টেট অবজেক্ট সংরক্ষণ করেন, তবে আপনি অসামঞ্জস্যপূর্ণ কী (key), অনুপস্থিত ফিল্ড এবং নীরব ব্যর্থতার (silent failures) সম্মুখীন হবেন যখন পুরনো সেভ ফাইলগুলো আপনার প্রত্যাশার সাথে মিলবে না।

Try/Catch দিয়ে ডিফেন্সিভ লোডিং

local storage কোনো ডাটাবেস নয়। এটি ব্রাউজারের একটি স্ট্রিং ক্লোজেট (string closet), এবং সেখানে যেকোনো কিছু জমা হতে পারে। ব্যবহারকারী হয়তো ম্যানুয়ালি কোনো ভ্যালু এডিট করেছেন, একটি অর্ধেক লেখা রাইট অপারেশন মাঝপথে থেমে গেছে, অথবা কোনো ব্রাউজার এক্সটেনশন আপনার নির্ধারিত কী-তে আবর্জনা (garbage) ফেলে দিয়েছে। যখন আপনি সেই স্ট্রিংটি বের করে JSON.parse-এ দেবেন, তখন একটি মাত্র ত্রুটিপূর্ণ ক্যারেক্টার একটি হার্ড এক্সেপশন (hard exception) তৈরি করবে। একটি Phaser গেমে, সেই আনহ্যান্ডেলড এররটি আপনার বুট সিকোয়েন্সকে ফ্রিজ করে দিতে পারে বা খেলোয়াড়কে একটি খালি স্ক্রিনে পাঠিয়ে দিতে পারে।

আপনার রিড (read) এবং পার্স (parse) লজিককে সবসময় একটি try/catch ব্লকের মধ্যে রাখুন। ব্যর্থ হলে, আপনার ডিফল্ট স্কিমায় ফিরে যান। লক্ষ্যটি সহজ: যদি সেভ ফাইলটি পড়া না যায়, তবে পুরো সেশন ক্র্যাশ করার পরিবর্তে খেলোয়াড়কে একজন নতুন ব্যবহারকারী হিসেবে গণ্য করুন। এই একটি অভ্যাস শখের প্রজেক্ট এবং প্রোডাকশন-গ্রেড বিল্ডের মধ্যে পার্থক্য তৈরি করে। এটি ইমপ্লিমেন্ট করতে প্রায় কোনো খরচ নেই এবং এটি আপনাকে এমন রহস্যময় বাগ রিপোর্ট থেকে বাঁচায় যা পুনরায় তৈরি করা (reproduce) অসম্ভব।

পুরনো ডেটাকে ডিফল্টের সাথে মার্জ করুন

সফলভাবে পার্স হওয়া মানেই আপনি নিরাপদ নন। পার্স করা ফলাফল দিয়ে আপনার ডিফল্ট অবজেক্টকে কখনোই পুরোপুরি প্রতিস্থাপন করবেন না। সেই পুরনো সেভ ফাইলে আপনার নতুন সেটিংসগুলো নাও থাকতে পারে। এতে screenShake থাকতে পারে কিন্তু vignette নাও থাকতে পারে। যদি আপনার গেম লজিক ধরে নেয় যে vignette আছে কারণ এটি সর্বশেষ আপডেটের সাথে এসেছে, তবে আপনি আবারও undefined এরর খুঁজতে বাধ্য হবেন।

পরিবর্তে, লোড করা ডেটাকে আপনার ডিফল্টগুলোর সাথে মার্জ করুন। বেসলাইন স্কিমার ওপর সেভ করা ভ্যালুগুলো লেয়ার করার জন্য Object.assign ব্যবহার করুন। ডিফল্টগুলো স্বয়ংক্রিয়ভাবে প্রতিটি অনুপস্থিত গ্যাপ পূরণ করে দেয়। ভার্সন দুই-এ আপনি যে নতুন প্রপার্টিগুলো যোগ করেছেন সেগুলো ডিফল্ট অবজেক্ট থেকে তাদের প্রাথমিক মান পায়। খেলোয়াড় প্রকৃতপক্ষে যে প্রপার্টিগুলো পরিবর্তন করেছেন সেগুলো তাদের সংরক্ষিত পছন্দ অনুযায়ী ওভাররাইট হয়। এতে সবাই লাভবান হয়। ফিরে আসা খেলোয়াড় তার হাই স্কোর বজায় রাখে এবং গেমটি কোনো সমস্যা ছাড়াই আপনার গতকাল যোগ করা নতুন টগলটি ব্যবহার করতে পারে।

মনে রাখবেন যে Object.assign একটি শ্যালো মার্জ (shallow merge) করে। যদি সময়ের সাথে সাথে আপনার সেটিংস অবজেক্টটি গভীরভাবে নেস্টেড (deeply nested) হয়ে যায়, তবে আপনাকে সেই অভ্যন্তরীণ অবজেক্টগুলো কিছুটা বেশি সতর্কতার সাথে হ্যান্ডেল করতে হতে পারে। তবুও, মূল নীতিটি একই থাকে: খেলোয়াড়ের ডেটা আপনার ডিফল্টগুলোকে সাজানো উচিত, সেগুলোকে পুরোপুরি প্রতিস্থাপন করা উচিত নয়।

আপনার কী (Key)-গুলোকে ভার্সন করুন

ব্রাউজার স্বয়ংক্রিয়ভাবে পুরনো local storage এন্ট্রি মুছে ফেলে না। আপনি যদি আপনার ডেটা স্ট্রাকচার নাটকীয়ভাবে পরিবর্তন করেন, তবে পুরনো ফরম্যাটটি ত্যাগ করার জন্য আপনার একটি পরিষ্কার উপায় প্রয়োজন। আপনার স্টোরেজ কী-তে একটি ভার্সন সাফিক্স (suffix) যোগ করুন। bitSurvivorsSave_v1 একটি স্পষ্ট নাম। এটি আপনাকে ঠিকভাবে বলে দেয় কোন স্কিমাটি সেই ফাইলটি লিখেছে। পরবর্তীতে, যখন আপনি প্রগ্রেশন (progression) পরিবর্তন করবেন বা একটি পূর্ণাঙ্গ ইনভেন্টরি সিস্টেম যোগ করবেন, তখন bitSurvivorsSave_v2-তে চলে যান।

এটি আপনাকে দুটি ব্যবহারিক সুবিধা দেয়। প্রথমত, আপনি ভুলবশত v2 লজিক দিয়ে v1 blob পার্স করবেন না। দ্বিতীয়ত, আপনি চাইলে মাইগ্রেশন কোড লিখতে পারেন। বুট করার সময় v1 চেক করুন। যদি এটি থাকে এবং v2 না থাকে, তবে পুরনো ডেটা নতুন স্ট্রাকচারে মাইগ্রেট করুন, নতুন কী-তে (key) লিখুন এবং এগিয়ে যান। আপনি যদি মাইগ্রেট করতে না চান, তবে অন্তত পুরনো কী-টি স্টোরেজে নিরাপদে থাকবে এবং আপনার নতুন কোড এটিকে উপেক্ষা করবে। যেভাবেই হোক, ভার্সনিং (versioning) নিঃশব্দ ডেটা করাপশন রোধ করে।

সেভ করা প্রক্রিয়াকে অদৃশ্য রাখুন

পারসিস্টেন্স (Persistence) হওয়া উচিত নিঃশ্বাসের মতো স্বাভাবিক। প্লেয়ারকে এটি নিয়ে কখনোই ভাবতে হবে না। আপনার সেটিংস মেনুতে কোনো 'Apply' বাটন যোগ করবেন না। 'Apply' বাটন ঘর্ষণ বা জটিলতা তৈরি করে এবং ব্যবহারকারীদের তাদের পছন্দগুলো আসলে কার্যকর হয়েছে কি না তা নিয়ে দুশ্চিন্তা করতে শেখায়। এছাড়া, যখন একজন প্লেয়ার তিনটি অপশন পরিবর্তন করেন কিন্তু 'Apply' করতে ভুলে গিয়ে ট্যাবটি বন্ধ করে দেন, তখন এটি ডেটা হারানোর ঝুঁকি তৈরি করে।

ইন্টারঅ্যাকশন হওয়ার মুহূর্তেই সেভ করুন। যখন প্লেয়ার স্ক্রিন শেক (screen shake) বন্ধ করার জন্য একটি চেকবক্সে ক্লিক করেন, তখনই আপনার write ফাংশনটি কল করুন। যখন একটি রান শেষ হয় এবং চূড়ান্ত স্কোর গণনা করা হয়, তখন গেম ওভার স্ক্রিন অ্যানিমেশন শেষ হওয়ার আগেই নতুন হাই স্কোরটি লিখে ফেলুন। ইভেন্ট-ড্রিভেন (Event-driven) সেভিং আপনার আর্কিটেকচারকে অনুমেয় (predictable) রাখে কারণ সেভ সবসময় সেই অ্যাকশনের সাথেই থাকে যা ডেটা পরিবর্তন করেছে। আপনাকে কখনোই কোনো সেন্ট্রাল ব্যাচিং ফাংশন খুঁজতে হবে না বা স্টেল স্টেট (stale state) নিয়ে দুশ্চিন্তা করতে হবে না।

এই পদ্ধতিটি আপনার মেন্টাল মডেলকেও সহজ করে তোলে। আপনি ঠিক জানবেন পারসিস্টেন্স কোথায় ঘটছে: টগল হ্যান্ডেল করা কলব্যাক-এ এবং ডেথ (death) হ্যান্ডেল করা ফাংশন-এ। পুরো কোডবেসে কোনো রহস্যময় রাইট (write) ছড়িয়ে থাকবে না।

নিজের জন্য একটি রিসেট বাটন তৈরি করুন

ডেভেলপমেন্টের সময় আপনি নিজেই নিজের সেভ ফাইল করাপ্ট করবেন। আপনি ভুল ডেটা লিখবেন, এজ কেস (edge cases) টেস্ট করবেন এবং দ্রুত একটি ক্লিন স্টেটে ফিরে যাওয়ার প্রয়োজন হবে। একটি ডিবাগ মেনু বা কোনো হিডেন কি কম্বিনেশনের (hidden key combination) মধ্যে একটি রিসেট বাটন তৈরি করুন। সেই রিসেট বাটনটিকে ঠিক এই ক্রমে দুটি কাজ করতে দিন: প্রথমে আপনার ইন-মেমরি স্টেটকে ডিফল্ট স্কিমাতে রিসেট করুন, তারপর অবিলম্বে সেই একই সেভ ফাংশনটি কল করুন যা লোকাল স্টোরেজে (local storage) ডেটা লেখে।

আপনি যদি শুধুমাত্র লোকাল ভেরিয়েবলটি ক্লিয়ার করেন এবং রাইট (write) ধাপটি বাদ দেন, তবে আপনি কিছুই অর্জন করলেন না। পরবর্তী পেজ রিফ্রেশ ব্রাউজার থেকে পুরনো ডেটা আবার টেনে আনবে এবং সেটি পুনরায় জীবিত করবে। পারসিস্ট করতে ভুলে যাওয়া একটি রিসেট হলো এমন এক ধরণের বাগ যা আপনার পুরো একটি বিকেল নষ্ট করে দিতে পারে। একবার এই সিকোয়েন্সটি ঠিকঠাক সেট করে ফেলুন, তাহলে প্রজেক্টের বাকি সময় আপনার টেস্টিং লুপ দ্রুত থাকবে।

আসল শিক্ষা

সেভিং কোনো ফিচার নয় যা আপনি একদম শেষে জুড়ে দেবেন। এটি এমন একটি ইনফ্রাস্ট্রাকচার যা নির্ধারণ করে আপনার গেমটি কতটা টেকসই এবং প্লেয়ারের সময়ের প্রতি কতটা শ্রদ্ধাশীল। একটি Phaser 4 সারভাইভার শ্যুটার গেম বারবার খেলার ওপর টিকে থাকে। যদি ব্রাউজার ট্যাবটি প্লেয়ারের প্রগতির বিপরীতে একটি লোডেড গানের মতো হয়, তবে তারা শেষ পর্যন্ত আর ফিরে আসবে না। একটি স্কিমা লিখুন, ভুল ডেটার বিরুদ্ধে সুরক্ষা নিশ্চিত করুন, রিপ্লেস করার পরিবর্তে মার্জ (merge) করুন, আপনার কী-গুলোকে ভার্সন করুন এবং প্রতিটি অর্থবহ ইভেন্টে সেভ করুন। আপনার ভবিষ্যৎ সত্তা এবং আপনার পরবর্তী আপডেটের পর ফিরে আসা প্রতিটি প্লেয়ার আপনাকে ধন্যবাদ জানাবে।