Recriando Space Invaders durante o fim de semana

Space Invader é um dos jogos mais influentes já criados. Lançado em 1978 para arcades e desenvolvido por Tomohiro Nishikado, o jogo ajudou a pavimentar o caminho para a indústria de games se tornar o que é hoje. O objetivo do jogo é derrotar ondas de alienígenas que se movem em direção à parte inferior da tela, movendo um canhão de um lado para o outro para conseguir a maior quantidade de pontos possível.

Com mecânicas simples para os dias atuais. Space Invader é um ótimo jogo para ser copiado e praticar desenvolvimento de jogos sem ter a pressão de criar um projeto do zero. A prova disso é que uma busca rápida na internet você encontra dezenas de versões do jogo.

Meu objetivo durante o fim de semana era recriar todas as mecânicas do jogo. Ou seja, não estava me preocupando com nenhum tipo de arte ou polimento, apenas queria algo jogável. Porém durante a semana fiz alguns polimentos para deixar o projeto mais apresentável.

O projeto completo está disponível no GitHub.

Movendo o jogador

A primeira coisa que fiz foi criar a movimentação do jogador, para isso é necessário apenas mover o canhão para esquerda e para a direita e não deixar que ele saia da tela. Há dois jeitos para fazer isso, a primeira usa colisores fora da tela, o problema que encontrei com essa solução em outros jogos que desenvolvi é que mesmo pequenos ajustes na câmera, por exemplo em uma câmera ortográfica o tamanho da lente mudar, os colisores ficarão no lugar errado.

No segundo você usa as proporções da câmera e o tamanho do jogador para calcular uma posição e então usa o método “Mathf.Clamp” para grampear a posição do jogador na tela.

private void CalculateScreenBounds()
{
    float playerSize = collider.bounds.size.x * 0.5f;
    float screenWidth = Camera.main.orthographicSize * Camera.main.aspect;
    screenBounds = screenWidth - playerSize;
}

Criando a invasão alienígena

O próximo passo após criar a movimentação do jogador é criar a invasão de alienígenas. Para isso é necessário distribuir os aliens em um padrão de grid como na imagem abaixo.

Como com a movimentação do personagem, existem algumas maneiras de fazer isso. Por exemplo, eu poderia ter criado o layout de uma linha usando o editor e salvo essa linha como Prefab para mais instanciar na cena mais tarde.

Porém decidi criar o layout diretamente no código, isso fez eu perder bastante tempo tentando distribuir eles da maneira que queria. Provavelmente teria economizado tempo e paciência se tivesse procurado pela fórmula no Google ao invés de ficar quebrando a cabeça por pura teimosia. E aqui é um exemplo claro de como eu gosto de complicar as coisas para mim mesmo.

Por outro lado, resolver o problema por conta própria, me dá uma satisfação muito grande, algo parecido como derrotar aquele chefão complicado em um jogo. Além disso, é um bom exemplo de como quebrar um problema em partes menores e entender o que você quer fazer facilita a resolução do problema. No momento que comecei a fazer isso ao invés de tentar resolver tudo de uma vez, tudo fluiu melhor.

private void CreateInvasion()
{
	invasionRows = new GameObject[invasionSize.y];

	float invasionBounds = invasionSize.x / 2;
	float invasionSpacingBounds = (invasionSize.x - 1) * spaceBetweenAliens / 2;
	float alienOffset = invasionBounds + invasionSpacingBounds;

	for (int y = 0; y < invasionSize.y; y++)
	{
		GameObject invasionRow = new GameObject($"InvasionRow_{y}");
		float invasionSpacing = y * spaceBetweenAliens;
		Vector3 invasionRowPosition = new Vector3(0, y + invasionSpacing);
		invasionRow.transform.position = invasionRowPosition;
		invasionRows[y] = invasionRow;

		for (int x = 0; x < invasionSize.x; x++)
		{
			float alienSpacing = x * spaceBetweenAliens;
			Vector3 alienPosition = new Vector3(x + alienSpacing - alienOffset, invasionRow.transform.position.y);
			Instantiate(alienPrefab, alienPosition, Quaternion.identity, invasionRow.transform);
		}
	}
}

Em seguida, fiz cada linha se mover de um lado para o outro. Isso é bem parecido com o que fiz para a movimentação do jogador, com a diferença que a mudança de direção deve ser feita de forma automática quando a linha chegar na borda da tela.

Isso era para ser algo simples, porém, encontrei um problema que deu trabalho para resolver. Ao invés da linha de alienígenas mudar de direção ao chegar na borda, ela parecia que parava. Mas ao olhar no inspetor percebi que tinha uma pequena mudança para esquerda e para a direita. Demorou um pouco mas descobri que isso estava acontecendo porque eu estava redefinindo a variável que controla a direção do movimento todo quadro.

Para resolver isso, criei uma nova classe que mantém a direção do movimento e adicionei ao GameObject que representa a linha de alienígenas. Dessa forma só mudo o valor da variável quando necessário. E provavelmente preciso mudar toda a lógica de movimento para essa classe. Mas isso é conversa para outra hora.

Que comece a matança

Antes de ir dormir no sábado, adicionei a habilidade do jogador de atirar, mas os aliens ainda não podiam ser mortos. Quando voltei a trabalhar no projeto no domingo, isso foi a primeira coisa que fiz, em seguida adicionei a habilidade dos aliens de atirarem e matarem o jogador, também adicionei telas para o menu principal e game over, também criei UI para exibir a quantidade de vidas e pontuação do jogador.

Falando em pontuação, essa é provavelmente a parte mais bagunçada do código fonte. E junto da movimentação dos aliens que falei na seção anterior, são as partes que mais precisam ser refatoradas.

Vou explicar por qual motivo penso isso. No momento cada alien envia um evento quando é morto, a classe InvasionCommander, que é responsável por criar os aliens escuta por esse evento e envia seu próprio evento, e a classe GameManager escuta esse evento e atualiza a pontuação. Por fim, a classe GameManager envia um evento quando a pontuação é atualizada, que a classe responsável pela UI escuta e atualiza o texto na tela.

O meu problema é que em todo projeto eu acabo criando uma classe GameManager que se torna uma classe que faz tudo. No jogo “The Adventures of the Lone Bounty Hunter”, por exemplo, tem uma classe com o mesmo nome, e possui mais de mil linhas de código. Essa classe é responsável por basicamente tudo, desde salvar e carregar o jogo, iniciar cada fase, e até elementos de direção de câmera. Aqui não chega a esse ponto, até mesmo porque o projeto é bem menor, mas eu posso ver que acabaria fazendo o mesmo caso o projeto fosse crescer. Vocês têm o mesmo problema em seus projetos? Como vocês resolvem isso?

Sistema de bunker

O próximo passo foi adicionar os bunkers. Se você olhar o jogo original conforme partes do bunker sofrem dano, elas vão sendo danificadas até que são destruídas. Eu estava preocupado com esse momento porque eu sendo eu, pensava que tinha algum algoritmo super complicado por trás disso.

Porém, pensando nas engines disponíveis no mercado hoje, em especial a Unity, que é a que eu uso. Como você iria criar esses bunkers? Bom se você não ficar querendo complicar, como eu queria, é até óbvio né, eu estou trabalhando com a solução o projeto inteiro, porque no final das contas qual a diferença entre um pedaço do bunker e um alien ou o canhão do jogador? Isso mesmo ele não se move.

Cada pedaço do bunker é seu próprio GameObject e cada um é responsável por saber quantas vidas ainda tem e quando sofre algum dano atualizar o componente “Sprite Renderer”. No domingo o código só mudava a cor, porém durante a semana modifiquei para que eu pudesse alterar o sprite. Você pode ver o código da classe “BunkerPart” abaixo, assim como uma imagem das peças que formam um bunker.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace SpaceInvadersRemake
{
    public class BunkerPart : MonoBehaviour, IDamageable
    {
        [SerializeField]
        private Sprite[] maxLives;
        [SerializeField]
        private SpriteRenderer spriteRenderer;

        private int currentLives;

        private void Awake()
        {
            currentLives = maxLives.Length;
        }

        public void Damage()
        {
            currentLives--;
            ChangeSprite();

            if (currentLives <= 0)
            {
                Destroy(gameObject);
            }
        }

        private void ChangeSprite()
        {
            int indexFromCurrentLives = currentLives - 1;

            if (indexFromCurrentLives >= 0)
            {
                spriteRenderer.sprite = maxLives[indexFromCurrentLives];
            }
        }
    }
}

Ondas e mais ondas de inimigos

Até esse ponto a velocidade da invasão não aumentava, para resolver esse problema eu usei um “AnimationCurve”, que apesar do nome nada mais é do que uma forma que a Unity fornece para representar valores em um gráfico. Geralmente, o eixo x representa uma quantidade de tempo e o eixo y do gráfico representa um valor.

Aqui, porém, o eixo x representa a porcentagem de aliens ainda vivos, enquanto o eixo y representa a velocidade com que os aliens devem se mover. Assim na direita do gráfico temos a velocidade mínima, quando todos os alien estão vivos, e na esquerda a velocidade máxima quando todos já foram mortos. 

Após isso adicionei ondas de invasões, isso é, cada vez que o jogador mata todos os aliens uma nova onda de inimigos aparece. E como no jogo original, a cada onda os inimigos aparecem mais próximos do chão para aumentar a dificuldade.

E para finalizar, modifiquei o código que cria os aliens para aceitar diferentes Prefabs de aliens para cada linha, assim eu posso adicionar aliens que fornecem pontuações diferentes quando são mortos. Também adicionei a navezinha que passa voando no topo da tela.

Conclusão

E com isso o meu clone de Space Invader está pronto.

Recriar jogos e mecânicas são uma ótima maneira de praticar desenvolvimento de games com menos pressão do que criar um projeto do zero. E uma vez que todas as decisões já foram tomadas por outra pessoa, podemos focar apenas na parte que nos interessa. No caso desse projeto a implementação das mecânicas.

Eu com certeza vou fazer mais projetos como esse. E você já recriou algum jogo ou mecânica? Gostaria que eu entrasse em mais detalhes na próxima vez? Deixe seus comentários abaixo!

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *