Tetris 2/3: Gameplay

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 ;)


Atenção: sempre que aparecer (...) nos meus exemplos, quer dizer que estou fazendo referência ao código que a gente já tinha feito nas etapas anteriores.


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

Na primeira parte desse tutorial, a gente chegou até o ponto em que o jogo cria uma peça aleatoriamente. Agora vamos continuar essa etapa e fazer essa peça se movimentar. Sabe como nos jogos de TETRIS tudo vai ficando mais rápido com o tempo? Vamos fazer algo parecido (essa parte de ficar mais rápido só vai ser feito na parte 3, mas vou iniciar a velocidade agora). Primeiramente, vou incrementar minha classCanvas.iniciar pra definir a velocidade inicial e também começar a mover a peça.

// método para iniciar o jogo
	this.iniciar = function() {
		(...)
        // seta o delay de movimento inicial (quanto menor, mais rápido) 
        this.delayMovimento = 1000;
        // inicia o novimento
        this.iniciaMovimento();
	};

Certo, também vou precisar adicionar uma linha extra na classCanvas.criaPiece pra deixar salvo qual é a peça atual a ser movimentada.

// método para criar uma nova peça
	this.criaPiece = function() {
		(...)
        // setando essa peça como a atual
        this.pieceAtual = piece;
	};

Ok, agora vamos implementar a classCanvas.iniciaMovimento que acabamos de adicionar a chamada na classCanvas.iniciar. Essa função basicamente gera um interval (algo a ser chamado de x em x milisegundos) que vai mover a peça atual pra baixo.

// método pra iniciar o movimento das peças
    this.iniciaMovimento = function() {
    	// caso já tinha um interval rodando, vamos cancelá-lo
        clearInterval(this.intervalMovimento);
    	// cria um interval pra mover a peça pra baixo a cada x milisegundos
    	this.intervalMovimento = setInterval(function() {
        	// chama a função que move a peça
        	this.movePieceAtual('down');
        }.bind(this),this.delayMovimento);
    };

E lógico, vamos também adicionar a classCanvas.movePieceAtual que é chamada dentro do setInterval.

// método pra mover a peça numa direção qualquer
    this.movePieceAtual = function(direcao) {
    	// configurando a quantidade que será adicionada ao x/y 
        var adds = {
        	'down':{'x':0,'y':+1},
            'left':{'x':-1,'y':0},
            'right':{'x':+1,'y':0}
            // não configurei o 'up' porque não existe esse movimento no tetris!
        };
        // pegando as quantidades de acordo com o parametro
        var add = adds[direcao];
        // se por acaso for passado uma parametro não configurado, a gente ignora
        if (!add) return;
        // pegando os novos valores de x/y da peça atual
        var newx = this.pieceAtual.x + add.x;
        var newy = this.pieceAtual.y + add.y;
        // finalmente atualizamos a posição da peça
		this.pieceAtual.setaPosicao(newx,newy);
    };

O resultado vai ser esse aqui: a peça vai começar a descer sozinha! Mas... ela vai 'atravessar' a borda de baixo, lol. Isso é porque a gente não fez nenhuma verificação disso ainda! Não se esqueça, você pode clicar em 'Result' pra reiniciar o script/jogo (isso provavelmente será necessário já que, quando você chegar nessa parte, a peça já passou faz tempo! lol)

Precisamos alterar essa lógica e fazer a verificação da borda de baixo. Pra verificar isso, nós vamos pegar o valor de y (isto é, a distancia que essa peça está do topo) e somar à 'altura' de cada posição dessa peça (posição = cada quadradinho dela). Se esse valor for igual a altura do grid, quer dizer que a peça está tocando o chão... e se isso acontecer, quer dizer que a peça deve parar de se mover ali mesmo. Entendeu? Agora vamos transformar essa lógica em código!

Primeiro, vamos voltar na nossa classCanvas.movePieceAtual e incluir uma verificação exatamente antes da chamada do setaPosicao.

this.movePieceAtual = function(direcao) {
		(...)
        // verificando se é possível ir pra próxima posição
        var pode = this.podeMoverPosicao(newx,newy,direcao);
        // se não puder, paramos por aqui
        if (!pode) return;   
		(...)
	};

Logicamente, precisamos implementar essa nova função.

// método pra verificar se podemos mover a peça atual pra nova posição
    this.podeMoverPosicao = function(newx,newy,direcao) {
    	// pegando a posição atual (só pra simplificar o código)
    	var p = this.pieceAtual;
        // pegando map atual dela
        var map = p.data.maps[p.mapAtual];        
		// vamos iterar as posições 
		for (var i = 0; i < map.length; i++) {
        	var pos = map[i];
			// calculando a linha dessa posição 
			var row = Math.floor(pos/4);
			// e a coluna dessa posição
			var col = pos%4;
            // com o newx,newy da peça e o row/col de cada posição, nós temos o x,y REAL dentro do grid
            var x = newx+col;
            var y = newy+row;
            // se uma posição estiver fora da borda esquerda... nao podemos mover!
            if (x < 0) return false;
            // se uma posição estiver fora da borda direita... nao podemos mover!
            if (x >= this.largura) return false;            
            // se uma posição estiver fora da bora superior... não podemos mover!
            if (y < 0) return false;                
            // se uma posição estiver fora da bora inferior... não podemos mover!
            if (y >= this.altura) {{
            	// se está, quer dizer que esse movimento iria gerar uma colisão!
				this.criaPiece();
            	return false;
            }      
        }        
        /

Repare que já acabamos de implementar também a parte que verifica o contato com a parte de baixo e cria uma nova peça. Olha como ficou! Também repare que até agora eu só verifiquei a colisão com as bordas, e não entre as peças. Isso quer dizer que as novas peças vai 'atravessar' as que já estão no grid. Ainda vamos resolver isso, aguenta ae!

Certo, existem algumas formas de verificar a colisão entre as peças. Eu não sei se essa é a melhor, mas veja só: Toda vez que uma peça terminar seu movimento, eu vou salvar numa variável todas as 'células' do grid que estão ocupadas por ela. Assim, quando as outras peças estiverem se movendo, eu vou verificar se as novas posições dessa peça estão sobrepondo qualquer célula ocupada. Se estiver, quer dizer que a peça não pode ir praquela nova posição!

Primeiro vou criar a variável que fala quais posições estão ocupadas ou não.

var classCanvas = function(x,y) {
	// salvando os parametros
	this.largura = x;
	this.altura = y;    
    
    // criando um grid x/y de posições não-ocupadas
    this.grid = [];    
    for (var x = 0; x < this.largura; x++) {
    	this.grid[x] = [];
    	for (var y = 0; y < this.altura; y++) {
        	this.grid[x][y] = false;
        }
    }
	(...)

Agora vou alterar o criaPiece pra caso ele seja chamado e já exista uma peça atual, marcar as posições dela como ocupadas.

// método para criar uma nova peça
	this.criaPiece = function() {
    	// caso já exista uma peça atual
    	if (this.pieceAtual) this.marcaPosicoesOcupada();
        // caso existam peças demais (isso é temporário, só enquanto não implementamos o fim de jogo)
        if (this.pieces.length >= 50) return;
		(...)
	};

E por ultimo, vamos implementar o método que efetivamente marca as posições.

// método pra marcar as posições da peça atual como ocupadas    
    this.marcaPosicoesOcupada = function() {
    	// pegando a posição atual (só pra simplificar o código)
    	var p = this.pieceAtual;
        // pegando map atual dela
        var map = p.data.maps[p.mapAtual];
		// vamos iterar as posições 
		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;
            // com o x,y da peça e o row/col de cada posição, nós temos o x,y REAL dentro do grid
            var x = p.x+col;
            var y = p.y+row;
            // marcamos essa posição como ocupada
            this.grid[x][y] = true;
		}.bind(this));
    };

Certo! Agora podemos alterar nosso classCanvas.podeMoverPosicao verificar se uma peça está colidindo com as outras! Vou adicionar essa lógica no final do for que itera as posições.

// método pra verificar se podemos mover a peça atual pra nova posição
    this.podeMoverPosicao = function(newx,newy,durecao) {
    	(...)
		for (var i = 0; i < map.length; i++) {
        	(...)  
            // agora só precisamos verificar se essa posição está ocupada.
            if (this.grid[x][y]) {
            	// se está, quer dizer que esse movimento iria gerar uma colisão!
                // caso a peça esteja se movendo pra baixo, é hora de criar uma nova peça!
            	if (direcao == 'down') this.criaPiece();
            	return false;
            }
        }        
		(...)

Olha o resultado! Ps: eu aumentei a velocidade pra agilizar os testes, hehe. Ainda falta verificar a colisão com o topo pra terminar o jogo, mas isso vai ficar pra parte 3 do tutorial.


O jogador pode mover a peça atual na horizontal, pode acelerar a sua descida, e pode girá-la.

Até agora acho que nem dava pra considerar isso um jogo, já que o 'jogador' não fazia nada, só assistia! Hora de adicionar CONTROLES! Os comandos que eu quero adicionar são WASD/direcionais pra mover as peças. Não existe movimentação pra cima, então vou usar o W/up pra girar a peça :)

No classCanvas, vou adicionar um método pra ouvir os eventos de teclas pressionadas. Aí é só eu ver qual foi e comparar com as que eu quero que façam alguma coisa.

// método para 'ouvir' as teclas pressionadas
    this.adicionaControles = function() {
        window.addEventListener('keydown',function(e) {
            // se o jogador apertar A ou a setinha pra esquerda
            if (e.keyCode == 65 || e.keyCode == 35) this.movePieceAtual('left');
            // se apertar D ou a setinha pra direita
            if (e.keyCode == 68 || e.keyCode == 39) this.movePieceAtual('right');
            // se apertar S ou a setinha pra baixo
            if (e.keyCode == 83 || e.keyCode == 40) this.movePieceAtual('down');
            // se apertar W ou a setinha pra cima
            if (e.keyCode == 87 || e.keyCode == 38) this.pieceAtual.gira();
        }.bind(this));
    };

E então é só alterar aquela nossa chamada principal pra também chamar esse método.

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

TA-DA! Não esqueça de clicar em qualquer lugar do iframe pra 'ativar' os controles. Também não se esqueça de clicar em 'Result', porque provavelmente o grid vai estar todo cheio quando você chegar nessa parte. Tá saindo da jaula o TETRIS! (ok ok, essa foi péssima)


E por aqui nós encerramos a segunda parte desse tutorial! Na parte 3 vamos implementar a destruição de linhas, a parte da pontuação e a verificação de fim de jogo.

Leave a Comment