কেস স্টাডি - SONAR, HTML5 গেম ডেভেলপমেন্ট

শন মিডলডিচ
শন মিডলডিচ

ভূমিকা

গত গ্রীষ্মে আমি SONAR নামক একটি বাণিজ্যিক WebGL গেমে প্রযুক্তিগত প্রধান হিসেবে কাজ করেছি। প্রকল্পটি সম্পূর্ণ হতে প্রায় তিন মাস সময় লেগেছিল, এবং জাভাস্ক্রিপ্টে স্ক্র্যাচ থেকে সম্পূর্ণরূপে সম্পন্ন হয়েছিল। SONAR-এর বিকাশের সময়, আমাদের নতুন এবং অপরীক্ষিত HTML5 জলে বেশ কয়েকটি সমস্যার উদ্ভাবনী সমাধান খুঁজে বের করতে হয়েছিল। বিশেষ করে, আমাদের একটি আপাতদৃষ্টিতে সহজ সমস্যার সমাধান প্রয়োজন: প্লেয়ার যখন গেমটি শুরু করে তখন আমরা কীভাবে 70+ এমবি গেম ডেটা ডাউনলোড এবং ক্যাশে করব?

অন্যান্য প্ল্যাটফর্মগুলিতে এই সমস্যার জন্য প্রস্তুত সমাধান রয়েছে। বেশিরভাগ কনসোল এবং পিসি গেম স্থানীয় সিডি/ডিভিডি থেকে বা হার্ড-ড্রাইভ থেকে সম্পদ লোড করে। ফ্ল্যাশ SWF ফাইলের অংশ হিসাবে সমস্ত সংস্থান প্যাকেজ করতে পারে যেখানে গেম রয়েছে এবং জাভা JAR ফাইলগুলির সাথে একই কাজ করতে পারে। স্টিম বা অ্যাপ স্টোরের মতো ডিজিটাল ডিস্ট্রিবিউশন প্ল্যাটফর্মগুলি নিশ্চিত করে যে প্লেয়ার এমনকি গেমটি শুরু করার আগে সমস্ত সংস্থান ডাউনলোড এবং ইনস্টল করা হয়েছে।

HTML5 আমাদের এই প্রক্রিয়াগুলি দেয় না, তবে এটি আমাদের নিজস্ব গেম রিসোর্স ডাউনলোড সিস্টেম তৈরি করার জন্য প্রয়োজনীয় সমস্ত সরঞ্জাম দেয়। আমাদের নিজস্ব সিস্টেম তৈরির উল্টো দিক হল যে আমরা আমাদের প্রয়োজনীয় সমস্ত নিয়ন্ত্রণ এবং নমনীয়তা পাই এবং এমন একটি সিস্টেম তৈরি করতে পারি যা আমাদের প্রয়োজনের সাথে মেলে।

পুনরুদ্ধার

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

এই সিস্টেমের ডিজাইন আমাদের সহজেই প্যাকেজ করা সংস্থান এবং স্থানীয় HTTP সার্ভারে পরিবেশিত (আনপ্যাকেজড) সংস্থানগুলির মধ্যে স্যুইচ করার অনুমতি দেয়, যা আমরা গেম কোড এবং ডেটা উভয়ই দ্রুত পুনরাবৃত্তি করতে পারি তা নিশ্চিত করতে সত্যিই সহায়ক ছিল৷

নিম্নলিখিত কোডটি আমাদের শৃঙ্খলিত রিসোর্স লোডারের মৌলিক নকশাকে চিত্রিত করে, ত্রুটি পরিচালনার সাথে এবং জিনিসগুলিকে পাঠযোগ্য রাখার জন্য আরও উন্নত XHR/ইমেজ লোডিং কোড সরানো হয়েছে।

function ResourceLoader() {
  this.pending = 0;
  this.baseurl = './';
  this.oncomplete = function() {};
}

ResourceLoader.prototype.request = function(path, callback) {
  var xhr = new XmlHttpRequest();
  xhr.open('GET', this.baseurl + path);
  var self = this;

  xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
      callback(path, xhr.response, self);

      if (--self.pending == 0) {
        self.oncomplete();
      }
    }
  };

  xhr.send();
};

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

প্রধান ResourceLoader দৃষ্টান্তের সাথে সংযুক্ত oncomplete কলব্যাকটি সমস্ত সংস্থান লোড হওয়ার পরেই কল করা হবে। গেম লোডিং স্ক্রীনটি পরবর্তী স্ক্রিনে স্থানান্তর করার আগে সেই কলব্যাকটি আহ্বান করার জন্য অপেক্ষা করতে পারে।

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

এই নিবন্ধের জন্য সবচেয়ে গুরুত্বপূর্ণ বৈশিষ্ট্য হল baseurl ক্ষেত্র, যা আমাদের অনুরোধ করা ফাইলগুলির উৎস সহজেই পরিবর্তন করতে দেয়। একই স্থানীয় ওয়েব সার্ভার (যেমন python -m SimpleHTTPServer ) যেটি গেমের জন্য প্রধান HTML নথি পরিবেশন করেছে, সেই একই স্থানীয় ওয়েব সার্ভার দ্বারা পরিবেশিত URL থেকে সংস্থানগুলির জন্য অনুরোধ করার জন্য URL-এ একটি ?uselocal ধরণের ক্যোয়ারী প্যারামিটারের অনুমতি দেওয়ার জন্য মূল ইঞ্জিন সেট আপ করা সহজ, প্যারামিটার সেট না থাকলে ক্যাশে সিস্টেম ব্যবহার করার সময়।

প্যাকেজিং সম্পদ

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

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

একটি কাস্টম বান্ডেল ফাইল বিন্যাস তৈরি করা খুব কঠিন নয় এবং এমনকি কয়েকটি সমস্যার সমাধানও করবে, তবে বান্ডেল বিন্যাস তৈরি করার জন্য একটি টুল তৈরি করতে হবে। একটি বিকল্প সমাধান হল একটি বিদ্যমান সংরক্ষণাগার বিন্যাস ব্যবহার করা যার জন্য সরঞ্জামগুলি ইতিমধ্যে বিদ্যমান, এবং তারপর ব্রাউজারে চালানোর জন্য একটি ডিকোডার লিখতে হবে। আমাদের একটি সংকুচিত সংরক্ষণাগার বিন্যাসের প্রয়োজন নেই কারণ HTTP ইতিমধ্যেই gzip ব্যবহার করে ডেটা সংকুচিত করতে পারে বা ঠিকঠাক অ্যালগরিদম ডিফ্লেট করতে পারে। এই কারণে, আমরা TAR ফাইল বিন্যাসে নিষ্পত্তি করেছি।

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

TAR বিন্যাসে হেডার ক্ষেত্রগুলি হেডার ব্লকে নির্দিষ্ট মাপের সাথে নির্দিষ্ট স্থানে সংরক্ষণ করা হয়। উদাহরণস্বরূপ, ফাইলের শেষ পরিবর্তন টাইমস্ট্যাম্পটি হেডারের শুরু থেকে 136 বাইটে সংরক্ষণ করা হয় এবং এটি 12 বাইট দীর্ঘ। সমস্ত সাংখ্যিক ক্ষেত্রগুলি ASCII বিন্যাসে সংরক্ষিত অক্টাল সংখ্যা হিসাবে এনকোড করা হয়। ক্ষেত্রগুলিকে পার্স করার জন্য, তারপরে, আমরা আমাদের অ্যারে বাফার থেকে ক্ষেত্রগুলি বের করি এবং সাংখ্যিক ক্ষেত্রগুলির জন্য আমরা parseInt() কল করি যাতে পছন্দসই অক্টাল বেস নির্দেশ করতে দ্বিতীয় প্যারামিটারে পাস করা নিশ্চিত হয়।

সবচেয়ে গুরুত্বপূর্ণ ক্ষেত্রগুলির মধ্যে একটি হল টাইপ ক্ষেত্র। এটি একটি একক সংখ্যার অক্টাল সংখ্যা যা আমাদের বলে যে রেকর্ডে কী ধরনের ফাইল রয়েছে। আমাদের উদ্দেশ্যে শুধুমাত্র দুটি আকর্ষণীয় রেকর্ডের ধরন হল নিয়মিত ফাইল ( '0' ) এবং ডিরেক্টরি ( '5' )। আমরা যদি নির্বিচারে TAR ফাইলগুলির সাথে কাজ করি তবে আমরা প্রতীকী লিঙ্কগুলি ( '2' ) এবং সম্ভবত হার্ড লিঙ্কগুলি ( '1' ) সম্পর্কেও যত্নবান হতে পারি।

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

// Read a string out of an array buffer with a maximum string length of 'len'.
// state is an object containing two fields: the array buffer in 'buffer' and
// the current input index in 'index'.
function readString (state, len) {
  var str = '';

  // We read out the characters one by one from the array buffer view.
  // this actually is a lot faster than it looks, at least on Chrome.
  for (var i = state.index, e = state.index + len; i != e; ++i) {
    var c = state.buffer[i];

    if (c == 0) { // at NUL byte, there's no more string
      break;
    }

    str += String.fromCharCode(c);
  }

  state.index += len;

  return str;
}

// Read the next file header out of a tar file stored in an array buffer.
// state is an object containing two fields: the array buffer in 'buffer' and
// the current input index in 'index'.
function readTarHeader (state) {
  // The offset of the file this header describes is always 512 bytes from
  // the start of the header
  var offset = state.index + 512;

  // The header is made up of several fields at fixed offsets within the
  // 512 byte block allocated for the header.  fields have a fixed length.
  // all numeric fields are stored as octal numbers encoded as ASCII
  // strings.
  var name = readString(state, 100);
  var mode = parseInt(readString(state, 8), 8);
  var uid = parseInt(readString(state, 8), 8);
  var gid = parseInt(readString(state, 8), 8);
  var size = parseInt(readString(state, 12), 8);
  var modified = parseInt(readString(state, 12), 8);
  var crc = parseInt(readString(state, 8), 8);
  var type = parseInt(readString(state, 1), 8);
  var link = readString(state, 100);

  // The header is followed by the file contents, then followed
  // by padding to ensure that the next header is on a 512-byte
  // boundary.  advanced the input state index to the next
  // header.
  state.index = offset + Math.ceil(size / 512) * 512;

  // Return the descriptor with the relevant fields we care about
  return {
    name : name,
    size : size,
    type : type,
    offset : offset
  };
};

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

প্রথম সমস্যাগুলির মধ্যে একটি যা আমাকে সমাধান করতে হয়েছিল তা হ'ল একটি XHR অনুরোধ থেকে কীভাবে ডেটা লোড করা যায়। আমি মূলত একটি "বাইনারী স্ট্রিং" পদ্ধতির সাথে শুরু করেছি। দুর্ভাগ্যবশত, বাইনারি স্ট্রিং থেকে ArrayBuffer এর মতো আরও সহজে ব্যবহারযোগ্য বাইনারি ফর্মগুলিতে রূপান্তর করা সোজা নয়, বা এই ধরনের রূপান্তরগুলি বিশেষভাবে দ্রুত নয়। Image অবজেক্টে রূপান্তর করা সমান বেদনাদায়ক।

আমি সরাসরি XHR অনুরোধ থেকে একটি ArrayBuffer হিসাবে TAR ফাইলগুলি লোড করার এবং ArrayBuffer থেকে অংশগুলিকে একটি স্ট্রিংয়ে রূপান্তর করার জন্য একটি ছোট সুবিধার ফাংশন যোগ করার বিষয়ে স্থির হয়েছি। বর্তমানে আমার কোড শুধুমাত্র মৌলিক ANSI/8-বিট অক্ষর পরিচালনা করে, কিন্তু ব্রাউজারগুলিতে আরও সুবিধাজনক রূপান্তর API উপলব্ধ হলে এটি ঠিক করা যেতে পারে।

কোডটি সহজভাবে ArrayBuffer এ স্ক্যান করে রেকর্ড শিরোলেখ পার্সিং করে, যার মধ্যে সমস্ত প্রাসঙ্গিক TAR হেডার ক্ষেত্র (এবং কয়েকটি অপ্রাসঙ্গিক) পাশাপাশি ArrayBuffer এর মধ্যে ফাইল ডেটার অবস্থান এবং আকার অন্তর্ভুক্ত রয়েছে। কোডটি ঐচ্ছিকভাবে ArrayBuffer ভিউ হিসাবে ডেটা বের করতে পারে এবং ফেরত রেকর্ড শিরোনাম তালিকায় সংরক্ষণ করতে পারে।

এই কোডটি https://github.com/subsonicllc/TarReader.js- এ বন্ধুত্বপূর্ণ, অনুমোদনযোগ্য ওপেন সোর্স লাইসেন্সের অধীনে অবাধে উপলব্ধ।

ফাইলসিস্টেম এপিআই

প্রকৃতপক্ষে ফাইলের বিষয়বস্তু সংরক্ষণ এবং পরে সেগুলি অ্যাক্সেস করার জন্য, আমরা FileSystem API ব্যবহার করেছি। APIটি বেশ নতুন কিন্তু ইতিমধ্যেই চমৎকার HTML5 Rocks FileSystem নিবন্ধ সহ কিছু দুর্দান্ত ডকুমেন্টেশন রয়েছে।

ফাইলসিস্টেম এপিআই এর সতর্কতা ছাড়া নয়। এক জিনিসের জন্য, এটি একটি ইভেন্ট-চালিত ইন্টারফেস; এটি উভয়ই এপিআইকে নন-ব্লকিং করে তোলে যা UI এর জন্য দুর্দান্ত তবে এটি ব্যবহার করা ব্যথাও করে। একটি WebWorker থেকে FileSystem API ব্যবহার করে এই সমস্যাটি দূর করতে পারে, কিন্তু এর জন্য সমগ্র ডাউনলোডিং এবং আনপ্যাকিং সিস্টেমটিকে একটি WebWorker-এ বিভক্ত করতে হবে। এটি সর্বোত্তম পদ্ধতিও হতে পারে, তবে সময়ের সীমাবদ্ধতার কারণে আমি যেটির সাথে গিয়েছিলাম তা নয় (আমি এখনও ওয়ার্কওয়ার্কারদের সাথে পরিচিত ছিলাম না), তাই আমাকে এপিআই-এর অ্যাসিঙ্ক্রোনাস ইভেন্ট-চালিত প্রকৃতির সাথে মোকাবিলা করতে হয়েছিল।

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

অ্যাকাউন্টে নেওয়া একটি দ্বিতীয় গুরুত্বপূর্ণ বিষয় হল FileSystem API এর PERSISTENT স্টোরেজ ফাইলের আকারের সীমা। আমরা ক্রমাগত সঞ্চয়স্থান চেয়েছিলাম কারণ অস্থায়ী সঞ্চয়স্থানটি যেকোন সময় সাফ করা যেতে পারে, ব্যবহারকারী যখন আমাদের গেম খেলার মাঝখানে থাকে তখন এটি উচ্ছেদ করা ফাইলটি লোড করার চেষ্টা করার আগে।

Chrome ওয়েব স্টোরকে লক্ষ্য করা অ্যাপগুলির জন্য, অ্যাপ্লিকেশনের ম্যানিফেস্ট ফাইলে unlimitedStorage অনুমতি ব্যবহার করার সময় কোনও সঞ্চয় সীমা নেই৷ যাইহোক, নিয়মিত ওয়েব অ্যাপগুলি এখনও পরীক্ষামূলক কোটা অনুরোধ ইন্টারফেসের সাথে স্থানের অনুরোধ করতে পারে।

function allocateStorage(space_in_bytes, success, error) {
  webkitStorageInfo.requestQuota(
    webkitStorageInfo.PERSISTENT,
    space_in_bytes,
    function() {
      webkitRequestFileSystem(PERSISTENT, space_in_bytes, success, error);      
    },
    error
  );
}