Fazendo da Defesa da Torre um Jogo de Unidade - Parte 2

imagem

Esta é a segunda parte do tutorial "Criando um jogo de Tower Defense no Unity" . Estamos criando um jogo de gênero de defesa de torre no Unity, e até o final da primeira parte , aprendemos como colocar e atualizar monstros. Também temos um inimigo atacando cookies.

No entanto, o inimigo ainda não sabe para onde olhar! Além disso, um ataque sozinho parece estranho. Nesta parte do tutorial, adicionaremos ondas de inimigos e monstros de braço para que eles possam defender um biscoito precioso.

Começando a trabalhar


Abra o projeto no Unity, que paramos na última parte. Se você se juntou a nós agora, faça o download do projeto preliminar e abra o TowerDefense-Part2-Starter .

Abra o GameScene na pasta Cenas .

Vire inimigos


No final do tutorial anterior, o inimigo aprendeu a se mover pela estrada, mas parece que ele não tem idéia de onde procurar.

Abra o script MoveEnemy.cs no IDE e adicione o seguinte método para corrigir a situação.

private void RotateIntoMoveDirection() { //1 Vector3 newStartPosition = waypoints [currentWaypoint].transform.position; Vector3 newEndPosition = waypoints [currentWaypoint + 1].transform.position; Vector3 newDirection = (newEndPosition - newStartPosition); //2 float x = newDirection.x; float y = newDirection.y; float rotationAngle = Mathf.Atan2 (y, x) * 180 / Mathf.PI; //3 GameObject sprite = gameObject.transform.Find("Sprite").gameObject; sprite.transform.rotation = Quaternion.AngleAxis(rotationAngle, Vector3.forward); } 

RotateIntoMoveDirection gira o inimigo para que ele sempre RotateIntoMoveDirection para a frente. Ele faz o seguinte:

  1. Calcula a direção atual do bug, subtraindo a posição do waypoint atual da posição do próximo ponto.
  2. Usa Mathf.Atan2 para determinar o ângulo em radianos para o qual newDirection direcionado (o ponto zero está à direita). Multiplica o resultado por 180 / Mathf.PI , convertendo o ângulo em graus.
  3. Por fim, ele obtém o filho Sprite e gira o eixo de rotationAngle ângulo. Observe que rotacionamos a criança , não o pai, de modo que a faixa de energia que adicionamos posteriormente permaneça horizontal.

Em Update() , substitua o comentário // TODO: próxima chamada para RotateIntoMoveDirection :

 RotateIntoMoveDirection(); 

Salve o arquivo e retorne ao Unity. Execute a cena; agora o inimigo sabe para onde está se movendo.


Agora o bug sabe para onde está indo.

O único inimigo não parece muito impressionante. Precisamos de hordas! E, como em qualquer jogo de defesa de torre, hordas correm em ondas!

Informar jogador


Antes de começarmos a mover as hordas, precisamos alertar o jogador sobre a batalha iminente. Além disso, vale a pena exibir o número da onda atual na parte superior da tela.

As informações de onda são exigidas por vários GameObjects, portanto, as adicionaremos ao componente GameManagerBehavior do GameManager .

Abra o GameManagerBehavior.cs no IDE e adicione as duas variáveis ​​a seguir:

 public Text waveLabel; public GameObject[] nextWaveLabels; 

waveLabel armazena um link para a etiqueta de saída do número da onda no canto superior direito da tela. nextWaveLabels armazena dois GameObjects que criam uma combinação de animação que mostraremos no início de uma nova onda:


Salve o arquivo e retorne ao Unity. Selecione GameManager na Hierarquia . Clique no círculo à direita do rótulo da onda e, na caixa de diálogo Selecionar texto , selecione WaveLabel na guia Cena .

Agora defina o tamanho para etiquetas da próxima onda como 2 . Agora defina o Elemento 0 como NextWaveBottomLabel e, para o Elemento 1, NextWaveTopLabel é o mesmo que fizemos com o Wave Label.


É assim que o Comportamento do Game Manager deve ser agora

Se o jogador perder, ele não verá uma mensagem sobre a próxima onda. Para lidar com essa situação, retorne ao GameManagerBehavior.cs e adicione outra variável:

 public bool gameOver = false; 

No gameOver armazenaremos o valor da perda do jogador.

Aqui, novamente usamos a propriedade para sincronizar os elementos do jogo com a onda atual. Adicione o seguinte código ao GameManagerBehavior :

 private int wave; public int Wave { get { return wave; } set { wave = value; if (!gameOver) { for (int i = 0; i < nextWaveLabels.Length; i++) { nextWaveLabels[i].GetComponent<Animator>().SetTrigger("nextWave"); } } waveLabel.text = "WAVE: " + (wave + 1); } } 

Criar uma variável privada, propriedade e getter já deve ser uma coisa familiar para você. Mas com o levantador, novamente, tudo é um pouco mais interessante.

Atribuímos a wave novo value .

Depois, verificamos se o jogo terminou. Caso contrário, percorra todos os rótulos nextWaveLabels - esses rótulos têm um componente Animator . Para ativar a animação do Animator , definimos um gatilho nextWave .

Por fim, definimos o text de waveLabel como wave + 1 . Por que +1 ? As pessoas comuns não começam a contar do zero (sim, isso é estranho).

Em Start() definimos o valor dessa propriedade:

 Wave = 0; 

Começamos a contagem com o número 0 Wave .

Salve o arquivo e execute a cena no Unity. O rótulo do Wave mostrará corretamente 1.


Para um jogador, tudo começa com a onda 1.

Ondas: crie montes de inimigos


Pode parecer óbvio, mas, para atacar com uma horda, é necessário criar mais inimigos - enquanto não sabemos como fazer isso. Além disso, não devemos criar a próxima onda até que a atual seja destruída.

Ou seja, o jogo deve ser capaz de reconhecer a presença de inimigos na cena, e as tags são uma boa maneira de identificar os objetos do jogo aqui.

Marcação inimiga


Selecione a pré-fabricada Inimigo no Project Browser. Na parte superior do Inspetor, clique na lista suspensa Marca e selecione Adicionar marca .


Crie uma tag chamada Inimigo .


Selecione o inimigo pré-fabricado. No Inspetor, defina a tag Inimigo para ele.

Definindo ondas de inimigos


Agora precisamos definir a onda de inimigos. Abra o SpawnEnemy.cs no IDE e adicione a seguinte implementação de classe antes do SpawnEnemy :

 [System.Serializable] public class Wave { public GameObject enemyPrefab; public float spawnInterval = 2; public int maxEnemies = 20; } 

Wave contém enemyPrefab - a base para criar instâncias de todos os inimigos nessa onda, spawnInterval - tempo entre os inimigos na onda em segundos e maxEnemies - o número de inimigos criados nessa onda.

A classe é serializável , ou seja, podemos alterar seus valores no Inspetor.

Adicione as seguintes variáveis ​​à classe SpawnEnemy :

 public Wave[] waves; public int timeBetweenWaves = 5; private GameManagerBehavior gameManager; private float lastSpawnTime; private int enemiesSpawned = 0; 

Aqui, definimos as variáveis ​​para gerar inimigos, o que é muito semelhante à forma como movemos os inimigos entre os pontos da rota.

Definimos as ondas de inimigos individuais em waves e rastreamos o número de inimigos criados e a hora em que foram criados em enemiesSpawned e lastSpawnTime .

Depois de todas essas mortes, os jogadores precisam de tempo para respirar, então defina timeBetweenWaves para 5 segundos.

Substitua o conteúdo de Start() seguinte código.

 lastSpawnTime = Time.time; gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>(); 

Aqui atribuímos lastSpawnTime valor do horário atual, ou seja, o horário em que o script foi iniciado após o carregamento da cena. Em seguida, obtemos o já conhecido GameManagerBehavior .

Adicione o seguinte código ao Update() :

 // 1 int currentWave = gameManager.Wave; if (currentWave < waves.Length) { // 2 float timeInterval = Time.time - lastSpawnTime; float spawnInterval = waves[currentWave].spawnInterval; if (((enemiesSpawned == 0 && timeInterval > timeBetweenWaves) || timeInterval > spawnInterval) && enemiesSpawned < waves[currentWave].maxEnemies) { // 3 lastSpawnTime = Time.time; GameObject newEnemy = (GameObject) Instantiate(waves[currentWave].enemyPrefab); newEnemy.GetComponent<MoveEnemy>().waypoints = waypoints; enemiesSpawned++; } // 4 if (enemiesSpawned == waves[currentWave].maxEnemies && GameObject.FindGameObjectWithTag("Enemy") == null) { gameManager.Wave++; gameManager.Gold = Mathf.RoundToInt(gameManager.Gold * 1.1f); enemiesSpawned = 0; lastSpawnTime = Time.time; } // 5 } else { gameManager.gameOver = true; GameObject gameOverText = GameObject.FindGameObjectWithTag ("GameWon"); gameOverText.GetComponent<Animator>().SetBool("gameOver", true); } 

Vamos analisá-lo passo a passo:

  1. Obtemos o índice da onda atual e verificamos se é a última.
  2. Nesse caso, calculamos o tempo decorrido após a geração anterior do inimigo e verificamos se é hora de criar um inimigo. Aqui levamos em conta dois casos. Se este é o primeiro inimigo na onda, verificamos se o timeInterval é timeInterval que o timeBetweenWaves . Caso contrário, verificamos se timeInterval é timeInterval que spawnInterval ondas spawnInterval . De qualquer forma, verificamos que não criamos todos os inimigos nesta onda.
  3. Se necessário, crie o inimigo, criando uma instância de enemyPrefab . Aumente também o valor dos enemiesSpawned .
  4. Verifique o número de inimigos na tela. Se eles não estão lá, e este foi o último inimigo na onda, criamos a próxima onda. Também no final da onda, damos ao jogador 10% de todo o ouro restante.
  5. Depois de derrotar a última onda, uma animação de vitória no jogo é jogada aqui.

Definir intervalos de reprodução


Salve o arquivo e retorne ao Unity. Selecione o objeto Estrada na Hierarquia . No Inspetor, defina o objeto Tamanho das ondas como 4 .

Por enquanto, selecione um objeto Inimigo para todos os quatro elementos como Pré-fabricado Inimigo . Configure os campos Intervalo de geração e Inimigos máximos da seguinte maneira:

  • Elemento 0 : Spawn Intervalo: 2,5 , Máximo Inimigos: 5
  • Elemento 1 : Spawn Intervalo: 2 , Máx. Inimigos: 10
  • Elemento 2 : Spawn Intervalo: 2 , Máx. Inimigos: 15
  • Elemento 3 : Spawn Intervalo: 1 , Máx. Inimigos: 5

O esquema final deve ficar assim:


Obviamente, você pode experimentar esses valores para aumentar ou diminuir a complexidade.

Inicie o jogo. Sim! Os besouros começaram a jornada para o seu biscoito!

insetos

Tarefa adicional: adicione diferentes tipos de inimigos


Nenhum jogo de defesa de torre pode ser considerado completo com apenas um tipo de inimigo. Felizmente, também existe o Enemy2 na pasta Prefabs .

No Inspetor, selecione Prefabs \ Enemy2 e adicione o script MoveEnemy a ele. Defina Speed como 3 e defina a tag Enemy . Agora você pode usar esse inimigo rápido para que o jogador não relaxe!

Atualização da vida do jogador


Mesmo que hordas de inimigos atacem o cookie, o jogador não sofre dano. Mas em breve vamos consertar. O jogador deve sofrer se permitir que o inimigo se esgueire.

Abra o GameManagerBehavior.cs no IDE e adicione as duas variáveis ​​a seguir:

 public Text healthLabel; public GameObject[] healthIndicator; 

Usamos healthLabel para acessar o valor da vida do jogador e healthIndicator para acessar os cinco monstrinhos verdes que mastigam cookies - eles simplesmente simbolizam a saúde do jogador; é mais engraçado que um indicador de saúde padrão.

Gestão em saúde


Agora adicione uma propriedade que armazena a saúde do jogador no GameManagerBehavior :

 private int health; public int Health { get { return health; } set { // 1 if (value < health) { Camera.main.GetComponent<CameraShake>().Shake(); } // 2 health = value; healthLabel.text = "HEALTH: " + health; // 3 if (health <= 0 && !gameOver) { gameOver = true; GameObject gameOverText = GameObject.FindGameObjectWithTag("GameOver"); gameOverText.GetComponent<Animator>().SetBool("gameOver", true); } // 4 for (int i = 0; i < healthIndicator.Length; i++) { if (i < Health) { healthIndicator[i].SetActive(true); } else { healthIndicator[i].SetActive(false); } } } } 

É assim que gerenciamos a saúde do jogador. E, novamente, a parte principal do código está localizada no setter:

  1. Se reduzirmos a saúde do jogador, usamos o componente CameraShake para criar um belo efeito de trepidação. Esse script está incluído no projeto para download e não será considerado aqui.
  2. Atualizamos a variável privada e o rótulo de integridade no canto superior esquerdo da tela.
  3. Se a saúde caiu para 0 e o final do jogo ainda não chegou, gameOver como true e inicie a animação gameOver .
  4. Removemos um dos monstros dos cookies. Se simplesmente desativá-los, essa parte pode ser escrita mais facilmente, mas aqui apoiamos a re-inclusão caso a integridade seja adicionada.

Inicializamos Health in Start() :

 Health = 5; 

Definimos Health como 5 quando a cena começa a ser reproduzida.

Tendo feito tudo isso, agora podemos atualizar a saúde do jogador quando o bug chegar ao cookie. Salve o arquivo e acesse o IDE no script MoveEnemy.cs .

Mudança de saúde


Para alterar sua saúde, localize o comentário em Update() com as palavras // TODO: e substitua-o por este código:

 GameManagerBehavior gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>(); gameManager.Health -= 1; 

Portanto, obtemos o GameManagerBehavior e GameManagerBehavior a unidade de sua Health .

Salve o arquivo e retorne ao Unity.

Selecione um GameManager na Hierarquia e selecione HealthLabel para seu Rótulo de Vida .

Expanda o objeto Cookie na Hierarquia e arraste seus cinco HealthIndicators filhos para a matriz de indicadores de saúde do GameManager - os indicadores de saúde serão pequenos monstros verdes comendo biscoitos.

Execute a cena e aguarde até que os insetos atinjam o cookie. Não faça nada até perder.

ataque de cookie

Monster Revenge


Monstros no lugar? Sim Os inimigos atacam? Sim, e eles parecem ameaçadores! É hora de responder a esses animais!

Para fazer isso, precisamos do seguinte:

  • Faixa de vida para que o jogador saiba quais inimigos são fortes e quais são fracos
  • Detectando inimigos ao alcance de um monstro
  • Tomar uma decisão - em qual inimigo atirar
  • Um monte de conchas

Barra de saúde inimiga


Para implementar a faixa de saúde, usamos duas imagens - uma para o fundo escuro e a segunda (a barra verde é um pouco menor), escalaremos de acordo com a saúde do inimigo.

Arraste do Project Browser para a cena Prefabs \ Enemy .

Em seguida, na Hierarquia, arraste e solte Images \ Objects \ HealthBarBackground no Enemy para adicioná-lo como um filho.

No Inspetor, defina a Posição do HealthBarBackground como (0, 1, -4) .

Em seguida, no Navegador de projeto, selecione Images \ Objects \ HealthBar e verifique se o Pivot está à esquerda . Em seguida, adicione-o como filho do Inimigo na Hierarquia e defina seu valor de Posição (-0,63, 1, -5) . Para Escala X , defina o valor como 125 .

Adicione um novo script C # chamado HealthBar ao objeto de jogo HealthBar . Mais tarde, vamos alterá-lo para que ele altere o comprimento da barra de saúde.

Depois de selecionar um objeto Inimigo na Hierarquia , verifique se sua posição é (20, 0, 0) .

Clique em Aplicar na parte superior do Inspetor para salvar todas as alterações como parte da pré-fabricada. Por fim, exclua o objeto Inimigo na Hierarquia .


Agora repita todas essas etapas para adicionar uma barra de integridade para Prefabs \ Enemy2 .

Alterar o comprimento da barra de integridade


Abra o IDE HealthBar.cs e adicione as seguintes variáveis:

 public float maxHealth = 100; public float currentHealth = 100; private float originalScale; 

No maxHealth a saúde máxima do inimigo é armazenada, e no currentHealth - o restante da saúde. Finalmente, em originalScale está o tamanho inicial da barra de integridade.

Salve o objeto originalScale em Start() :

 originalScale = gameObject.transform.localScale.x; 

Armazenamos o valor x da propriedade localScale .

Defina a escala da barra de integridade adicionando o seguinte código a Update() :

 Vector3 tmpScale = gameObject.transform.localScale; tmpScale.x = currentHealth / maxHealth * originalScale; gameObject.transform.localScale = tmpScale; 

Podemos copiar localScale em uma variável temporária porque não podemos alterar seu valor x separadamente. Depois calculamos a nova escala x com base na saúde atual do besouro e novamente atribuímos o valor localScale a uma variável temporária.

Salve o arquivo e inicie o jogo no Unity. Sobre os inimigos, você verá faixas de saúde.


Enquanto o jogo está em execução, expanda um dos objetos Inimigo (Clone) na Hierarquia e selecione sua barra de saúde filho. Altere seu valor de integridade atual e veja como a barra de integridade muda.


Detecção de inimigos dentro do alcance


Agora nossos monstros precisam descobrir quais inimigos apontar. Mas antes que você perceba essa oportunidade, você precisa preparar Monster e Inemy.

Selecione Prefabs do Project Browser \ Monster e adicione o componente 2D do Circle Collider a ele no Inspector .

Defina o parâmetro Radius do colisor como 2.5 - isso indicará o raio de ataque dos monstros.

Marque a caixa de seleção É acionador para que os objetos passem por essa área em vez de colidir com ela.

Finalmente, na parte superior do Inspetor , defina a Camada do monstro como Ignorar Raycast . Na caixa de diálogo, clique em Sim, altere filhos . Se Ignorar Raycast não estiver selecionado, o colisor responderá aos eventos de clique do mouse. Isso será um problema, porque os monstros bloqueiam eventos destinados aos objetos Openspot abaixo deles.


Para garantir que o inimigo seja detectado na área de gatilho, precisamos adicionar um colisor e um corpo rígido a ele, porque o Unity envia apenas eventos de gatilho quando um corpo rígido é anexado a um dos colisores.

No Navegador de projeto, selecione Prefabs \ Enemy . Adicione o componente Rigidbody 2D e selecione Kinematic para Body Type . Isso significa que o corpo não será afetado pela física.

Adicione Circle Collider 2D com um raio de 1 . Repita essas etapas para Prefabs \ Enemy 2 .

Os gatilhos são configurados para que os monstros entendam que os inimigos estão dentro do seu raio de ação.

Precisamos preparar mais uma coisa: um script dizendo aos monstros quando o inimigo é destruído, para que eles não levantem uma exceção enquanto continuam atirando.

Crie um novo script C # chamado EnemyDestructionDelegate e adicione-o aos prefabs do Enemy e Enemy2 .

Abra EnemyDestructionDelegate.cs no IDE e adicione a seguinte declaração de delegação:

 public delegate void EnemyDelegate (GameObject enemy); public EnemyDelegate enemyDelegate; 

Aqui, criamos um delegate , ou seja, um contêiner para uma função que pode ser passada como uma variável.

Nota : os delegados são usados ​​quando um objeto do jogo deve notificar ativamente outros objetos do jogo sobre alterações. Leia mais sobre os delegados na documentação do Unity .

Adicione o seguinte método:

 void OnDestroy() { if (enemyDelegate != null) { enemyDelegate(gameObject); } } 

Quando um objeto do jogo é destruído, o Unity chama automaticamente esse método e verifica o delegado quanto à desigualdade null . No nosso caso, chamamos isso de gameObject como parâmetro. Isso permite que todos os entrevistados registrados como delegados saibam que o inimigo está destruído.

Salve o arquivo e retorne ao Unity.

Damos aos monstros uma licença para matar


E agora os monstros podem detectar inimigos dentro do raio de sua ação. Adicione um novo script C # à pré-fabricada Monster e chame -a de ShootEnemies .

Abra ShootEnemies.cs no IDE e adicione o seguinte using construção para acessar os Generics .

 using System.Collections.Generic; 

Adicione uma variável para rastrear todos os inimigos dentro do alcance:

 public List<GameObject> enemiesInRange; 

Em enemiesInRange , armazenaremos todos os inimigos dentro do alcance.

Inicialize o campo em Start() .

 enemiesInRange = new List<GameObject>(); 

No começo, não há inimigos no raio de ação, por isso criamos uma lista vazia.

Preencha a lista de enemiesInRange ! Adicione o seguinte código ao script:

 // 1 void OnEnemyDestroy(GameObject enemy) { enemiesInRange.Remove (enemy); } void OnTriggerEnter2D (Collider2D other) { // 2 if (other.gameObject.tag.Equals("Enemy")) { enemiesInRange.Add(other.gameObject); EnemyDestructionDelegate del = other.gameObject.GetComponent<EnemyDestructionDelegate>(); del.enemyDelegate += OnEnemyDestroy; } } // 3 void OnTriggerExit2D (Collider2D other) { if (other.gameObject.tag.Equals("Enemy")) { enemiesInRange.Remove(other.gameObject); EnemyDestructionDelegate del = other.gameObject.GetComponent<EnemyDestructionDelegate>(); del.enemyDelegate -= OnEnemyDestroy; } } 

  1. No OnEnemyDestroy removemos o inimigo de enemiesInRange . Quando um inimigo pisa em um gatilho em torno de um monstro, OnTriggerEnter2D é OnTriggerEnter2D .
  2. Em seguida, adicionamos o inimigo à lista enemiesInRange e adicionamos o evento OnEnemyDestroy . Portanto, garantimos que, após a destruição do inimigo, o OnEnemyDestroy será chamado. Não queremos que os monstros gastem munição em inimigos mortos, certo?
  3. No OnTriggerExit2D removemos o inimigo da lista e cancelamos o registro do delegado. Agora sabemos quais inimigos estão ao alcance.

Salve o arquivo e inicie o jogo no Unity. Para garantir que tudo esteja funcionando, posicione o monstro, selecione-o e siga as alterações na lista enemiesInRange no enemiesInRange .

Seleção de alvo


Os monstros agora sabem qual inimigo está ao alcance. Mas o que eles farão quando houver vários inimigos no raio?

Claro, eles atacarão o mais próximo do fígado!

Abra o script IDE MoveEnemy.cs e adicione um novo método que calcula esse monstro:

 public float DistanceToGoal() { float distance = 0; distance += Vector2.Distance( gameObject.transform.position, waypoints [currentWaypoint + 1].transform.position); for (int i = currentWaypoint + 1; i < waypoints.Length - 1; i++) { Vector3 startPosition = waypoints [i].transform.position; Vector3 endPosition = waypoints [i + 1].transform.position; distance += Vector2.Distance(startPosition, endPosition); } return distance; } 

O código calcula o comprimento do caminho ainda não percorrido pelo inimigo. Para fazer isso, ele usa Distance , calculado como a distância entre duas instâncias do Vector3 .

Usaremos esse método posteriormente para descobrir qual alvo atacar. No entanto, enquanto nossos monstros não estão armados e desamparados, primeiro faremos isso.

Salve o arquivo e retorne ao Unity para começar a configurar seus shells.

Vamos dar conchas aos monstros. Muitas conchas!


Arraste do Project Browser para a cena Images / Objects / Bullet1 . Defina a posição em z como -2 - as posições em xey não são importantes, porque as definimos sempre que criamos uma nova instância do projétil durante a execução do programa.

Adicione um novo script C # chamado BulletBehavior e, no IDE, adicione as seguintes variáveis:

 public float speed = 10; public int damage; public GameObject target; public Vector3 startPosition; public Vector3 targetPosition; private float distance; private float startTime; private GameManagerBehavior gameManager; 

speed determina a velocidade dos projéteis; o damage claro no nome.

target , targetPosition e targetPosition determinam a direção do projétil.

distance e startTime rastreiam a posição atual do projétil. gameManager recompensa o jogador quando ele mata o inimigo.

Atribua os valores dessas variáveis ​​em Start() :

 startTime = Time.time; distance = Vector2.Distance (startPosition, targetPosition); GameObject gm = GameObject.Find("GameManager"); gameManager = gm.GetComponent<GameManagerBehavior>(); 

startTimedefinimos o valor do tempo atual e calculamos a distância entre as posições inicial e de destino. Além disso, como sempre, conseguimos GameManagerBehavior.

Para controlar o movimento do projétil, adicione o Update()seguinte código:

 // 1 float timeInterval = Time.time - startTime; gameObject.transform.position = Vector3.Lerp(startPosition, targetPosition, timeInterval * speed / distance); // 2 if (gameObject.transform.position.Equals(targetPosition)) { if (target != null) { // 3 Transform healthBarTransform = target.transform.Find("HealthBar"); HealthBar healthBar = healthBarTransform.gameObject.GetComponent<HealthBar>(); healthBar.currentHealth -= Mathf.Max(damage, 0); // 4 if (healthBar.currentHealth <= 0) { Destroy(target); AudioSource audioSource = target.GetComponent<AudioSource>(); AudioSource.PlayClipAtPoint(audioSource.clip, transform.position); gameManager.Gold += 50; } } Destroy(gameObject); } 

  1. Calculamos a nova posição do projétil, usando Vector3.Lerppara interpolação entre as posições inicial e final.
  2. Se o projétil chegar targetPosition, verificaremos se ele ainda existe target.
  3. Obtemos o componente do HealthBaralvo e reduzimos sua saúde pelo tamanho do damageprojétil.
  4. Se a saúde do inimigo for reduzida a zero, nós a destruímos, reproduzimos o efeito sonoro e recompensamos o jogador pela precisão.

Salve o arquivo e retorne ao Unity.

Fazemos conchas grandes


Não seria ótimo se o monstro começasse a disparar mais projéteis em níveis altos? Felizmente, isso é fácil de implementar.

Arraste o objeto de jogo Bullet1 da Hierarquia para a guia Projeto para criar uma pré-fabricada de projétil. Remova o objeto original da cena - não será mais necessário.

Duplique o pref1 Bullet1 duas vezes . Nomeie as cópias de Bullet2 e Bullet3 .

Selecione Bullet2 . No Inspetor, defina o campo Sprite do componente Sprite Renderer como Images / Objects / Bullet2. Então, tornaremos o Bullet2 um pouco mais que o Bullet1.

Repita o procedimento para alterar o sprite pré-fabricada bullet3 em Imagens relacionadas / Objetos / bullet3 .

Além disso, em Comportamento de balas , ajustaremos a quantidade de danos causados ​​por projéteis.

Selecione o marcador pré - fabricado Bullet1 na guia Projeto . No Inspetor, você verá Comportamento do marcador (Script) , no qual é possível definir Dano para 10 no marcador1 , 15 no marcador2 e 20 no marcador3 - ou quaisquer outros valores que você desejar.

Nota : Alterei os valores para que, em níveis mais altos, o preço do dano se torne mais alto. Isso impede que a atualização permita que o jogador atualize monstros nos melhores pontos.


Cascas pré-fabricadas - o tamanho aumenta com o nível

Alterando o nível de conchas


Atribua conchas diferentes para diferentes níveis de monstros, para que monstros mais fortes destruam os inimigos mais rapidamente.

Abra MonsterData.cs no IDE e adicione às MonsterLevelseguintes variáveis:

 public GameObject bullet; public float fireRate; 

Então, definimos o pré-fabricado do projétil e a frequência de tiro para cada nível de monstros. Salve o arquivo e retorne ao Unity para concluir a configuração do monstro.

Selecione a pré-fabricada Monster no Project Browser . No Inspetor, expanda Níveis no componente Monster Data (Script) . Defina a taxa de incêndio de cada item como 1 . Em seguida, defina o parâmetro Bullet do elemento 0, 1 e 2 como Bullet1 , Bullet2 e Bullet3 .

Os níveis de monstro devem ser definidos da seguinte forma:


Conchas matam inimigos? Sim Vamos abrir o fogo!

Fogo aberto


Abra ShootEnemies.cs no IDE e adicione as seguintes variáveis:

 private float lastShotTime; private MonsterData monsterData; 

Como o nome indica, essas variáveis ​​rastreiam o tempo do último tiro do monstro, bem como a estrutura MonsterDataque contém informações sobre o tipo de projétil de monstro, a frequência do disparo e assim por diante.

Defina os valores desses campos em Start():

 lastShotTime = Time.time; monsterData = gameObject.GetComponentInChildren<MonsterData>(); 

Aqui, atribuímos o lastShotTimevalor da hora atual e obtemos acesso ao componente MonsterDatadeste objeto.

Adicione o seguinte método para implementar o disparo:

 void Shoot(Collider2D target) { GameObject bulletPrefab = monsterData.CurrentLevel.bullet; // 1 Vector3 startPosition = gameObject.transform.position; Vector3 targetPosition = target.transform.position; startPosition.z = bulletPrefab.transform.position.z; targetPosition.z = bulletPrefab.transform.position.z; // 2 GameObject newBullet = (GameObject)Instantiate (bulletPrefab); newBullet.transform.position = startPosition; BulletBehavior bulletComp = newBullet.GetComponent<BulletBehavior>(); bulletComp.target = target.gameObject; bulletComp.startPosition = startPosition; bulletComp.targetPosition = targetPosition; // 3 Animator animator = monsterData.CurrentLevel.visualization.GetComponent<Animator>(); animator.SetTrigger("fireShot"); AudioSource audioSource = gameObject.GetComponent<AudioSource>(); audioSource.PlayOneShot(audioSource.clip); } 

  1. Temos as posições inicial e alvo da bala. Defina a posição z igual a z bulletPrefab. Anteriormente, definimos a posição pré-fabricada do projétil em z, para que o projétil apareça sob o monstro atirador, mas acima dos inimigos.
  2. Criamos uma instância de um novo shell usando o bulletPrefabapropriado MonsterLevel. Designe startPositione targetPositionprojete.
  3. Tornamos o jogo mais interessante: quando o monstro disparar, inicie a animação do disparo e toque o som do laser.

Juntando tudo


É hora de juntar tudo. Defina o alvo e faça o monstro olhar para ele.


No script ShootEnemies.cs, adicione a Update()este código:

 GameObject target = null; // 1 float minimalEnemyDistance = float.MaxValue; foreach (GameObject enemy in enemiesInRange) { float distanceToGoal = enemy.GetComponent<MoveEnemy>().DistanceToGoal(); if (distanceToGoal < minimalEnemyDistance) { target = enemy; minimalEnemyDistance = distanceToGoal; } } // 2 if (target != null) { if (Time.time - lastShotTime > monsterData.CurrentLevel.fireRate) { Shoot(target.GetComponent<Collider2D>()); lastShotTime = Time.time; } // 3 Vector3 direction = gameObject.transform.position - target.transform.position; gameObject.transform.rotation = Quaternion.AngleAxis( Mathf.Atan2 (direction.y, direction.x) * 180 / Mathf.PI, new Vector3 (0, 0, 1)); } 

Considere este código passo a passo.

  1. Determine o propósito do monstro. Começamos na distância máxima possível em minimalEnemyDistance. Percorremos um ciclo de todos os inimigos dentro do alcance e transformamos o inimigo em um novo alvo, se a distância dele para o biscoito for menor que a menor atual.
  2. Chamamos Shootse o tempo decorrido é maior que a frequência de disparo do monstro e definimos o lastShotTimevalor do tempo atual.
  3. Calculamos o ângulo de rotação entre o monstro e seu alvo. Nós giramos o monstro para esse ângulo. Agora ele sempre olha para o alvo.

Salve o arquivo e inicie o jogo no Unity. Monstros começarão a proteger desesperadamente os cookies. Finalmente terminamos!

Para onde ir a seguir


O projeto finalizado pode ser baixado aqui .

Fizemos um ótimo trabalho neste tutorial e agora temos um ótimo jogo.

Aqui estão algumas idéias para um maior desenvolvimento do projeto:

  • Mais tipos de inimigos e monstros
  • Diferentes rotas de inimigos
  • Níveis de jogo diferentes

Cada um desses aspectos exigirá mudanças mínimas e pode tornar o jogo mais divertido. Se você criar um novo jogo com base neste tutorial, será um prazer jogá-lo, então compartilhe um link para ele.

Pensamentos interessantes sobre a criação de um jogo de sucesso de defesa de torre podem ser encontrados nesta entrevista .

Source: https://habr.com/ru/post/pt413915/


All Articles