Creación de defensa de la torre en la unidad: torres y enemigos que disparan

[ La primera y segunda parte del tutorial]

  • Lo colocamos en el campo de la torre.
  • Apuntamos a los enemigos con la ayuda de la física.
  • Los rastreamos mientras es posible.
  • Les disparamos con un rayo láser.

Esta es la tercera parte de una serie de tutoriales sobre cómo crear un género de defensa de torre simple. Describe la creación de torres, apuntando y disparando a los enemigos.

El tutorial fue creado en Unity 2018.3.0f2.


Vamos a calentar a los enemigos.

Creación de la torre


Los muros solo ralentizan a los enemigos, lo que aumenta la longitud del camino que deben recorrer. Pero el objetivo del juego es destruir a los enemigos antes de que lleguen al punto final. Este problema se resuelve colocando torres en el campo que les disparará.

Contenido del azulejo


Las torres son otro tipo de contenido de mosaico, así GameTileContent agreguemos una entrada para ellas en GameTileContent .

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

En este tutorial, solo GameTileContentFactory un tipo de torre, que se puede implementar al proporcionar GameTileContentFactory un enlace al prefabricado de la torre, una instancia de la cual también se puede crear a través de Get .

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

Pero las torres deben disparar, por lo que su condición deberá actualizarse y necesitarán su propio código. Crea una clase Tower para este propósito que amplíe la clase GameTileContent .

 using UnityEngine; public class Tower : GameTileContent {} 

Puede hacer que la torre prefabricada tenga su propio componente cambiando el tipo de campo de fábrica a Tower . Dado que la clase todavía se considera un GameTileContent , no es necesario cambiar nada más.

  Tower towerPrefab = default; 

Prefabricados


Crea una casa prefabricada para la torre. Puede comenzar duplicando el prefabricado de pared y reemplazando su componente GameTileContent con el componente Tower , y luego cambiar su tipo a Torre . Para hacer que la torre se ajuste a las paredes, guarde el cubo de la pared como la base de la torre. Luego coloca otro cubo encima. Le di una escala de 0.5. Pon otro cubo sobre él, indicando una torreta, esta parte apuntará y disparará a los enemigos.



Tres cubos formando una torre.

La torre girará, y dado que tiene un colisionador, será rastreada por un motor físico. Pero no necesitamos ser tan precisos, porque usamos colisionadores de torre solo para seleccionar celdas. Esto se puede hacer aproximadamente. Retire el colisionador del cubo de la torreta y cambie el colisionador del cubo de la torre para que cubra ambos cubos.



Torre del cubo colisionador.

La torre disparará un rayo láser. Se puede visualizar de muchas maneras, pero solo usamos un cubo translúcido, que estiraremos para formar una viga. Cada torre debe tener su propio haz, así que agréguelo a la torre prefabricada. Colóquelo dentro de la torreta para que quede oculto de manera predeterminada y déle una escala más pequeña, por ejemplo 0.2. Hagámoslo hijo de la raíz prefabricada, no del cubo de la torreta.

rayo láser

jerarquía

Cubo oculto de un rayo láser.

Cree un material adecuado para el rayo láser. Simplemente utilicé el material negro translúcido estándar y apagué todos los reflejos, y también le di un color rojo emitido.

color

sin reflejos

El material del rayo láser.

Verifique que el rayo láser no tenga un colisionador, y también apague su yeso y sombra.


El rayo láser no interactúa con las sombras.

Una vez completada la creación de la torre prefabricada, la agregaremos a la fábrica.


Fábrica con una torre.

Colocación de la torre


Agregaremos y eliminaremos torres usando otro método de cambio. Simplemente puede duplicar GameBoard.ToggleWall cambiando el nombre del método y el tipo de contenido.

  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(); } } } 

En Game.HandleTouch , al mantener presionada la tecla Mayús se cambiarán las torres en lugar de las paredes.

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


Torres en el campo.

Bloqueo de ruta


Hasta ahora, solo las paredes pueden bloquear la búsqueda de un camino, por lo que los enemigos se mueven a través de las torres. GameTileContent propiedad auxiliar a GameTileContent que indica si el contenido bloquea la ruta. El camino está bloqueado si es un muro o una torre.

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

Use esta propiedad en GameTile.GrowPathTo en lugar de verificar el tipo de contenido.

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


Ahora el camino está bloqueado por muros y torres.

Reemplazar las paredes


Lo más probable es que el jugador a menudo reemplace las paredes con torres. Será inconveniente para él quitar la pared primero, y además, los enemigos pueden penetrar en este espacio temporalmente aparecido. Puede implementar un reemplazo directo forzando GameBoard.ToggleTower para verificar si la pared está actualmente en el mosaico. Si es así, reemplácelo inmediatamente con una torre. En este caso, no tenemos que buscar otras formas, porque el mosaico todavía las bloquea.

  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); } } 

Apuntamos a los enemigos.


Una torre puede cumplir su tarea solo cuando encuentra un enemigo. Después de encontrar al enemigo, debe decidir a qué parte apuntar.

Punto de puntería


Para detectar objetivos, utilizaremos el motor de física. Como en el caso del colisionador de la torre, no necesitamos que el colisionador enemigo coincida necesariamente con su forma. Puedes elegir el colisionador más simple, es decir, una esfera. Después de detectar al enemigo, utilizaremos la posición del objeto del juego con el colisionador adjunto como un punto para apuntar.

No podemos unir el colisionador al objeto raíz del enemigo, porque no siempre coincide con la posición del modelo y hará que la torre apunte al suelo. Es decir, debe colocar el colisionador en algún lugar del modelo. El motor de física nos dará un enlace a este objeto, que podemos usar para apuntar, pero aún necesitamos acceso al componente Enemy del objeto raíz. Para simplificar la tarea, TargetPoint componente TargetPoint . Vamos a darle una propiedad para asignación privada y recibo público del componente Enemy , y otra propiedad para obtener su posición en el mundo.

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

Vamos a darle un método Awake que configura un enlace a su componente Enemy . Vaya directamente al objeto raíz usando transform.root . Si el componente Enemy no existe, cometimos un error al crear el enemigo, así que agreguemos una declaración para esto.

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

Además, el colisionador debe estar conectado al mismo objeto del juego al que TargetPoint conectado TargetPoint .

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

Agrega un componente y un colisionador al cubo prefabricado del enemigo. Esto hará que las torres apunten al centro del cubo. Utilizamos un colisionador esférico con un radio de 0.25. El cubo tiene una escala de 0.5, por lo que el radio verdadero del colisionador será 0.125. Gracias a esto, el enemigo tendrá que cruzar visualmente el círculo de alcance de la torre, y solo después de un tiempo se convierte en el objetivo real. El tamaño del colisionador también se ve afectado por la escala aleatoria del enemigo, por lo que su tamaño en el juego también variará ligeramente.


inspector

Un enemigo con un punto de puntería y un colisionador en un cubo.

Capa enemiga


Las torres solo se preocupan por los enemigos y no apuntan a nada más, por lo que colocaremos a todos los enemigos en una capa separada. Usaremos la capa 9. Cambie su nombre a Enemigo en la ventana Capas y etiquetas , que se puede abrir a través de la opción Editar capas en el menú desplegable Capas en la esquina superior derecha del editor.


La capa 9 se usará para los enemigos.

Esta capa es necesaria solo para el reconocimiento de los enemigos, y no para las interacciones físicas. Vamos a señalarlo deshabilitándolos en la Matriz de colisión de capas , que se encuentra en el panel Física de los parámetros del proyecto.


Matriz de colisiones de capas.

Asegúrese de que el objeto del juego del punto de puntería esté en la capa deseada. El resto de la casa prefabricada del enemigo puede estar en otras capas, pero será más fácil coordinar todo y colocar toda la casa prefabricada en la capa Enemiga . Si cambia la capa del objeto raíz, se le pedirá que cambie la capa para todos sus objetos secundarios.


Enemigo en la capa derecha.

TargetPoint la afirmación de que TargetPoint realmente en la capa correcta.

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

Además, las acciones del jugador deben ser ignoradas por los colisionadores enemigos. Esto se puede lograr agregando un argumento de máscara de capa a Physics.Raycast en GameBoard.GetTile . Este método tiene una forma que toma la distancia al haz y la máscara de capa como argumentos adicionales. Le daremos la distancia máxima y la máscara de capa por defecto, es decir, 1.

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

¿No debería ser la máscara de capa 0?
El índice de capa predeterminado es cero, pero pasamos la máscara de capa. La máscara cambia los bits individuales de un entero a 1 si la capa necesita ser activada. En este caso, debe establecer solo el primer bit, es decir, el menos significativo, lo que significa 2 0 , que equivale a 1.

Actualización del contenido del mosaico


Las torres pueden realizar su tarea solo cuando se actualiza su estado. Lo mismo se aplica al contenido de todos los mosaicos, aunque el resto de los contenidos no hacen nada hasta ahora. Por lo tanto, agregue un método virtual GameTileContent a GameUpdate , que no hace nada por defecto.

  public virtual void GameUpdate () {} 

Hagamos que Tower redefina, incluso si por ahora simplemente muestra en la consola que está buscando un objetivo.

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

GameBoard ocupa de los mosaicos y sus contenidos, por lo que también realizará un seguimiento de qué contenido debe actualizarse. Para hacer esto, agregue la lista y el método público GameUpdate , que actualiza todo en la lista.

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

En nuestro tutorial solo necesita actualizar las torres. Cambie ToggleTower para que agregue y elimine contenido si es necesario. Si también se necesita otro contenido, necesitaremos un enfoque más general, pero por ahora, esto es 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 que esto funcione, ahora es suficiente para nosotros simplemente actualizar el campo en Game.Update . Actualizaremos el campo después de los enemigos. Gracias a esto, las torres podrán apuntar exactamente donde están los enemigos. Si hiciéramos lo contrario, las torres apuntarían donde estaban los enemigos en el último cuadro.

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

Rango de puntería


Las torres tienen un radio de puntería limitado. Hagámoslo personalizado agregando un campo a la clase Tower . La distancia se mide desde el centro del mosaico de la torre, por lo que en un rango de 0.5 solo cubrirá su propio mosaico. Por lo tanto, un rango mínimo y estándar razonable sería 1.5, cubriendo la mayoría de las losetas vecinas.

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


Rango de puntería 2.5.

Visualicemos el rango con gizmo. No necesitamos verlo constantemente, por lo tanto crearemos el método OnDrawGizmosSelected llamado solo para los objetos seleccionados. Dibujamos el marco amarillo de la esfera con un radio igual a la distancia y centrado en relación con la torre. Colóquelo ligeramente sobre el suelo para que siempre sea claramente visible.

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


Gizmo rango de puntería.

Ahora podemos ver cuál de los enemigos es un objetivo asequible para cada una de las torres. Pero elegir torres en la ventana de escena es inconveniente, porque tenemos que seleccionar uno de los cubos secundarios y luego cambiar al objeto raíz de la torre. Otros tipos de contenido de mosaico también sufren el mismo problema. Podemos forzar la selección de la raíz del contenido del GameTileContent en la ventana de escena agregando el atributo SelectionBase al GameTileContent .

 [SelectionBase] public class GameTileContent : MonoBehaviour { … } 

Captura de objetivos


Agregue un campo TargetPoint a la clase Tower para que pueda rastrear su objetivo capturado. Luego GameUpdate para llamar al nuevo método AquireTarget , que devuelve información sobre si encontró el objetivo. Tras la detección, mostrará un mensaje en la consola.

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

En AcquireTarget obtenemos todos los objetivos disponibles llamando a Physics.OverlapSphere con una posición de torre y rango como argumentos. El resultado será una matriz Collider contiene todos los colisionadores en contacto con la esfera. Si la longitud de la matriz es positiva, entonces hay al menos un punto de puntería, y simplemente seleccionamos el primero. Tome su componente TargetPoint , que siempre debe existir, TargetPoint al campo objetivo e informe el éxito. De lo contrario, despejamos el objetivo e informamos el fallo.

  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; } 

Tenemos la garantía de obtener los puntos de puntería correctos, si tenemos en cuenta los colisionadores solo en la capa de enemigos. Esta es la capa 9, por lo que pasaremos la máscara de capa correspondiente.

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

¿Cómo funciona esta máscara de bits?
Como la capa enemiga tiene un índice de 9, el décimo bit de la máscara de bits debe tener el valor 1. Esto corresponde a un número entero 2 9 , que es 512. Pero dicho registro de máscara de bits no es intuitivo. También podemos escribir un literal binario, por ejemplo 0b10_0000_0000 , pero luego tenemos que contar ceros. En este caso, la entrada más conveniente sería usar el operador de desplazamiento a la izquierda << , que desplaza los bits a la izquierda. que corresponde a un número en el poder de dos.

Puede visualizar el objetivo capturado dibujando una línea de artilugio entre las posiciones de la torre y el objetivo.

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


Visualización de objetivos.

¿Por qué no utilizar métodos como OnTriggerEnter?
La ventaja de verificar manualmente los objetivos transversales es que solo podemos hacer esto cuando sea necesario. No hay razón para buscar objetivos si la torre ya tiene uno. Además, al obtener todos los objetivos potenciales a la vez, no tenemos que procesar una lista de objetivos potenciales para cada torre, que cambia constantemente.

Bloqueo de objetivo


El objetivo elegido para capturar depende del orden en que están representados por el motor físico, es decir, de hecho, es arbitrario. Por lo tanto, parecerá que el objetivo capturado está cambiando sin ningún motivo. Después de que la torre recibe el objetivo, es más lógico que rastree a su objetivo y no cambie a otro. Agregue un método TrackTarget que implemente dicho seguimiento y devuelva información sobre si fue exitoso. Primero, le informaremos si se captura el objetivo.

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

Llamaremos a este método en GameUpdate y solo cuando GameUpdate falso llamaremos a AcquireTarget . Si el método devuelve verdadero, entonces tenemos un objetivo. Esto se puede hacer colocando ambas llamadas de método en una comprobación de if con el operador OR, porque si el primer operando devuelve true , el segundo no se comprobará y se perderá la llamada. El operador AND actúa de manera similar.

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


Seguimiento de objetivos.

Como resultado, las torres se fijan en el objetivo hasta que alcanza el punto final y se destruye. Si usa enemigos repetidamente, entonces debe verificar la corrección del enlace, como se hace con los enlaces a las figuras procesadas en una serie de tutoriales de Gestión de Objetos .

Para rastrear objetivos solo cuando están dentro del alcance, TrackTarget debe rastrear la distancia entre la torre y el objetivo. Si excede el valor del rango, entonces el objetivo debe restablecerse y devolver falso. Puede usar el método Vector3.Distance para esta verificación.

  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; } 

Sin embargo, este código no tiene en cuenta el radio del colisionador. Por lo tanto, como resultado, la torre puede perder el objetivo, luego capturarlo nuevamente, solo para dejar de seguirlo en el siguiente cuadro, y así sucesivamente. Podemos evitar esto agregando un radio de colisionador al rango.

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

Esto nos da los resultados correctos, pero solo si la escala del enemigo no cambia. Como le damos a cada enemigo una escala aleatoria, debemos tenerla en cuenta al cambiar el rango. Para hacer esto, necesitamos recordar la escala dada por Enemy y abrirla usando la propiedad getter.

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

Ahora podemos verificar el rango correcto en Tower.TrackTarget .

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

Sincronizamos física


Todo parece estar funcionando bien, pero las torres que pueden apuntar al centro del campo son capaces de capturar objetivos que deberían estar fuera del alcance. No podrán seguir estos objetivos, por lo que se fijan en ellos solo durante un fotograma.


Apuntamiento incorrecto.

Esto sucede porque el estado del motor físico está imperfectamente sincronizado con el estado del juego. Las instancias de todos los enemigos se crean en el origen del mundo, que coincide con el centro del campo. Luego los movemos al punto de creación, pero el motor de física no lo sabe de inmediato.

Puede habilitar la sincronización instantánea que ocurre cuando cambia las transformaciones de objetos estableciendo Physics.autoSyncTransforms en true . Pero por defecto está deshabilitado, porque es mucho más eficiente sincronizar todo junto y si es necesario. En nuestro caso, la sincronización se requiere solo cuando se actualiza el estado de las torres. Podemos ejecutarlo llamando a Physics.SyncTransforms entre actualizaciones enemigas y de campo en Game.Update .

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

Ignora la altura


De hecho, nuestra jugabilidad tiene lugar en 2D. Por lo tanto, cambiemos la Tower para que, al apuntar y seguir, tenga en cuenta solo las coordenadas X y Z. El motor físico funciona en el espacio 3D, pero en esencia podemos realizar AcquireTarget en 2D: estire la esfera para que cubra todos los colisionadores, independientemente desde su posición vertical. Esto se puede hacer usando una cápsula en lugar de una esfera, cuyo segundo punto estará a varias unidades sobre el suelo (por ejemplo, tres).

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

¿No es posible usar un motor físico 2D?
, XZ, 2D- XY. , , 2D- . 3D-.

También es necesario cambiar TrackTarget. Por supuesto, podemos usar vectores 2D y Vector2.Distance, pero hagamos los cálculos nosotros mismos y, en su lugar, compararemos los cuadrados de distancias, esto será suficiente. Entonces nos deshacemos de la operación de calcular la raíz cuadrada.

  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; } 

¿Cómo funcionan estos cálculos matemáticos?
2D- , . , . , , .

Evitar la asignación de memoria


Physics.OverlapCapsule , . , OverlapCapsuleNonAlloc . . . , 1.

OverlapCapsuleNonAlloc , , .

  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; } 


, , . , .


Para dirigir la torreta hacia el objetivo, la clase Towernecesita tener un enlace al componente de la Transformtorreta. Agregue un campo de configuración para esto y conéctelo a la torre prefabricada.

  [SerializeField] Transform turret = default; 


La torreta adjunta.

Si GameUpdatehay un objetivo real, entonces debemos dispararlo. Ponga el código de disparo en un método separado. Hazle girar la torreta hacia el objetivo, llamando a su método Transform.LookAtcon el punto de puntería como argumento.

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


Solo apuntando.

Disparamos un laser


Para posicionar el rayo láser, la clase Towertambién necesita un enlace.

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


Conectamos un rayo láser.

Para convertir un cubo en un rayo láser real, debe seguir tres pasos. En primer lugar, su orientación debe corresponder a la orientación de la torreta. Esto se puede hacer copiando su rotación.

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

- , . Z, , . XY, (Awake) .

  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; } 

-, .

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


.

?
, , forward. , . .

, . , . , GameUpdate 0.

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


.


. , . , Enemy . , 100. , , .

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

, ApplyDamage , . , , .

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

, . GameUpdate .

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

, , , , .


, . Tower . , (damage per second). Shoot Enemy .

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

inspector


— 20 .


, , . , , , . , .

, , . , , 100. , , .

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

Ahora, en lugar de elegir el primer objetivo potencial, seleccionaremos un elemento aleatorio de la matriz.

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


Apuntando al azar.

¿Se pueden utilizar otros criterios para elegir objetivos?
, , . , , . . .

Entonces, en nuestro juego de defensa de la torre, las torres finalmente han aparecido. En la siguiente parte, el juego tomará su forma final aún más.

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


All Articles