Tetris 1/3: Conceitos Básicos

Com certeza você já jogou TETRIS. É um dos jogos mais famosos de todos os tempos, e é por ele que vou começar meus tutoriais de desenvolvimento de jogos em HTML5. Quero lembrar a todos que eu faço jogos por diversão, então não assumam que o conteúdo aqui é exatamente profissional. Espero que vocês consigam aprender alguns conceitos sobre jogos em HTML5 comigo e que talvez seja o suficiente pra você se aventurar e fazer seus próprios jogos. Dito isso, vamos ao que interessa ;)

Ok, vamos começar então listando os conceitos/características do jogo. Eles vão servir como objetivos para o nosso tutorial.

  • O jogo se passa num grid de tamanho X (largura) por Y (altura).
  • Existem vários tipos de peças diferentes, mas elas se comportam da mesma maneira.
  • O jogo começa com uma peça (escolhida aleatoriamente) descendo até colidir com o chão ou com outra peça.
  • O jogador pode mover a peça atual na horizontal, pode acelerar a sua descida, e pode girá-la.
  • Quando uma peça colide com algo, o jogo deve verificar se alguma linha está completa. Se ela estiver, deve ser destruída.
  • Cada linha destruída gera pontos para o jogador. Destruir mais de uma linha de uma só vez gera pontos bonus.
  • O fluxo continua até que uma das peças, ao colidir, esteja encostada no topo do grid.

Fazer esse tipo de lista é uma ótima forma de você parar pra pensar e entender realmente o que está acontecendo. É bem parecido com aquela primeira aula de algorítimos da faculdade :)


Não vou usar jQuery nesse tutorial, então vou criar algumas funções pra agilizar as coisas, ok?

$ = function(selector) {
	// simulando o $(selector)
	return document.querySelectorAll(selector);
};

O jogo se passa num grid de tamanho X (largura) por Y (altura).

Vamos começar pelo grid. Eu poderia fazê-lo usando vários elementos divs, mas temos o famoso canvas pra isso. 

<canvas id='grid'></canvas>

Agora vamos criar uma classe javascript para gerenciar o canvas.

var classCanvas = function(x,y) {
	// salvando os parametros
	this.largura = x;
	this.altura = y;
	// setando o tamanho de cada 'posição'
	this.tamanhoPos = 30;
	// redimensionando o canvas
	var $canvas = $('#grid')[0];
	$canvas.setAttribute('width',this.tamanhoPos*this.largura);
	$canvas.setAttribute('height',this.tamanhoPos*this.altura);
};

E iniciar nossa grid no tamanho desejado. Pode ser qualquer valor que você achar interessante.

var canvas = new classCanvas(10,15);

Vou colocar uma borda no grid só pra gente ver como ficou.

#grid {
    border:1px solid black;
    background:white;
}

Existem vários tipos de peças diferentes, mas elas se comportam da mesma maneira.

O TETRIS original tem 7 tipos de peças, identificadas por letras: I, T, S, J, O, L, e Z. Vamos criar uma classe javascript pra essas peças.

var classPiece = function(tipo) {
	// salvando os parametros
	this.tipo = tipo;
};

Cada tipo de peça tem sua configuração de quais quadrados são 'preenchidos', além também de suas opções ao serem giradas.

Eu poderia usar vetores multidimensionais pra mapear essa configuração, usando '1' pra quadrado ocupado e '0' pra vazio... mas daria muito trabalho. Achei melhor usar um novo modelo de mapeamento. Vejam só:

Certo, agora podemos criar um objeto que vai guardar essas configurações ;)

var CONFIG_MAP = {
	O:{
		cor:'#de60cc', // rosa
		maps:[
			[5,6,9,10]
		]
	},
	I:{
		cor:'#ff0000', // vermelho
		maps:[
			[4,5,6,7],
			[2,6,10,14]
		]
	},
	S:{
		cor:'#66ffff', // azul claro
		maps:[
			[6,7,9,10],
			[2,6,7,11]
		]
	},
	Z:{
		cor:'#4f81bd', // azul escuro
		maps:[
			[5,6,10,11],
			[3,6,7,10]
		]
	},
	L:{
		cor:'#00b050', // verde
		maps: [
			[5,6,7,9],
			[2,6,10,11],
			[3,5,6,7],
			[1,2,6,10]
		]
	},
	J:{
		cor:'#ffff00', // amarelo
		maps: [
			[5,6,7,11],
			[2,3,6,10],
			[1,5,6,7],
			[2,6,9,10]
		]
	},
	T:{
		cor:'#f79646', // laranja
		maps: [
			[5,6,7,10],
			[2,6,7,10],
			[2,5,6,7],
			[2,5,6,10]
		]
	}
};

Com isso eu posso melhorar a minha classPiece e adicionar esses dados. Além disso, vou criar um método que 'gira' a peça, o que basicamente significa pegar o seu próximo mapping (apesar de que só vamos usar isso na próxima parte desse tutorial).

var classPiece = function(tipo) {
	// salvando os parametros
	this.tipo = tipo;	
	// buscando os dados desse tipo de peça
	this.data = CONFIG_MAP[this.tipo];
	// usando o primeiro map por padrão
	this.mapAtual = 0;	

	// método  pra girar a peça
	this.gira = function() {
		// mudando para o próximo map dessa peça
		this.mapAtual++;
		// se ele não existir, voltamos pro primeiro mapa
		if (!this.data.maps[this.mapAtual]) this.mapAtual = 0;
	}
};

O jogo começa com uma peça (escolhida aleatoriamente) descendo até colidir com o chão ou com outra peça.

Precisamos adicionar peças ao grid, né? Todo jogo de TETRIS começa com uma peça aparecendo lá no topo. Pra fazer isso, vamos começar adicionando alguns métodos à classCanvas.

// método para iniciar o jogo
	this.iniciar = function() {
		// iniciando um vetor pra guardar todas as peças do jogo
		this.pieces = [];
		// criando a primeira peça
		this.criaPiece();
		// começa a desenhar
		this.desenhaTudo();
	};
	
	// método para criar uma nova peça
	this.criaPiece = function() {
		// pegando todos tipos de peça definidos
		var tipos = Object.keys(CONFIG_MAP);
		// escolhendo um aleatoriamente
		var tipo = tipos[Math.floor(Math.random()*tipos.length)];
		// inicializando a nova peça
		var piece = new classPiece(tipo);
		// adicionando ela à lista de peças
		this.pieces.push(piece);
		// posicionando a peça no meio do topo 
		var midx = Math.floor(this.largura/2)-2;
		piece.setaPosicao(midx,0,true);
	};
	
	// buscando context
	this.ctx = $canvas.getContext('2d');
	this.ctx_width = this.tamanhoPos*this.largura;
	this.ctx_height = this.tamanhoPos*this.altura;
	
	// método para desenhar tudo
	this.desenhaTudo = function() {
	};

Tem um método novo da classPiece pra ser adicionado também:

// método pra posicionar a peça
	this.x = 0;
	this.y = 0;
	this.setaPosicao = function(newx,newy,ajuste) {
		// setando os valores de x e y
		this.x = newx;
		this.y = newy;
		// caso a flag de ajuste tenha sido passada
		if (ajuste) {
			// pegamos o mapa atual
			var map = this.data.maps[this.mapAtual];
			// se a primeira posição for depois da primeira linha, subimos uma linha
			if (map[0] >= 4) this.y--;
		}
	};

Eu deixei o classCanvas.desenhaTudo em branco porque queria falar um pouco mais sobre canvas primeiro. Diferente do javascript 'normal' onde você posiciona/movimenta objetos alterando o left/top deles (assumindo que estejam com position relative ou absolute), no canvas você precisa realmente DESENHAR as coisas. A cada 'frame' você está apagando tudo e desenhando de novo. Se você tem um background com um personagem, a cada frame você precisa apagar tudo, desenhar o background e desenhar o personagem na posição left/top adequada. Se a intenção é fazer esse personagem se movimentar, você precisa de algum outro controle atualizando os valores de left/top dele pra que ele seja desenhado na nova posição. Não dá pra 'arrastar' o elemento do personagem... porque não existe elemento! É só um desenho no canvas!

// método para desenhar tudo
	this.desenhaTudo = function() {
		// limpando o canvas
		this.ctx.clearRect(0, 0, this.ctx_width, this.ctx_height);
		// iterando todas as peças e desenhando elas
		this.pieces.forEach(function(piece) {
			this.desenhaPiece(piece);
		}.bind(this));
		// cancelando algum request pendente 
		cancelAnimationFrame(this.requestFrame);
		// realizando um novo request, pra redesenhar assim que terminar esse frame
		this.requestFrame = window.requestAnimationFrame(function() { 
			this.desenhaTudo(); 
		}.bind(this));
	};
	
	// metodo para desenhar uma peça
	this.desenhaPiece = function(piece) {
		// buscando o map atual dessa peça
		var map = piece.data.maps[piece.mapAtual];
		// vamos iterar as posições e desenhá-las
		map.forEach(function(pos) {
			// calculando a linha dessa posição 
			var row = Math.floor(pos/4);
			// e a coluna dessa posição
			var col = pos%4;
			// calculando a posição left/top dele de acordo com seu x,y
			var left = (col+piece.x)*this.tamanhoPos;
			var top = (row+piece.y)*this.tamanhoPos;
			// iniciando um traçado no context
			this.ctx.beginPath();
			// preparando um retangulo pra essa posição
      		this.ctx.rect(left,top,this.tamanhoPos,this.tamanhoPos);
			// setando a cor do desenho
			this.ctx.fillStyle = piece.data.cor;
			// setando a cor da borda
			this.ctx.strokeStyle = '#000000';
			// desenhando
      		this.ctx.fill(); 
			this.ctx.stroke();
		}.bind(this));
	};

E, logicamente, nossa primeira chamada precisa agora iniciar o jogo.

var canvas = new classCanvas(10,15);
canvas.iniciar();

Se você fez tudo direitinho, provavelmente vai ter um resultado parecido com esse aqui. Clique em "result" para reiniciar o script. Como nós fizemos ele iniciar com um tipo de peça aleatoriamente, a peça pode mudar a cada vez. Não, ela ainda não 'desce'. Afinal não fizemos nada referente a isso né?


E por aqui nós encerramos a primeira parte desse tutorial!

Na parte 2 vamos fazer a peça descer sozinha, implementar os controles e verificar as colisões.

Na parte 3 vamos implementar a destruição de linhas, a parte da pontuação e a verificação de fim de jogo.

Espero que tenham gostado! Esse é meu primeiro post/tutorial então estou meio perdido no melhor formato/abordagem, lol. Fiquem a vontade pra comentar.

Leave a Comment