[
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 Game
a 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 GameScenario
controle 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.State
deve 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 Progress
executaremos após a conclusão do incremento do ciclo e retornaremos false
apenas 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 GameScenario
controle 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.deltaTime
antes 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 dorepositório