पिछली गर्मियों में, मैंने SONAR नाम के कमर्शियल WebGL गेम पर टेक्निकल लीड के तौर पर काम किया था. इस प्रोजेक्ट को पूरा होने में करीब तीन महीने लगे. इसे पूरी तरह से JavaScript में बनाया गया था. SONAR को डेवलप करते समय, हमें एचटीएमएल5 से जुड़ी कई नई समस्याओं के लिए इनोवेटिव समाधान ढूंढने पड़े. खास तौर पर, हमें एक ऐसी समस्या का समाधान चाहिए था जो देखने में तो आसान लगती है, लेकिन है नहीं: जब कोई खिलाड़ी गेम शुरू करता है, तो हम 70 एमबी से ज़्यादा के गेम डेटा को कैसे डाउनलोड और कैश मेमोरी में सेव करें?
अन्य प्लैटफ़ॉर्म पर, इस समस्या को हल करने के लिए पहले से तैयार समाधान उपलब्ध हैं. ज़्यादातर कंसोल और पीसी गेम, लोकल सीडी/डीवीडी या हार्ड-ड्राइव से संसाधन लोड करते हैं. Flash, सभी संसाधनों को SWF फ़ाइल के तौर पर पैकेज कर सकता है. इस फ़ाइल में गेम होता है. वहीं, Java, JAR फ़ाइलों के साथ ऐसा कर सकता है. Steam या App Store जैसे डिजिटल डिस्ट्रिब्यूशन प्लैटफ़ॉर्म यह पक्का करते हैं कि गेम शुरू करने से पहले, सभी संसाधन डाउनलोड और इंस्टॉल हो जाएं.
HTML5 में ये सुविधाएं नहीं मिलती हैं. हालांकि, इसमें हमें वे सभी टूल मिलते हैं जिनकी मदद से हम गेम के संसाधन डाउनलोड करने का अपना सिस्टम बना सकते हैं. अपना सिस्टम बनाने का फ़ायदा यह है कि हमें अपनी ज़रूरत के हिसाब से पूरा कंट्रोल और सुविधा मिलती है. साथ ही, हम ऐसा सिस्टम बना सकते हैं जो हमारी ज़रूरतों के हिसाब से हो.
फिर से पाना
इससे पहले, हमारे पास संसाधन कैश मेमोरी की सुविधा नहीं थी. हमारे पास एक सामान्य चेन वाला संसाधन लोडर था. इस सिस्टम की मदद से, हम रिलेटिव पाथ के हिसाब से अलग-अलग संसाधनों का अनुरोध कर सकते थे. इससे ज़्यादा संसाधनों का अनुरोध किया जा सकता था. हमारी लोडिंग स्क्रीन पर, एक प्रोग्रेस मीटर दिखता था. इससे यह पता चलता था कि कितना डेटा लोड होना बाकी है. इसके अलावा, यह अगली स्क्रीन पर सिर्फ़ तब जाती थी, जब रिसॉर्स लोडर की कतार खाली हो जाती थी.
इस सिस्टम के डिज़ाइन की वजह से, हम पैकेज किए गए संसाधनों और लोकल एचटीटीपी सर्वर पर दिखाए गए लूज़ (अनपैकेज किए गए) संसाधनों के बीच आसानी से स्विच कर पाए. इससे हमें गेम कोड और डेटा, दोनों को तेज़ी से दोहराने में मदद मिली.
यहां दिए गए कोड में, हमारे चेन किए गए रिसॉर्स लोडर का बुनियादी डिज़ाइन दिखाया गया है. इसमें, गड़बड़ी ठीक करने और ज़्यादा बेहतर 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 फ़ील्ड है. इसकी मदद से, हम उन फ़ाइलों के सोर्स को आसानी से बदल सकते हैं जिनके लिए हमने अनुरोध किया है. कोर इंजन को सेट अप करना आसान है. इससे यूआरएल में ?uselocal
टाइप के क्वेरी पैरामीटर का इस्तेमाल किया जा सकता है. इससे, उसी लोकल वेब सर्वर (जैसे कि python -m SimpleHTTPServer
) से संसाधन का अनुरोध किया जा सकता है जिसने गेम के लिए मुख्य एचटीएमएल दस्तावेज़ को दिखाया था. साथ ही, अगर पैरामीटर सेट नहीं है, तो कैश मेमोरी सिस्टम का इस्तेमाल किया जा सकता है.
पैकेजिंग के संसाधन
चेन बनाकर संसाधनों को लोड करने की एक समस्या यह है कि सभी डेटा के कुल बाइट की संख्या का पता नहीं लगाया जा सकता. इस वजह से, डाउनलोड के लिए आसान और भरोसेमंद प्रोग्रेस डायलॉग बनाने का कोई तरीका नहीं है. हम सभी कॉन्टेंट को डाउनलोड करके कैश मेमोरी में सेव करेंगे. बड़े गेम के लिए, इसमें काफ़ी समय लग सकता है. इसलिए, खिलाड़ी को प्रोग्रेस डायलॉग दिखाना ज़रूरी है.
इस समस्या को ठीक करने का सबसे आसान तरीका यह है कि सभी संसाधन फ़ाइलों को एक बंडल में पैकेज किया जाए. इससे हमें कुछ अन्य फ़ायदे भी मिलते हैं. हम इस बंडल को एक XHR कॉल से डाउनलोड करेंगे. इससे हमें प्रोग्रेस इवेंट मिलेंगे, ताकि हम एक अच्छा प्रोग्रेस बार दिखा सकें.
कस्टम बंडल फ़ाइल फ़ॉर्मैट बनाना बहुत मुश्किल नहीं है. इससे कुछ समस्याएं भी हल हो जाएंगी. हालांकि, इसके लिए बंडल फ़ॉर्मैट बनाने वाला टूल बनाना होगा. इसके अलावा, किसी ऐसे मौजूदा संग्रह फ़ॉर्मैट का इस्तेमाल किया जा सकता है जिसके लिए टूल पहले से मौजूद हैं. इसके बाद, ब्राउज़र में चलाने के लिए एक डिकोडर लिखना होगा. हमें कंप्रेस किए गए संग्रह फ़ॉर्मैट की ज़रूरत नहीं है, क्योंकि एचटीटीपी पहले से ही gzip या डिफ़्लेट एल्गोरिदम का इस्तेमाल करके डेटा को कंप्रेस कर सकता है. इन वजहों से, हमने टीएआर फ़ाइल फ़ॉर्मैट को चुना है.
TAR एक आसान फ़ॉर्मैट है. हर रिकॉर्ड (फ़ाइल) में 512 बाइट का हेडर होता है. इसके बाद, फ़ाइल का कॉन्टेंट होता है, जिसे 512 बाइट तक पैड किया जाता है. हेडर में हमारे काम के कुछ ही फ़ील्ड होते हैं. इनमें मुख्य रूप से फ़ाइल टाइप और नाम शामिल हैं. ये हेडर में तय की गई जगहों पर सेव होते हैं.
TAR फ़ॉर्मैट में हेडर फ़ील्ड, हेडर ब्लॉक में तय की गई जगहों पर और तय किए गए साइज़ में सेव किए जाते हैं. उदाहरण के लिए, फ़ाइल में पिछली बार बदलाव करने का टाइमस्टैंप, हेडर की शुरुआत से 136 बाइट पर सेव होता है. इसकी लंबाई 12 बाइट होती है. सभी संख्या वाले फ़ील्ड को ऑक्टल नंबर के तौर पर एन्कोड किया जाता है. इन्हें ASCII फ़ॉर्मैट में सेव किया जाता है. इसके बाद, फ़ील्ड को पार्स करने के लिए, हम अपने ऐरे बफ़र से फ़ील्ड निकालते हैं. साथ ही, न्यूमेरिक फ़ील्ड के लिए parseInt()
को कॉल करते हैं. ऐसा करते समय, हम यह पक्का करते हैं कि दूसरे पैरामीटर को पास किया गया हो, ताकि मनचाहे ऑक्टल बेस का पता चल सके.
टाइप फ़ील्ड सबसे ज़रूरी फ़ील्ड में से एक है. यह एक अंक की ऑक्टल संख्या है. इससे हमें पता चलता है कि रिकॉर्ड में किस तरह की फ़ाइल है. हमारे काम के लिए, सिर्फ़ दो तरह के रिकॉर्ड टाइप हैं: सामान्य फ़ाइलें ('0'
) और डायरेक्ट्री ('5'
). अगर हमें किसी भी टीएआर फ़ाइल से डील करना होता, तो हम सिंबॉलिक लिंक ('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
};
};
हमने टीएआर फ़ाइलें पढ़ने वाले मौजूदा टूल खोजे. हमें कुछ टूल मिले, लेकिन उनमें से कोई भी ऐसा नहीं था जो अन्य डिपेंडेंसी के बिना काम करता हो या जिसे हमारे मौजूदा कोडबेस में आसानी से शामिल किया जा सके. इसलिए, मैंने खुद ही लिखने का फ़ैसला किया. मैंने लोडिंग को ज़्यादा से ज़्यादा ऑप्टिमाइज़ करने के लिए भी समय निकाला. साथ ही, यह पक्का किया कि डिकोडर, आर्काइव में मौजूद बाइनरी और स्ट्रिंग डेटा, दोनों को आसानी से हैंडल कर सके.
मुझे सबसे पहले यह समस्या हल करनी थी कि XHR अनुरोध से डेटा को कैसे लोड किया जाए. मैंने शुरुआत में "बाइनरी स्ट्रिंग" का इस्तेमाल किया था. माफ़ करें, बाइनरी स्ट्रिंग को ArrayBuffer
जैसे ज़्यादा आसानी से इस्तेमाल किए जा सकने वाले बाइनरी फ़ॉर्म में बदलना आसान नहीं है. साथ ही, इस तरह के कन्वर्ज़न में समय भी लगता है. Image
ऑब्जेक्ट में बदलने में भी उतनी ही परेशानी होती है.
मैंने TAR फ़ाइलों को XHR अनुरोध से सीधे तौर पर ArrayBuffer
के तौर पर लोड करने का फ़ैसला किया. साथ ही, ArrayBuffer
से स्ट्रिंग में बदलने के लिए एक छोटा सा फ़ंक्शन जोड़ा. फ़िलहाल, मेरा कोड सिर्फ़ बेसिक ANSI/8-बिट वर्णों को हैंडल करता है. हालांकि, ब्राउज़र में ज़्यादा सुविधाजनक कन्वर्ज़न एपीआई उपलब्ध होने पर, इस समस्या को ठीक किया जा सकता है.
यह कोड, ArrayBuffer
में मौजूद रिकॉर्ड हेडर को स्कैन करता है. इसमें TAR हेडर फ़ील्ड (और कुछ काम के फ़ील्ड) के साथ-साथ, ArrayBuffer
में मौजूद फ़ाइल डेटा की जगह और साइज़ की जानकारी भी शामिल होती है. यह कोड, डेटा को ArrayBuffer
के तौर पर भी निकाल सकता है. साथ ही, इसे रिकॉर्ड हेडर की दिखाई गई सूची में सेव कर सकता है.
यह कोड, https://github.com/subsonicllc/TarReader.js पर, ओपन सोर्स लाइसेंस के तहत मुफ़्त में उपलब्ध है.
FileSystem API
फ़ाइल के कॉन्टेंट को सेव करने और बाद में उसे ऐक्सेस करने के लिए, हमने FileSystem API का इस्तेमाल किया. यह एपीआई नया है, लेकिन इसके बारे में पहले से ही कुछ बेहतरीन दस्तावेज़ उपलब्ध हैं. इनमें HTML5 Rocks FileSystem लेख भी शामिल है.
FileSystem API के इस्तेमाल से जुड़ी कुछ समस्याएं भी हैं. पहली बात, यह इवेंट-ड्रिवन इंटरफ़ेस है. इससे एपीआई नॉन-ब्लॉकिंग हो जाता है, जो यूज़र इंटरफ़ेस (यूआई) के लिए बहुत अच्छा है. हालांकि, इससे इसका इस्तेमाल करना मुश्किल हो जाता है. WebWorker से FileSystem API का इस्तेमाल करने पर इस समस्या को कम किया जा सकता है. हालांकि, इसके लिए पूरे डाउनलोड और अनपैक सिस्टम को WebWorker में बांटना होगा. यह सबसे अच्छा तरीका हो सकता है. हालांकि, समय की कमी की वजह से मैंने इसका इस्तेमाल नहीं किया. मुझे WorkWorkers के बारे में जानकारी नहीं थी. इसलिए, मुझे एपीआई के एसिंक्रोनस इवेंट-ड्रिवन नेचर से निपटना पड़ा.
हमारी ज़रूरतें, डायरेक्ट्री स्ट्रक्चर में फ़ाइलें लिखने पर ज़्यादा फ़ोकस करती हैं. इसके लिए, हर फ़ाइल के लिए कई चरणों वाली प्रोसेस पूरी करनी होती है. सबसे पहले, हमें फ़ाइल पाथ लेना होगा और उसे एक सूची में बदलना होगा. ऐसा पाथ सेपरेटर वर्ण (जो हमेशा फ़ॉरवर्ड-स्लैश होता है, जैसे कि यूआरएल) पर पाथ स्ट्रिंग को स्प्लिट करके आसानी से किया जा सकता है. इसके बाद, हमें सेव की गई सूची के हर एलिमेंट पर तब तक दोहराना होगा, जब तक कि आखिरी एलिमेंट न आ जाए. साथ ही, लोकल फ़ाइल सिस्टम में डायरेक्ट्री बनानी होगी. इसके बाद, हम फ़ाइल बना सकते हैं. इसके बाद, FileWriter
बना सकते हैं. आखिर में, फ़ाइल का कॉन्टेंट लिख सकते हैं.
दूसरी ज़रूरी बात यह है कि FileSystem API के PERSISTENT
स्टोरेज के लिए, फ़ाइल के साइज़ की सीमा तय की गई है. हमें परसिस्टेंट स्टोरेज की ज़रूरत थी, क्योंकि टेंपररी स्टोरेज को कभी भी मिटाया जा सकता है. ऐसा तब भी हो सकता है, जब उपयोगकर्ता हमारा गेम खेल रहा हो और गेम, हटाई गई फ़ाइल को लोड करने की कोशिश कर रहा हो.
Chrome Web Store को टारगेट करने वाले ऐप्लिकेशन के लिए, ऐप्लिकेशन की मेनिफ़ेस्ट फ़ाइल में unlimitedStorage
अनुमति का इस्तेमाल करते समय स्टोरेज की कोई सीमा नहीं होती. हालांकि, सामान्य वेब ऐप्लिकेशन अब भी एक्सपेरिमेंटल कोटा के अनुरोध वाले इंटरफ़ेस का इस्तेमाल करके, स्टोरेज के लिए अनुरोध कर सकते हैं.
function allocateStorage(space_in_bytes, success, error) {
webkitStorageInfo.requestQuota(
webkitStorageInfo.PERSISTENT,
space_in_bytes,
function() {
webkitRequestFileSystem(PERSISTENT, space_in_bytes, success, error);
},
error
);
}