Search code examples
htmlcachinglocal-storageassetshtml5-appcache

HTML5 Game - caching whole game locally


I'm creating an HTML5 2D game and I want to request each asset only once and then store them in the user's filesystem, I'm using localStorage for this task, however AFAIK it has a limit of 5mb per origin, (my whole game will have more than that), and I want to know how to store my game assets in the user's machine without that limitation, this is what I've done until now:

items.js:

/**
 * Copyright 2014 - Edgar Alexander Franco.
 *
 * @author Edgar Alexander Franco
 * @version 1.0.0
 */

var items = [
  {
    name : 'characters_scott', 
    url : './img/game/characters/scott', 
    type : 'png'
  }, 
  {
    name : 'map_1', 
    url : './img/game/map/1', 
    type : 'jpg'
  }, 
  {
    name : 'map_2', 
    url : './img/game/map/2', 
    type : 'jpg'
  }
];

Resource.js

/**
 * Copyright 2014 - Edgar Alexander Franco.
 *
 * @author Edgar Alexander Franco
 * @version 1.0.0
 */

var Resource = (function () {
  var self = {};

  self.get = {};

  self.load = function (items) {
    var xhr = (typeof XMLHttpRequest != 'undefined') ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP') ;
    var item, content, mime;

    for (var i in items) {
      item = items[ i ];
      content = localStorage.getItem(item.url);

      if (content == null) {
        xhr.open('GET', item.url, false);
        xhr.send();
        content = xhr.responseText;
        localStorage.setItem(item.url, content);
      }

      if (item.type != 'audio') {
        mime = (item.type == 'jpg') ? 'image/jpeg' : 'image/png' ;
        self.get[ item.name ] = new Image();
        self.get[ item.name ].src = 'data:' + mime + ';base64,' + content;
      } else {
        // Not yet...
      }
    }
  }

  return self;
})();

The code from above works great, but doesn't cover my needs, as you can see I'm using localStore and it has it's limitations, I want to adapt the same code but for an unlimited storage, any ideas?


Solution

  • After a long research I concluded that IndexedDB is probably the most capable local storage we can find for this task, so, I re-designed my code and now this is the full class:

    /**
     * Copyright 2014 - Edgar Alexander Franco.
     *
     * @author Edgar Alexander Franco
     * @version 1.0.0
     */
    
    var Resources = (function () {
      var self = {};
    
      self.get = {};
    
      self.DOWNLOADED = 1;
      self.LOADED = 2;
    
      var DB_NAME = 'evilition';
      var TABLE_NAME = 'resources';
    
      self.audioType = (document.createElement('audio').canPlayType('audio/mp3') == '') ? '.ogg' : '.mp3' ;
    
      /**
       * Load the assets from the server / filesystem depending if each is cached or not.
       *
       * @param {object} resources Resources of the game.
       * @param {function} callback1 Function to be called on progress.
       * @param {function} callback2 Function to be called on error.
       * @param {function} callback3 Function to be called once the resources are loaded.
       */
      self.load = function (resources, callback1, callback2, callback3) {
        var request = indexedDB.open(DB_NAME, 3);
    
        request.onerror = function (evt) {
          callback2({});
        }
    
        request.onupgradeneeded = function (evt) {
          var db = evt.target.result;
          var table = db.createObjectStore(TABLE_NAME, {
            keyPath : 'name'
          });
          table.createIndex('name', 'name', {
            unique : true
          });
        }
    
        request.onsuccess = function (evt) {
          function loadResource () {
            var resource = resources[ i ];
            var request = objectStore.get(resource.name);
    
            request.onerror = function (evt) {
              callback2(resource);
            }
    
            request.onsuccess = function (evt) {
              var progress = Math.round(((i + 1) * 100) / total);
              var content;
    
              if (typeof request.result == 'undefined') {
                if (resource.type == 'audio') {
                  resource.path += self.audioType;
                }
    
                xhr.open('GET', resource.path + '.b64', false);
                xhr.send();
    
                if (xhr.readyState == 4 && xhr.status == 200) {
                  content = xhr.responseText;
                  objectStore.add({
                    name : resource.name, 
                    content : content
                  });
                  callback1(resource, progress, self.DOWNLOADED);
                } else {
                  callback2(resource);
    
                  return;
                }
              } else {
                content = request.result.content;
                callback1(resource, progress, self.LOADED);
              }
    
              var mime;
    
              if (resource.type != 'audio') {
                mime = (resource.type == 'jpg') ? 'image/jpeg' : 'image/png' ;
                self.get[ resource.name ] = new Image;
              } else {
                mime = (self.audioType == '.mp3') ? 'audio/mp3' : 'audio/ogg' ;
                self.get[ resource.name ] = new Audio;
              }
    
              self.get[ resource.name ].src = 'data:' + mime + ';base64,' + content;
              i++;
    
              if (i == total) {
                db.close();
                callback3();
              } else {
                loadResource();
              }
            }
          }
    
          var db = evt.target.result;
          var objectStore = db.transaction(TABLE_NAME, 'readwrite').objectStore(TABLE_NAME);
          var xhr = (typeof XMLHttpRequest != 'undefined') ? new XMLHttpRequest : new ActiveXObject('Microsoft.XMLHTTP') ;
          var total = resources.length;
          var i = 0;
    
          loadResource();
        }
      }
    
      /**
       * Delete the resources database requiring the creation of a new one in the next load.
       */
      self.clearLocalCache = function () {
        indexedDB.deleteDatabase(DB_NAME);
      }
    
      return self;
    })();
    

    Thank you Gregor for you recommendation :)