Creación de la defensa de la torre en la unidad: escenarios y olas de enemigos

[ La primera , segunda , tercera y cuarta partes del tutorial]

  • Soporte para enemigos de tamaños pequeño, mediano y grande.
  • Crea escenarios de juego con múltiples oleadas de enemigos.
  • Separación de la configuración de activos y el estado del juego.
  • Inicia, pausa, gana, derrota y acelera el juego.
  • Crea escenarios que se repiten sin cesar.

Esta es la quinta parte de una serie de tutoriales sobre cómo crear un juego de defensa de torre simple. En él, aprenderemos cómo crear escenarios de juego que generen oleadas de varios enemigos.

El tutorial fue creado en Unity 2018.4.6f1.


Se está volviendo bastante cómodo.

Más enemigos


No es muy interesante crear el mismo cubo azul cada vez. El primer paso para soportar escenarios de juego más interesantes será soportar varios tipos de enemigos.

Configuraciones enemigas


Hay muchas maneras de hacer que los enemigos sean únicos, pero no los complicaremos: los clasificamos como pequeños, medianos y grandes. Para etiquetarlos, cree una enumeración EnemyType .

 public enum EnemyType { Small, Medium, Large } 

Cambia EnemyFactory para que admita los tres tipos de enemigos en lugar de uno. Para los tres enemigos, se necesitan los mismos campos de configuración, por lo que agregamos la clase anidada EnemyConfig los contiene a todos, y luego agregamos tres campos de configuración de este tipo a la fábrica. Como esta clase se usa solo para la configuración y no la usaremos en ningún otro lugar, simplemente puede hacer públicos sus campos para que la fábrica pueda acceder a ellos. EnemyConfig sí no EnemyConfig obligado a ser público.

 public class EnemyFactory : GameObjectFactory { [System.Serializable] class EnemyConfig { public Enemy prefab = default; [FloatRangeSlider(0.5f, 2f)] public FloatRange scale = new FloatRange(1f); [FloatRangeSlider(0.2f, 5f)] public FloatRange speed = new FloatRange(1f); [FloatRangeSlider(-0.4f, 0.4f)] public FloatRange pathOffset = new FloatRange(0f); } [SerializeField] EnemyConfig small = default, medium = default, large = default; … } 

También hagamos que la salud sea personalizable para cada enemigo, porque es lógico que los enemigos grandes tengan más que los pequeños.

  [FloatRangeSlider(10f, 1000f)] public FloatRange health = new FloatRange(100f); 

Agregue un parámetro de tipo a Get para que pueda obtener un tipo de enemigo específico, y el tipo predeterminado será medio. Usaremos el tipo para obtener la configuración correcta, para la cual es útil un método separado, y luego crearemos e inicializaremos al enemigo como antes, solo con el argumento de salud agregado.

  EnemyConfig GetConfig (EnemyType type) { switch (type) { case EnemyType.Small: return small; case EnemyType.Medium: return medium; case EnemyType.Large: return large; } Debug.Assert(false, "Unsupported enemy type!"); return null; } public Enemy Get (EnemyType type = EnemyType.Medium) { EnemyConfig config = GetConfig(type); Enemy instance = CreateGameObjectInstance(config.prefab); instance.OriginFactory = this; instance.Initialize( config.scale.RandomValueInRange, config.speed.RandomValueInRange, config.pathOffset.RandomValueInRange, config.health.RandomValueInRange ); return instance; } 

Agregue el parámetro de salud requerido a Enemy.Initialize y Enemy.Initialize para establecer la salud en lugar de determinarlo por el tamaño del enemigo.

  public void Initialize ( float scale, float speed, float pathOffset, float health ) { … Health = health; } 

Creamos el diseño de diferentes enemigos.


Puedes elegir cuál será el diseño de los tres enemigos, pero en el tutorial me esforzaré por lograr la máxima simplicidad. Dupliqué el prefabricado original del enemigo y lo usé para los tres tamaños, cambiando solo el material: amarillo por pequeño, azul por mediano y rojo por grande. No cambié la escala del cubo prefabricado, pero utilicé la configuración de escala de fábrica para establecer las dimensiones. Además, dependiendo del tamaño, aumenté su salud y reduje la velocidad.


Fábrica de cubos enemigos de tres tamaños.

La forma más rápida es hacer que los tres tipos aparezcan en el juego cambiando Game.SpawnEnemy para que obtenga un tipo aleatorio de enemigo en lugar del medio.

  void SpawnEnemy () { GameTile spawnPoint = board.GetSpawnPoint(Random.Range(0, board.SpawnPointCount)); Enemy enemy = enemyFactory.Get((EnemyType)(Random.Range(0, 3))); enemy.SpawnOn(spawnPoint); enemies.Add(enemy); } 


Enemigos de diferentes tipos.

Varias fábricas


Ahora la fábrica de enemigos establece muchos tres enemigos. La fábrica existente crea cubos de tres tamaños, pero nada nos impide hacer otra fábrica que cree algo más, por ejemplo, esferas de tres tamaños. Podemos cambiar los enemigos creados al nombrar otra fábrica en el juego, cambiando así a un tema diferente.


Enemigos esféricos.

Olas de enemigos


El segundo paso para crear escenarios de juego será el rechazo de los enemigos engendrados con una frecuencia constante. Los enemigos deben crearse en oleadas sucesivas hasta que finalice el guión o el jugador pierda.

Secuencias de creación


Una ola de enemigos consiste en un grupo de enemigos creados uno tras otro hasta que se completa la ola. Una ola puede contener diferentes tipos de enemigos, y el retraso entre su creación puede variar. Para no complicar la implementación, comenzaremos con una secuencia de desove simple que crea el mismo tipo de enemigos con una frecuencia constante. Entonces la ola será solo una lista de tales secuencias.

Para configurar cada secuencia, cree una clase EnemySpawnSequence . Como es bastante complicado, póngalo en un archivo separado. La secuencia debe saber qué fábrica usar, qué tipo de enemigo crear, su número y frecuencia. Para simplificar la configuración, haremos una pausa en el último parámetro, que determina cuánto tiempo debe pasar antes de crear el próximo enemigo. Tenga en cuenta que este enfoque le permite utilizar varias fábricas enemigas en la ola.

 using UnityEngine; [System.Serializable] public class EnemySpawnSequence { [SerializeField] EnemyFactory factory = default; [SerializeField] EnemyType type = EnemyType.Medium; [SerializeField, Range(1, 100)] int amount = 1; [SerializeField, Range(0.1f, 10f)] float cooldown = 1f; } 

Las olas


Una ola es un conjunto simple de secuencias de creación enemigas. Cree un tipo de EnemyWave EnemyWave para él que comience con una secuencia estándar.

 using UnityEngine; [CreateAssetMenu] public class EnemyWave : ScriptableObject { [SerializeField] EnemySpawnSequence[] spawnSequences = { new EnemySpawnSequence() }; } 

Ahora podemos crear oleadas de enemigos. Por ejemplo, creé una ola que genera un grupo de enemigos cúbicos, comenzando con diez pequeños, con una frecuencia de dos por segundo. Les siguen cinco promedios, creados una vez por segundo, y, finalmente, un gran enemigo con una pausa de cinco segundos.


Una ola de cubos crecientes.

¿Puedo agregar un retraso entre secuencias?
Puedes implementarlo indirectamente. Por ejemplo, inserte un retraso de cuatro segundos entre cubos pequeños y medianos, reduzca el número de cubos pequeños en uno e inserte una secuencia de un cubo pequeño con una pausa de cuatro segundos.


Cuatro segundos de retraso entre cubos pequeños y medianos.

Escenarios


El escenario de juego se crea a partir de una secuencia de olas. Para esto, cree un GameScenario activo GameScenario con una sola matriz de ondas y luego GameScenario para crear el escenario.

 using UnityEngine; [CreateAssetMenu] public class GameScenario : ScriptableObject { [SerializeField] EnemyWave[] waves = {}; } 

Por ejemplo, creé un escenario con dos oleadas de enemigos pequeños, medianos y grandes (MSC), primero con cubos y luego con esferas.


Escenario con dos olas de MSC.

Movimiento de secuencia


Los tipos de activos se utilizan para crear scripts, pero como son activos, deben contener datos que no cambien durante el juego. Sin embargo, para avanzar en el escenario, de alguna manera necesitamos rastrear su estado. Una forma es duplicar el activo utilizado en el juego para que el duplicado rastree su condición. Pero no necesitamos duplicar todo el activo, solo el estado y los enlaces al activo son suficientes. Así que EnemySpawnSequence una clase de State separada, primero para EnemySpawnSequence . Como se aplica solo a una secuencia, la hacemos anidada. Es válido solo cuando tiene una referencia a una secuencia, por lo que le daremos un método constructor con un parámetro de secuencia.


Un tipo de estado anidado que se refiere a su secuencia.

 public class EnemySpawnSequence { … public class State { EnemySpawnSequence sequence; public State (EnemySpawnSequence sequence) { this.sequence = sequence; } } } 

Cuando queremos comenzar a avanzar en una secuencia, necesitamos una nueva instancia del estado para esto. Agregue secuencias al método Begin , que construye y devuelve el estado. Gracias a esto, todos los que llamen a Begin serán responsables de hacer coincidir el estado, y la secuencia misma seguirá sin estado. Incluso será posible avanzar en paralelo varias veces en la misma secuencia.

 public class EnemySpawnSequence { … public State Begin () => new State(this); public class State { … } } 

Para que el estado sobreviva después de reinicios en caliente, debe hacerlo serializable.

  [System.Serializable] public class State { … } 

La desventaja de este enfoque es que cada vez que ejecutamos una secuencia, necesitamos crear un nuevo objeto de estado. Podemos evitar la asignación de memoria convirtiéndola en una estructura en lugar de una clase. Esto es normal siempre que la afección permanezca pequeña. Solo tenga en cuenta que el estado es un tipo de valor. Cuando se transfiere, se copia, así que síguelo en un solo lugar.

  [System.Serializable] public struct State { … } 

El estado de la secuencia consta de solo dos aspectos: el número de enemigos generados y el progreso del tiempo de pausa. Agregamos el método Progress , que aumenta el valor de la pausa por delta de tiempo, y luego lo restablece cuando se alcanza el valor configurado, similar a lo que sucede con el tiempo de generación en Game.Update . Incrementaremos la cuenta de enemigos cada vez que esto suceda. Además, el valor de pausa debe comenzar con el valor máximo para que la secuencia cree enemigos sin pausa al principio.

  int count; float cooldown; public State (EnemySpawnSequence sequence) { this.sequence = sequence; count = 0; cooldown = sequence.cooldown; } public void Progress () { cooldown += Time.deltaTime; while (cooldown >= sequence.cooldown) { cooldown -= sequence.cooldown; count += 1; } } 


El estado contiene solo los datos necesarios.

¿Puedo acceder a EnemySpawnSequence.cooldown desde State?
Sí, porque el State se establece en el mismo ámbito. Por lo tanto, los tipos anidados conocen los miembros privados de los tipos que los contienen.

El progreso debe continuar hasta que se cree el número deseado de enemigos y finalice la pausa. En este punto, el Progress debe informar la finalización, pero lo más probable es que saltemos un poco por encima del valor. Por lo tanto, en este momento debemos devolver el tiempo extra para usarlo en la secuencia siguiente. Para que esto funcione, debe convertir el delta de tiempo en un parámetro. También debemos indicar que aún no hemos terminado, y esto se puede lograr devolviendo un valor negativo.

  public float Progress (float deltaTime) { cooldown += deltaTime; while (cooldown >= sequence.cooldown) { cooldown -= sequence.cooldown; if (count >= sequence.amount) { return cooldown; } count += 1; } return -1f; } 

Crea enemigos en cualquier lugar


Para que las secuencias generen enemigos, necesitamos convertir Game.SpawnEnemy a otro método estático público.

  public static void SpawnEnemy (EnemyFactory factory, EnemyType type) { GameTile spawnPoint = instance.board.GetSpawnPoint( Random.Range(0, instance.board.SpawnPointCount) ); Enemy enemy = factory.Get(type); enemy.SpawnOn(spawnPoint); instance.enemies.Add(enemy); } 

Dado que el Game sí ya no generará enemigos, podemos eliminar la Update la fábrica enemiga, la velocidad de creación, el proceso de promoción de creación y el código de creación del enemigo.

  void Update () { } 

Llamaremos a Game.SpawnEnemy en EnemySpawnSequence.State.Progress después de aumentar el recuento de enemigos.

  public float Progress (float deltaTime) { cooldown += deltaTime; while (cooldown >= sequence.cooldown) { … count += 1; Game.SpawnEnemy(sequence.factory, sequence.type); } return -1f; } 

Avance de la ola


Tomemos el mismo enfoque para moverse a lo largo de una secuencia que cuando se mueve a lo largo de una onda completa. Vamos a darle a EnemyWave su propio método Begin , que devuelve una nueva instancia de la estructura de State anidada. En este caso, el estado contiene el índice de onda y el estado de la secuencia activa, que inicializamos con el comienzo de la primera secuencia.


Un estado de onda que contiene el estado de una secuencia.

 public class EnemyWave : ScriptableObject { [SerializeField] EnemySpawnSequence[] spawnSequences = { new EnemySpawnSequence() }; public State Begin() => new State(this); [System.Serializable] public struct State { EnemyWave wave; int index; EnemySpawnSequence.State sequence; public State (EnemyWave wave) { this.wave = wave; index = 0; Debug.Assert(wave.spawnSequences.Length > 0, "Empty wave!"); sequence = wave.spawnSequences[0].Begin(); } } } 

También agregamos el método EnemyWave.State Progress , que utiliza el mismo enfoque que antes, con cambios menores. Comenzamos moviéndonos a lo largo de la secuencia activa y reemplazamos el delta de tiempo con el resultado de esta llamada. Mientras queda tiempo, pasamos a la siguiente secuencia, si se accede a ella, y avanzamos en ella. Si no quedan secuencias, entonces devolvemos el tiempo restante; de lo contrario, devuelve un valor negativo.

  public float Progress (float deltaTime) { deltaTime = sequence.Progress(deltaTime); while (deltaTime >= 0f) { if (++index >= wave.spawnSequences.Length) { return deltaTime; } sequence = wave.spawnSequences[index].Begin(); deltaTime = sequence.Progress(deltaTime); } return -1f; } 

Promoción de guiones


Agregue GameScenario el mismo procesamiento. En este caso, el estado contiene el índice de onda y el estado de la onda activa.

 public class GameScenario : ScriptableObject { [SerializeField] EnemyWave[] waves = {}; public State Begin () => new State(this); [System.Serializable] public struct State { GameScenario scenario; int index; EnemyWave.State wave; public State (GameScenario scenario) { this.scenario = scenario; index = 0; Debug.Assert(scenario.waves.Length > 0, "Empty scenario!"); wave = scenario.waves[0].Begin(); } } } 

Como estamos en el nivel superior, el método Progress no requiere un parámetro y puede usar Time.deltaTime directamente. No necesitamos devolver el tiempo restante, pero debemos mostrar si el script se ha completado. Volveremos false después del final de la última ola y true para mostrar que el script aún está activo.

  public bool Progress () { float deltaTime = wave.Progress(Time.deltaTime); while (deltaTime >= 0f) { if (++index >= scenario.waves.Length) { return false; } wave = scenario.waves[index].Begin(); deltaTime = wave.Progress(deltaTime); } return true; } 

Script ejecutado


Para jugar un script de Game , necesita un campo de configuración de script y un seguimiento de su estado. Simplemente ejecutaremos el script en Awake y ejecutaremos Update en él hasta que se Update el estado del resto del juego.

  [SerializeField] GameScenario scenario = default; GameScenario.State activeScenario; … void Awake () { board.Initialize(boardSize, tileContentFactory); board.ShowGrid = true; activeScenario = scenario.Begin(); } … void Update () { … activeScenario.Progress(); enemies.GameUpdate(); Physics.SyncTransforms(); board.GameUpdate(); nonEnemies.GameUpdate(); } 

Ahora el script configurado se lanzará al comienzo del juego. La promoción se llevará a cabo hasta su finalización, y después de eso no pasa nada.


Dos olas aceleraron 10 veces.

Iniciar y finalizar juegos


Podemos reproducir un escenario, pero después de su finalización no aparecerán nuevos enemigos. Para que el juego continúe, necesitamos hacer posible comenzar un nuevo escenario, ya sea manualmente o porque el jugador perdió / ganó. También puede implementar una selección de varios escenarios, pero en este tutorial no lo consideraremos.

El comienzo de un nuevo juego.


Idealmente, necesitamos la oportunidad de comenzar un nuevo juego en cualquier momento. Para hacer esto, debes restablecer el estado actual de todo el juego, es decir, tendremos que restablecer muchos objetos. Primero, agregue un método Clear a GameBehaviorCollection que utilice todos sus comportamientos.

  public void Clear () { for (int i = 0; i < behaviors.Count; i++) { behaviors[i].Recycle(); } behaviors.Clear(); } 

Esto sugiere que todos los comportamientos pueden eliminarse, pero hasta ahora este no es el caso. Para que esto funcione, agregue GameBehavior método de Recycle abstracto a GameBehavior .

  public abstract void Recycle (); 

El método de Recycle de la clase WarEntity debe anularlo explícitamente.

  public override void Recycle () { originFactory.Reclaim(this); } 

Enemy aún no tiene un método de Recycle , así que agrégalo. Todo lo que tiene que hacer es obligar a la fábrica a devolverlo. Luego llamamos a Recycle donde accedemos directamente a la fábrica.

  public override bool GameUpdate () { if (Health <= 0f) { Recycle(); return false; } progress += Time.deltaTime * progressFactor; while (progress >= 1f) { if (tileTo == null) { Recycle(); return false; } … } … } public override void Recycle () { OriginFactory.Reclaim(this); } 

GameBoard también debe reiniciarse, así que demos el método Clear , que vacía todos los mosaicos, restablece todos los puntos de creación y actualiza el contenido, y luego establece los puntos de inicio y finalización estándar. Luego, en lugar de repetir el código, podemos llamar a Clear al final de Initialize .

  public void Initialize ( Vector2Int size, GameTileContentFactory contentFactory ) { … for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { … } } Clear(); } public void Clear () { foreach (GameTile tile in tiles) { tile.Content = contentFactory.Get(GameTileContentType.Empty); } spawnPoints.Clear(); updatingContent.Clear(); ToggleDestination(tiles[tiles.Length / 2]); ToggleSpawnPoint(tiles[0]); } 

Ahora podemos agregar el método BeginNewGame al Game , eliminar enemigos, otros objetos y el campo, y luego comenzar un nuevo script.

  void BeginNewGame () { enemies.Clear(); nonEnemies.Clear(); board.Clear(); activeScenario = scenario.Begin(); } 

Llamaremos a este método en Update si presiona B antes de pasar al script.

  void Update () { … if (Input.GetKeyDown(KeyCode.B)) { BeginNewGame(); } activeScenario.Progress(); … } 

Perdiendo


El objetivo del juego es derrotar a todos los enemigos antes de que un cierto número de ellos llegue al punto final. El número de enemigos necesarios para activar la condición de derrota depende de la salud inicial del jugador, para lo cual agregaremos un campo de configuración al Game . Como contamos enemigos, usaremos enteros, no flotantes.

  [SerializeField, Range(0, 100)] int startingPlayerHealth = 10; 



Inicialmente, un jugador tiene 10 puntos de vida.

En el caso de Despertar o el comienzo de un nuevo juego, asignamos el valor inicial a la salud actual del jugador.

  int playerHealth; … void Awake () { playerHealth = startingPlayerHealth; … } void BeginNewGame () { playerHealth = startingPlayerHealth; … } 

Agrega un método público estático EnemyReachedDestination que los enemigos puedan decirle a Game que han llegado al punto final. Cuando esto sucede, reduce la salud del jugador.

  public static void EnemyReachedDestination () { instance.playerHealth -= 1; } 

Llame a este método en Enemy.GameUpdate en el momento apropiado.

  if (tileTo == null) { Game.EnemyReachedDestination(); Recycle(); return false; } 

Ahora podemos verificar la condición de la derrota en Game.Update . Si la salud del jugador es igual o inferior a cero, se activa la condición de derrota. Simplemente registramos esta información e inmediatamente comenzamos un nuevo juego antes de seguir adelante. Pero haremos esto solo con una salud inicial positiva. Esto nos permite usar 0 como salud inicial, por lo que es imposible perder. Por lo tanto, será conveniente para nosotros probar los guiones.

  if (playerHealth <= 0 && startingPlayerHealth > 0) { Debug.Log("Defeat!"); BeginNewGame(); } activeScenario.Progress(); 

Victoria


Una alternativa a la derrota es la victoria, que se logra al final del escenario, si el jugador aún está vivo. Es decir, cuando el resultado de GameScenario.Progess es false , mostramos un mensaje de victoria en el registro, iniciamos un nuevo juego e inmediatamente GameScenario.Progess .

  if (playerHealth <= 0) { Debug.Log("Defeat!"); BeginNewGame(); } if (!activeScenario.Progress()) { Debug.Log("Victory!"); BeginNewGame(); activeScenario.Progress(); } 

Sin embargo, la victoria llegará después del final de la última pausa, incluso si todavía hay enemigos en el campo. Necesitamos posponer la victoria hasta que todos los enemigos desaparezcan, lo que se puede lograr verificando si la colección de enemigos está vacía. Suponemos que tiene la propiedad IsEmpty .

  if (!activeScenario.Progress() && enemies.IsEmpty) { Debug.Log("Victory!"); BeginNewGame(); activeScenario.Progress(); } 

Agregue la propiedad deseada a GameBehaviorCollection .

  public bool IsEmpty => behaviors.Count == 0; 

Control del tiempo


Implementemos también la función de gestión del tiempo, esto ayudará en las pruebas y a menudo es una función de juego. Para comenzar, deja que Game.Update una barra espaciadora y usa este evento para habilitar / deshabilitar pausas en el juego. Esto se puede hacer cambiando los valores de Time.timeScale entre cero y uno. Esto no cambiará la lógica del juego, pero hará que todos los objetos se congelen en su lugar. O puede usar un valor muy pequeño en lugar de 0, por ejemplo 0.01, para crear una cámara extremadamente lenta.

  const float pausedTimeScale = 0f; … void Update () { … if (Input.GetKeyDown(KeyCode.Space)) { Time.timeScale = Time.timeScale > pausedTimeScale ? pausedTimeScale : 1f; } if (Input.GetKeyDown(KeyCode.B)) { BeginNewGame(); } … } 

En segundo lugar, agregaremos Gamela velocidad del juego al control deslizante para que pueda acelerar el tiempo.

  [SerializeField, Range(1f, 10f)] float playSpeed = 1f; 


Velocidad de juego

Si la pausa no está activada y el valor de pausa no está asignado a la escala de tiempo, lo hacemos igual a la velocidad del juego. Además, al eliminar una pausa, usamos la velocidad del juego en lugar de la unidad.

  if (Input.GetKeyDown(KeyCode.Space)) { Time.timeScale = Time.timeScale > pausedTimeScale ? pausedTimeScale : playSpeed; } else if (Time.timeScale > pausedTimeScale) { Time.timeScale = playSpeed; } 

Escenarios de bucle


En algunos escenarios, puede ser necesario atravesar todas las olas varias veces. Es posible implementar el soporte para dicha función haciendo posible repetir los escenarios recorriendo todas las ondas varias veces. Puede mejorar aún más esta función, por ejemplo, habilitando la repetición de solo la última ola, pero en este tutorial solo repetiremos todo el script.

Avance cíclico sobre las olas


Agregue al GameScenariocontrol deslizante de configuración para establecer el número de ciclos, por defecto, asígnele un valor de 1. Como mínimo, haga cero, y el script se repetirá sin cesar. Entonces crearemos un escenario de supervivencia que no puede ser derrotado, y el punto es verificar cuánto puede resistir el jugador.

  [SerializeField, Range(0, 10)] int cycles = 1; 


Escenario de dos ciclos.

Ahora GameScenario.Statedebería seguir el número del ciclo.

  int cycle, index; EnemyWave.State wave; public State (GameScenario scenario) { this.scenario = scenario; cycle = 0; index = 0; wave = scenario.waves[0].Begin(); } 

En Progressejecutaremos después de completar el incremento del ciclo, y regresaremos falsesolo si ha pasado un número suficiente de ciclos. De lo contrario, restablecemos el índice de onda a cero y continuamos moviéndonos.

  public bool Progress () { float deltaTime = wave.Progress(Time.deltaTime); while (deltaTime >= 0f) { if (++index >= scenario.waves.Length) { if (++cycle >= scenario.cycles && scenario.cycles > 0) { return false; } index = 0; } wave = scenario.waves[index].Begin(); deltaTime = wave.Progress(deltaTime); } return true; } 

Aceleración


Si el jugador logró derrotar el ciclo una vez, entonces podrá derrotarlo nuevamente sin ningún problema. Para mantener el escenario complejo, necesitamos aumentar la complejidad. La forma más fácil de hacerlo, reduciendo en ciclos posteriores todas las pausas entre la creación de enemigos. Entonces los enemigos aparecerán más rápido e inevitablemente vencerán al jugador en el escenario de supervivencia.

Agregue un GameScenariocontrol deslizante de configuración para controlar la aceleración por ciclo. Este valor se agrega a la escala de tiempo después de cada ciclo solo para reducir las pausas. Por ejemplo, con una aceleración de 0.5, el primer ciclo tiene una velocidad de pausa de × 1, el segundo ciclo tiene una velocidad de × 1.5, el tercero × 2, el cuarto × 2.5, y así sucesivamente.

  [SerializeField, Range(0f, 1f)] float cycleSpeedUp = 0.5f; 

Ahora necesita agregar la escala de tiempo y GameScenario.State. Siempre es inicialmente igual a 1 y aumenta en un valor dado de aceleración después de cada ciclo. Úselo para escalar Time.deltaTimeantes de moverse a lo largo de la ola.

  float timeScale; EnemyWave.State wave; public State (GameScenario scenario) { this.scenario = scenario; cycle = 0; index = 0; timeScale = 1f; wave = scenario.waves[0].Begin(); } public bool Progress () { float deltaTime = wave.Progress(timeScale * Time.deltaTime); while (deltaTime >= 0f) { if (++index >= scenario.waves.Length) { if (++cycle >= scenario.cycles && scenario.cycles > 0) { return false; } index = 0; timeScale += scenario.cycleSpeedUp; } wave = scenario.waves[index].Begin(); deltaTime = wave.Progress(deltaTime); } return true; } 


Tres ciclos con mayor velocidad de creación del enemigo; acelerado diez veces.

¿Desea recibir información sobre el lanzamiento de nuevos tutoriales? ¡Sigue mi página en Patreon ! Artículo en PDF del

repositorio

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


All Articles