Front End

2 mai, 2007

Jogos em JavaScript – Parte 03

Publicidade

Olá amigos. Esse artigo atrasou um pouco porque fim de semana passado foi cheio e eu normalmente escrevo nos fins de semana. Esse foi escrito nos intervalos no trabalho.

Bom, vamos ao que interessa!

Iremos terminar o nosso background do jogo, e para isso precisaremos fazer o nosso carregador de imagens. Mais a frente deste artigo também veremos sobre uso de textos. Antes de iniciar, por favor façam o download das imagens que serão utilizadas aqui.

Image Loader

Alguns devem saber disso, mas é possível carregar imagens diretamente no JavaScript. Muitas pessoas usam isso para cache de imagens em efeitos como mudar a imagem quando passar o mouse por exemplo. Para o tipo de uso descrito anteriormente existe um problema. pois a imagem é carregada no cache do browser, e quando você chama ela novamente o browser puxa ela do seu cache. Isso parece muito bom, mas lembre-se que muitas vezes o cache do browser não funciona como deveria, e isso faz a imagem ser recarregada e o cache acaba sendo inútil. Ainda bem que não teremos esse problema.

No nosso caso do canvas, nós desenhamos imagens usando referências de imagens já carregadas. Não vamos um “reload by cache” e assim não teremos esse problema de cache do browser (ufaaa).

Vamos ver como fazer nosso carregador de imagens (colocar no engine.js):

ImagesLoader = function() {
	this.names = [];
	this.images = {};
	
	this.onLoad = function() {};
	this.onLoadAll = function() {};
	this.loaded = 0;
	this.total = 0;
};

ImagesLoader.prototype = {
	load: function() {
		this.total += arguments.length;
		
		for(var i = 0; i < arguments.length; i++) {
			this.names[i] = arguments[i].match(/([^.]+)\..+$/)[1];
			
			var img = new Image();
			img.onload = this._loadImage.bind(this, img, i);
			
			img.src = arguments[i];
		}
	},
	
	_loadImage: function(image, index) {
		this.loaded++;
		
		this.images[this.names[index]] = image;
		
		this.onLoad(image, this.loaded, this.total);
		
		if(this.loaded == this.total)
			this.onLoadAll(this.loaded, this.total);
	}
};

Ok. No inicializador nós simplesmente iniciamos as variáveis zeradas, entra ela 2 eventos, um para quando houver o carregamento de cada imagem individualmente (que pode servir para fazer um barra de loading futuramente) e um quando tudo termina de carregar (que vamos usar para iniciar o jogo).

O método load faz o carregamento das imagens. Ele recebe N argumentos, onde cada argumento deve ser o caminho para a imagem a ser carregada e ele também adiciona o número de imagens ao número total de imagens que devem ser carregadas. Na URL, ele pega apenas o nome do arquivo sem a extensão (foi usada uma expressão regular para isso) e grava. Isso vai servir para acessar os recursos depois de carregados. Depois de saber o nome, nós iniciamos uma imagem vazia, antes de definir a URL dela, nós informamos o evento onload (quando carregar) para que avise nossa classe que a imagem foi carregada. Reparem no uso do bind novamente. Dessa vez usamos o bind trocando tanto o escopo como definindo as variáveis, e finalmente, mandamos carregar a imagem (mudando a propriedade src da imagem).

Esse ciclo se repete por todos os argumentos (caminhos de imagem) passadas para o método load. Na parte do callback dos onload, ele adiciona 1 ao contador de imagens carregadas, dispara o evento de carregamento individual, e caso o números de imagens carregadas seja igual ao número total de imagens, ele dispara o evento de carregar todas as imagens.

Acho que deu pra explicar direitinho como funciona, mas em caso de dúvidas fiquem a vontade para perguntar.

Agora que podemos usar imagens tranquilamente, vamos criar a outra camada do background do jogo!

Fazer essa camada vai passar para vocês um conhecimento legal sobre uma técnica de jogos chamada Scrolling. No caso do nosso jogo, essa técnica vai ser empregada para que o background pareça estar descendo na tela. Em outros jogos, como aquela que o boneco vai andando pela tela, essa técnica pode ser empregada para desenhar o cenário corretamente. Como de costume, vamos ao código primeiro, explicações depois (esse deve ir direto no HTML.

ScrollSky = function(image) {
	this.image = image;
	this.offset = 0;
};

ScrollSky.prototype = {
	update: function(elapsed) {
		this.offset += elapsed * .04;
		this.offset %= this.image.height;
	},
	
	render: function(ctx, canvas) {
		var top = this.image.height - canvas.height - this.offset;
		
		if(top >= 0)
			ctx.drawImage(this.image, 0, top, this.image.width, canvas.height, 0, 0, this.image.width, canvas.height);
		else {
			top = -top;
			ctx.drawImage(this.image, 0, 0, this.image.width, canvas.height - top + 1, 0, top - 1, this.image.width, canvas.height - top + 1);
			ctx.drawImage(this.image, 0, this.image.height - top, this.image.width, top, 0, 0, this.image.width, top);
		}
	}
};

Bom, o funcionamento é simples. Temos nossa imagem de background (que naturalmente é maior que nossa tela), e nós vamos fazendo ela “rolar” pela tela, de cima para baixo, exibindo inicialmente a parte final, e após terminar de rolar ela se repete. Como é de se esperar, em um certo momento teremos que desenhar ela 2 vezes, uma para a parte que esta terminado de descer, e outra para completar a tela (para não ficar sem background naquela parte). Para entender melhor, eu fiz algumas imagens para ilustrar o esquema:

Como podem ver nessa imagem, uma parte do background está “fora” da tela. Ou seja, ele não é desenhado. Temos valores como o top (que seria a posição da parte de cima do background, nesse caso esse valor está negativo), e também temos o offset, que é a diferença entre a parte de baixo da imagem e parte de baixo do canvas, nesse estado inicial o offset é zero.

Esse passo é um passo normal apenas para mostrar a troca de posição para vocês. O top e o offset vão aumentando gradativamente, como vocês podem perceber. Uma hora o valor top será positivo, e com isso vai ficar sem nada desenhado acima dele. Para resolver isso vamos a imagem 3:

Ahaa, agora sim, começamos a complicar.

Como podem ver, agora temos 2 cópias do nosso background. O principal que está mais abaixo (azul) e a segunda cópia que está sendo desenhada para cobrir a parte de cima (o verde). Para entender os cálculos, olhe lá no código, mas se você raciocinar olhando para a figura fica mais fácil.

E finalmente, percebam que após o top passar a parte de baixo do canvas, o offset vai ser igual a altura do background (não precisa de uma régua para perceber isso) e nesses pontos nós devemos reiniciar a posição do elemento. Por isso que no código tem a parte this.offset %= this.image.height que serve justamente para que o background reinicie a posição e já se ajuste.

Escrevendo textos

Bom, ta aí uma coisa que o Canvas não resolve, o tal do desenhar textos.

Diferente do que eu imaginei quando eu me deparei com ela, o Canvas não tem nada do tipo “drawText”. Então, nesse caso, vamos criar!

A idéia é simples. O que faremos é usar um esquema de “bitmap fonts”. O arquivo de fonte na verdade é uma imagem com um tamanho padrão para cada caractere. Dessa forma, na hora de desenhar uma letra, buscaremos nesse nosso “alfabeto” e iremos desenhar na tela. Com alguns códigos, podemos fazer a coisa ficar tão simples quanto um drawText.

Font = function(image, charset) {
	this.image = image;
	this.charset = charset || " !+,-./0123456789:=?abcdefghijklmnopqrstuvwxyz";
	this.caseSensitive = false;
	this.letterSize = this.image.width / this.charset.length;
	this.letterSpacing = 0;
	this.lineSpacing = 2;
};

Font.prototype.draw = function(ctx, x, y, text) {
	var index = 0;
	var basex = x;
	
	if(!this.caseSensitive)
		text = text.toLowerCase();
	
	for(var i = 0; i < text.length; i++) {
		if(text.charAt(i) == "\n") {
			x = basex;
			y += this.image.height + this.lineSpacing;
			continue;
		}
		
		index = Math.max(this.charset.indexOf(text.charAt(i)), 0);
		ctx.drawImage(this.image, index * this.letterSize, 0, this.letterSize, this.image.height, x, y, this.letterSize, this.image.height);
		
		x += this.letterSize + this.letterSpacing;
	}
};

Coloquem o código acima no engine.js

Como podem ver, o código para nossa fonte é relativamente simples. Para esse código funcionar, vamos ter que ter uma imagem com as letras (que vai ser a nossa fonte na verdade). No início do código, vocês podem ver que deve ser passado a imagem da fonte (que tem as letras) e também pode ser passado o alfabeto dessa fonte; para jogos eu acho que o alfabeto padrão que eu fiz ali é de bom tamanho, com letras normais, números e alguns caracteres especiais, mas mesmo assim, se você não achar sulficiente, você pode criar a sua própria fonte e definir o seu próprio alfabeto.

Lembrando que na imagem da fonte, os caracteres devem ter exatamente o mesmo tamanho. Verifiquem isso na imagem de fonte que eu passei (ta dentro do download descrito no início do artigo). Vejam que todas as letras ocupam 10×9 pixels (isso na minha fonte, você pode usar o tamanho que você quiser, mas todas devem ter o mesmo tamanho). Já para ajudar vocês, eu passei também no zip uma versão em Photoshop que contém as réguas separando as letras, para vocês poderem criar suas fontes por cima.

Outro detalhe importante é que o alfabeto que você colocar no JavaScript, as letras devem estar exatamente na mesma ordem que estão desenhadas na imagem. O resto ali são detalhes e são alto-descritíveis. São variáveis que vocês podem configurar depois de iniciar o objeto da fonte, mas para deixar claro:

  • caseSensitive – essa variável é para procurar diferenças entre maiúsculas e minúsculas na fonte. Em caso de false, ele sempre vai procurar minúsculas
  • letterSize – não mexa nessa, é um calculo automático para saber qual o tamanho de cada caractere
  • letterSpacing – espaçamento extra entre cada caractere
  • lineSpacing – espaçamento extra na quebra de linha

Agora, no nosso método draw. Ele é que vai desenhar a fonte. Na hora de usá-lo, você deve passar o context onde ele vai ser desenhado, a posição (x e y), e depois o texto a ser desenhado. Esse comando repete essa tarefa em cada letra do texto:

  1. Se o caractere for quebra de linha, reinicie a posição x e desca uma casa em y e pule para o próximo caractere
  2. Procurar o índice do caractere dentro do vocabulário. Caso não ache, use o índice zero
  3. Desenhe apenas a letra requisitava (fazendo um cálculo de onde está a letra usando o índice conseguido anteriormente)
  4. Mova o x uma casa

E assim nosso texto vai ser desenhado na tela!

Vamos colocar tudo junto agora! Para isso vamos ter que mudar um pouco a parte do onload. O código abaixo é referente a parte do html, a partir da declaração das variáveis (não apaguem as libs do próprio jogo, que são nosso FallRain e ScrollSky).

canvas = null;
bgFall = null;
font = null;

window.onload = function() {
	res = new ImagesLoader();
	
	res.onLoadAll = function() {
		canvas = new GameCanvas('gamecanvas');
		bgFall = new FallDots(8, canvas.canvas.width, canvas.canvas.height);
		sky = new ScrollSky(res.images.space);
		font = new Font(res.images.font);
		
		canvas.update = function(elapsed, canvas) {
			bgFall.update(elapsed, canvas);
			sky.update(elapsed);
		};
		
		canvas.render = function(ctx, canvas) {
			ctx.save();
			
			ctx.fillRect(0, 0, canvas.width, canvas.height);			

			sky.render(ctx, canvas);
			bgFall.render(ctx, canvas);

			font.draw(ctx, 60, 150, "Temos texto!");
			
			ctx.restore();
		};
	};
	
	res.load('space.jpg', 'font.gif');
};

Olhem como mudamos um pouco. Agora não mais inicializamos as coisas assim que a tela é carregada. Agora nós iniciamos o nosso ImageLoader, e só iniciamos as coisas do jogo depois que todas as imagens forem carregadas. Assim nós garantimos que elas irão aparecer.

O resto do procedimento é igual. Nós iniciamos os objetos (agora temos mais alguns, hehehe) e depois fazemos o update/render nos locais corretos. Notem que, por motivos lógicos, não temos um update para a fonte. Apenas mandamos desenhá-la e pronto. Importante também lembrar que você só precisa de um objeto de fonte por fonte realmente, e não um objeto fonte por texto. O mesmo objeto de fonte você pode usar para desenhar vários textos expalhados pela tela.

Antes de ir, está aqui o link para baixar os códigos usados: baixe aqui lembrando que as imagens estão no arquivo no início do artigo.

E por hoje ficamos por aqui. Desculpem novamente o atraso, mas espero que tenha valido a pena esperar 😉

No próximo artigo falaremos sobre teclado e sprites (animações).

Até a próxima galera!