HTML5 - - Modernize seu jogo HTML5 Canvas: APIs offline, de arrastar e soltar e de arquivo

David Rousset

Technical Evangelist

Recursos HTML5 em navegadores modernos como o Internet Explorer 10 possibilitam uma classe totalmente nova de aplicativos Web e de cenários de jogos. Este artigo em duas partes demonstra como usei alguns desses recursos novos para modernizar meu último, jogo de plataforma HTML5. Na Parte 1 deste artigo, falei sobre como usar transformações 3D, transições e layout de grade CSS3. Neste artigo, mostrarei como usar as APIs offline, de arrastar e soltar e de arquivo para implementar algumas ideias interessantes.

Jogo no modo offline

A versão original de meu jogo funcionava somente se o seu dispositivo estivesse conectado à Internet. Se você quisesse jogar meu jogo fabuloso no trem, em um táxi ou qualquer outro lugar sem uma conexão de Internet, estava sem sorte—preso sem acesso àquela maravilha. E isso era uma pena, porque realmente não há nada em meu jogo que precise de uma conexão ativa com o servidor Web uma vez que todos os recursos tenham sido baixados. Felizmente, APIs offline fornecem uma solução para isso em HTML5.

Etapa 1: Escolha os recursos que quiser armazenar no cache

Na verdade é bem fácil dizer ao navegador que recursos você quer armazenar no cache para uso offline. Mas, antes de ir adiante, recomendo que você leia estes dois artigos:

Para o meu jogo, criei o arquivo dragDropLogic.js contendo o código mostrado na Figura 1.

CACHE MANIFEST
# Version 1.5
CACHE:
index.html
modernplatformer.css
img/MonsterA.png
.. up to ..
img/MonsterD.png
img/Player.png
img/offlinelogoblack.png
img/Backgrounds/Layer0_0.png
.. up to ..
img/Backgrounds/Layer2_2.png
img/Tiles/BlockA0.png
.. up to ..
img/Tiles/BlockA6.png
img/Tiles/BlockB0.png
img/Tiles/BlockB1.png
img/Tiles/Gem.png
img/Tiles/Exit.png
img/Tiles/Platform.png
overlays/you_died.png
overlays/you_lose.png
overlays/you_win.png
src/dragDropLogic.js
src/main.js
src/ModernizrCSS3.js
src/easeljs/utils/UID.js
src/easeljs/geom/Matrix2D.js
src/easeljs/events/MouseEvent.js
src/easeljs/utils/SpriteSheetUtils.js
src/easeljs/display/SpriteSheet.js
src/easeljs/display/Shadow.js
src/easeljs/display/DisplayObject.js
src/easeljs/display/Container.js
src/easeljs/display/Stage.js
src/easeljs/display/Bitmap.js
src/easeljs/display/BitmapAnimation.js
src/easeljs/display/Text.js
src/easeljs/utils/Ticker.js
src/easeljs/geom/Rectangle.js
src/easeljs/geom/Point.js
src/easeljs/XNARectangle.js
src/easeljs/PlatformerHelper.js
src/easeljs/ContentManager.js
src/easeljs/Tile.js
src/easeljs/Gem.js
src/easeljs/Enemy.js
src/easeljs/Player.js
src/easeljs/Level.js
src/easeljs/PlatformerGame.js
NETWORK:
*

Figura 1 - Platformer.cache

Inseri todos os arquivos PNG contendo meus sprites, fundo, camadas e sobreposições; os arquivos JS necessários da estrutura EaseJS; e minha própria lógica de jogo e os principais arquivos HTML e de CSS. Depois simplesmente indico que quero usar esse arquivo de manifesto em minha página HTML principal. Neste caso, é index.html:

<!DOCTYPE html>
<html manifest="platformer.cache">
<head>
    <title>HTML5 Platformer Game</title>
    // ...
</head>
<html>

Note que o arquivo de seu manifesto deve ser servido como “text/cache-manifest”. Para o meu jogo, adicionei um novo tipo de conteúdo .cache mapeado para text/cache-manifest, pois é armazenado no armazenamento de blob do Windows Azure.

Tenha em mente que essa especificação não permite alterações delta. Mesmo que um de seus arquivos mude, você precisa forçar um redownload completo para o navegador se atualizar para a nova versão. Contudo, qualquer alteração no arquivo de seu manifesto será excluída pelo navegador, que então baixa todos os recursos especificados no arquivo. A alteração pode ser um número de versão, uma data, um GUID—o que quer que funcione para você—no início do arquivo através de um comentário (“Versão 1.5” na Figura 1).

Etapa 2: Modificação da lógica para o carregamento dos níveis

A versão original de meu código baixava cada nível através de uma chamada de XHR para o servidor Web. Preciso mudar isso para fazer meu jogo funcionar no modo offline. Além disso, gostaria de indicar ao usuário que ele está jogando offline no modo offline incluindo o logotipo HTML5 associado "oficial" dentro da tela do jogo.

Vejamos as mudanças para fazer isso acontecer. Na primeira vez que o usuário lançar meu jogo, todos os níveis serão baixados (descritos nos arquivos {x}.txt) para o armazenamento local. Essa operação é amplamente suportada (desde o Internet Explorer 8) e bastante fácil de usar—e, mais importante, está disponível no modo offline.

A Figura 2 mostra o código que eu adicionei em PlatformerGame.js.

PlatformerGame.prototype.DownloadAllLevels = function () {
    // Searching where we are currently hosted
    var levelsUrl = window.location.href.replace('index.html', '') +
      "levels/";
    var that = this;
    for (var i = 0; i < numberOfLevels; i++) {
        try {
            var request = new XMLHttpRequest();
request.open('GET', levelsUrl + i + ".txt", true);
request.onreadystatechange = makeStoreCallback(i, request, that);
request.send(null);
        }
        catch (e) {
            // Loading the hard coded error level to have at least something
            // to play with console.log("Error in XHR. Are you offline?");
            if (!window.localStorage["platformer_level_0"]) {
window.localStorage["platformer_level_0"] =
hardcodedErrorTextLevel;
            }
        }
    }
};
// Closure of the index
function makeStoreCallback(index, request, that) {
    return function () {
storeLevel(index, request, that);
    }
}
function storeLevel(index, request, that) {
    if (request.readyState == 4) {
        // If everything was OK
        if (request.status == 200) {
            // storing the level in the local storage
            // with the key "platformer_level_{index}
window.localStorage["platformer_level_" + index] =
request.responseText.replace(/[\n\r\t]/g, '');
numberOfLevelDownloaded++;
        }
                else {
            // Loading a hard coded level in case of error
window.localStorage["platformer_level_" + index] =
hardcodedErrorTextLevel;
        }
        if (numberOfLevelDownloaded === numberOfLevels) {
that.LoadNextLevel();
        }
    }
}

Figura 2 - PlatformerGame.js

Todos os níveis no construtor PlatformerGame serão baixados de forma assíncrona. Uma vez que todos os níveis tenham sido baixados (numberOfLevelDownloaded = = = numberOfLevels), o primeiro nível é carregado. Aqui está o código para a nova função:

  1. // Loading the next level contained into the localStorage in
  2. // platformer_level_{index}
  3. PlatformerGame.prototype.LoadNextLevel = function () {
  4. this.loadNextLevel = false;
  5. // Setting back the initialRotation class will trigger the transition
  6. this.platformerGameStage.canvas.className = "initialRotation";
  7. this.levelIndex = (this.levelIndex + 1) % numberOfLevels;
  8. var newTextLevel = window.localStorage["platformer_level_" +
  9. this.levelIndex];
  10. this.LoadThisTextLevel(newTextLevel);
  11. };

O início do código lida com as transições de CSS3 descritas na Parte 1. O jogo simplesmente acessará o armazenamento local através da chave apropriada para recuperar o conteúdo baixado anteriormente.

Etapa 3: Confirmação de online/offline e exibição de um logotipo quando lançado no modo offline

Dois testes são necessários para confirmar que o jogo está sendo executado no modo offline. O primeiro é se o navegador implementou eventos offline/online, como a maioria dos navegadores faz. Se o navegador disser que o usuário está offline, isso é confirmado e o jogo deve se alternar automaticamente para a lógica offline Contudo, frequentemente essa verificação simples não é suficiente. O navegador pode dizer que está online, mas não sabe se o servidor Web ainda está online ou não. Assim, você precisa de uma segunda verificação executando ping no servidor com uma XHR simples. A Figura 3 mostra código para as duas verificações.

  1. PlatformerGame.prototype.CheckIfOnline = function () {
  2. if (!navigator.onLine) return false;
  3. var levelsUrl = window.location.href.replace('index.html', '') +
  4. "levels/";
  5. try {
  6. var request = new XMLHttpRequest();
  7. request.open('GET', levelsUrl + "0.txt", false);
  8. request.send(null);
  9. }
  10. catch (e) {
  11. return false;
  12. }
  13. if (request.status !== 200)
  14. return false;
  15. else
  16. return true;
  17. };

Figura 3 - Verificação do modo offline

Meu teste é tentar baixar o primeiro nível. Se falhar, o jogo alterna para a parte offline de meu código. Agora aqui está o código lançado na parte do construtor de PlatformerGame.js:

  1. PlatformerGame.IsOnline = this.CheckIfOnline();
  2. // If we're online, we're downloading/updating all the levels
  3. // from the webserver
  4. if (PlatformerGame.IsOnline) {
  5. this.DownloadAllLevels();
  6. }
  7. // If we're offline, we're loading the first level
  8. // from the cache handled by the local storage
  9. else {
  10. this.LoadNextLevel();
  11. }

E aqui está o código exibindo o logotipo de offline em Level.js na função CreateAndAddRandomBackground:

  1. if (!PlatformerGame.IsOnline) {
  2. offlineLogo.x = 710;
  3. offlineLogo.y = -1;
  4. offlineLogo.scaleX = 0.5;
  5. offlineLogo.scaleY = 0.5;
  6. this.levelStage.addChild(offlineLogo);
  7. }

Com essas alterações implementadas, a Figura 4 mostra a aparência de meu jogo quando é lançado sem uma conexão de rede.

JJ938038.765891B96FA71B886410657D4BD4517E(pt-br,MSDN.10).png

Figura 4 - Jogo de plataforma HTML5 no modo offline

O logotipo de offline é exibido logo acima da taxa de quadros, indicando ao usuário que o jogo está sendo executado puramente offline.

Etapa 4: Download condicional dos arquivos MP3 ou OGG e armazenamento deles como blob em IndexedDB

Isso é algo que não implementei, mas gostaria de compartilhar o conceito com você como um bônus, para que você mesmo possa implementá-lo.

Você pode ter notado que eu não incluí os efeitos sonoros e a música de meu jogo no arquivo do manifesto na Etapa 1. Quando escrevi esse jogo HTML5, meu primeiro objetivo era que fosse compatível com o maior número de navegadores possível. Para isso, tenho duas versões dos sons: MP3 para o Internet Explorer e o Safari, e OGG para o Chrome, Firefox e o Opera. O gerenciador de downloads de conteúdo baixa apenas o tipo de codec suportado pelo navegador que lançar meu jogo. Isso porque não há necessidade de baixar a versão OGG dos arquivos se eu estiver jogando o jogo no Internet Explorer, assim como não é preciso baixar a versão MP3 para o Firefox.

O problema com o arquivo de manifesto é que você não indica continuamente qual recurso carregar com base no suporte do navegador do momento. Cheguei a três soluções para contornar essa limitação:

  1. Baixar as duas versões colocando todas as versões de arquivo no arquivo de manifesto. Isso é muito simples de implementar e funciona bem, mas os usuários estarão baixando alguns arquivos que nunca serão usados por alguns navegadores.
  2. Criar um arquivo de manifesto dinâmico no lado do servidor detectando o agente do navegador para reconhecer o codec suportado. Essa é definitivamente uma prática muito ruim!
  3. Usar um recurso no lado do cliente para detectar o suporte a codec no objeto do gerenciador de conteúdo e depois baixar o formato de arquivo apropriado para IndexedDB ou para o armazenamento local para uso offline.

Creio que a terceira solução é a melhor, mas você precisa ficar atendo a algumas coisas para fazê-la funcionar:

  • Se estiver usando armazenamento local, precisa codificar os arquivos em base64, e pode se deparar com limites de cota se tiver muitos arquivos ou se estes forem grandes demais.
  • Se estiver usando IndexedDB, você pode armazenar a versão codificada em base64 dos arquivos ou armazená-los como um blob.

A abordagem de blob é definitivamente a solução mais inteligente e eficiente, mas requer um navegador muito atual, como a última versão do Internet Explorer 10 (PP5) ou do Firefox (11). Se estiver curioso com esta ideia, veja a demo do complemento para Facebook de nosso site de test-drive do IE aqui:

JJ938038.F1EC11107B37A19E7A0A1F9CCA6AD660(pt-br,MSDN.10).png

Você encontrará mais detalhes sobre essa demo neste arquivo: Atualizações de IndexedDB para o IE10 e aplicativos estilo Metro.

Na versão do jogo fornecida com esse artigo, decidi armazenar todos os formatos no cache (solução 1). Posso atualizar isso em um artigo futuro implementando um armazenamento de IndexedDB em cache. Fique ligado!

APIs de arrastar e soltar e de arquivo

Aqui está um recurso novo divertido que tira proveito das novas APIs de arrastar e soltar e de arquivo. O usuário pode criar ou editar um nível usando seu editor de texto favorito e depois simplesmente arrastá-lo eu explorador de arquivos e soltá-lo no jogo HTML5 e jogar!

Não entrarei em muitos detalhes sobre arrastar e soltar, pois isso foi muito bem coberto em Arrastar e soltar HTML5 no IE10, que explica como a demo Magnetic Poetry foi criada. Recomendo a leitura daquele artigo primeiro para um entendimento completo do código a seguir.

Para o meu jogo, criei o arquivo dragDropLogic.js contendo o código mostrado na Figura 5.

(function () {
    "use strict";
    var DragDropLogic = DragDropLogic || {};
    var _elementToMonitor;
    var _platformerGameInstance;
    // We need the canvas to monitor its drag&drop events
    // and the platformer game instance to trigger the loadnextlevel
    // function
    DragDropLogic.monitorElement = function (elementToMonitor,
platformerGameInstance) {
_elementToMonitor = elementToMonitor;
_platformerGameInstance = platformerGameInstance;
_elementToMonitor.addEventListener("dragenter",
DragDropLogic.drag,false);
_elementToMonitor.addEventListener("dragover",
DragDropLogic.drag, false);
_elementToMonitor.addEventListener("drop",
DragDropLogic.drop, false);
    };
    // We don't need to do specific actions
    // enter & over, we're only interested in drop
DragDropLogic.drag = function (e) {
e.stopPropagation();
e.preventDefault();
    };
DragDropLogic.drop = function (e) {
e.stopPropagation();
e.preventDefault();
        var dt = e.dataTransfer;
        var files = dt.files;
        // Taking only the first dropped file
        var firstFileDropped = files[0];
        // Basic check of the type of file dropped
        if (firstFileDropped.type.indexOf("text") == 0) {
  var reader = new FileReader();
            // Callback function
reader.onload = function(e){
                // get file content
                var text = e.target.result;
                var textLevel = text.replace(/[\s\n\r\t]/g, '');
                // Warning, there is no real check on the consistency
                // of the file.
_platformerGameInstance.LoadThisTextLevel(textLevel);
            }
            // Asynchronous read
reader.readAsText(firstFileDropped);
        }
    };
window.DragDropLogic = DragDropLogic;
})();

Figura 5 - dragDropLogic.js

O código de arrastar e soltar é chamado dentro de main.js na função startGame:

// Callback function once everything has been downloaded
function startGame() {
platformerGame = new PlatformerGame(stage, contentManager, 800, 480,
window.innerWidth, window.innerHeight);
window.addEventListener("resize", OnResizeCalled, false);
OnResizeCalled();
DragDropLogic.monitorElement(canvas, platformerGame);
platformerGame.StartGame();
}

E isso é tudo! Por exemplo, copie e cole este bloco de texto em um arquivo .txt novo:

....................

....................

....................

.1..................

######.........#####

....................

.........###........

....................

.G.G.GGG.G.G.G......

.GGG..G..GGG.G......

.G.G..G..G.G.GGG....

....................

....................

.X................C.

####################

Agora arraste e solte-o no meu jogo. O novo nível será carregado como mágica, como pode ver na Figura 6.

JJ938038.C622870F6A1FE51020C0F881E5EBFC61(pt-br,MSDN.10).png

Figura 6 - Um novo nível adicionado através da API de arrastar e soltar

Demo e código-fonte

Se você quiser ver uma demonstração no Internet Explorer de todos os recursos implementados neste artigo, assista a este vídeo curto:

Baixar o vídeo:MP4, WebM, HTML5 Video Player de VideoJS

JJ938038.A7F68DA60B3E90C164CBAC79F545C759(pt-br,MSDN.10).png

Você também pode brincar com essa demo no Internet Explorer 10 ou seu navegador favorito aqui:. Jogo de plataforma HTML5 moderno.

JJ938038.91106CE3430C81D5A826436E28DFBCC6(pt-br,MSDN.10).png

Como você fez a gentileza de ler este artigo inteiro, desfrute o código-fonte inteiro aqui: Código-fonte do jogo de plataforma HTML5 moderno.

Mostrar: