Criando Tower Defense na Unity: Cenários e Ondas de Inimigos

[ A primeira , segunda , terceira e quarta partes do tutorial]

  • Suporte para inimigos de tamanhos pequeno, médio e grande.
  • Crie cenários de jogos com várias ondas de inimigos.
  • Separação de configuração de ativos e estado de jogo.
  • Iniciar, pausar, vencer, derrotar e acelerar o jogo.
  • Crie cenários repetidamente intermináveis.

Esta é a quinta parte de uma série de tutoriais sobre como criar um jogo simples de defesa de torre . Nele, aprenderemos como criar cenários de jogo que geram ondas de vários inimigos.

O tutorial foi criado no Unity 2018.4.6f1.


Está ficando bem confortável.

Mais inimigos


Não é muito interessante criar sempre o mesmo cubo azul. O primeiro passo para suportar cenários de jogo mais interessantes será o suporte a vários tipos de inimigos.

Configurações inimigas


Existem muitas maneiras de tornar os inimigos únicos, mas não vamos complicar: os classificamos como pequenos, médios e grandes. Para marcá-los, crie uma enumeração EnemyType .

 public enum EnemyType { Small, Medium, Large } 

Mude o EnemyFactory para que ele suporte todos os três tipos de inimigos em vez de um. Para todos os três inimigos, os mesmos campos de configuração são necessários; portanto, adicionamos a classe aninhada EnemyConfig contendo todos eles e, em seguida, adicionamos três campos de configuração desse tipo à fábrica. Como essa classe é usada apenas para configuração e não a usaremos em nenhum outro lugar, você pode simplesmente tornar seus campos públicos para que a fábrica possa acessá-los. EnemyConfig próprio EnemyConfig não precisa 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; … } 

Vamos também tornar a saúde personalizável para cada inimigo, porque é lógico que inimigos grandes tenham mais que inimigos pequenos.

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

Adicione um parâmetro de tipo a Get para obter um tipo de inimigo específico e o tipo padrão será médio. Usaremos o tipo para obter a configuração correta, para a qual um método separado é útil, e depois criar e inicializar o inimigo como antes, somente com o argumento de integridade adicionado.

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

Adicione o parâmetro necessário health ao Enemy.Initialize e use-o para definir a saúde em vez de determiná-la pelo tamanho do inimigo.

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

Criamos o design de diferentes inimigos


Você pode escolher qual será o design dos três inimigos, mas no tutorial tentarei obter o máximo de simplicidade. Dupliquei o pré-fabricado original do inimigo e o usei para todos os três tamanhos, alterando apenas o material: amarelo para pequeno, azul para médio e vermelho para grande. Não alterei a escala do cubo pré-fabricado, mas usei a configuração de escala de fábrica para definir as dimensões. Além disso, dependendo do tamanho, aumentei a saúde deles e reduzi a velocidade.


Fábrica para inimigos cubos de três tamanhos.

A maneira mais rápida é fazer com que todos os três tipos apareçam no jogo, alterando Game.SpawnEnemy para que ele obtenha um tipo aleatório de inimigo em vez do meio.

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


Inimigos de diferentes tipos.

Várias fábricas


Agora a fábrica de inimigos define muitos três inimigos. A fábrica existente cria cubos de três tamanhos, mas nada nos impede de fazer outra fábrica que cria outra coisa, por exemplo, esferas de três tamanhos. Podemos mudar os inimigos criados nomeando outra fábrica no jogo, mudando para um tópico diferente.


Inimigos esféricos.

Ondas de inimigos


O segundo passo na criação de cenários de jogo será a rejeição de gerar inimigos com uma frequência constante. Os inimigos devem ser criados em ondas sucessivas até o script terminar ou o jogador perder.

Sequências de criação


Uma onda de inimigos consiste em um grupo de inimigos criados um após o outro até que a onda seja concluída. Uma onda pode conter diferentes tipos de inimigos, e o atraso entre sua criação pode variar. Para não complicar a implementação, começaremos com uma simples sequência de criação que cria o mesmo tipo de inimigos com uma frequência constante. Então a onda será apenas uma lista dessas seqüências.

Para configurar cada sequência, crie uma classe EnemySpawnSequence . Como é bastante complicado, coloque-o em um arquivo separado. A sequência deve saber qual fábrica usar, que tipo de inimigo criar, seu número e frequência. Para simplificar a configuração, faremos uma pausa no último parâmetro, que determina quanto tempo deve passar antes de criar o próximo inimigo. Observe que essa abordagem permite que você use várias fábricas inimigas na onda.

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

As ondas


Uma onda é uma matriz simples de sequências de criação de inimigos. Crie um tipo de EnemyWave EnemyWave para ele que comece com uma sequência padrão.

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

Agora podemos criar ondas de inimigos. Por exemplo, criei uma onda que gera um grupo de inimigos cúbicos, começando com dez pequenos, com uma frequência de dois por segundo. Eles são seguidos por cinco médias, criadas uma vez por segundo e, finalmente, um grande inimigo com uma pausa de cinco segundos.


Uma onda de cubos crescentes.

Posso adicionar um atraso entre as sequências?
Você pode implementá-lo indiretamente. Por exemplo, insira um atraso de quatro segundos entre cubos pequenos e médios, reduza o número de cubos pequenos em um e insira a sequência de um cubo pequeno com uma pausa de quatro segundos.


Atraso de quatro segundos entre cubos pequenos e médios.

Cenários


O cenário de jogo é criado a partir de uma sequência de ondas. Para isso, crie um GameScenario ativo GameScenario com uma única matriz de ondas e use-o para criar o cenário.

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

Por exemplo, criei um cenário com duas ondas de inimigos pequeno-médio-grande (MSC), primeiro com cubos e depois com esferas.


Cenário com duas vagas de MSC.

Movimento de sequência


Os tipos de ativos são usados ​​para criar scripts, mas, como são ativos, eles devem conter dados que não mudam durante o jogo. No entanto, para avançar no cenário, precisamos de alguma forma rastrear seu status. Uma maneira é duplicar o ativo usado no jogo para que o duplicado rastreie sua condição. Mas não precisamos duplicar todo o ativo, apenas o estado e os links são suficientes. Então, vamos criar uma classe State separada, primeiro para EnemySpawnSequence . Como se aplica apenas a uma sequência, nós a aninhamos. Ela é válida apenas quando tem uma referência a uma sequência, portanto, forneceremos um método construtor com um parâmetro de sequência.


Um tipo de estado aninhado que se refere à sua sequência.

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

Quando queremos começar a avançar em sequência, precisamos de uma nova instância do estado para isso. Adicione sequências ao método Begin , que constrói e retorna o estado. Graças a isso, todo mundo que ligar para Begin será responsável por corresponder ao estado, e a própria sequência permanecerá sem estado. Até será possível avançar em paralelo várias vezes na mesma sequência.

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

Para que o estado sobreviva após reinicializações a quente, você precisa torná-lo serializável.

  [System.Serializable] public class State { … } 

A desvantagem dessa abordagem é que toda vez que executamos uma sequência, precisamos criar um novo objeto de estado. Podemos evitar a alocação de memória, tornando-a uma estrutura em vez de uma classe. Isso é normal desde que a condição permaneça pequena. Lembre-se de que estado é um tipo de valor. Quando é transferido, é copiado, então acompanhe-o em um só lugar.

  [System.Serializable] public struct State { … } 

O estado da sequência consiste em apenas dois aspectos: o número de inimigos gerados e o progresso do tempo de pausa. Adicionamos o método Progress , que aumenta o valor da pausa pelo delta do tempo e o redefine quando o valor configurado é atingido, semelhante ao que acontece com o tempo de geração no Game.Update . Aumentaremos a contagem de inimigos toda vez que isso acontecer. Além disso, o valor da pausa deve começar com o valor máximo para que a sequência crie inimigos sem pausa no início.

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


O estado contém apenas os dados necessários.

Posso acessar EnemySpawnSequence.cooldown do State?
Sim, porque State está definido no mesmo escopo. Portanto, os tipos aninhados conhecem os membros particulares dos tipos que os contêm.

O progresso deve continuar até que o número desejado de inimigos seja criado e a pausa termine. Neste ponto, o Progress deve relatar a conclusão, mas provavelmente saltaremos um pouco acima do valor. Portanto, neste momento, devemos retornar o tempo extra para usá-lo com antecedência na sequência a seguir. Para que isso funcione, é necessário transformar o delta do tempo em um parâmetro. Também precisamos indicar que ainda não terminamos, e isso pode ser realizado retornando um 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; } 

Crie inimigos em qualquer lugar


Para que sequências gerem inimigos, precisamos converter Game.SpawnEnemy para outro 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); } 

Como o próprio Game não gerará mais inimigos, podemos remover a fábrica, a velocidade de criação, o processo de promoção da criação e o código de criação de inimigos do Update .

  void Update () { } 

Chamaremos Game.SpawnEnemy em EnemySpawnSequence.State.Progress depois de aumentar a contagem de inimigos.

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

Avanço da onda


Vamos seguir a mesma abordagem para mover-se ao longo de uma sequência, como ao longo de uma onda inteira. Vamos dar ao EnemyWave seu próprio método Begin , que retorna uma nova instância da estrutura State aninhada. Nesse caso, o estado contém o índice de ondas e o estado da sequência ativa, que inicializamos com o início da primeira sequência.


Um estado de onda contendo o estado de uma sequência.

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

Também adicionamos o método EnemyWave.State Progress , que usa a mesma abordagem de antes, com pequenas alterações. Começamos movendo-se ao longo da sequência ativa e substituindo o delta do tempo pelo resultado dessa chamada. Enquanto resta tempo, passamos para a próxima sequência, se for acessada, e realizamos progresso nela. Se não houver seqüências restantes, retornamos o tempo restante; caso contrário, retorne um 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; } 

Promoção de scripts


Adicione GameScenario o mesmo processamento. Nesse caso, o estado contém o índice da onda e o estado da onda ativa.

 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 no nível superior, o método Progress não requer um parâmetro e você pode usar Time.deltaTime diretamente. Não precisamos retornar o tempo restante, mas precisamos mostrar se o script foi concluído. Retornaremos false após o final da última onda e true para mostrar que o script ainda está ativo.

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

Execução de script


Para reproduzir um script de Game , você precisa de um campo de configuração de script e rastreamento de seu status. Apenas rodaremos o script em Despertar e Update até que o status do resto do jogo seja atualizado.

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

Agora, o script configurado será iniciado no início do jogo. A promoção será realizada até a conclusão e, depois disso, nada acontece.


Duas ondas aceleraram 10 vezes.

Início e fim dos jogos


Podemos reproduzir um cenário, mas após a conclusão, novos inimigos não aparecerão. Para o jogo continuar, precisamos possibilitar o início de um novo cenário, manualmente, ou porque o jogador perdeu / venceu. Você também pode implementar uma escolha de vários cenários, mas neste tutorial não iremos considerá-lo.

O começo de um novo jogo


Idealmente, precisamos da oportunidade de iniciar um novo jogo a qualquer momento. Para fazer isso, você precisa redefinir o estado atual de todo o jogo, ou seja, teremos que redefinir muitos objetos. Primeiro, adicione um método Clear ao GameBehaviorCollection que utiliza todos os seus comportamentos.

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

Isso sugere que todos os comportamentos podem ser descartados, mas até agora esse não é o caso. Para fazer isso funcionar, adicione GameBehavior método abstrato de Recycle ao GameBehavior .

  public abstract void Recycle (); 

O método Recycle da classe WarEntity deve substituí-lo explicitamente.

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

Enemy ainda não possui um método de Recycle , então adicione-o. Tudo o que ele precisa fazer é forçar a fábrica a devolvê-lo. Em seguida, ligamos para a Recycle onde quer que acessemos diretamente a 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 também precisa ser redefinido, portanto, vamos dar o método Clear , que esvazia todos os blocos, redefine todos os pontos de criação e atualiza o conteúdo e, em seguida, define os pontos inicial e final padrão. Então, em vez de repetir o código, podemos chamar Clear no 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]); } 

Agora podemos adicionar o método BeginNewGame ao Game , despejar inimigos, outros objetos e o campo e iniciar um novo script.

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

Chamaremos esse método no Update se você pressionar B antes de passar para o script.

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

Perdendo


O objetivo do jogo é derrotar todos os inimigos antes que um certo número deles atinja o ponto final. O número de inimigos necessários para desencadear a condição de derrota depende da saúde inicial do jogador, para a qual adicionaremos um campo de configuração ao Game . Como contamos inimigos, usaremos inteiro, não flutuante.

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



Inicialmente, um jogador tem 10 de vida.

No caso de Awake ou no início de um novo jogo, atribuímos o valor inicial à saúde atual do jogador.

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

Adicione um método público EnemyReachedDestination estático EnemyReachedDestination que os inimigos possam dizer ao Game que atingiram o ponto final. Quando isso acontecer, reduza a saúde do jogador.

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

Chame esse método no Enemy.GameUpdate no momento apropriado.

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

Agora podemos verificar a condição de derrota no Game.Update . Se a saúde do jogador for igual ou menor que zero, a condição de derrota será acionada. Simplesmente registramos essas informações e iniciamos imediatamente um novo jogo antes de avançar. Mas faremos isso apenas com uma saúde inicial positiva. Isso nos permite usar 0 como saúde inicial, tornando impossível a perda. Portanto, será conveniente testar os scripts.

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

Vitória


Uma alternativa à derrota é a vitória, que é alcançada no final do cenário, se o jogador ainda estiver vivo. Ou seja, quando o resultado do GameScenario.Progess é false , exibimos uma mensagem de vitória no log, iniciamos um novo jogo e seguimos imediatamente.

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

No entanto, a vitória ocorrerá após o final da última pausa, mesmo se ainda houver inimigos em campo. Precisamos adiar a vitória até que todos os inimigos desapareçam, o que pode ser realizado verificando se a coleção de inimigos está vazia. Assumimos que ele tenha a propriedade IsEmpty .

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

Adicione a propriedade desejada ao GameBehaviorCollection .

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

Controle de tempo


Vamos também implementar o recurso de gerenciamento de tempo, isso ajudará nos testes e geralmente é uma função de jogabilidade. Para começar, deixe Game.Update procurar uma barra de espaço e use esse evento para ativar / desativar as pausas no jogo. Isso pode ser feito alternando os valores Time.timeScale entre zero e um. Isso não mudará a lógica do jogo, mas fará com que todos os objetos congelem no lugar. Ou você pode usar um valor muito pequeno em vez de 0, por exemplo, 0,01, para criar uma câmera extremamente 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(); } … } 

Em segundo lugar, adicionaremos Gamea velocidade do jogo ao controle deslizante, para que você possa acelerar o tempo.

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


Velocidade do jogo.

Se a pausa não estiver ativada e o valor da pausa não estiver atribuído à escala de tempo, será igual à velocidade do jogo. Além disso, ao remover uma pausa, usamos a velocidade do jogo em vez da unidade.

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

Cenários de loop


Em alguns cenários, pode ser necessário passar por todas as ondas várias vezes. É possível implementar o suporte para essa função, possibilitando repetir os cenários fazendo um loop por todas as ondas várias vezes. Você pode melhorar ainda mais essa função, por exemplo, ativando a repetição apenas da última onda, mas neste tutorial apenas repetiremos o script inteiro.

Avanço cíclico nas ondas


Adicione ao GameScenariocontrole deslizante de configuração para definir o número de ciclos, por padrão, atribua a ele um valor de 1. No mínimo, faça zero e o script será repetido indefinidamente. Portanto, criaremos um cenário de sobrevivência que não pode ser derrotado, e o objetivo é verificar quanto o jogador aguenta.

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


Cenário de dois ciclos.

Agora ele GameScenario.Statedeve rastrear o número do ciclo.

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

Em Progressexecutaremos após a conclusão do incremento do ciclo e retornaremos falseapenas se um número suficiente de ciclos tiver passado. Caso contrário, redefinimos o índice de ondas para zero e continuamos a se mover.

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

Aceleração


Se o jogador conseguiu derrotar o ciclo uma vez, ele será capaz de derrotá-lo novamente sem problemas. Para manter o cenário complexo, precisamos aumentar a complexidade. A maneira mais fácil de fazer isso, reduzindo em ciclos subsequentes todas as pausas entre a criação de inimigos. Então os inimigos aparecerão mais rapidamente e inevitavelmente derrotarão o jogador no cenário de sobrevivência.

Adicione um GameScenariocontrole deslizante de configuração para controlar a aceleração por ciclo. Este valor é adicionado à escala de tempo após cada ciclo apenas para reduzir as pausas. Por exemplo, com uma aceleração de 0,5, o primeiro ciclo tem uma velocidade de pausa de × 1, o segundo ciclo tem uma velocidade de × 1,5, o terceiro × 2, o quarto × 2,5 e assim por diante.

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

Agora você precisa adicionar a escala de tempo e a GameScenario.State. Sempre é inicialmente igual a 1 e aumenta em um determinado valor de aceleração após cada ciclo. Use-o para escalar Time.deltaTimeantes de se mover ao longo da onda.

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


Três ciclos com aumento da velocidade de criação do inimigo; acelerado dez vezes.

Deseja receber informações sobre o lançamento de novos tutoriais? Siga minha página no Patreon ! Artigo PDF do

repositório

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


All Articles