[
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.
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.
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
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.
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);
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 OverlapCapsuleNonAlloc
com 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 OverlapCapsuleNonAlloc
retorna 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 Tower
precisa ter um link para o componente da Transform
torre. Adicione um campo de configuração para isso e conecte-o ao prefab da torre. [SerializeField] Transform turret = default;
A torre anexada.Se GameUpdate
houver 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.LookAt
com o ponto de mira como argumento. public override void GameUpdate () { if (TrackTarget() || AcquireTarget()) {
Apenas apontando.Filmamos um laser
Para posicionar o raio laser, a classe Tower
també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 GameUpdate
ajustando 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 Enemy
propriedade 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 ApplyDamage
que 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 Tower
campo de configuração. Como o raio laser causa dano contínuo, nós o expressaremos como dano por segundo. Nós o Shoot
aplicamos ao componente de Enemy
destino com multiplicação pelo tempo delta. [SerializeField, Range(1f, 100f)] float damagePerSecond = 10f; … void Shoot () { … target.Enemy.ApplyDamage(damagePerSecond * Time.deltaTime); }
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.