[
Les première ,
deuxième ,
troisième et
quatrième parties du didacticiel]
- Prise en charge des ennemis de petites, moyennes et grandes tailles.
- Créez des scénarios de jeu avec plusieurs vagues d'ennemis.
- Séparation de la configuration des actifs et de l'état du gameplay.
- Commencez, arrêtez, gagnez, battez et accélérez le jeu.
- Créez des scénarios répétitifs sans fin.
Ceci est la cinquième partie d'une série de tutoriels sur la création d'un jeu de
tower defense simple. Dans ce document, nous apprendrons à créer des scénarios de gameplay qui génèrent des vagues de divers ennemis.
Le didacticiel a été créé dans Unity 2018.4.6f1.
Ça devient assez confortable.Plus d'ennemis
Ce n'est pas très intéressant de créer le même cube bleu à chaque fois. La première étape pour prendre en charge des scénarios de jeu plus intéressants sera de prendre en charge plusieurs types d'ennemis.
Configurations ennemies
Il existe de nombreuses façons de rendre les ennemis uniques, mais nous ne compliquerons pas: nous les classons comme petits, moyens et grands. Pour les étiqueter, créez une énumération
EnemyType
.
public enum EnemyType { Small, Medium, Large }
Changez
EnemyFactory
pour qu'il
EnemyFactory
en charge les trois types d'ennemis au lieu d'un. Pour les trois ennemis, les mêmes champs de configuration sont nécessaires, nous ajoutons donc une classe
EnemyConfig
imbriquée les contenant tous, puis ajoutons trois champs de configuration de ce type à l'usine. Étant donné que cette classe est utilisée uniquement pour la configuration et que nous ne l'utiliserons nulle part ailleurs, vous pouvez simplement rendre ses champs publics afin que l'usine puisse y accéder.
EnemyConfig
lui
EnemyConfig
même
EnemyConfig
pas tenu d'être public.
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; … }
Rendons également la santé personnalisable pour chaque ennemi, car il est logique que les grands ennemis en aient plus que les petits.
[FloatRangeSlider(10f, 1000f)] public FloatRange health = new FloatRange(100f);
Ajoutez un paramètre de type à
Get
pour obtenir un type d'ennemi spécifique, et le type par défaut sera moyen. Nous allons utiliser le type pour obtenir la configuration correcte, pour laquelle une méthode distincte est utile, puis créer et initialiser l'ennemi comme auparavant, uniquement avec l'argument santé ajouté.
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; }
Ajoutez le paramètre de santé requis à
Enemy.Initialize
et utilisez-le pour définir la santé au lieu de la déterminer par la taille de l'ennemi.
public void Initialize ( float scale, float speed, float pathOffset, float health ) { … Health = health; }
Nous créons le design de différents ennemis
Vous pouvez choisir quelle sera la conception des trois ennemis, mais dans le tutoriel, je m'efforcerai de simplifier au maximum. J'ai dupliqué le préfabriqué d'origine de l'ennemi et l'ai utilisé pour les trois tailles, en changeant uniquement le matériau: jaune pour petit, bleu pour moyen et rouge pour grand. Je n'ai pas changé l'échelle du cube préfabriqué, mais j'ai utilisé la configuration d'échelle d'usine pour définir les dimensions. De plus, selon la taille, j'ai augmenté leur santé et réduit leur vitesse.
Usine de cubes ennemis de trois tailles.Le moyen le plus rapide consiste à faire apparaître les trois types dans le jeu en changeant
Game.SpawnEnemy
afin qu'il obtienne un type d'ennemi aléatoire au lieu de l'ennemi du milieu.
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); }
Ennemis de différents types.Plusieurs usines
Maintenant, l'usine d'ennemis définit un grand nombre de trois ennemis. L'usine existante crée des cubes de trois tailles, mais rien ne nous empêche de faire une autre usine qui crée autre chose, par exemple des sphères de trois tailles. Nous pouvons changer les ennemis créés en nommant une autre usine dans le jeu, passant ainsi à un sujet différent.
Ennemis sphériques.Vagues d'ennemis
La deuxième étape de la création de scénarios de gameplay sera le rejet des ennemis qui se reproduisent avec une fréquence constante. Les ennemis doivent être créés par vagues successives jusqu'à la fin du script ou la perte du joueur.
Séquences de création
Une vague d'ennemis se compose d'un groupe d'ennemis créés l'un après l'autre jusqu'à ce que la vague soit terminée. Une vague peut contenir différents types d'ennemis et le délai entre leur création peut varier. Afin de ne pas compliquer l'implémentation, nous commencerons par une simple séquence d'apparition qui crée le même type d'ennemis avec une fréquence constante. Ensuite, l'onde ne sera qu'une liste de ces séquences.
Pour configurer chaque séquence, créez une classe
EnemySpawnSequence
. Comme c'est assez compliqué, mettez-le dans un fichier séparé. La séquence doit savoir quelle usine utiliser, quel type d'ennemi créer, leur nombre et leur fréquence. Pour simplifier la configuration, nous allons faire du dernier paramètre une pause, qui détermine le temps qui doit s'écouler avant de créer l'ennemi suivant. Notez que cette approche vous permet d'utiliser plusieurs usines ennemies dans la vague.
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; }
Les vagues
Une vague est un simple tableau de séquences de création ennemies. Créez pour lui un type d'
EnemyWave
EnemyWave qui commence par une séquence standard.
using UnityEngine; [CreateAssetMenu] public class EnemyWave : ScriptableObject { [SerializeField] EnemySpawnSequence[] spawnSequences = { new EnemySpawnSequence() }; }
Maintenant, nous pouvons créer des vagues d'ennemis. Par exemple, j'ai créé une vague qui génère un groupe d'ennemis cubiques, en commençant par dix petits, avec une fréquence de deux par seconde. Ils sont suivis de cinq moyennes, créées une fois par seconde, et, enfin, d'un gros ennemi avec une pause de cinq secondes.
Une vague de cubes croissants.Puis-je ajouter un délai entre les séquences?Vous pouvez l'implémenter indirectement. Par exemple, insérez un délai de quatre secondes entre les petits et les cubes moyens, réduisez le nombre de petits cubes d'un et insérez une séquence d'un petit cube avec une pause de quatre secondes.
Délai de quatre secondes entre les cubes petits et moyens. Scénarios
Le scénario de gameplay est créé à partir d'une séquence de vagues. Pour cela, créez un
GameScenario
actif
GameScenario
avec un seul tableau de vagues, puis utilisez-le pour créer le scénario.
using UnityEngine; [CreateAssetMenu] public class GameScenario : ScriptableObject { [SerializeField] EnemyWave[] waves = {}; }
Par exemple, j'ai créé un scénario avec deux vagues d'ennemis petits-moyens-grands (MSC), d'abord avec des cubes, puis avec des sphères.
Scénario avec deux vagues de MSC.Mouvement séquentiel
Les types d'actifs sont utilisés pour créer des scripts, mais comme il s'agit d'actifs, ils doivent contenir des données qui ne changent pas pendant le jeu. Cependant, pour faire avancer le scénario, nous devons en quelque sorte suivre leur statut. Une façon consiste à dupliquer l'actif utilisé dans le jeu afin que le doublon suive son état. Mais nous n'avons pas besoin de dupliquer l'ensemble de l'actif, il suffit de préciser et les liens vers l'actif suffisent.
EnemySpawnSequence
donc une classe d'
State
distincte, d'abord pour
EnemySpawnSequence
. Puisqu'il ne s'applique qu'à une séquence, nous le faisons imbriqué. Elle n'est valide que lorsqu'elle a une référence à une séquence, nous allons donc lui donner une méthode constructeur avec un paramètre de séquence.
Un type d'état imbriqué qui fait référence à sa séquence. public class EnemySpawnSequence { … public class State { EnemySpawnSequence sequence; public State (EnemySpawnSequence sequence) { this.sequence = sequence; } } }
Lorsque nous voulons commencer à avancer dans une séquence, nous avons besoin d'une nouvelle instance de l'état pour cela. Ajoutez des séquences à la méthode
Begin
, qui construit et renvoie l'état. Grâce à cela, tous ceux qui appellent
Begin
seront responsables de faire correspondre l'état, et la séquence elle-même restera apatride. Il sera même possible d'avancer en parallèle plusieurs fois dans la même séquence.
public class EnemySpawnSequence { … public State Begin () => new State(this); public class State { … } }
Pour que l'État survive après des redémarrages à chaud, vous devez le rendre sérialisable.
[System.Serializable] public class State { … }
L'inconvénient de cette approche est que chaque fois que nous exécutons une séquence, nous devons créer un nouvel objet d'état. Nous pouvons éviter l'allocation de mémoire en en faisant une structure au lieu d'une classe. C'est normal tant que la condition reste petite. Gardez juste à l'esprit que l'état est un type de valeur. Lorsqu'il est transféré, il est copié, alors suivez-le au même endroit.
[System.Serializable] public struct State { … }
L'état de la séquence ne comprend que deux aspects: le nombre d'ennemis générés et la progression du temps de pause. Nous ajoutons la méthode
Progress
, qui augmente la valeur de la pause du delta temporel, puis la réinitialise lorsque la valeur configurée est atteinte, similaire à ce qui se passe avec le temps de génération dans
Game.Update
. Nous augmenterons le nombre d'ennemis chaque fois que cela se produira. De plus, la valeur de pause doit commencer par la valeur maximale afin que la séquence crée des ennemis sans pause au début.
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; } }
L'état ne contient que les données nécessaires.Puis-je accéder à EnemySpawnSequence.cooldown depuis State?Oui, car l' State
est défini dans la même portée. Par conséquent, les types imbriqués connaissent les membres privés des types qui les contiennent.
La progression doit se poursuivre jusqu'à ce que le nombre d'ennemis souhaité soit créé et que la pause se termine. À ce stade,
Progress
devrait signaler l'achèvement, mais il est fort probable que nous surpassions un peu la valeur. Par conséquent, à ce moment, nous devons retourner le temps supplémentaire afin de l'utiliser dans l'avancement dans la séquence suivante. Pour que cela fonctionne, vous devez transformer le delta temporel en paramètre. Nous devons également indiquer que nous n'avons pas encore terminé, et cela peut être réalisé en renvoyant une valeur négative.
public float Progress (float deltaTime) { cooldown += deltaTime; while (cooldown >= sequence.cooldown) { cooldown -= sequence.cooldown; if (count >= sequence.amount) { return cooldown; } count += 1; } return -1f; }
Créez des ennemis n'importe où
Pour que les séquences engendrent des ennemis, nous devons convertir
Game.SpawnEnemy
en une autre méthode statique publique.
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); }
Puisque le
Game
lui-même ne générera plus d'ennemis, nous pouvons supprimer l'usine ennemie, la vitesse de création, le processus de promotion de la création et le code de création ennemi de
Update
.
void Update () { }
Nous appellerons
Game.SpawnEnemy
dans
EnemySpawnSequence.State.Progress
après avoir augmenté le nombre d'ennemis.
public float Progress (float deltaTime) { cooldown += deltaTime; while (cooldown >= sequence.cooldown) { … count += 1; Game.SpawnEnemy(sequence.factory, sequence.type); } return -1f; }
Avancement des vagues
Prenons la même approche pour se déplacer le long d'une séquence que pour se déplacer le long d'une vague entière. Donnons à
EnemyWave
sa propre méthode
Begin
, qui renvoie une nouvelle instance de la structure
State
imbriquée. Dans ce cas, l'état contient l'indice d'onde et l'état de la séquence active, que nous initialisons au début de la première séquence.
Un état d'onde contenant l'état d'une séquence. 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(); } } }
Nous ajoutons également la méthode
EnemyWave.State
Progress
, qui utilise la même approche que précédemment, avec des modifications mineures. Nous commençons par nous déplacer le long de la séquence active et remplaçons le delta temporel par le résultat de cet appel. Tant qu'il reste du temps, nous passons à la séquence suivante, si elle est accessible, et nous progressons. S'il n'y a plus de séquence, nous renvoyons le temps restant; sinon, retournez une valeur négative.
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; }
Promotion de script
Ajoutez
GameScenario
au même traitement. Dans ce cas, l'état contient l'indice d'onde et l'état de l'onde active.
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(); } } }
Puisque nous sommes au niveau supérieur, la méthode
Progress
ne nécessite pas de paramètre et vous pouvez utiliser directement
Time.deltaTime
. Nous n'avons pas besoin de renvoyer le temps restant, mais nous devons montrer si le script est terminé. Nous retournerons
false
après la fin de la dernière vague et
true
pour montrer que le script est toujours actif.
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; }
Exécution de script
Pour jouer à un script de
Game
, vous avez besoin d'un champ de configuration de script et d'un suivi de son état. Nous allons simplement exécuter le script dans Awake et exécuter
Update
sur celui-ci jusqu'à ce que le statut du reste du jeu soit mis à jour.
[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(); }
Maintenant, le script configuré sera lancé au début du jeu. La promotion sera effectuée jusqu'à son achèvement, et après quoi rien ne se passera.
Deux vagues ont accéléré 10 fois.Démarrer et terminer les jeux
Nous pouvons reproduire un scénario, mais après son achèvement, de nouveaux ennemis n'apparaîtront pas. Pour que le jeu continue, nous devons permettre de démarrer un nouveau scénario, soit manuellement, soit parce que le joueur a perdu / gagné. Vous pouvez également implémenter un choix de plusieurs scénarios, mais dans ce tutoriel, nous ne le considérerons pas.
Le début d'un nouveau jeu
Idéalement, nous devons avoir la possibilité de commencer un nouveau jeu à tout moment. Pour ce faire, vous devez réinitialiser l'état actuel de l'ensemble du jeu, c'est-à-dire que nous devrons réinitialiser de nombreux objets. Tout d'abord, ajoutez une méthode
Clear
à la
GameBehaviorCollection
qui utilise tous ses comportements.
public void Clear () { for (int i = 0; i < behaviors.Count; i++) { behaviors[i].Recycle(); } behaviors.Clear(); }
Cela suggère que tous les comportements peuvent être éliminés, mais jusqu'à présent ce n'est pas le cas. Pour que cela fonctionne, ajoutez
GameBehavior
méthode de
Recycle
abstraite à
GameBehavior
.
public abstract void Recycle ();
La méthode
Recycle
de la classe
WarEntity
doit la remplacer explicitement.
public override void Recycle () { originFactory.Reclaim(this); }
Enemy
n'a pas encore de méthode de
Recycle
, alors ajoutez-le. Tout ce qu'il a à faire est de forcer l'usine à la retourner. Ensuite, nous appelons
Recycle
partout où nous accédons directement à l'usine.
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
doit également être réinitialisé, nous allons donc lui donner la méthode
Clear
, qui vide toutes les tuiles, réinitialise tous les points de création et met à jour le contenu, puis définit les points de début et de fin standard. Ensuite, au lieu de répéter le code, nous pouvons appeler
Clear
à la fin 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]); }
Nous pouvons maintenant ajouter la méthode
BeginNewGame
au
Game
, en
BeginNewGame
les ennemis, d'autres objets et le champ, puis en démarrant un nouveau script.
void BeginNewGame () { enemies.Clear(); nonEnemies.Clear(); board.Clear(); activeScenario = scenario.Begin(); }
Nous appellerons cette méthode dans
Update
si vous appuyez sur B avant de passer au script.
void Update () { … if (Input.GetKeyDown(KeyCode.B)) { BeginNewGame(); } activeScenario.Progress(); … }
Perdre
Le but du jeu est de vaincre tous les ennemis avant qu'un certain nombre d'entre eux n'atteignent le point final. Le nombre d'ennemis nécessaires pour déclencher la condition de défaite dépend de la santé initiale du joueur, pour laquelle nous ajouterons un champ de configuration au
Game
. Puisque nous comptons les ennemis, nous utiliserons un entier, pas un flottant.
[SerializeField, Range(0, 100)] int startingPlayerHealth = 10;
Initialement, un joueur a 10 points de vie.Dans le cas de Awake ou du début d'une nouvelle partie, nous attribuons la valeur initiale à la santé actuelle du joueur.
int playerHealth; … void Awake () { playerHealth = startingPlayerHealth; … } void BeginNewGame () { playerHealth = startingPlayerHealth; … }
Ajoutez une méthode
EnemyReachedDestination
statique publique
EnemyReachedDestination
que les ennemis puissent dire à
Game
qu'ils ont atteint le point final. Dans ce cas, réduisez la santé du joueur.
public static void EnemyReachedDestination () { instance.playerHealth -= 1; }
Appelez cette méthode dans
Enemy.GameUpdate
au moment approprié.
if (tileTo == null) { Game.EnemyReachedDestination(); Recycle(); return false; }
Nous pouvons maintenant vérifier l'état de la défaite dans
Game.Update
. Si la santé du joueur est égale ou inférieure à zéro, la condition de défaite est déclenchée. Nous enregistrons simplement ces informations et commençons immédiatement une nouvelle partie avant d'avancer. Mais nous ne le ferons qu'avec une santé initiale positive. Cela nous permet d'utiliser 0 comme santé initiale, ce qui rend impossible la perte. Il nous sera donc pratique de tester les scripts.
if (playerHealth <= 0 && startingPlayerHealth > 0) { Debug.Log("Defeat!"); BeginNewGame(); } activeScenario.Progress();
Victoire
Une alternative à la défaite est la victoire, qui est obtenue à la fin du scénario, si le joueur est toujours en vie. Autrement dit, lorsque le résultat de
GameScenario.Progess
est
false
, nous
GameScenario.Progess
un message de victoire dans le journal, commençons une nouvelle partie et passons immédiatement dessus.
if (playerHealth <= 0) { Debug.Log("Defeat!"); BeginNewGame(); } if (!activeScenario.Progress()) { Debug.Log("Victory!"); BeginNewGame(); activeScenario.Progress(); }
Cependant, la victoire interviendra après la fin de la dernière pause, même s'il reste des ennemis sur le terrain. Nous devons reporter la victoire jusqu'à ce que tous les ennemis disparaissent, ce qui peut être réalisé en vérifiant si la collection d'ennemis est vide. Nous supposons qu'il a la propriété
IsEmpty
.
if (!activeScenario.Progress() && enemies.IsEmpty) { Debug.Log("Victory!"); BeginNewGame(); activeScenario.Progress(); }
Ajoutez la propriété souhaitée à
GameBehaviorCollection
.
public bool IsEmpty => behaviors.Count == 0;
Contrôle du temps
Implémentons également la fonction de gestion du temps, cela vous aidera dans les tests et est souvent une fonction de gameplay. Pour commencer, laissez
Game.Update
rechercher une barre d'espace et utilisez cet événement pour activer / désactiver les pauses dans le jeu. Cela peut être fait en commutant les valeurs
Time.timeScale
entre zéro et un. Cela ne changera pas la logique du jeu, mais fera geler tous les objets en place. Ou vous pouvez utiliser une très petite valeur au lieu de 0, par exemple 0,01, pour créer un mouvement extrêmement lent.
const float pausedTimeScale = 0f; … void Update () { … if (Input.GetKeyDown(KeyCode.Space)) { Time.timeScale = Time.timeScale > pausedTimeScale ? pausedTimeScale : 1f; } if (Input.GetKeyDown(KeyCode.B)) { BeginNewGame(); } … }
Deuxièmement, nous ajouterons Game
la vitesse du jeu au curseur pour accélérer le temps. [SerializeField, Range(1f, 10f)] float playSpeed = 1f;
Vitesse de jeu.Si la pause n'est pas activée et que la valeur de pause n'est pas affectée à l'échelle de temps, nous la rendons égale à la vitesse du jeu. De plus, lors de la suppression d'une pause, nous utilisons la vitesse du jeu au lieu de l'unité. if (Input.GetKeyDown(KeyCode.Space)) { Time.timeScale = Time.timeScale > pausedTimeScale ? pausedTimeScale : playSpeed; } else if (Time.timeScale > pausedTimeScale) { Time.timeScale = playSpeed; }
Scénarios de boucle
Dans certains scénarios, il peut être nécessaire de parcourir toutes les vagues plusieurs fois. Il est possible d'implémenter la prise en charge d'une telle fonction en permettant de répéter les scénarios en bouclant plusieurs fois toutes les vagues. Vous pouvez encore améliorer cette fonction, par exemple, en incluant la possibilité de répéter uniquement la dernière vague, mais dans ce tutoriel, nous allons simplement répéter tout le script.Avancement cyclique sur les vagues
Ajoutez au GameScenario
curseur de configuration pour définir le nombre de cycles, par défaut, affectez-lui une valeur de 1. Au minimum, faites zéro et le script se répétera sans fin. Nous allons donc créer un scénario de survie qui ne peut pas être vaincu, et le but est de vérifier combien le joueur peut tenir. [SerializeField, Range(0, 10)] int cycles = 1;
Scénario à deux cycles.Maintenant, il GameScenario.State
devrait suivre le numéro de cycle. int cycle, index; EnemyWave.State wave; public State (GameScenario scenario) { this.scenario = scenario; cycle = 0; index = 0; wave = scenario.waves[0].Begin(); }
Dans Progress
nous exécuterons après l'achèvement de l'incrément du cycle, et false
ne reviendrons que si un nombre suffisant de cycles s'est écoulé. Sinon, nous remettons à zéro l'index des vagues et continuons de bouger. 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; }
Accélération
Si le joueur a réussi à vaincre le cycle une fois, il pourra le vaincre à nouveau sans aucun problème. Pour garder le scénario complexe, nous devons augmenter la complexité. La façon la plus simple de le faire, en réduisant dans les cycles suivants toutes les pauses entre la création d'ennemis. Ensuite, les ennemis apparaîtront plus rapidement et vaincront inévitablement le joueur dans le scénario de survie.Ajoutez un GameScenario
curseur de configuration pour contrôler l'accélération par cycle. Cette valeur est ajoutée à l'échelle de temps après chaque cycle uniquement pour réduire les pauses. Par exemple, avec une accélération de 0,5, le premier cycle a une vitesse de pause de × 1, le deuxième cycle a une vitesse de × 1,5, le troisième × 2, le quatrième × 2,5, etc. [SerializeField, Range(0f, 1f)] float cycleSpeedUp = 0.5f;
Vous devez maintenant ajouter l'échelle de temps et GameScenario.State
. Elle est toujours initialement égale à 1 et augmente d'une valeur donnée d'accélération après chaque cycle. Utilisez-le à l'échelle Time.deltaTime
avant de vous déplacer le long de la vague. 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; }
Trois cycles avec une vitesse de création ennemie croissante; accéléré dix fois.Souhaitez-vous recevoir des informations sur la sortie de nouveaux tutoriels? Suivez ma page sur Patreon ! Article PDF duréférentiel