Criando Defesa de Torre na Unidade: Torres e Inimigos de Tiro

[ A primeira e a segunda partes do tutorial]

  • Colocamos no campo da torre.
  • Visamos inimigos com a ajuda da física.
  • Nós os rastreamos enquanto é possível.
  • Nós atiramos neles com um raio laser.

Esta é a terceira parte de uma série de tutoriais sobre como criar um gênero simples de defesa de torre. Descreve a criação de torres, mirando e atirando nos inimigos.

O tutorial foi criado no Unity 2018.3.0f2.


Vamos aquecer os inimigos.

Criação da torre


As paredes apenas diminuem a velocidade dos inimigos, aumentando o comprimento do caminho que eles precisam seguir. Mas o objetivo do jogo é destruir os inimigos antes que eles atinjam o ponto final. Esse problema é resolvido colocando torres no campo que dispara contra elas.

Conteúdo em bloco


Torres são outro tipo de conteúdo de bloco, então GameTileContent adicionar uma entrada para eles no GameTileContent .

 public enum GameTileContentType { Empty, Destination, Wall, SpawnPoint, Tower€ } 

Neste tutorial, daremos suporte apenas a um tipo de torre, que pode ser implementado fornecendo à GameTileContentFactory um link para a pré-fabricada da torre, cuja instância também pode ser criada via Get .

  [SerializeField] GameTileContent towerPrefab = default; public GameTileContent Get (GameTileContentType type) { switch (type) { … case GameTileContentType.Tower€: return Get(towerPrefab); } … } 

Mas as torres devem disparar, portanto, sua condição precisará ser atualizada e eles precisam de seu próprio código. Crie uma classe Tower para esse fim que estende a classe GameTileContent .

 using UnityEngine; public class Tower : GameTileContent {} 

Você pode fazer com que a pré-fabricada da torre tenha seu próprio componente alterando o tipo de campo de fábrica para Tower . Como a classe ainda é considerada um GameTileContent , nada mais precisa ser alterado.

  Tower towerPrefab = default; 

Prefab


Crie uma casa pré-fabricada para a torre. Você pode começar duplicando a pré-fabricada da parede e substituindo o componente GameTileContent componente Tower e, em seguida, altere seu tipo para Tower . Para fazer a torre encaixar nas paredes, salve o cubo da parede como base da torre. Em seguida, coloque outro cubo em cima dele. Eu dei a ele uma escala de 0,5. Coloque outro cubo, indicando uma torre, esta parte mirará e atirará nos inimigos.



Três cubos formando uma torre.

A torre girará e, como possui um colisor, será rastreada por um mecanismo físico. Mas não precisamos ser tão precisos, porque usamos colisores de torre apenas para selecionar células. Isso pode ser feito aproximadamente. Remova o colisor do cubo da torre e troque-o para que cubra os dois cubos.



Torre de cubo Collider.

A torre dispara um raio laser. Ele pode ser visualizado de várias maneiras, mas apenas usamos um cubo translúcido, que iremos esticar para formar um feixe. Cada torre deve ter seu próprio feixe, portanto, adicione-o ao pré-fabricado da torre. Coloque-o dentro da torre para que fique oculto por padrão e reduza a escala, por exemplo, 0,2. Vamos torná-lo um filho da raiz pré-fabricada, não do cubo da torre.

raio laser

hierarquia

Cubo escondido de um raio laser.

Crie um material adequado para o raio laser. Acabei de usar o material preto translúcido padrão, desliguei todas as reflexões e também dei uma cor vermelha emitida.

cor

sem reflexões

O material do raio laser.

Verifique se o raio laser não possui um colisor e também desligue o tom e a sombra.


O raio laser não interage com sombras.

Após concluir a criação da pré-fabricada da torre, a adicionaremos à fábrica.


Fábrica com uma torre.

Colocação da torre


Adicionaremos e removeremos torres usando outro método de comutação. Você pode simplesmente duplicar o GameBoard.ToggleWall alterando o nome do método e o tipo de conteúdo.

  public void ToggleTower (GameTile tile) { if (tile.Content.Type == GameTileContentType.Tower€) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.Tower€); if (!FindPaths()) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } } } 

No Game.HandleTouch , pressionar a tecla Shift alterna as torres em vez de paredes.

  void HandleTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { if (Input.GetKey(KeyCode.LeftShift)) { board.ToggleTower(tile); } else { board.ToggleWall(tile); } } } 


Torres no campo.

Bloqueio de caminho


Até agora, apenas paredes podem bloquear a busca de um caminho, então os inimigos se movem pelas torres. Vamos adicionar GameTileContent propriedade auxiliar ao GameTileContent que indica se o conteúdo bloqueia o caminho. O caminho é bloqueado se for uma parede ou uma torre.

  public bool BlocksPath => Type == GameTileContentType.Wall || Type == GameTileContentType.Tower€; 

Use esta propriedade em GameTile.GrowPathTo em vez de verificar o tipo de conteúdo.

  GameTile GrowPathTo (GameTile neighbor, Direction direction) { … return //neighbor.Content.Type != GameTileContentType.Wall ? neighbor : null; neighbor.Content.BlocksPath ? null : neighbor; } 


Agora o caminho está bloqueado por muros e torres.

Substitua as paredes


Muito provavelmente, o jogador geralmente substitui as paredes por torres. Será inconveniente para ele remover a parede primeiro e, além disso, os inimigos podem penetrar nessa brecha que apareceu temporariamente. Você pode implementar uma substituição direta forçando o GameBoard.ToggleTower a verificar se a parede está atualmente no bloco. Nesse caso, substitua-o imediatamente por uma torre. Nesse caso, não precisamos procurar outras maneiras, porque o bloco ainda as bloqueia.

  public void ToggleTower (GameTile tile) { if (tile.Content.Type == GameTileContentType.Tower) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else if (tile.Content.Type == GameTileContentType.Empty) { … } else if (tile.Content.Type == GameTileContentType.Wall) { tile.Content = contentFactory.Get(GameTileContentType.Tower); } } 

Nosso objetivo é inimigos


Uma torre só pode cumprir sua tarefa quando encontra um inimigo. Depois de encontrar o inimigo, ela deve decidir qual parte dele apontar.

Ponto de mira


Para detectar alvos, usaremos o mecanismo de física. Como no caso do colisor de torre, não precisamos que o colisor inimigo coincida necessariamente com sua forma. Você pode escolher o colisor mais simples, ou seja, uma esfera. Depois de detectar o inimigo, usaremos a posição do objeto de jogo com o colisor anexado a ele como ponto de mira.

Não podemos anexar o colisor ao objeto raiz do inimigo, porque ele nem sempre coincide com a posição do modelo e fará a torre apontar para o chão. Ou seja, você precisa colocar o colisor em algum lugar do modelo. O mecanismo de física nos dará um link para esse objeto, que podemos usar para apontar, mas ainda precisamos acessar o componente Enemy do objeto raiz. Para simplificar a tarefa, vamos criar o componente TargetPoint . Vamos dar uma propriedade para atribuição privada e recebimento público do componente Enemy , e outra propriedade para obter sua posição no mundo.

 using UnityEngine; public class TargetPoint : MonoBehaviour { public Enemy Enemy€ { get; private set; } public Vector3 Position => transform.position; } 

Vamos dar a ele um método Awake que configura um link para seu componente Enemy . Vá diretamente para o objeto raiz usando transform.root . Se o componente Enemy não existir, cometemos um erro ao criar o inimigo, então vamos adicionar uma declaração para isso.

  void Awake () { Enemy€ = transform.root.GetComponent<Enemy>(); Debug.Assert(Enemy€ != null, "Target point without Enemy root!", this); } 

Além disso, o colisor deve estar anexado ao mesmo objeto de jogo ao qual o TargetPoint conectado.

  Debug.Assert(Enemy€ != null, "Target point without Enemy root!", this); Debug.Assert( GetComponent<SphereCollider>() != null, "Target point without sphere collider!", this ); 

Adicione um componente e um colisor ao cubo pré-fabricado do inimigo. Isso fará as torres apontarem para o centro do cubo. Utilizamos um colisor esférico com um raio de 0,25. O cubo tem uma escala de 0,5; portanto, o raio real do colisor será 0,125. Graças a isso, o inimigo terá que cruzar visualmente o círculo de alcance da torre e somente depois de algum tempo o objetivo real se tornará. O tamanho do colisor também é afetado pela escala aleatória do inimigo, portanto seu tamanho no jogo também varia um pouco.


inspetor

Um inimigo com um ponto de mira e um colisor em um cubo.

Camada inimiga


As torres se importam apenas com os inimigos, e elas não apontam para mais nada, então colocaremos todos os inimigos em uma camada separada. Usaremos a camada 9. Altere seu nome para Inimigo na janela Camadas e tags , que pode ser aberta através da opção Editar camadas no menu suspenso Camadas , no canto superior direito do editor.


A camada 9 será usada para inimigos.

Essa camada é necessária apenas para o reconhecimento de inimigos, e não para interações físicas. Vamos destacar desabilitando-os na Layer Collision Matrix , localizada no painel Physics dos parâmetros do projeto.


Matriz de colisões de camadas.

Certifique-se de que o objeto de jogo do ponto de mira esteja na camada desejada. O restante da pré-fabricada do inimigo pode estar em outras camadas, mas será mais fácil coordenar tudo e colocar toda a pré-fabricada na camada Inimiga . Se você alterar a camada do objeto raiz, você será solicitado a alterar a camada para todos os seus objetos filhos.


Inimigo na camada certa.

Vamos adicionar a declaração de que o TargetPoint realmente na camada correta.

  void Awake () { … Debug.Assert(gameObject.layer == 9, "Target point on wrong layer!", this); } 

Além disso, as ações do jogador devem ser ignoradas pelos jogadores inimigos. Isso pode ser conseguido adicionando um argumento de máscara de camada ao Physics.Raycast no GameBoard.GetTile . Esse método possui uma forma que leva a distância para o feixe e a máscara de camada como argumentos adicionais. Por padrão, forneceremos a distância máxima e a máscara de camada, ou seja, 1.

  public GameTile GetTile (Ray ray) { if (Physics.Raycast(ray, out RaycastHit hit, float.MaxValue, 1)) { … } return null; } 

A máscara de camada não deve ser 0?
O índice de camada padrão é zero, mas passamos a máscara de camada. A máscara altera os bits individuais de um número inteiro para 1 se a camada precisar ser ativada. Nesse caso, você precisa definir apenas o primeiro bit, ou seja, o menos significativo, o que significa 2 0 , que é igual a 1.

Atualizando o conteúdo do bloco


As torres podem executar suas tarefas apenas quando seu status é atualizado. O mesmo se aplica ao conteúdo de todos os blocos, embora o restante do conteúdo não faça nada até agora. Portanto, adicione um método virtual GameTileContent ao GameUpdate , que não faz nada por padrão.

  public virtual void GameUpdate () {} 

Vamos fazer com que o Tower redefina, mesmo que por enquanto ele simplesmente mostre no console que está procurando um alvo.

  public override void GameUpdate () { Debug.Log("Searching for target..."); } 

GameBoard lida com blocos e seu conteúdo, para que ele também acompanhe o conteúdo que precisa ser atualizado. Para fazer isso, adicione a lista e o método público GameUpdate , que atualiza tudo na lista.

  List<GameTileContent> updatingContent = new List<GameTileContent>(); … public void GameUpdate () { for (int i = 0; i < updatingContent.Count; i++) { updatingContent[i].GameUpdate(); } } 

Em nosso tutorial, você só precisa atualizar as torres. Altere ToggleTower para adicionar e remover conteúdo, se necessário. Se também for necessário outro conteúdo, precisaremos de uma abordagem mais geral, mas, por enquanto, isso é suficiente.

  public void ToggleTower (GameTile tile) { if (tile.Content.Type == GameTileContentType.Tower) { updatingContent.Remove(tile.Content); tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.Tower); //if (!FindPaths()) { if (FindPaths()) { updatingContent.Add(tile.Content); } else { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } } else if (tile.Content.Type == GameTileContentType.Wall) { tile.Content = contentFactory.Get(GameTileContentType.Tower); updatingContent.Add(tile.Content); } } 

Para fazer isso funcionar, basta atualizar o campo em Game.Update . Vamos atualizar o campo após os inimigos. Graças a isso, as torres poderão apontar exatamente onde estão os inimigos. Se fizéssemos o contrário, as torres mirariam onde os inimigos estavam no último quadro.

  void Update () { … enemies.GameUpdate(); board.GameUpdate(); } 

Visando a faixa


As torres têm um raio de mira limitado. Vamos personalizá-lo adicionando um campo à classe Tower . A distância é medida a partir do centro do bloco da torre, portanto, no intervalo de 0,5, ele cobrirá apenas seu próprio bloco. Portanto, um intervalo mínimo e padrão razoável seria 1,5, cobrindo a maioria dos ladrilhos vizinhos.

  [SerializeField, Range(1.5f, 10.5f)] float targetingRange = 1.5f; 


Alcance do objetivo 2.5.

Vamos visualizar o intervalo com o gizmo. Não precisamos vê-lo constantemente; portanto, criaremos o método OnDrawGizmosSelected chamado apenas para os objetos selecionados. Desenhamos a moldura amarela da esfera com um raio igual à distância e centralizado em relação à torre. Coloque-o ligeiramente acima do solo, para que fique sempre claramente visível.

  void OnDrawGizmosSelected () { Gizmos.color = Color.yellow; Vector3 position = transform.localPosition; position.y += 0.01f; Gizmos.DrawWireSphere(position, targetingRange); } 


Gizmo que aponta o alcance.

Agora podemos ver qual dos inimigos é um alvo acessível para cada uma das torres. Mas escolher torres na janela de cena é inconveniente, porque precisamos selecionar um dos cubos filhos e depois mudar para o objeto raiz da torre. Outros tipos de conteúdo de bloco também sofrem do mesmo problema. Podemos forçar a seleção da raiz do conteúdo do GameTileContent na janela da cena, adicionando o atributo SelectionBase ao GameTileContent .

 [SelectionBase] public class GameTileContent : MonoBehaviour { … } 

Captura de alvo


Adicione um campo TargetPoint à classe Tower para que ele possa rastrear seu destino capturado. Em seguida, GameUpdate para chamar o novo método AquireTarget , que retorna informações sobre se encontrou o destino. Após a detecção, ele exibirá uma mensagem no console.

  TargetPoint target; public override void GameUpdate () { if (AcquireTarget()) { Debug.Log("Acquired target!"); } } 

No AcquireTarget obtemos todos os destinos disponíveis chamando Physics.OverlapSphere com uma posição de torre e intervalo como argumentos. O resultado será uma matriz Collider contendo todos os colliders em contato com a esfera. Se o comprimento da matriz é positivo, existe pelo menos um ponto de mira e simplesmente selecionamos o primeiro. Pegue o componente TargetPoint , que sempre deve existir, atribua-o ao campo de destino e relate o sucesso. Caso contrário, limpamos o alvo e relatamos a falha.

  bool AcquireTarget () { Collider[] targets = Physics.OverlapSphere( transform.localPosition, targetingRange ); if (targets.Length > 0) { target = targets[0].GetComponent<TargetPoint>(); Debug.Assert(target != null, "Targeted non-enemy!", targets[0]); return true; } target = null; return false; } 

Temos a garantia de obter os pontos de mira corretos, se levarmos em conta colididores apenas na camada de inimigos. Essa é a camada 9, então passaremos a máscara de camada correspondente.

  const int enemyLayerMask = 1 << 9; … bool AcquireTarget () { Collider[] targets = Physics.OverlapSphere( transform.localPosition, targetingRange, enemyLayerMask ); … } 

Como esta máscara de bits funciona?
Como a camada inimiga tem um índice de 9, o décimo bit da máscara de bit deve ter o valor 1. Isso corresponde a um número inteiro 2 9 , ou seja, 512. Mas esse registro de máscara de bit não é intuitivo. Também podemos escrever um literal binário, por exemplo, 0b10_0000_0000 , mas precisamos contar zeros. Nesse caso, a entrada mais conveniente seria usar o operador de deslocamento esquerdo << , que desloca os bits para a esquerda. que corresponde a um número na potência de dois.

Você pode visualizar o alvo capturado desenhando uma linha de aparelhos entre as posições da torre e o alvo.

  void OnDrawGizmosSelected () { … if (target != null) { Gizmos.DrawLine(position, target.Position); } } 


Visualização de objetivos.

Por que não usar métodos como o OnTriggerEnter?
A vantagem de verificar manualmente as metas transversais é que só podemos fazer isso quando necessário. Não há razão para verificar se há alvos na torre. Além disso, ao obter todos os objetivos em potencial ao mesmo tempo, não precisamos processar uma lista de objetivos em potencial para cada torre, que muda constantemente.

Target Lock


O alvo escolhido para capturar depende da ordem em que são representados pelo mecanismo físico, ou seja, é arbitrário. Portanto, parece que o alvo capturado está mudando sem motivo. Depois que a torre recebe o alvo, é mais lógico para ela rastreá-la e não mudar para outra. Adicione um método TrackTarget que implemente esse rastreamento e retorne informações sobre se foi bem-sucedido. Primeiro, informaremos se o alvo foi capturado.

  bool TrackTarget () { if (target == null) { return false; } return true; } 

Chamaremos esse método no GameUpdate e somente ao retornar false chamaremos AcquireTarget . Se o método retornou verdadeiro, temos um objetivo. Isso pode ser feito colocando as duas chamadas de método em uma verificação if com o operador OR, porque se o primeiro operando retornar true , o segundo não será verificado e a chamada será perdida. O operador AND atua de maneira semelhante.

  public override void GameUpdate () { if (TrackTarget() || AcquireTarget()) { Debug.Log("Locked on target!"); } } 


Acompanhamento de metas.

Como resultado, as torres são fixadas no alvo até atingir o ponto final e serem destruídas. Se você usar inimigos repetidamente, precisará verificar a exatidão do link, como é feito com os links para as figuras processadas em uma série de tutoriais sobre gerenciamento de objetos .

Para rastrear alvos apenas quando estão dentro do alcance, o TrackTarget deve rastrear a distância entre a torre e o alvo. Se exceder o valor do intervalo, o destino deverá ser redefinido e retornar falso. Você pode usar o método Vector3.Distance para esta verificação.

  bool TrackTarget () { if (target == null) { return false; } Vector3 a = transform.localPosition; Vector3 b = target.Position; if (Vector3.Distance(a, b) > targetingRange) { target = null; return false; } return true; } 

No entanto, esse código não leva em consideração o raio do colisor. Portanto, como resultado, a torre pode perder o alvo e capturá-lo novamente, apenas para parar de rastreá-lo no próximo quadro e assim por diante. Podemos evitar isso adicionando um raio de colisão ao intervalo.

  if (Vector3.Distance(a, b) > targetingRange + 0.125f) { … } 

Isso nos dá os resultados corretos, mas somente se a escala do inimigo não for alterada. Como atribuímos a cada inimigo uma escala aleatória, devemos levar isso em consideração ao alterar o alcance. Para fazer isso, precisamos lembrar a escala fornecida pelo Enemy e abri-la usando a propriedade getter.

  public float Scale { get; private set; } … public void Initialize (float scale, float speed, float pathOffset) { Scale = scale; … } 

Agora podemos verificar o intervalo correto no Tower.TrackTarget .

  if (Vector3.Distance(a, b) > targetingRange + 0.125f * target.Enemy€.Scale) { … } 

Sincronizamos a física


Tudo parece estar funcionando bem, mas torres que podem mirar no centro do campo são capazes de capturar alvos que devem estar fora de alcance. Eles não serão capazes de rastrear essas metas; portanto, elas são fixadas nelas apenas por um quadro.


Meta incorreta.

Isso acontece porque o estado do mecanismo físico é imperfeitamente sincronizado com o estado do jogo. Instâncias de todos os inimigos são criados na origem do mundo, que coincide com o centro do campo. Então, nós os movemos para o ponto de criação, mas o mecanismo de física não sabe sobre isso imediatamente.

Você pode ativar a sincronização instantânea que ocorre quando você altera as transformações de objeto, definindo Physics.autoSyncTransforms como true . Mas, por padrão, está desativado, porque é muito mais eficiente sincronizar tudo junto e, se necessário. No nosso caso, a sincronização é necessária apenas ao atualizar o estado das torres. Podemos executá-lo chamando Physics.SyncTransforms entre atualizações de inimigos e de campo em Game.Update .

  void Update () { … enemies.GameUpdate(); Physics.SyncTransforms(); board.GameUpdate(); } 

Ignore a altura


De fato, nossa jogabilidade ocorre em 2D. Então, vamos mudar a Tower para que, ao mirar e rastrear, leve em consideração apenas as coordenadas X e Z. O mecanismo físico funciona no espaço 3D, mas, em essência, podemos executar o AcquireTarget em 2D: estique a esfera para cima para cobrir todos os colididores, independentemente da posição vertical. Isso pode ser feito usando uma cápsula em vez de uma esfera, cujo segundo ponto será várias unidades acima do solo (por exemplo, três).

  bool AcquireTarget () { Vector3 a = transform.localPosition; Vector3 b = a; by += 3f; Collider[] targets = Physics.OverlapCapsule( a, b, targetingRange, enemyLayerMask ); … } 

Não é possível usar um mecanismo 2D físico?
, XZ, 2D- XY. , , 2D- . 3D-.

Também é necessário mudar TrackTarget. Obviamente, podemos usar vetores 2D e Vector2.Distance, mas vamos fazer os cálculos nós mesmos e, em vez disso, compararemos os quadrados das distâncias, isso será suficiente. Então nos livramos da operação de calcular a raiz quadrada.

  bool TrackTarget () { if (target == null) { return false; } Vector3 a = transform.localPosition; Vector3 b = target.Position; float x = ax - bx; float z = az - bz; float r = targetingRange + 0.125f * target.Enemy€.Scale; if (x * x + z * z > r * r) { target = null; return false; } return true; } 

Como esses cálculos matemáticos funcionam?
2D- , . , . , , .

Evite alocação de memória


A desvantagem de usá-lo Physics.OverlapCapsuleé que, para cada chamada, ele aloca uma nova matriz. Isso pode ser evitado alocando a matriz uma vez e chamando um método alternativo OverlapCapsuleNonAlloccom a matriz como argumento adicional. O comprimento da matriz transmitida determina o número de resultados. Todos os destinos em potencial fora da matriz são descartados. Mesmo assim, usaremos apenas o primeiro elemento, portanto, uma matriz de comprimento 1. é suficiente para nós.

Em vez de uma matriz, ele OverlapCapsuleNonAllocretorna o número de colisões que ocorreram, até o máximo permitido, e este é o número que verificaremos em vez do comprimento da matriz.

  static Collider[] targetsBuffer = new Collider[1]; … bool AcquireTarget () { Vector3 a = transform.localPosition; Vector3 b = a; by += 2f; int hits = Physics.OverlapCapsuleNonAlloc( a, b, targetingRange, targetsBuffer, enemyLayerMask ); if (hits > 0) { target = targetsBuffer[0].GetComponent<TargetPoint>(); Debug.Assert(target != null, "Targeted non-enemy!", targetsBuffer[0]); return true; } target = null; return false; } 

Atiramos nos inimigos


Agora que temos um objetivo real, é hora de atingi-lo. O disparo inclui apontar, um tiro a laser e causar dano.

Torre de mira


Para direcionar a torre para o alvo, a classe Towerprecisa ter um link para o componente da Transformtorre. Adicione um campo de configuração para isso e conecte-o ao prefab da torre.

  [SerializeField] Transform turret = default; 


A torre anexada.

Se GameUpdatehouver um alvo real, devemos atirar nele. Coloque o código de disparo em um método separado. Faça-o girar a torre em direção ao alvo, chamando seu método Transform.LookAtcom o ponto de mira como argumento.

  public override void GameUpdate () { if (TrackTarget() || AcquireTarget()) { //Debug.Log("Locked on target!"); Shoot(); } } void Shoot () { Vector3 point = target.Position; turret.LookAt(point); } 


Apenas apontando.

Filmamos um laser


Para posicionar o raio laser, a classe Towertambém precisa de um link para ele.

  [SerializeField] Transform turret = default, laserBeam = default; 


Nós conectamos um raio laser.

Para transformar um cubo em um raio laser real, você precisa executar três etapas. Primeiramente, sua orientação deve corresponder à orientação da torre. Isso pode ser feito copiando sua rotação.

  void Shoot () { Vector3 point = target.Position; turret.LookAt(point); laserBeam.localRotation = turret.localRotation; } 

Em segundo lugar, escalamos o feixe de laser para que seu comprimento seja igual à distância entre o ponto de origem local da torre e o ponto de mira. Nós escalamos ao longo do eixo Z, ou seja, o eixo local direcionado para o alvo. Para preservar a escala XY original, anotamos a escala original quando acordamos a torre Desperta.

  Vector3 laserBeamScale; void Awake () { laserBeamScale = laserBeam.localScale; } … void Shoot () { Vector3 point = target.Position; turret.LookAt(point); laserBeam.localRotation = turret.localRotation; float d = Vector3.Distance(turret.position, point); laserBeamScale.z = d; laserBeam.localScale = laserBeamScale; } 

Em terceiro lugar, colocamos o raio laser no meio entre a torre e o ponto de mira.

  laserBeam.localScale = laserBeamScale; laserBeam.localPosition = turret.localPosition + 0.5f * d * laserBeam.forward; 


Tiro a laser.

Não é possível transformar um raio laser em filho de uma torre?
, , forward. , . .

Isso funciona enquanto a torre está fixa no alvo. Mas quando não há alvo, o laser permanece ativo. Podemos desligar a tela do laser GameUpdateajustando sua escala para 0.

  public override void GameUpdate () { if (TrackTarget() || AcquireTarget()) { Shoot(); } else { laserBeam.localScale = Vector3.zero; } } 


Torres inativas não disparam.

Saúde do inimigo


Até agora, nossos raios laser apenas tocam os inimigos e não os afetam mais. É necessário garantir que o laser danifique os inimigos. Não queremos destruir os inimigos instantaneamente, por isso daremos a Enemypropriedade da saúde. Você pode escolher qualquer valor como saúde, então vamos usar 100. Mas será mais lógico que inimigos grandes tenham mais saúde, portanto, apresentaremos um coeficiente para isso.

  float Health { get; set; } … public void Initialize (float scale, float speed, float pathOffset) { … Health = 100f * scale; } 

Para adicionar suporte para causar dano, adicione um método público ApplyDamageque subtraia seu parâmetro da saúde. Assumiremos que o dano não é negativo; portanto, adicionamos uma declaração sobre isso.

  public void ApplyDamage (float damage) { Debug.Assert(damage >= 0f, "Negative damage applied."); Health -= damage; } 

Não nos livraremos instantaneamente do inimigo assim que sua saúde chegar a zero. A verificação de exaustão da saúde e destruição do inimigo será realizada no início GameUpdate.

  public bool GameUpdate () { if (Health <= 0f) { OriginFactory.Reclaim(this); return false; } … } 

Graças a isso, todas as torres dispararão essencialmente simultaneamente, e não por sua vez, o que lhes permitirá mudar para outros alvos se a torre anterior destruir o inimigo, para o qual também miraram.

Dano por segundo


Agora precisamos determinar quanto dano o laser causará. Para fazer isso, adicione ao Towercampo de configuração. Como o raio laser causa dano contínuo, nós o expressaremos como dano por segundo. Nós o Shootaplicamos ao componente de Enemydestino com multiplicação pelo tempo delta.

  [SerializeField, Range(1f, 100f)] float damagePerSecond = 10f; … void Shoot () { … target.Enemy.ApplyDamage(damagePerSecond * Time.deltaTime); } 

inspetor


O dano de cada torre é de 20 unidades por segundo.

Meta aleatória


Como sempre escolhemos o primeiro alvo disponível, o comportamento da mira depende da ordem em que o mecanismo físico verifica os coletores que se cruzam. Essa dependência não é muito boa, porque não conhecemos os detalhes, não podemos controlá-la; além disso, parecerá estranha e inconsistente. Muitas vezes, esse comportamento leva ao fogo concentrado, mas nem sempre é esse o caso.

Em vez de confiar inteiramente no mecanismo de física, vamos adicionar alguma aleatoriedade. Isso pode ser feito aumentando o número de interseções recebidas pelos coletores, por exemplo, até 100. Talvez isso não seja suficiente para obter todos os alvos possíveis em um campo densamente cheio de inimigos, mas isso será suficiente para melhorar a pontaria.

  static Collider[] targetsBuffer = new Collider[100]; 

Agora, em vez de escolher o primeiro alvo em potencial, selecionaremos um elemento aleatório da matriz.

  bool AcquireTarget () { … if (hits > 0) { target = targetsBuffer[Random.Range(0, hits)].GetComponent<TargetPoint>(); … } target = null; return false; } 


Meta aleatória.

Outros critérios para a escolha de metas podem ser usados?
, , . , , . . .

Então, em nosso jogo de defesa de torre, finalmente surgiram torres. Na próxima parte, o jogo terá sua forma final ainda mais.

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


All Articles