[
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 Game
la velocidad del juego al control deslizante para que pueda acelerar el tiempo. [SerializeField, Range(1f, 10f)] float playSpeed = 1f;
Velocidad de juegoSi 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 GameScenario
control 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.State
deberí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 Progress
ejecutaremos después de completar el incremento del ciclo, y regresaremos false
solo 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 GameScenario
control 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.deltaTime
antes 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 delrepositorio