Turmverteidigung in Einheit schaffen: Szenarien und Wellen der Feinde

[ Der erste , zweite , dritte und vierte Teil des Tutorials]

  • Unterstützung für Feinde kleiner, mittlerer und großer Größe.
  • Erstellen Sie Spielszenarien mit mehreren Wellen von Feinden.
  • Trennung von Asset-Konfiguration und Gameplay-Status.
  • Starten, pausieren, gewinnen, besiegen und beschleunigen Sie das Spiel.
  • Erstellen Sie sich endlos wiederholende Szenarien.

Dies ist der fünfte Teil einer Reihe von Tutorials zum Erstellen eines einfachen Tower Defense- Spiels. Darin lernen wir, wie man Spielszenarien erstellt, die Wellen verschiedener Feinde erzeugen.

Das Tutorial wurde in Unity 2018.4.6f1 erstellt.


Es wird ziemlich bequem.

Noch mehr Feinde


Es ist nicht sehr interessant, jedes Mal denselben blauen Würfel zu erstellen. Der erste Schritt zur Unterstützung interessanterer Spielszenarien besteht darin, verschiedene Arten von Feinden zu unterstützen.

Feindliche Konfigurationen


Es gibt viele Möglichkeiten, Feinde einzigartig zu machen, aber wir werden es nicht komplizieren: Wir klassifizieren sie als klein, mittel und groß. Erstellen Sie eine EnemyType Aufzählung, um sie zu EnemyType .

 public enum EnemyType { Small, Medium, Large } 

Ändern Sie EnemyFactory so, dass alle drei Arten von Feinden anstelle von einem unterstützt werden. Für alle drei Feinde werden dieselben Konfigurationsfelder benötigt, daher fügen wir eine verschachtelte EnemyConfig Klasse hinzu, die alle enthält, und fügen dann der Fabrik drei Konfigurationsfelder dieses Typs hinzu. Da diese Klasse nur für die Konfiguration verwendet wird und wir sie nirgendwo anders verwenden, können Sie ihre Felder einfach öffentlich machen, damit die Factory darauf zugreifen kann. EnemyConfig selbst muss nicht öffentlich sein.

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

Lassen Sie uns auch die Gesundheit für jeden Feind anpassbar machen, da es logisch ist, dass große Feinde mehr als kleine haben.

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

Fügen Sie Get einen Typparameter hinzu, damit Sie einen bestimmten Feindtyp erhalten können. Der Standardtyp ist mittel. Wir werden den Typ verwenden, um die richtige Konfiguration zu erhalten, für die eine separate Methode nützlich ist, und dann den Feind wie zuvor erstellen und initialisieren, nur mit dem hinzugefügten Gesundheitsargument.

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

Fügen Sie Enemy.Initialize den erforderlichen Parameter health Enemy.Initialize und verwenden Sie ihn, um den Enemy.Initialize anstatt ihn anhand der Größe des Feindes zu bestimmen.

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

Wir erstellen das Design verschiedener Feinde


Sie können wählen, wie die drei Feinde aussehen sollen, aber im Tutorial werde ich mich um maximale Einfachheit bemühen. Ich habe das ursprüngliche Fertighaus des Feindes dupliziert und es für alle drei Größen verwendet, wobei nur das Material geändert wurde: Gelb für Klein, Blau für Mittel und Rot für Groß. Ich habe den Maßstab des Fertigwürfels nicht geändert, sondern die werkseitige Maßstabskonfiguration verwendet, um die Abmessungen festzulegen. Außerdem habe ich je nach Größe ihre Gesundheit erhöht und die Geschwindigkeit verringert.


Fabrik für feindliche Würfel in drei Größen.

Der schnellste Weg ist, alle drei Typen im Spiel erscheinen zu lassen, indem Sie Game.SpawnEnemy so Game.SpawnEnemy , dass er einen zufälligen Game.SpawnEnemy anstelle des mittleren erhält.

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


Feinde verschiedener Art.

Mehrere Fabriken


Jetzt setzt die Fabrik der Feinde viele drei Feinde. Die bestehende Fabrik schafft Würfel in drei Größen, aber nichts hindert uns daran, eine andere Fabrik zu bauen, die etwas anderes schafft, zum Beispiel Kugeln in drei Größen. Wir können die erstellten Feinde ändern, indem wir eine andere Fabrik im Spiel ernennen und so zu einem anderen Thema wechseln.


Sphärische Feinde.

Wellen von Feinden


Der zweite Schritt bei der Erstellung von Spielszenarien ist die Ablehnung von Laichfeinden mit konstanter Häufigkeit. Gegner müssen in aufeinanderfolgenden Wellen erstellt werden, bis das Skript endet oder der Spieler verliert.

Erstellungssequenzen


Eine Welle von Feinden besteht aus einer Gruppe von Feinden, die nacheinander erstellt werden, bis die Welle abgeschlossen ist. Eine Welle kann verschiedene Arten von Feinden enthalten, und die Verzögerung zwischen ihrer Entstehung kann variieren. Um die Implementierung nicht zu verkomplizieren, beginnen wir mit einer einfachen Laichsequenz, die dieselbe Art von Feinden mit einer konstanten Häufigkeit erzeugt. Dann ist die Welle nur eine Liste solcher Sequenzen.

Erstellen EnemySpawnSequence zum Konfigurieren jeder Sequenz eine EnemySpawnSequence Klasse. Da es ziemlich kompliziert ist, legen Sie es in eine separate Datei. Die Sequenz muss wissen, welche Fabrik verwendet werden soll, welche Art von Feind erstellt werden soll, wie viele und wie häufig sie sind. Um die Konfiguration zu vereinfachen, machen wir den letzten Parameter zu einer Pause, die bestimmt, wie viel Zeit vergehen soll, bevor der nächste Feind erstellt wird. Beachten Sie, dass Sie mit diesem Ansatz mehrere feindliche Fabriken in der Welle verwenden können.

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

Die Wellen


Eine Welle ist eine einfache Anordnung feindlicher Erstellungssequenzen. Erstellen Sie einen EnemyWave- EnemyWave Typ, der mit einer Standardsequenz beginnt.

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

Jetzt können wir Wellen von Feinden erzeugen. Zum Beispiel habe ich eine Welle erstellt, die eine Gruppe kubischer Feinde erzeugt, beginnend mit zehn kleinen, mit einer Frequenz von zwei pro Sekunde. Es folgen fünf Durchschnittswerte, die einmal pro Sekunde erstellt werden, und schließlich ein großer Feind mit einer Pause von fünf Sekunden.


Eine Welle zunehmender Würfel.

Kann ich eine Verzögerung zwischen Sequenzen hinzufügen?
Sie können es indirekt implementieren. Fügen Sie beispielsweise eine Verzögerung von vier Sekunden zwischen kleinen und mittleren Würfeln ein, reduzieren Sie die Anzahl der kleinen Würfel um eins und fügen Sie eine Folge eines kleinen Würfels mit einer Pause von vier Sekunden ein.


Vier Sekunden Verzögerung zwischen kleinen und mittleren Würfeln.

Szenarien


Das Gameplay-Szenario wird aus einer Folge von Wellen erstellt. Erstellen Sie dazu einen GameScenario Asset- GameScenario mit einem einzelnen Array von Wellen und erstellen Sie daraus das Szenario.

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

Zum Beispiel habe ich ein Szenario mit zwei Wellen kleiner, mittlerer und großer Feinde (MSC) erstellt, zuerst mit Würfeln, dann mit Kugeln.


Szenario mit zwei MSC-Wellen.

Sequenzbewegung


Asset-Typen werden zum Erstellen von Skripten verwendet. Da es sich jedoch um Assets handelt, müssen sie Daten enthalten, die sich während des Spiels nicht ändern. Um das Szenario voranzutreiben, müssen wir jedoch irgendwie ihren Status verfolgen. Eine Möglichkeit besteht darin, das im Spiel verwendete Asset zu duplizieren, damit das Duplikat seinen Zustand verfolgt. Wir müssen jedoch nicht das gesamte Asset duplizieren, sondern nur den Status und die Links zum Asset. Erstellen wir also zunächst eine separate EnemySpawnSequence für EnemySpawnSequence . Da es nur für eine Sequenz gilt, machen wir es verschachtelt. Es ist nur gültig, wenn es einen Verweis auf eine Sequenz enthält, daher geben wir ihm eine Konstruktormethode mit einem Sequenzparameter.


Ein verschachtelter Zustandstyp, der sich auf seine Sequenz bezieht.

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

Wenn wir in einer Sequenz vorwärts gehen wollen, brauchen wir dafür eine neue Instanz des Zustands. Fügen Sie der Begin Methode Sequenzen hinzu, die den Status erstellt und zurückgibt. Dank dessen ist jeder, der Begin anruft, dafür verantwortlich, den Status abzugleichen, und die Sequenz selbst bleibt zustandslos. Es wird sogar möglich sein, mehrmals in derselben Reihenfolge parallel vorzurücken.

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

Damit der Status nach einem heißen Neustart überlebt, müssen Sie ihn serialisierbar machen.

  [System.Serializable] public class State { … } 

Der Nachteil dieses Ansatzes besteht darin, dass jedes Mal, wenn wir eine Sequenz ausführen, ein neues Statusobjekt erstellt werden muss. Wir können die Speicherzuweisung vermeiden, indem wir sie zu einer Struktur anstelle einer Klasse machen. Dies ist normal, solange der Zustand klein bleibt. Denken Sie daran, dass state ein Werttyp ist. Wenn es übertragen wird, wird es kopiert, also verfolgen Sie es an einer Stelle.

  [System.Serializable] public struct State { … } 

Der Zustand der Sequenz besteht nur aus zwei Aspekten: der Anzahl der erzeugten Feinde und dem Verlauf der Pausenzeit. Wir fügen die Progress Methode hinzu, die den Wert der Pause um das Game.Update erhöht und sie dann zurücksetzt, wenn der konfigurierte Wert erreicht ist, ähnlich wie bei der Generierungszeit in Game.Update . Wir werden die Anzahl der Feinde jedes Mal erhöhen, wenn dies geschieht. Außerdem muss der Pausenwert mit dem Maximalwert beginnen, damit die Sequenz zu Beginn Feinde ohne Pause erzeugt.

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


Der Zustand enthält nur die notwendigen Daten.

Kann ich von State aus auf EnemySpawnSequence.cooldown zugreifen?
Ja, da der Status im selben Bereich festgelegt ist. Daher kennen verschachtelte Typen die privaten Mitglieder der Typen, die sie enthalten.

Der Fortschritt muss fortgesetzt werden, bis die gewünschte Anzahl von Feinden erreicht ist und die Pause endet. Zu diesem Zeitpunkt sollte Progress Abschluss melden, aber höchstwahrscheinlich werden wir ein wenig über den Wert springen. Daher müssen wir in diesem Moment die zusätzliche Zeit zurückgeben, um sie in der folgenden Reihenfolge für den Fortschritt zu verwenden. Damit dies funktioniert, müssen Sie das Zeitdelta in einen Parameter umwandeln. Wir müssen auch angeben, dass wir noch nicht fertig sind, und dies kann durch Rückgabe eines negativen Wertes realisiert werden.

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

Erschaffe überall Feinde


Damit Sequenzen Feinde Game.SpawnEnemy können, müssen wir Game.SpawnEnemy in eine andere öffentliche statische Methode konvertieren.

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

Da das Game selbst keine Feinde mehr erzeugt, können wir die feindliche Fabrik, die Erstellungsgeschwindigkeit, den Erstellungsförderungsprozess und den Erstellungscode des Feindes aus Update entfernen.

  void Update () { } 

Wir werden Game.SpawnEnemy in EnemySpawnSequence.State.Progress nachdem wir die Anzahl der Feinde erhöht haben.

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

Wellenfortschritt


Gehen wir bei der Bewegung entlang einer Sequenz genauso vor wie bei der Bewegung entlang einer ganzen Welle. Geben EnemyWave eine eigene Begin Methode, die eine neue Instanz der verschachtelten EnemyWave zurückgibt. In diesem Fall enthält der Zustand den Wellenindex und den Zustand der aktiven Sequenz, die wir mit dem Beginn der ersten Sequenz initialisieren.


Ein Wellenzustand, der den Zustand einer Sequenz enthält.

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

Wir fügen auch die EnemyWave.State Methode Progress , die mit geringfügigen Änderungen denselben Ansatz wie zuvor verwendet. Wir beginnen mit der aktiven Sequenz und ersetzen das Zeitdelta durch das Ergebnis dieses Aufrufs. Solange noch Zeit ist, fahren wir mit der nächsten Sequenz fort, wenn darauf zugegriffen wird, und führen den Fortschritt durch. Wenn keine Sequenzen mehr vorhanden sind, geben wir die verbleibende Zeit zurück. Andernfalls wird ein negativer Wert zurückgegeben.

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

Skript-Promotion


Fügen Sie GameScenario die gleiche Verarbeitung hinzu. In diesem Fall enthält der Zustand den Wellenindex und den Zustand der aktiven Welle.

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

Da wir uns auf der obersten Ebene befinden, benötigt die Progress Methode keinen Parameter und Sie können Time.deltaTime direkt verwenden. Wir müssen die verbleibende Zeit nicht zurückgeben, aber wir müssen zeigen, ob das Skript abgeschlossen ist. Wir werden nach dem Ende der letzten Welle false und true zu zeigen, dass das Skript noch aktiv ist.

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

Skript ausführen


Um ein Game zu spielen, benötigen Sie ein Skriptkonfigurationsfeld und die Verfolgung seines Status. Wir werden das Skript einfach in Awake ausführen und Update ausführen, bis der Status des restlichen Spiels aktualisiert ist.

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

Jetzt wird das konfigurierte Skript zu Beginn des Spiels gestartet. Die Promotion wird bis zur Fertigstellung durchgeführt, und danach passiert nichts mehr.


Zwei Wellen beschleunigten sich zehnmal.

Spiele starten und beenden


Wir können ein Szenario reproduzieren, aber nach seiner Fertigstellung werden keine neuen Feinde erscheinen. Damit das Spiel fortgesetzt werden kann, müssen wir es ermöglichen, ein neues Szenario entweder manuell oder weil der Spieler verloren / gewonnen hat, zu starten. Sie können auch eine Auswahl mehrerer Szenarien implementieren. In diesem Lernprogramm wird dies jedoch nicht berücksichtigt.

Der Beginn eines neuen Spiels


Idealerweise brauchen wir die Möglichkeit, jederzeit ein neues Spiel zu starten. Dazu müssen Sie den aktuellen Status des gesamten Spiels zurücksetzen, dh wir müssen viele Objekte zurücksetzen. GameBehaviorCollection der GameBehaviorCollection eine Clear Methode GameBehaviorCollection , die alle ihre Verhaltensweisen nutzt.

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

Dies deutet darauf hin, dass alle Verhaltensweisen beseitigt werden können, dies ist jedoch bisher nicht der Fall. GameBehavior abstrakte Recycle Methode hinzu, damit dies GameBehavior .

  public abstract void Recycle (); 

Die Recycle Methode der WarEntity Klasse muss sie explizit überschreiben.

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

Enemy verfügt noch nicht über eine Recycle Methode. Fügen Sie sie hinzu. Alles was er tun muss, ist die Fabrik zu zwingen, es zurückzugeben. Dann rufen wir Recycle wo immer wir direkt auf die Fabrik zugreifen.

  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 ebenfalls zurückgesetzt werden. Geben Sie ihm also die Clear Methode, mit der alle Kacheln geleert, alle Erstellungspunkte zurückgesetzt und der Inhalt aktualisiert sowie die Standardstart- und -endpunkte festgelegt werden. Anstatt den Code zu wiederholen, können wir am Ende von Initialize Clear aufrufen.

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

Jetzt können wir dem Game die BeginNewGame Methode hinzufügen, Feinde, andere Objekte und das Feld BeginNewGame und dann ein neues Skript starten.

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

Wir werden diese Methode in Update aufrufen, wenn Sie B drücken, bevor Sie mit dem Skript fortfahren.

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

Verlieren


Das Ziel des Spiels ist es, alle Feinde zu besiegen, bevor eine bestimmte Anzahl von ihnen den Endpunkt erreicht. Die Anzahl der Gegner, die benötigt werden, um die Niederlage auszulösen, hängt von der anfänglichen Gesundheit des Spielers ab, für die wir dem Game ein Konfigurationsfeld hinzufügen. Da wir Feinde zählen, verwenden wir eine Ganzzahl, nicht Float.

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



Anfangs hat ein Spieler 10 Gesundheit.

Im Falle von Awake oder dem Start eines neuen Spiels weisen wir den Anfangswert der aktuellen Gesundheit des Spielers zu.

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

Fügen Sie eine öffentliche statische EnemyReachedDestination Methode hinzu, EnemyReachedDestination Gegner Game mitteilen können, dass sie den Endpunkt erreicht haben. Verringern Sie in diesem Fall die Gesundheit des Spielers.

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

Rufen Sie diese Methode zum richtigen Zeitpunkt in Enemy.GameUpdate auf.

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

Jetzt können wir den Zustand der Niederlage in Game.Update . Wenn die Gesundheit des Spielers gleich oder kleiner als Null ist, wird die Niederlage ausgelöst. Wir protokollieren diese Informationen einfach und starten sofort ein neues Spiel, bevor wir fortfahren. Aber wir werden dies nur mit einer positiven Anfangsgesundheit tun. Dies ermöglicht es uns, 0 als anfängliche Gesundheit zu verwenden, was es unmöglich macht, zu verlieren. Daher ist es für uns bequem, die Skripte zu testen.

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

Sieg


Eine Alternative zur Niederlage ist der Sieg, der am Ende des Szenarios erreicht wird, wenn der Spieler noch am Leben ist. Das heißt, wenn das Ergebnis von GameScenario.Progess false , zeigen wir eine Siegesmeldung im Protokoll an, starten ein neues Spiel und fahren sofort damit fort.

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

Der Sieg wird jedoch nach dem Ende der letzten Pause kommen, auch wenn sich noch Feinde auf dem Spielfeld befinden. Wir müssen den Sieg verschieben, bis alle Feinde verschwunden sind. Dies kann erreicht werden, indem überprüft wird, ob die Sammlung der Feinde leer ist. Wir gehen davon aus, dass es die IsEmpty Eigenschaft hat.

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

Fügen Sie der GameBehaviorCollection die gewünschte Eigenschaft GameBehaviorCollection .

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

Zeitsteuerung


Lassen Sie uns auch die Zeitverwaltungsfunktion implementieren. Dies hilft beim Testen und ist häufig eine Gameplay-Funktion. Lassen Sie Game.Update nach einer Leertaste Game.Update und verwenden Sie dieses Ereignis, um Pausen im Spiel zu aktivieren / deaktivieren. Dies kann durch Umschalten der Time.timeScale Werte zwischen Null und Eins erfolgen. Dadurch wird die Spiellogik nicht geändert, aber alle Objekte frieren ein. Oder Sie können einen sehr kleinen Wert anstelle von 0 verwenden, z. B. 0,01, um eine extrem langsame Bewegung zu erstellen.

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

-, Game , .

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


.

, . .

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


. , . , , , .


Fügen Sie dem GameScenarioKonfigurationsregler hinzu, um die Anzahl der Zyklen festzulegen. Weisen Sie ihm standardmäßig den Wert 1 zu. Machen Sie mindestens Null, und das Skript wird endlos wiederholt. Wir werden also ein Überlebensszenario erstellen, das nicht besiegt werden kann, und es geht darum zu überprüfen, wie viel der Spieler aushalten kann.

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


Zweitaktszenario.

Jetzt GameScenario.Statesollte es die Zyklusnummer verfolgen.

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

In werden Progresswir nach Abschluss des Inkrements des Zyklus ausführen und falsenur zurückkehren, wenn eine ausreichende Anzahl von Zyklen vergangen ist. Andernfalls setzen wir den Wellenindex auf Null zurück und bewegen uns weiter.

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

Beschleunigung


Wenn es dem Spieler gelungen ist, den Zyklus einmal zu besiegen, kann er ihn problemlos wieder besiegen. Um das Szenario komplex zu halten, müssen wir die Komplexität erhöhen. Der einfachste Weg, dies zu tun, besteht darin, in nachfolgenden Zyklen alle Pausen zwischen der Schaffung von Feinden zu reduzieren. Dann erscheinen die Feinde schneller und besiegen den Spieler im Überlebensszenario unweigerlich.

Fügen Sie einen GameScenarioKonfigurationsregler hinzu, um die Beschleunigung pro Zyklus zu steuern. Dieser Wert wird nach jedem Zyklus nur zur Reduzierung der Pausen zur Zeitskala hinzugefügt. Beispielsweise hat bei einer Beschleunigung von 0,5 der erste Zyklus eine Pausengeschwindigkeit von × 1, der zweite Zyklus eine Geschwindigkeit von × 1,5, der dritte × 2, der vierte × 2,5 und so weiter.

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

Jetzt müssen Sie die Zeitskala und zu hinzufügen GameScenario.State. Sie ist anfangs immer gleich 1 und erhöht sich nach jedem Zyklus um einen bestimmten Beschleunigungswert. Verwenden Sie es zum Skalieren, Time.deltaTimebevor Sie sich entlang der Welle bewegen.

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


Drei Zyklen mit zunehmender Geschwindigkeit der feindlichen Schöpfung; zehnmal beschleunigt.

Möchten Sie Informationen zu neuen Tutorials erhalten? Folgen Sie meiner Seite auf Patreon !

Repository

PDF-Artikel

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


All Articles