Landstalker
Lenda da internet
- Mensagens
- 19.356
- Reações
- 39.661
- Pontos
- 1.584
:: Tópicos
- Destruindo a Bullet depois de um tempo
- Criando novos Inimigos
- Waves
- Aprimorando as Waves
# Destruindo a Bullet depois de um tempo
Se repararmos quando atiramos com a nossa nave e as balas não acertam a um inimigo o que o ocorre com elas? Elas ficam lá, na memória ainda ocupando espaço. Em nossa Hierarchy, as balas vão se somando continuamente enquanto estamos atirando. Isso é ruim porque depois de um tempo, criamos vários objetos que não utilizamos mais, elas ficam infinitamente navegando pelo espaço e não deveremos permitir isso.
Há várias maneiras de lidar com isso. Poderemos criar um sprite com um collider afastado da área da câmera cuja a colisão com a bala faça essa ser destruída. O problema dessa forma é que ficam as coisas mais enrijecidas, se eu projetar o meu jogo o tempo todo considerando que as balas do jogador são atiradas pra cima, enquanto a dos inimigos pra baixo, vá lá que eu resolva mudar isso, então terei que toda hora criar vários colliders para prever todas as situações de posição e impacto da bala, essa não é melhor prática possível, ao menos para essa situação.
Então, uma forma ainda mais simples e e eficiente de resolver esse problema é criar um temporizador. Depois de n segundos a bala automaticamente se destrói, permitindo a liberação de recursos.
Abra o arquivo Bullet.cs e adicione a seguinte variável de classe:
Código:
private float destroyTimeCouner;
Agora adicione o método Update que irá contar essa variável até alcançar o segundo desejado e então eliminar o nosso Game Object.
Código:
private void Update()
{
destroyTimeCouner += Time.deltaTime;
if (destroyTimeCouner >= 5)
{
Destroy(gameObject);
}
}
Eu coloquei 5 segundos, mas poderemos tanto aumentar ou diminuir esse tempo. O ideal é colocar um tempo que você saiba que a bala já saiu da visão do jogador, porque seria inconveniente pôr um tempo muito baixo onde a bala estaria ainda na área da câmera e o jogador visse a sua bala sumir assim do nada.
Execute o jogo agora e repare que os Game Objects da bala simplesmente desaparecem depois de um tempo.
NOTA - Essa ainda não é a melhor maneira de tratar uma sucessão de objetos clonados na tela.
Pra tornar essa parte mais dinâmica e objetiva, estou destruindo os objetos criados ao invés de reutilizá-los, mas essa não é a melhor forma de lidar com isso. Lembra que lá atrás eu falei sobre o Pool de Objetos e como isso é importante para os jogos? Então, o modelo ideal é criar já uma quantidade inicial de balas e reaproveitá-las sem eliminá-las da memória. O pool funciona, como vimos brevemente antes, como um gerenciador de recursos, um gerenciador de objetos para reaproveitá-los durante uma instância do jogo. Criar e deletar Game Objects constantemente pode ser um trabalho penoso para a engine. A nossa sorte é que os nossos GOs são simples, demandando de poucos recursos do hardware e ocupando muito pouco do espaço em memória e processamento. Mas como via geral, é bom manter a prática de usar um pool. Ao término desse tutorial, farei uma parte explicando isso na prática.
Pra tornar essa parte mais dinâmica e objetiva, estou destruindo os objetos criados ao invés de reutilizá-los, mas essa não é a melhor forma de lidar com isso. Lembra que lá atrás eu falei sobre o Pool de Objetos e como isso é importante para os jogos? Então, o modelo ideal é criar já uma quantidade inicial de balas e reaproveitá-las sem eliminá-las da memória. O pool funciona, como vimos brevemente antes, como um gerenciador de recursos, um gerenciador de objetos para reaproveitá-los durante uma instância do jogo. Criar e deletar Game Objects constantemente pode ser um trabalho penoso para a engine. A nossa sorte é que os nossos GOs são simples, demandando de poucos recursos do hardware e ocupando muito pouco do espaço em memória e processamento. Mas como via geral, é bom manter a prática de usar um pool. Ao término desse tutorial, farei uma parte explicando isso na prática.
# Criando novos Inimigos
Como já diz o ditado: "É lamentável um homem de apenas um só livro", aqui eu falo: "É lamentável um jogo de apenas um só inimigo". Nada como um jogo recheado de inimigos com jogabilidades e mecânicas distintas, não é mesmo?
Vamos deixar o KongClassic sem munição, só para distinguir dos demais. Antes, deveremos criar o prefab dele assim como fizemos com a bala que o nosso Player atira. Arraste o KongClassic que está em sua Hierarchy até a pasta de Prefabs e teremos o prefab dele criado. Vamos precisar dos prefabs de cada um dos inimigos para "spawmá-los" durante o jogo quando formos criar as waves. Lembre-se que para se certificar que o seu prefab foi criado, o KongClassic ficará marcado como azul na Hierarchy e na pasta de Prefabs ele será marcado com uma caixa azul, igual a da bala.
Agora vamos criar um novo inimigo, o KongFeliz. Com o GO do KongClassic selecionado na Hierarchy, aperte CTRL+D e duplique o objeto, teremos algo como "KongClassic (1)". Esse kong servirá como molde para a criação do novo kong, mas antes de fazermos qualquer alteração, temos que desvinculá-lo ao seu prefab original, senão, qualquer alteração feita será repercutida em KongClassic ao invés do novo KongFeliz.
Com o "KongClassic (1)" selecionado, vá até ao menu "GameObject > Break Prefab Instance". Essa opção irá quebrar o vínculo de um GO com o seu prefab, ficaremos, portanto, seguros em alterar qualquer propriedade sem problemas. Verificamos que deu tudo certo quando ele deixou de ficar azul, indicando que esse GO não tem vínculo com quaisquer prefab do jogo criado.
Dê um rename para KongFeliz, e no componente Sprite Renderer na propriedade Sprite deveremos mudar a imagem que ele faz referência, trocando de kClassic para kFeliz.
Aperte a bolinha que fica ao lado da caixa do Sprite e teremos a seguinte tela:
É só começar a digitar o nome da imagem e selecioná-la e o nosso sprite já estará com a imagem atualizada.
A outra forma de se alcançar isso seria arrastar diretamente a imagem do kFeliz para a propriedade Sprite.
Quase que não haverá diferenças entre os kongs, já que basicamente eles possuem quase as mesmas propriedades além da mudança do nome, da imagem e o tipo de bala que cada um pode atirar.
Note que você pode fazer um ajuste no BulletPoint do KongFeliz para ficar mais centralizado na boca.
Agora que já temos o prefab do KongClassic criado, poderemos apagá-lo de nossa Hierarchy que não teremos problema pois iremos instanciá-lo futuramente quando tivermos às waves criadas.
Pegue agora o prefab da BulletPlayer e arraste até à cena na posição próxima ao KongFeliz.
Assim como fizemos entre o KongClassic e o KongFeliz, iremos fazer com a bala do jogador e a do KongFeliz nessa ordem:
- Duplique o objeto (CTRL+D);
- Quebre o seu vínculo com o prefab original via menu "GameObject > Break Prefab Instance";
- Renomeie o GameObject para "BulletFeliz";
- No Inspector vá até o componente Bullet e mude a propriedade Caster de "Player" para "Enemy" (assim será possível saber em quem a bala irá acertar).
Para a bala do jogador, definimos dois scripts, o Bullet.cs e o BulletPlayer.cs. O primeiro irá permanecer, já que ele é genérico e vai servir para qualquer bala do jogo, o segundo iremos descartar para o inimigo porque iremos criar um novo script para o controle do comportamento dessa bala.
Remova o componente BulletPlayer do GameObject BulletFeliz através do Inspector seguindo a imagem abaixo:
Crie o script BulletFeliz.cs e o adicione ao GameObject, agora abra-o em sua IDE. O BulletFeliz ficará bem parecido com o BulletPlayer, ambos herdam de BulletAbstract e, como consequência iremos implementar um movimento para ele via método Movement; o que acontece quando a bala acerta algo, método Hit e como a bala é destruída via método Kill.
Código:
public class BulletFeliz : BulletAbstract
{
void Start()
{
}
void Update()
{
}
public override void Movement()
{
}
public override void Hit()
{
}
public override void Kill()
{
}
}
O seu movimento ficará bem parecido com a bala do jogador, com a diferença que em vez de ela ir pra cima, ela irá se deslocar pra baixo. O sinal de negativo no eixo Y é que indicará o sentido que a bala irá percorrer nesse eixo. No método Movement temos:
Código:
public override void Movement()
{
transform.Translate(0, - bullet.speed * Time.deltaTime, 0);
}
Dê Play e verá que a bala se move à mesma velocidade da bala do jogador, vamos deixá-la na metade dessa velocidade. Dê Stop (caso executou o jogo) e altere via Inspector o valor de sua propriedade Speed para "5".
No método Hit, o nosso target será sempre o jogador, já que definimos o jogador como alvo da nossa colisão para esse tipo de bala:
Código:
public override void Hit()
{
var player = bullet.target.GetComponent<Player>();
player.hp -= bullet.damage;
if (player.hp <= 0)
player.Kill();
}
Essa bala não irá atravessar alvos, portanto, uma vez que o alcance, ela irá ser destruída.
Código:
public override void Kill()
{
Destroy(gameObject);
}
Agora o que deveremos fazer é pôr essa bala para ser usada no nosso KongFeliz. Vamos reutilizar algumas variáveis que já usamos no script do Player.cs, elas são as mesmas variáveis para controle das munições, coloque-as no Enemy.cs:
Código:
public GameObject bulletPrefab;
public GameObject firePoint;
public float fireRate = 0;
Coloque também a variável fireCounter pois será ela que irá armazenar o tempo até chegar no fireRate:
Código:
private float fireCounter;
Não quero que o KongFeliz tenha sempre o mesmo tempo de cast da bala, então defini um método para gerar um tempo aleatório para a variável fireRate, assim a cada tiro dado, o próximo tiro será lançado num tempo provavelmente diferente do anterior.
Código:
private void GenerateRandomFireRate()
{
fireRate = Random.Range(0.5f, 2f);
}
O tempo dado varia entre meio segundo até dois, mas fique à vontade para aumentar ou diminuir esses tempos ou até mesmo deixá-lo fixo, sem precisar chamar esse método.
No método Start deveremos chamar o método GenerateRandomFireRate para que tenhamos o nosso primeiro tempo definido quando esse objeto for instanciado:
Código:
void Start()
{
GenerateRandomFireRate();
}
Adicione o método Update, que fará um cast da bala a cada intervalo definido pelo fireRate:
Código:
void Update()
{
//somente quando tem bala
if (bulletPrefab != null && firePoint != null)
{
//conta o tempo em fireCounter
fireCounter += Time.deltaTime;
//caso o tempo de fireCounter seja maior ou igual ao de fireRate...
if (fireCounter >= fireRate)
{
//reseta fireCounter, pega-se um novo tempo para fireRate e instancia a bala
fireCounter = 0;
GenerateRandomFireRate();
Instantiate(bulletPrefab, firePoint.transform.position, Quaternion.identity);
}
}
}
O processo é repetido várias vezes enquanto o inimigo existir no jogo. Para concluirmos, deveremos ir ao Inspector do KongFeliz e configurar as variáveis de bulletPrefab e firePoint, lembre-se de criar agora o prefab da bala BulletFeliz de antemão. É já esperado que você nessa altura saiba fazer esse procedimento descrito nesse paragrafo sozinho.
Pronto, já temos o nosso segundo inimigo praticamente pronto. Para testar os seus conhecimentos, encorajo a você tentar criar novos inimigos com base das imagens já disponibilizadas tanto dos sprites dos inimigos quanto das balas.
# Waves
Hordas de inimigos devem aparecer no espaço tentando ferozmente arrebatar o coração metálico de nossa pobre navezinha. O espaço sideral é um lugar perigoso, vasto e amplo não apenas em suas dimensões, mas nos mais diversos perigos que o assola. :)
Para termos uma sensação de fase, vamos criar várias waves de inimigos de forma aleatória, definiremos alguns pontos fora da visão da câmera onde essas naves surgirão. Como as estrelas do nosso background se deslocam para baixo, dando a sensação de que a nave está indo para cima (já que estamos usando a visão TOP-DOWN), portanto, os inimigos se deslocaram no sentido contrário, mas nem sempre em linha reta.
Ao olharmos para a dimensão de nossa câmera, em sua parte superior, as naves inimigas irão surgir. Dê um scroll segurando a tecla Ctrl + Mouse Wheel e aproxime à sua extremidade esquerda, se criarmos um Game Object qualquer apenas para testar e pegar a sua posição, iremos pegar em seu centro algo como (-4.5 , 4.5), conforme a figura abaixo ilustra:
Cada quadrado desse representa uma 1 unidade na escala da Unity. Portanto, se esse quadrado está em (-4.5, 4.5), o próximo à sua direita está em (-3.5, 4.5), o seguinte em (-2.5, 4.5) e assim sucessivamente até chegarmos na ponta da outra extremidade que termina em:
Como estamos deslocando apenas no eixo X, os valores de Y continuam os mesmos. Temos agora 10 posições diferentes, que vai do X em -4.5 até 4.5.
Basicamente, essa primeira parte da wave será bem simples. De um spawn a outro, cria-se um número n de Kongs, instanciando eles numa dessas posições quaisquer. Deveremos só nos assegurar que quando instanciarmos mais de um Kong ao mesmo tempo, eles não acabem pegando a mesma posição que foi usada em um mesmo spawn. Quando um spawn termina, se reseta as posições, podendo novos Kongs pegá-las novamente.
Vamos criar dois novos scripts C#:
- Cell.cs
- WaveSpawner.cs
A struct Cell representará justamente as posições necessárias onde os Kongs irão ser instanciados. A classe WaveSpawner irá controlar tudo isso, "spawmando" nossos inimigos de tempo em tempo numa dessas posições. Abra em sua IDE o arquivo Cell.cs e remova a sua herança com MonoBehaviour e mude a keyword de class para struct removendo logo em seguida também os métodos Start e Update. Teremos algo assim:
Código:
public struct Cell
{
}
Cadê a classe? O que diabos é Struct?
Struct é uma outra forma de organização de dados, assim como as classes são. É muito complicado falar disso aqui onde envolve conceitos de programação que muitas vezes nem vemos em jogos e às vezes nem fora deles também. Basicamente um struct é usado para resolver coisas simples como posição em um mapa e armazenamento de valores simples sem referência. A nossa struct representará apenas isso, a posição X e Y em nossa Scene. Uma struct não pode herdar de nenhuma classe e o seu construtor não pode ser default.
Struct é uma outra forma de organização de dados, assim como as classes são. É muito complicado falar disso aqui onde envolve conceitos de programação que muitas vezes nem vemos em jogos e às vezes nem fora deles também. Basicamente um struct é usado para resolver coisas simples como posição em um mapa e armazenamento de valores simples sem referência. A nossa struct representará apenas isso, a posição X e Y em nossa Scene. Uma struct não pode herdar de nenhuma classe e o seu construtor não pode ser default.
Adicione as seguintes variáveis públicas em nossa Cell. Repare que uma delas é estática para acesso rápido, caso quisermos usar:
Código:
public float x;
public float y;
public static Cell zero = new Cell(0, 0);
Vamos criar um construtor (Constructor) para o nossa Cell. Um construtor é como se fosse a porta de entrada de uma classe. Não precisávamos usar nenhum deles antes porque as nossas classes herdavam de MonoBehaviour que por si só, na Unity, tem a sua própria forma de instanciá-las como já vimos. Todo construtor tem que ter exatamente o nome da classe ou struct ao qual faz parte e não pode ter retorno de nenhum tipo, mesmo o "void".
Dentro de nossa struct e abaixo das variáveis declaradas, faça:
Código:
public Cell(float x, float y)
{
this.x = x;
this.y = y;
}
Simples, não? Agora deveremos desenvolver a WaveSpawner que usaremos a Cell para instanciar os Kongs criados. Abra o arquivo WaveSpawner.cs e adicione as seguintes variáveis como variáveis de classe:
Código:
public List<Cell> cells;
public List<GameObject> kongs;
private Cell baseCell;
private float spawnRate = 2f;
private float timeCounter;
A variável cells irá definir a cada spawn quais células já foram usadas. Fazemos isso para não permitir criar kongs em uma mesma posição de uma mesma wave. A variável kongs comporta os prefabs dos Kongs criados (KongClassic, KongFeliz), deveremos via Inspector arrastar os prefabs para a nossa lista. A variável baseCell representa a nossa primeira posição base, que é na posição (-4.5, 4.5), teremos 10 posições possíveis, cada uma com uma unidade de diferença no eixo X. Variável spawnRate determina o tempo necessário de um spawn para outro. timerCounter, por sua vez, determina o tempo percorrido até chegar ou ultrapassar o spawnRate e, então, resetar sua variável para um próximo spawn.
Implemente os métodos Awake e Start. De costume, usamos o Awake para instanciar elementos no nosso jogo e o Start para inicializá-los.
Código:
void Awake()
{
cells = new List<Cell>();
}
void Start()
{
baseCell = new Cell(-4.5f, 4.5f);
}
Vamos criar um método que irá nos retornar a primeira posição válida de um spawn. Para isso, ele deverá verificar a lista de células e ver se a célula que ele criou não se encontra nela, caso esteja presente na lista, então ele cria uma nova posição e executa o mesmo processo até ter certeza que não há célula usada.
Código:
public Cell GetRandomCell()
{
Cell cell;
do
{
var x = baseCell.x + Random.Range(1, 9);
var y = baseCell.y;
cell = new Cell(x, y);
}
while (cells.Contains(cell));
cells.Add(cell);
return cell;
}
A nova célula (cell) usa como base a célula baseCell, onde tem sua posição fixada em x:-4.5 e y:4.5. Como vamos apenas variar no eixo x, a variável y fica inalterada, pegando sempre o valor definido em baseCell. A variável x pega o valor base e soma com um número aleatório entre 1 e 9. Com a cell criada, ele testa while (cells.Contains(cell)) se essa célula não foi usada. Leia-se: "Faça ...isso.. enquanto cells contém cell", caso não tenha, ele procede normalmente com o restante do código, adicionando a nova célula na lista de células.
Agora com o Update, teremos a seguinte situação:
- contar timeCounter até chegar em spawnRate;
- chegando no tempo do spawnRate, faça: resetar o contador, limpar a lista de células (já que é um novo spawn), escolher a quantidade de kongs a serem instanciados, pegar uma posição válida qualquer (GetRandomCell), escolher algum Kong aleatoriamente e, por fim, instancia o Kong na posição obtida.
Código:
void Update()
{
timeCounter += Time.deltaTime;
if (timeCounter >= spawnRate)
{
timeCounter = 0;
cells.Clear();
var kongNumber = Random.Range(1, 5);
for (var i = 0; i < kongNumber; i++)
{
var cell = GetRandomCell();
Instantiate(kongs[Random.Range(0, kongs.Count)], new Vector3(cell.x, cell.y), Quaternion.identity);
}
}
}
Nomenclaturas
Eu usei Cell para simbolizar a posição (x e y) como representação da posição 2D na Scene. Mas é comum usar para as mesmas coisas outras nomenclaturas como Tile ou Node. Como essa informação é apenas das coordenadas X e Y, não senti necessidade de tratar isso como se fosse um outro termo, mesmo podendo caber qualquer outro termo citado. Tiles é comumente usado para representar "azulejos" ou pequenas posições retangulares no mapa. Node é um termo usado para também representar uma posição 2D ou 3D mas, mais voltado às técnicas de Pathfinding (técnicas avançadas para pesquisa de nós num espaço).
Eu usei Cell para simbolizar a posição (x e y) como representação da posição 2D na Scene. Mas é comum usar para as mesmas coisas outras nomenclaturas como Tile ou Node. Como essa informação é apenas das coordenadas X e Y, não senti necessidade de tratar isso como se fosse um outro termo, mesmo podendo caber qualquer outro termo citado. Tiles é comumente usado para representar "azulejos" ou pequenas posições retangulares no mapa. Node é um termo usado para também representar uma posição 2D ou 3D mas, mais voltado às técnicas de Pathfinding (técnicas avançadas para pesquisa de nós num espaço).
Temos que criar o Game Object WaveSpawner e adicionar o nosso script correspondente. Lembre-se de popular também a lista de Kongs via Inspector conforme a imagem abaixo.
Execute o jogo e teremos hordas de kongs aparecendo querendo comer a sua banana.
# Aprimorando as Waves
Se repararmos, as waves não soam totalmente naturais, inimigos surgindo sempre na mesma posição e velocidade horizontal. Mesmo para tentar desempenhar um jogo retrô, temos que deixar as coisas mais orgânicas, deixando o jogo com uma experiência ainda melhor de se jogar.
Para isso, vamos deixar as waves sendo ainda mais variáveis. Criando formações fixas de inimigos e "spawmando" com velocidades distintas.
Para criarmos algumas formações fixas, dando as formas e posições que quisermos, vamos trabalhar diretamente na Scene do nosso jogo. Arraste, por exemplo, o KongClassic até a posição acima da câmera, de preferência na x:4.5, y:4.5. Agora vamos no item de menu: "Edit > Snap Settings...", uma pequena janela como a seguinte irá abrir:
A janela de snap, permite-nos configurar o snap em forma de grid em uma Scene, alinhando os objetos conforme os seus parâmetros. Como cada Kong cabe bem no centro de cada unidade, então vamos deixá-los lado a lado para formar formações interessantes. Em Move X, Y e Z, definimos a posição média que cada objeto ficará em relação a nossa unidade. Exemplo, se colocar dois objetos em Snap com 1 unit, então a distância de um para outro ficará sendo 1 unit. Como o pivot (lembra dele?) de cada Kong está em seu centro, então definiremos o snap em x:0.5 e y:0.5 ao invés de 1 e 1. Deixe essa janela aperta durante todo o processo.
Agora vamos colocar um Kong em (4.5, 4.5), com o Kong ainda selecionado, aperte CTRL+D para duplicá-lo, repare que na duplicação, o objeto criado se encontra na mesma posição do objeto de origem, basta arrastá-lo pro lado que veremos o seu deslocamento e a sua sobreposição (overlap). A ideia aqui é criar vários Kongs em sequência e um ao lado do outro. O snap serve para que a distância de um para outro seja simétrica.
Então com o Kong criado e arrastado ao lado esquerdo, aperte o botão X e depois Y para ajustá-lo corretamente na nossa grid, gerando uma distância equidistante ao Kong anterior. Faremos isso várias vezes até completarmos a nossa formação de Kongs malignos e cruéis.
Recapitulando...
- Posicione algum Kong em (4.5, 4.5);
- Com o Kong selecione, duplique-o usando CTRL+D;
- Arraste-o à esquerda (ou direita se você começou numa outra posição) até se encaixar mais ou menos dentro de uma unidade;
- Aperte os botões X e Y para ajustar o Kong selecionado na posição correta;
- Repita todo o processo até formar um grupo de Kongs malignos e cruéis;
Complete a formação até ficar algo como a imagem abaixo:
Você não precisa fazer o processo de duplicação e snapping passo a passo, ao completar uma fileira, selecione ela por completo, dê um CTRL+D e arraste-a pra cima, usando as opções de snapping em toda a fileira. Repita o procedimento até formar 3 fileiras como mostrado na imagem acima.
Agora temos que organizar essa bando de macaquitos siderais num grupo único, dessa forma movendo ou posicionando um único objeto, todos os objetos filhos dele serão movimentos uniformemente.
Crie, portanto, um novo Game Object (CTRL+SHIFT+N) e o posicione em (-4.5, 4.5), da qual é a mesma posição do Kong da extrema esquerda de baixo.
Vamos selecionar pela Hierarchy todos os Kongs criados e arrastá-los para dentro desse novo Game Object:
Renomeie o GameObject para KongGroup01, arraste esse Game Object para a nossa pasta de Prefabs. e poderemos apagá-lo de nossa Hierarchy e reutilizá-lo posteriormente em nosso jogo instanciando-o via WaveSpawner.
Temos a nossa primeira formação personalidade de Kongs do mal criada.
Boa pergunta, jovem Pandoin. Sim, poderemos criar prefabs que contém outros prefabs sem problema algum. Porém, contudo, todavia, entretanto, mas... temos que fazer uma ressalva aqui. Uma vez que eu criei um prefab que contém outros prefabs dentro dele, os prefabs contidos perdem os seus vínculos originais com os prefabs de origem. Ou seja, em nosso exemplo aqui, Os KongsClassics que estão dentro de KongGroup01 não fazem mais parte do prefab KongClassic, portanto, se eu atualizar o prefab KongClassic, adicionando, por exemplo, um novo GameObject e apertando "Apply", isso não irá repercutir para os Kongs que estão dentro de KongGroup01, porque na verdade, criamos um único prefab que continha dados de um prefab anterior. KongGroup01 é um prefab, e os seus GameObjects fazem parte dele.
A imagem abaixo ilustra bem isso, eu criei como teste (você não precisa repetir esse exemplo, se quiser) um GameObject dentro de KongClassic e o chamei de "Teste", ao apertar "Apply", esse novo GameObject não apareceu nos KongsClassic contidos dentro de KongGroup01, mas aparecerá em qualquer outro KongClassic normalmente:
Então, fique ciente de como usar esses recursos de forma a não cometer erros que irão lhe prejudicar lá na frente. Retire o "Teste" e aperte "Apply" novamente, caso você tem feito esse mesmo teste.
Agora vamos mexer na nossa classes Enemy e WaveSpawner à permitir as nossas novas alterações.
Em Enemy.cs, faremos que a sua velocidade seja variável a partir de uma velocidade base já definida chamada speed. Assim, todos os inimigos que serão instanciados de uma mesma wave mas que não fazem parte de um grupo (a exemplo do KongGroup01) terão suas velocidades alteradas para mais ou menos da velocidade base definida.
Em Enemy.cs, adicione o seguinte método privado:
Código:
private void SwingSpeed()
{
speed += Random.Range(-1f, 1f);
}
O SwingSpeed será chamado no Start, mas apenas na condição que o Kong atual não faça parte de nenhum grupo, e para justamente checar essa configuração de grupo, basta vermos se o Kong não tem objeto pai. Crie o seguinte getter logo abaixo de todas as variáveis, mas acima de todos os métodos:
Código:
public bool withinGroup
{
get { return (gameObject.transform.parent != null); }
}
Exemplo: KongGroup01 é o Game Object pai, porque cada um daqueles Kongs que estão dentro dele (KongClassic, KongClassic (1), KongClassic (2), ...) são filhos de KongGroup01. A variável parent de cada Kong aponta para o transform do Game Object pai, que é o próprio KongGroup01. Se tentarmos saber qual é o pai de KongGroup01, o valor de seu "transform.parent" ficará nulo, porque ele não faz parte de nenhum outro Game Object.
Por que então criamos o KongGroup01? Porque assim será mais fácil de mover e instanciar todos aqueles outros Kongs que criamos.
Agora voltando ao código... No método Start, adicione à chamada ao SwingTeste apenas quando o Kong não fizer parte de nenhum grupo.
Código:
void Start()
{
GenerateRandomFireRate();
if (!withinGroup)
SwingSpeed();
}
Teste o nosso jogo e veremos que os Kongs descem numa velocidade variável com base da velocidade que você definiu pela variável speed.
Pronto, agora para finalizar, temos que fazer a WaveSpawner instanciar a KongGroup01 também.
Quero poder ensinar algo novo que ainda não foi abordado em nenhuma das partes desse tutorial e que agora criei a oportunidade para tal que é o conceito de carregamento de recursos da Unity.
Como instanciamos um prefab até agora? Via Inspector arrastamos um prefab para a propriedade Game Object de algum componente definido em algum código e dentro dele usamos o método Instanciate para criar instâncias desse prefab, certo?
Certo! Mas agora para o exemplo dos grupos que criamos, como o KongGroup01, faremos de uma forma um pouco diferente. Dentro da pasta Prefab em nossa janela de Project. Crie a pasta "Resources" e arraste KongGroup01 para dentro dela. Teremos algo assim:
Lembra que lá atrás eu falei que algumas pastas criadas em Project poderiam ter significados diferentes à Unity? Então, a pasta Resources é uma delas. Quando o seu jogo é gerado e compilado para distribuição, todo o conteúdo que está dentro de Resources é indexado pela Unity de uma forma única e agregada, possibilitado todos os seus assets serem acessados vai código como usando Resources.Load.
Resources é, sem dúvida, uma mão na roda para quem trabalha com a Unity. Pois uma vez assets como imagens, sons, fontes, ou Game Objects (oriundos de Prefabs) são acessados de forma simples, prática e única, independente, inclusive, do sistema operacional ao qual o jogo será destinado. Em meu jogo, eu posso criar várias pastas Resources, no final, tudo que estará contido nas Resources, serão indexados e agrupados como sendo de uma única pasta resource, isso é automático feito pela Unity e não teremos controle sobre isso, portanto, cabendo aqui uma ressalva: "Não deixe Prefabs com nome duplicados em Resources". Para ilustrar essa situação, poderemos ter várias pastas Resources em nosso jogo sem problema algum. Vamos supor que tenhamos as seguintes pastas:
- Textures
- Resources
- Music* GrassTile
* Enemy
* Enemy
- Resources
- Prefabs* ExplosionFX
* Enemy
* Enemy
- Resources
- Other Prefabs* Bullet
* Enemy
* Enemy
- Resources
* Enemy
Cada uma dessas pastas Resources em diferentes outras pastas de cada tipo de recurso, apresentam em comum o arquivo "Enemy" que pode significar tanto uma textura, um áudio, quanto mesmo um prefab. A Unity tem como identificar tranquilamente, mesmo os recursos com nomes iguais, quais são eles através da natureza/tipo do arquivo. Exemplo:
Código:
var texture2D = Resources.Load<Texture2D>("Enemy");
var audioClip = Resources.Load<AudioClip>("Enemy");
Eu identifico à Unity que estou carregando "Enemy" cujo o arquivo será uma textura, ela automaticamente me retorna para a variável texture2D um arquivo cujo o nome é "Enemy" e o seu tipo é de Textura 2D, assim como estou identificando também um arquivo "Enemy" cujo o tipo é de áudio e retornando-o propriamente. A Unity consegue distinguir quem é quem, nesse caso. Repare que eles NÃO são Game Objects, quando uso Resources.Load para carregar fontes, texturas, áudio, arquivos textos, xml, etc. Esses recursos não são tratados como Game Object, são conhecidos como raw data e preciso criar um Game Objects ou associá-los a Game Objects existentes se eu quiser executá-los "perceptivelmente" em meu jogo.
Para carregarmos um Prefab, a história muda um pouco e preciso me assegurar que não exista mais de um prefab com o mesmo nome no meu jogo que estejam em pastas Resources. Isso porque um prefab é um prefab, que é na verdade um GameObject que pode ser reutilizado à vontande. Então mesmo que eu criei um prefab para comportar um arquivo de audio e outro para comportar um arquivo de textura, antes de tudo, para a Unity, eles são prefabs e não audio ou textura, que são carregados como componentes individuais para cada prefab. Dessa forma, quando se trata de prefabs, você tem que garantir que os nomes não podem ser duplicados.
Para instanciar um prefab, como o exemplo fictício de nossas pastas acima, faça algo como a amostra a seguir:
Código:
var enemyPrefab = Resources.Load("Enemy");
var enemyGO = Instantiate(enemyPrefab, Vector3.zero, Quaternion.identity);
Só por medida de curiosidade, se você quiser utilizar arquivos como de textura e áudio no Resources.Load, deveremos criar ou instanciar GOs para eles, assim como eu disse antes.
Então façamos algo como o do exemplo a seguir:
Código:
var texture2D = Resources.Load<Texture2D>("Enemy");
var audioClip = Resources.Load<AudioClip>("Enemy");
//instanciando uma textura
var textureGO = new GameObject();
textureGO.AddComponent<SpriteRenderer>();
textureGO.GetComponent<SpriteRenderer>().sprite =
Sprite.Create(texture2D, new Rect(0, 0, texture2D.width, texture2D.height), new Vector2(0.5f, 0.5f), 30);
//instanciando um áudio
var audioGO = new GameObject();
audioGO.AddComponent<AudioSource>();
audioGO.GetComponent<AudioSource>().clip = audioClip;
audioGO.GetComponent<AudioSource>().Play();
Desculpe pela longa explanação, é apenas para evitar dúvidas que acabam sendo meio que frequentes para quem vai começar a programar em Unity e acaba populando seus fóruns e answers da vida com esse tipo de pergunta. Há mais coisas sobre Resources, mas não cabe a explicar isso aqui para não ficar mais extenso o assunto, por hora, essas informações são mais que suficientes para o que pretendemos fazer que é instanciar o nosso grupo de Kongs. Antes de irmos aos Kongs direto, o meu último alerta sobre Resources:
Resources é uma mão na roda, mas CUIDADO!!
É muito prático e conveniente usar Resources. Basta criar essas pastinhas onde seja o que for, que a Unity indexa todos os arquivos nelas contidas e poderemos acessá-los via código sem problema. Porém, essa conveniência toda tem um preço, e esse preço se chama espaço e performance. Diferente de tratarmos GameObjects diretamente numa Scene e seus componentes e propriedades via Inspector e código. Todos os arquivos que você deixar em Resources, serão colocados no seu jogo, mesmo que entre eles sejam arquivos que você não vai precisar usar numa Scene de seu jogo. Então poderemos ocupar o espaço com arquivos desnecessários. A outra coisa, porém, são excessivas chamadas de arquivos via Resources durante a execução do jogo, o que não pode ser muito aconselhado porque diminui a performance uma vez que estamos carregando os arquivos via HD (se o jogo for para PC ou mesmo consoles) ou via SD Card quando é mobiles e outros periféricos que fazem uso disso. Então durante uma sessão de jogo, durante uma mesma fase, ficar puxando às vezes dezenas ou centenas de arquivos, mesmo com os recursos de threads, o seu jogo poderá cair em drops de fps. O recomendável nesse caso, quando for carregar muitos arquivos que serão tratados via código durante a execução de uma Scene de seu jogo é carregá-los antes da fase, estágio do jogo propriamente aparecer pronta para o usuário, dessa forma, trazemos arquivos do HD para a memória RAM do sistema via Resources e reaproveitamos os recursos já carregados na memória para instanciar novos GameObjects, deixando o jogo com menos callback e menos possíveis slowdowns.
É muito prático e conveniente usar Resources. Basta criar essas pastinhas onde seja o que for, que a Unity indexa todos os arquivos nelas contidas e poderemos acessá-los via código sem problema. Porém, essa conveniência toda tem um preço, e esse preço se chama espaço e performance. Diferente de tratarmos GameObjects diretamente numa Scene e seus componentes e propriedades via Inspector e código. Todos os arquivos que você deixar em Resources, serão colocados no seu jogo, mesmo que entre eles sejam arquivos que você não vai precisar usar numa Scene de seu jogo. Então poderemos ocupar o espaço com arquivos desnecessários. A outra coisa, porém, são excessivas chamadas de arquivos via Resources durante a execução do jogo, o que não pode ser muito aconselhado porque diminui a performance uma vez que estamos carregando os arquivos via HD (se o jogo for para PC ou mesmo consoles) ou via SD Card quando é mobiles e outros periféricos que fazem uso disso. Então durante uma sessão de jogo, durante uma mesma fase, ficar puxando às vezes dezenas ou centenas de arquivos, mesmo com os recursos de threads, o seu jogo poderá cair em drops de fps. O recomendável nesse caso, quando for carregar muitos arquivos que serão tratados via código durante a execução de uma Scene de seu jogo é carregá-los antes da fase, estágio do jogo propriamente aparecer pronta para o usuário, dessa forma, trazemos arquivos do HD para a memória RAM do sistema via Resources e reaproveitamos os recursos já carregados na memória para instanciar novos GameObjects, deixando o jogo com menos callback e menos possíveis slowdowns.
Ufa, agora vamos instanciar o bagulho lá que ainda quero jogar hoje.
No arquivo WaveSpawner.cs vamos fazer...
Crie a variável de classe:
Código:
private List<KongGroup> kongGroups;
Essa lista irá armazenar todo o grupo de Kongs que criarmos para o jogo. Adicione o seguinte struct logo após as variáveis privadas:
Código:
private struct KongGroup
{
public Vector3 position;
public GameObject prefab;
}
Esse struct tem o prefab a ser instanciado na posição informada. No método Awake de WaveSpawner crie a instância da lista de kongs:
Código:
kongGroups = new List<KongGroup>();
No método Start, vamos usar o Resource.Load para carregar todos os grupos de kongs em nossa lista. Eles serão chamados de forma aleatória depois em Update.
Código:
//carregando os grupos de kongs (formação fixa)
var kongGroup01 = new KongGroup
{
prefab = Resources.Load("KongGroup01") as GameObject,
position = new Vector3(baseCell.x, baseCell.y)
};
kongGroups.Add(kongGroup01);
No método Update, temos a maior mudança. Agora além da geração aleatória de kongs individuais em cada uma das 10 posições definidas, vamos verificar se a cada spawn vamos chamar a formação aleatória (que é o código atual) ou vamos chamar um dos grupos que criamos (no caso só criei um KongGroup, criei outros também!!! I trust u!).
Código:
void Update()
{
//vai contando o tempo até chegar em spawnRate
timeCounter += Time.deltaTime;
if (timeCounter >= spawnRate)
{
//reseta no contador
timeCounter = 0;
//o cooldown do spawnRate variará entre 1s até 2s.
spawnRate = Random.Range(1f, 2f);
//formação aleatória? (quando for 0)
if (Random.Range(0, 9) >= 5)
{
cells.Clear();
var kongNumber = Random.Range(1, 5);
for (var i = 0; i < kongNumber; i++)
{
//pega uma das 10 possiveis posições
var cell = GetRandomCell();
//instancia a formação aleatória de kongs
Instantiate(kongs[Random.Range(0, kongs.Count)], new Vector3(cell.x, cell.y), Quaternion.identity);
}
}
//formação fixa? (quando for diferente de 0).
else
{
//pega um grupo qualquer...
var group = kongGroups[Random.Range(0, kongGroups.Count)];
//instancia o prefab do grupo para criar o seu Game Object
Instantiate(group.prefab, group.position, Quaternion.identity);
//depois de executar uma formação fixa e não random, o tempo para o próximo spawn será de 3s.
spawnRate = 3f;
}
}
}
Para finalizar, deveremos destruir depois de um tempo os inimigos que foram criados só que não foram destruídos pelo jogador. Mas vou deixar isso ao seu encargo.
Temos ao fim desse capitulo, os seguintes arquivos modificados ou criados:
Enemy_02.cs
WaveSpawner_01.cs
Cell_01.cs
Bullet_02.cs
BulletFeliz_01.cs
No próximo capítulo, vamos ver coisas bem interessantes como deixar efeitos de morte tanto do jogador quanto dos kongs, criar uma HUD para identificar coisas como life, colocar músicas, etc. :)
TO BE CONTINUED