Hacer de Tower Defense un juego de unidad - Parte 2

imagen

Esta es la segunda parte del tutorial "Creación de un juego de Tower Defense en Unity" . Estamos creando un juego de género de torre de defensa en Unity, y al final de la primera parte , aprendimos cómo colocar y actualizar monstruos. También tenemos un enemigo atacando cookies.

Sin embargo, ¡el enemigo aún no sabe dónde mirar! Además, un ataque solo parece extraño. En esta parte del tutorial, agregaremos oleadas de enemigos y monstruos armados para que puedan defender una galleta preciosa.

Llegar al trabajo


Abra el proyecto en Unity, que detuvimos en la última parte. Si se unió a nosotros en este momento, descargue el proyecto borrador y abra TowerDefense-Part2-Starter .

Abre GameScene desde la carpeta Scenes .

Convertir enemigos


Al final del tutorial anterior, el enemigo aprendió a moverse por el camino, pero parece que no tiene idea de dónde mirar.

Abra el script MoveEnemy.cs en el IDE y agregue el siguiente método para solucionar la situación.

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 al enemigo para que siempre RotateIntoMoveDirection hacia adelante. Lo hace de la siguiente manera:

  1. Calcula la dirección actual del error, restando la posición del waypoint actual de la posición del siguiente punto.
  2. Utiliza Mathf.Atan2 para determinar el ángulo en radianes hacia donde newDirection dirige newDirection (el punto cero está a la derecha). Multiplica el resultado por 180 / Mathf.PI , convirtiendo el ángulo en grados.
  3. Finalmente, obtiene el elemento secundario Sprite y gira los grados de rotationAngle del eje. Observe que rotamos al niño , no al padre, de modo que la tira de energía que agreguemos más tarde permanezca horizontal.

En Update() , reemplace el comentario // TODO: próxima llamada a RotateIntoMoveDirection :

 RotateIntoMoveDirection(); 

Guarde el archivo y regrese a Unity. Corre la escena; ahora el enemigo sabe a dónde se está moviendo.


Ahora el error sabe a dónde va.

El único enemigo no se ve muy impresionante. ¡Necesitamos hordas! ¡Y como en cualquier juego de defensa de torres, las hordas corren en oleadas!

Informar al jugador


Antes de comenzar a mover las hordas, debemos advertir al jugador sobre la inminente batalla. Además, vale la pena mostrar el número de onda actual en la parte superior de la pantalla.

Varios GameObjects requieren la información de la onda , por lo que la agregaremos al componente GameManagerBehavior del GameManager .

Abra GameManagerBehavior.cs en el IDE y agregue las siguientes dos variables:

 public Text waveLabel; public GameObject[] nextWaveLabels; 

waveLabel almacena un enlace a la etiqueta de salida del número de onda en la esquina superior derecha de la pantalla. nextWaveLabels almacena dos GameObjects que crean una combinación de animación que mostraremos al comienzo de una nueva ola:


Guarde el archivo y regrese a Unity. Seleccione GameManager en la Jerarquía . Haga clic en el círculo a la derecha de la etiqueta Wave y en el cuadro de diálogo Seleccionar texto , seleccione WaveLabel en la pestaña Escena .

Ahora configure el Tamaño para las etiquetas de la próxima ola en 2 . Ahora configure el Elemento 0 en NextWaveBottomLabel , y para el Elemento 1 NextWaveTopLabel es lo mismo que hicimos con Wave Label.


Así es como debería verse ahora el comportamiento de Game Manager

Si el jugador pierde, entonces no debería ver un mensaje sobre la próxima ola. Para manejar esta situación, regrese a GameManagerBehavior.cs y agregue otra variable:

 public bool gameOver = false; 

En gameOver almacenaremos el valor de si el jugador perdió.

Aquí nuevamente usamos la propiedad para sincronizar los elementos del juego con la ola actual. Agregue el siguiente código a 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); } } 

Crear una variable privada, propiedad y getter ya debería ser algo familiar para usted. Pero con el setter, nuevamente, todo es un poco más interesante.

Asignamos a wave nuevo value .

Luego verificamos si el juego ha terminado. De lo contrario, recorra todas las etiquetas nextWaveLabels ; estas etiquetas tienen un componente Animator . Para habilitar la animación Animator , definimos un activador nextWave .

Finalmente, configuramos el text de waveLabel para wave + 1 . ¿Por qué +1 ? La gente común no comienza a contar desde cero (sí, esto es extraño).

En Start() establecemos el valor de esta propiedad:

 Wave = 0; 

Comenzamos el conteo con el número 0 Wave .

Guarde el archivo y ejecute la escena en Unity. La etiqueta Wave mostrará correctamente 1.


Para un jugador, todo comienza con la ola 1.

Olas: crea montones de enemigos


Puede parecer obvio, pero para atacar con una horda es necesario crear más enemigos, aunque no sabemos cómo hacerlo. Además, no debemos crear la próxima ola hasta que se destruya la actual.

Es decir, el juego debería poder reconocer la presencia de enemigos en la escena, y las etiquetas son una buena manera de identificar los objetos del juego aquí.

Etiquetado enemigo


Selecciona el enemigo prefabricado en el Navegador de proyectos. En la parte superior del Inspector, haga clic en la lista desplegable Etiqueta y seleccione Agregar etiqueta .


Crea una etiqueta llamada Enemy .


Selecciona el enemigo prefabricado. En el Inspector, establece la etiqueta de Enemigo para ello.

Definiendo oleadas de enemigos


Ahora necesitamos establecer la ola de enemigos. Abra SpawnEnemy.cs en el IDE y agregue la siguiente implementación de clase antes de SpawnEnemy :

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

Wave contiene enemyPrefab : la base para crear instancias de todos los enemigos en esta ola, spawnInterval : tiempo entre enemigos en la ola en segundos y maxEnemies : la cantidad de enemigos creados en esta ola.

La clase es serializable , es decir, podemos cambiar sus valores en el Inspector.

Agregue las siguientes variables a la clase SpawnEnemy :

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

Aquí establecemos las variables para generar enemigos, que es muy similar a cómo movimos enemigos entre puntos en la ruta.

Ponemos las olas de los enemigos individuales en waves y hacemos un seguimiento de la cantidad de enemigos creados y el tiempo en que se crearon en enemiesSpawned y lastSpawnTime .

Después de todas estas muertes, los jugadores necesitan tiempo para respirar, así que establece timeBetweenWaves en 5 segundos.

Reemplace el contenido de Start() siguiente código.

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

Aquí asignamos a lastSpawnTime valor de la hora actual, es decir, la hora en que se inició el script después de cargar la escena. Luego obtenemos el GameManagerBehavior ya familiar.

Agregue el siguiente código a 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); } 

Analicémoslo paso a paso:

  1. Obtenemos el índice de la onda actual y verificamos si es la última.
  2. Si es así, calculamos el tiempo transcurrido después del engendro enemigo anterior y verificamos si es hora de crear un enemigo. Aquí tenemos en cuenta dos casos. Si este es el primer enemigo en la ola, verificamos si timeInterval es timeInterval que timeBetweenWaves . De lo contrario, verificamos si timeInterval es timeInterval que spawnInterval ondas. En cualquier caso, verificamos que no hemos creado todos los enemigos en esta ola.
  3. Si es necesario, genera al enemigo, creando una instancia de enemyPrefab . También aumenta el valor de los enemiesSpawned .
  4. Comprueba el número de enemigos en la pantalla. Si no están allí, y este fue el último enemigo en la ola, entonces creamos la próxima ola. También al final de la ola, le damos al jugador el 10 por ciento de todo el oro restante.
  5. Después de derrotar a la última ola, aquí se juega una animación de victoria en el juego.

Establecer intervalos de generación


Guarde el archivo y regrese a Unity. Seleccione el objeto Carretera en la Jerarquía . En el Inspector, establezca el tamaño del objeto Ondas en 4 .

Por ahora, selecciona un objeto Enemy para los cuatro elementos como Enemy Prefab . Configure los campos Spawn Interval y Max Enemies de la siguiente manera:

  • Elemento 0 : Intervalo de generación: 2.5 , Enemigos máximos: 5
  • Elemento 1 : Intervalo de generación: 2 , Enemigos máximos: 10
  • Elemento 2 : Intervalo de generación: 2 , Enemigos máximos: 15
  • Elemento 3 : Intervalo de generación: 1 , Enemigos máximos: 5

El esquema final debería verse así:


Por supuesto, puede experimentar con estos valores para aumentar o disminuir la complejidad.

Lanza el juego. Si! ¡Los escarabajos han comenzado el viaje hacia tu galleta!

bichos

Tarea adicional: agrega diferentes tipos de enemigos


Ningún juego de defensa de la torre puede considerarse completo con un solo tipo de enemigo. Afortunadamente, también hay Enemy2 en la carpeta Prefabs .

En el Inspector, seleccione Prefabs \ Enemy2 y agregue el script MoveEnemy . Establezca Velocidad en 3 y establezca la etiqueta Enemigo . ¡Ahora puedes usar este enemigo rápido para que el jugador no se relaje!

Actualización de la vida del jugador


A pesar de que hordas de enemigos atacan la galleta, el jugador no sufre daño. Pero pronto lo arreglaremos. El jugador debe sufrir si permite que el enemigo se acerque sigilosamente.

Abra GameManagerBehavior.cs en el IDE y agregue las siguientes dos variables:

 public Text healthLabel; public GameObject[] healthIndicator; 

Usamos healthLabel para acceder al valor de vida del jugador, y healthIndicator para acceder a los cinco pequeños monstruos verdes que mastican galletas: simplemente simbolizan la salud del jugador; Es más divertido que un indicador de salud estándar.

Gestión de la salud


Ahora agrega una propiedad que almacena la salud del jugador en 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); } } } } 

Así es como gestionamos la salud del jugador. Y nuevamente, la parte principal del código se encuentra en el setter:

  1. Si reducimos la salud del jugador, usamos el componente CameraShake para crear un hermoso efecto de sacudida. Este script está incluido en el proyecto descargable y no lo consideraremos aquí.
  2. Actualizamos la variable privada y la etiqueta de estado en la esquina superior izquierda de la pantalla.
  3. Si la salud se ha reducido a 0 y el final del juego aún no ha llegado, gameOver en true e inicie la animación gameOver .
  4. Eliminamos uno de los monstruos de las cookies. Si los desactivamos, esta parte se puede escribir más fácilmente, pero aquí apoyamos la reincorporación en caso de que se agregue salud.

Inicializamos Health en Start() :

 Health = 5; 

Establecemos Health en 5 cuando la escena comienza a reproducirse.

Una vez hecho todo esto, ahora podemos actualizar la salud del jugador cuando el error llega a la cookie. Guarde el archivo y vaya al IDE al script MoveEnemy.cs .

Cambio de salud


Para cambiar su estado, busque el comentario en Update() con las palabras // TODO: y reemplácelo con este código:

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

Entonces obtenemos el GameManagerBehavior y restamos la unidad de su Health .

Guarde el archivo y regrese a Unity.

Seleccione un GameManager en la Jerarquía y seleccione HealthLabel para su Etiqueta de salud .

Expanda el objeto Cookie en la Jerarquía y arrastre sus cinco indicadores de salud secundarios a la matriz de indicadores de salud de GameManager : los indicadores de salud serán pequeños monstruos verdes que comen galletas.

Ejecute la escena y espere hasta que los errores lleguen a la galleta. No hagas nada hasta que pierdas.

ataque de galletas

Venganza monstruo


Monstruos en su lugar? Si ¿Atacan los enemigos? ¡Sí, y se ven amenazantes! ¡Es hora de responder a estos animales!

Para hacer esto, necesitamos lo siguiente:

  • Carril de salud para que el jugador sepa qué enemigos son fuertes y cuáles son débiles.
  • Detectar enemigos dentro del alcance de un monstruo
  • Tomar una decisión: a qué enemigo dispararle
  • Un montón de conchas

Barra de salud enemiga


Para implementar la banda de salud, utilizamos dos imágenes: una para el fondo oscuro y la segunda (la barra verde es un poco más pequeña) escalaremos de acuerdo con la salud del enemigo.

Arrastre desde el Navegador de proyectos a la escena Prefabs \ Enemy .

Luego, en la Jerarquía, arrastre y suelte Imágenes \ Objetos \ HealthBarBackground en Enemy para agregarlo como elemento secundario.

En el Inspector, establezca la Posición del HealthBarBackground en (0, 1, -4) .

Luego, en el Navegador de proyectos, seleccione Imágenes \ Objetos \ Barra de salud y asegúrese de que su Pivote esté a la izquierda . Luego agréguelo como hijo de Enemy en la Jerarquía y establezca su valor de Posición (-0.63, 1, -5) . Para X Scale, establezca el valor en 125 .

Agregue un nuevo script de C # llamado HealthBar al objeto del juego HealthBar . Más tarde lo cambiaremos para que cambie la longitud de la barra de salud.

Después de seleccionar un objeto Enemigo en la Jerarquía , asegúrese de que su posición sea (20, 0, 0) .

Haga clic en Aplicar en la parte superior del Inspector para guardar todos los cambios como parte del prefabricado. Finalmente, elimine el objeto Enemigo en la Jerarquía .


Ahora repita todos estos pasos para agregar una barra de salud para Prefabs \ Enemy2 .

Cambiar la longitud de la barra de salud


Abra el IDE HealthBar.cs y agregue las siguientes variables:

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

En maxHealth la salud máxima del enemigo, y en currentHealth , la salud restante. Finalmente, en originalScale es el tamaño inicial de la barra de salud.

Guarde el objeto originalScale en Start() :

 originalScale = gameObject.transform.localScale.x; 

Almacenamos el valor x de la propiedad localScale .

Establezca la escala de la barra de estado agregando el siguiente código a Update() :

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

Podemos copiar localScale en una variable temporal porque no podemos cambiar su valor x por separado. Luego calculamos la nueva escala x en función del estado actual del escarabajo y nuevamente asignamos el valor localScale a una variable temporal.

Guarde el archivo e inicie el juego en Unity. Sobre los enemigos verás rayas de salud.


Mientras el juego se está ejecutando, expande uno de los objetos Enemigos (Clon) en la Jerarquía y selecciona su Barra de salud secundaria. Cambie su valor de Salud actual y vea cómo cambia su barra de salud.


Detección de enemigos dentro del alcance.


Ahora nuestros monstruos necesitan descubrir a qué enemigos apuntar. Pero antes de que te des cuenta de esta oportunidad, debes preparar Monster y Enemy.

Seleccione Project Browser Prefabs \ Monster y agregue el componente Circle Collider 2D en el Inspector .

Establezca el parámetro Radio del colisionador en 2.5 ; esto indicará el radio de ataque de los monstruos.

Seleccione la casilla de verificación Is Trigger para que los objetos pasen por esta área en lugar de colisionar con ella.

Finalmente, en la parte superior del Inspector , configura la Capa del Monstruo para Ignorar Raycast . En el cuadro de diálogo, haga clic en Sí, cambiar hijos . Si no se selecciona Ignorar Raycast, el colisionador responderá a los eventos de clic del mouse. Esto será un problema porque los monstruos bloquean los eventos destinados a los objetos de Openspot debajo de ellos.


Para asegurarnos de que el enemigo sea detectado en el área de activación, debemos agregarle un colisionador y un cuerpo rígido, porque Unity envía eventos de activación solo cuando un cuerpo rígido está unido a uno de los colisionadores.

En el Navegador de proyectos, seleccione Prefabs \ Enemy . Agregue el componente 2D de Rigidbody y seleccione Cinemática para Tipo de cuerpo . Esto significa que el cuerpo no se verá afectado por la física.

Agregue Circle Collider 2D con un radio de 1 . Repita estos pasos para Prefabs \ Enemy 2 .

Los disparadores están configurados, por lo que los monstruos entenderán que los enemigos están dentro de su radio de acción.

Necesitamos preparar una cosa más: un guión que le dice a los monstruos cuando el enemigo es destruido para que no generen una excepción mientras continúan disparando.

Cree un nuevo script de C # llamado EnemyDestructionDelegate y agréguelo a los prefabricados Enemy y Enemy2 .

Abra EnemyDestructionDelegate.cs en el IDE y agregue la siguiente declaración de delegación:

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

Aquí creamos un delegate , es decir, un contenedor para una función que se puede pasar como una variable.

Nota : los delegados se usan cuando un objeto del juego debe notificar activamente los cambios a otros objetos del juego. Lea más sobre los delegados en la documentación de Unity .

Agregue el siguiente método:

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

Cuando se destruye un objeto de juego, Unity llama automáticamente a este método y verifica al delegado por desigualdad null . En nuestro caso, lo llamamos con gameObject como parámetro. Esto permite a todos los encuestados registrados como delegados saber que el enemigo está destruido.

Guarde el archivo y regrese a Unity.

Les damos a los monstruos una licencia para matar


Y ahora los monstruos pueden detectar enemigos dentro del radio de su acción. Agregue un nuevo script de C # a la prefabricación de Monster y asígnele el nombre ShootEnemies .

Abra ShootEnemies.cs en el IDE y agregue la siguiente construcción para acceder a Generics .

 using System.Collections.Generic; 

Agrega una variable para rastrear a todos los enemigos dentro del alcance:

 public List<GameObject> enemiesInRange; 

En enemigos en rango almacenaremos a todos los enemigos dentro del alcance.

Inicialice el campo en Start() .

 enemiesInRange = new List<GameObject>(); 

Al principio, no hay enemigos en el radio de acción, por lo que creamos una lista vacía.

¡Llena la lista de enemiesInRange en enemiesInRange ! Agregue el siguiente código al 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. En OnEnemyDestroy eliminamos al enemigo de los enemigos en enemiesInRange . Cuando un enemigo pisa un gatillo alrededor de un monstruo, se OnTriggerEnter2D .
  2. Luego agregamos el enemigo a la lista de enemigos enemiesInRange y agregamos el evento OnEnemyDestroy . Por lo tanto, garantizamos que tras la destrucción del enemigo se OnEnemyDestroy a OnEnemyDestroy . No queremos que los monstruos gasten municiones en enemigos muertos, ¿verdad?
  3. En OnTriggerExit2D eliminamos al enemigo de la lista y anulamos el registro del delegado. Ahora sabemos qué enemigos están al alcance.

Guarde el archivo e inicie el juego en Unity. Para asegurarte de que todo funciona, coloca el monstruo, selecciónalo y sigue los cambios en la lista de enemiesInRange en enemiesInRange en el enemiesInRange .

Selección de objetivo


Los monstruos ahora saben qué enemigo está al alcance. Pero, ¿qué harán cuando haya varios enemigos en el radio?

¡Por supuesto, atacarán al más cercano al hígado!

Abra el script MoveEnemy.cs IDE y agregue un nuevo método que calcule este monstruo:

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

El código calcula la longitud del camino aún no recorrido por el enemigo. Para hacer esto, usa Distance , que se calcula como la distancia entre dos instancias de Vector3 .

Usaremos este método más adelante para averiguar qué objetivo atacar. Sin embargo, si bien nuestros monstruos no están armados ni indefensos, primero lo haremos.

Guarde el archivo y regrese a Unity para comenzar a configurar sus shells.

Vamos a darles a los monstruos conchas. ¡Muchas conchas!


Arrastre desde el Navegador de proyectos a la escena Imágenes / Objetos / Bullet1 . Establezca la posición en z en -2 : las posiciones en x e y no son importantes, porque las configuramos cada vez que creamos una nueva instancia del proyectil durante la ejecución del programa.

Agregue un nuevo script de C # llamado BulletBehavior , y luego en el IDE agregue las siguientes variables:

 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 la velocidad de los proyectiles; El damage claro en el nombre.

target , startPosition y targetPosition determinan la dirección del proyectil.

distance y startTime rastrean la posición actual del proyectil. gameManager recompensa al jugador cuando mata al enemigo.

Asigne los valores de estas variables en Start() :

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

startTimeestablecemos el valor del tiempo actual y calculamos la distancia entre las posiciones de inicio y objetivo. Además, como siempre, lo conseguimos GameManagerBehavior.

Para controlar el movimiento del proyectil, agregue el Update()siguiente 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 la nueva posición del proyectil, utilizando Vector3.Lerppara la interpolación entre las posiciones inicial y final.
  2. Si el proyectil llega targetPosition, verificamos si todavía existe target.
  3. Obtenemos el componente del HealthBarobjetivo y reducimos su salud por el tamaño del damageproyectil.
  4. Si la salud del enemigo se reduce a cero, entonces la destruimos, reproducimos el efecto de sonido y recompensamos al jugador por su precisión.

Guarde el archivo y regrese a Unity.

Hacemos grandes conchas


¿No sería genial si el monstruo comienza a disparar más proyectiles a niveles altos? Afortunadamente, esto es fácil de implementar.

Arrastre el objeto de juego Bullet1 desde la Jerarquía a la pestaña Proyecto para crear una prefabrica de proyectiles. Elimine el objeto original de la escena; ya no lo necesitaremos.

Duplicar el prefabricado Bullet1 dos veces . Nombra las copias de Bullet2 y Bullet3 .

Seleccione Bullet2 . En el Inspector , configure el campo Sprite componente Sprite Procesador de valor Images / Objetos / Bullet2. Entonces haremos que Bullet2 sea un poco más que Bullet1.

Repita el procedimiento para cambiar el sprite del prefabricado Bullet3 a Imágenes / Objetos / Bullet3 .

Más adelante en Bullet Behavior , ajustaremos la cantidad de daño causado por los proyectiles.

Seleccione el Bullet1 prefabricado en la pestaña Proyecto . En el Inspector, verá Bullet Behavior (Script) , en el que puede establecer Damage en 10 para Bullet1 , 15 para Bullet2 y 20 para Bullet3 - o cualquier otro valor que te guste.

Nota : Cambié los valores para que a niveles más altos el precio del daño se vuelva más alto. Esto evita que la actualización permita al jugador actualizar monstruos en los mejores puntos.


Conchas prefabricadas: el tamaño aumenta con el nivel

Cambiar el nivel de conchas


Asigna diferentes proyectiles a diferentes niveles de monstruos, para que los monstruos más fuertes destruyan a los enemigos más rápido.

Abra MonsterData.cs en el IDE y agregue a las MonsterLevelsiguientes variables:

 public GameObject bullet; public float fireRate; 

Así que establecemos la prefabricación del proyectil y la frecuencia de fuego para cada nivel de monstruos. Guarde el archivo y regrese a Unity para completar la configuración del monstruo.

Seleccione el monstruo prefabricado en el Navegador de proyectos . En el Inspector, expanda Niveles en el componente Monster Data (Script) . Establezca la tasa de fuego de cada elemento en 1 . Luego configure el parámetro Bullet del Elemento 0, 1 y 2 en Bullet1 , Bullet2 y Bullet3 .

Los niveles de monstruos deben establecerse de la siguiente manera:


Los proyectiles matan enemigos? Si! ¡Abramos el fuego!

Fuego abierto


Abra ShootEnemies.cs en el IDE y agregue las siguientes variables:

 private float lastShotTime; private MonsterData monsterData; 

Como su nombre lo indica, estas variables rastrean el tiempo del último disparo de monstruo, así como la estructura MonsterDataque contiene información sobre el tipo de proyectiles de monstruos, la frecuencia del fuego, etc.

Establezca los valores de estos campos en Start():

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

Aquí asignamos el lastShotTimevalor de la hora actual y tenemos acceso al componente de MonsterDataeste objeto.

Agregue el siguiente método para implementar el 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. Obtenemos las posiciones inicial y objetivo de la bala. Establezca la posición z igual a z bulletPrefab. Anteriormente, establecimos la posición prefabricada del proyectil en z para que el proyectil aparezca debajo del monstruo disparador, pero por encima de los enemigos.
  2. Creamos una instancia de un nuevo shell utilizando el bulletPrefabapropiado MonsterLevel. Asignar startPositiony targetPositionproyectil.
  3. Hacemos que el juego sea más interesante: cuando el monstruo dispara, comienza la animación del disparo y reproduce el sonido del láser.

Poniendo todo junto


Es hora de poner todo junto. Define el objetivo y haz que el monstruo lo mire.


En el script ShootEnemies.cs, agregue 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 paso a paso.

  1. Determina el propósito del monstruo. Comenzamos a la máxima distancia posible en minimalEnemyDistance. Damos la vuelta en un ciclo de todos los enemigos dentro del alcance y hacemos del enemigo un nuevo objetivo si su distancia a la galleta es menor que la más pequeña actual.
  2. Llamamos Shootsi el tiempo transcurrido es mayor que la frecuencia de disparo del monstruo y establecemos el lastShotTimevalor de la hora actual.
  3. Calculamos el ángulo de rotación entre el monstruo y su objetivo. Rotamos al monstruo a este ángulo. Ahora él siempre mirará al objetivo.

Guarde el archivo e inicie el juego en Unity. Los monstruos comenzarán a proteger desesperadamente las cookies. Finalmente hemos terminado!

A donde ir ahora


El proyecto terminado se puede descargar desde aquí .

Hicimos un gran trabajo en este tutorial y ahora tenemos un gran juego.

Aquí hay algunas ideas para un mayor desarrollo del proyecto:

  • Más tipos de enemigos y monstruos.
  • Diferentes rutas de enemigos.
  • Diferentes niveles de juego

Cada uno de estos aspectos requerirá cambios mínimos y puede hacer que el juego sea más divertido. Si creas un nuevo juego basado en este tutorial, estaré encantado de jugarlo, así que comparte un enlace.

En esta entrevista se pueden encontrar ideas interesantes sobre cómo crear un juego exitoso de defensa de la torre .

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


All Articles