Tower Defense zu einem Einheitsspiel machen - Teil 2

Bild

Dies ist der zweite Teil des Tutorials „Erstellen eines Tower Defense-Spiels in Unity“ . Wir entwickeln in Unity ein Tower Defense-Genre-Spiel und am Ende des ersten Teils haben wir gelernt, wie man Monster platziert und verbessert. Wir haben auch einen Feind, der Cookies angreift.

Der Feind weiß jedoch noch nicht, wo er suchen soll! Außerdem sieht ein Angriff allein seltsam aus. In diesem Teil des Tutorials werden wir Wellen von Feinden und Armmonstern hinzufügen, damit diese einen wertvollen Keks verteidigen können.

An die Arbeit gehen


Öffnen Sie das Projekt in Unity, das wir im letzten Teil gestoppt haben. Wenn Sie gerade zu uns gekommen sind, laden Sie den Projektentwurf herunter und öffnen Sie TowerDefense-Part2-Starter .

Öffne GameScene aus dem Ordner " Szenen ".

Wende Feinde ab


Am Ende des vorherigen Tutorials lernte der Feind, sich auf der Straße zu bewegen, aber es scheint, dass er keine Ahnung hat, wo er suchen soll.

Öffnen Sie das Skript MoveEnemy.cs in der IDE und fügen Sie die folgende Methode hinzu, um die Situation zu beheben.

private void RotateIntoMoveDirection() { //1 Vector3 newStartPosition = waypoints [currentWaypoint].transform.position; Vector3 newEndPosition = waypoints [currentWaypoint + 1].transform.position; Vector3 newDirection = (newEndPosition - newStartPosition); //2 float x = newDirection.x; float y = newDirection.y; float rotationAngle = Mathf.Atan2 (y, x) * 180 / Mathf.PI; //3 GameObject sprite = gameObject.transform.Find("Sprite").gameObject; sprite.transform.rotation = Quaternion.AngleAxis(rotationAngle, Vector3.forward); } 

RotateIntoMoveDirection dreht den Feind so, dass er immer RotateIntoMoveDirection vorne RotateIntoMoveDirection . Er macht es wie folgt:

  1. Berechnet die aktuelle Richtung des Fehlers, indem die Position des aktuellen Wegpunkts von der Position des nächsten Punkts abgezogen wird.
  2. Verwendet Mathf.Atan2 , um den Winkel im Bogenmaß zu bestimmen, in den Mathf.Atan2 gerichtet ist (der Nullpunkt befindet sich rechts). Multipliziert das Ergebnis mit 180 / Mathf.PI und konvertiert den Winkel in Grad.
  3. Schließlich erhält es das Sprite- Kind und dreht die Achse um Winkelwinkel. Beachten Sie, dass wir das Kind und nicht das Elternteil drehen, damit der Energiestreifen, den wir später hinzufügen, horizontal bleibt.

Ersetzen Sie in Update() den Kommentar // TODO: nächsten Aufruf von RotateIntoMoveDirection :

 RotateIntoMoveDirection(); 

Speichern Sie die Datei und kehren Sie zu Unity zurück. Führen Sie die Szene aus; Jetzt weiß der Feind, wohin er sich bewegt.


Jetzt weiß der Fehler, wohin es geht.

Der einzige Feind sieht nicht sehr beeindruckend aus. Wir brauchen Horden! Und wie in jedem Tower Defense-Spiel laufen Horden in Wellen!

Spieler informieren


Bevor wir anfangen, die Horden zu bewegen, müssen wir den Spieler vor dem bevorstehenden Kampf warnen. Außerdem lohnt es sich, die aktuelle Wellenzahl oben auf dem Bildschirm anzuzeigen.

Wave- Informationen werden von mehreren GameObjects benötigt, daher werden sie der GameManagerBehavior- Komponente des GameManager hinzugefügt .

Öffnen Sie die GameManagerBehavior.cs in der IDE und fügen Sie die folgenden zwei Variablen hinzu:

 public Text waveLabel; public GameObject[] nextWaveLabels; 

waveLabel speichert einen Link zum Wellenzahl-Ausgabeetikett in der oberen rechten Ecke des Bildschirms. nextWaveLabels speichert zwei GameObjects, die eine Kombination von Animationen erstellen, die wir zu Beginn einer neuen Welle zeigen werden:


Speichern Sie die Datei und kehren Sie zu Unity zurück. Wählen Sie GameManager in der Hierarchie . Klicken Sie auf den Kreis rechts neben der Wellenbeschriftung und wählen Sie im Dialogfeld Text auswählen auf der Registerkarte Szene die Option Wellenbeschriftung aus.

Stellen Sie nun die Größe für die Beschriftungen der nächsten Welle auf 2 ein . Setzen Sie nun Element 0 auf NextWaveBottomLabel , und für Element 1 ist NextWaveTopLabel dasselbe wie bei Wave Label.


So sollte das Verhalten von Game Managern jetzt aussehen

Wenn der Spieler verliert, sollte er keine Nachricht über die nächste Welle sehen. Um diese Situation zu bewältigen, kehren Sie zu GameManagerBehavior.cs zurück und fügen Sie eine weitere Variable hinzu:

 public bool gameOver = false; 

In gameOver speichern wir den Wert, ob der Spieler verloren hat.

Hier verwenden wir wieder die Eigenschaft, um die Elemente des Spiels mit der aktuellen Welle zu synchronisieren. Fügen Sie GameManagerBehavior den folgenden Code GameManagerBehavior :

 private int wave; public int Wave { get { return wave; } set { wave = value; if (!gameOver) { for (int i = 0; i < nextWaveLabels.Length; i++) { nextWaveLabels[i].GetComponent<Animator>().SetTrigger("nextWave"); } } waveLabel.text = "WAVE: " + (wave + 1); } } 

Das Erstellen einer privaten Variablen, einer Eigenschaft und eines Getters sollte Ihnen bereits vertraut sein. Aber mit dem Setter ist wieder alles etwas interessanter.

Wir weisen wave neuen value .

Dann prüfen wir, ob das Spiel beendet ist. Wenn nicht, durchlaufen Sie alle nextWaveLabels- Labels - diese Labels haben eine Animator- Komponente. Um die Animator- Animation zu aktivieren, definieren wir einen nextWave- Trigger.

Schließlich setzen wir den text von waveLabel auf wave + 1 . Warum +1 ? Normale Leute zählen nicht von vorne (ja, das ist seltsam).

In Start() den Wert dieser Eigenschaft:

 Wave = 0; 

Wir beginnen die Zählung mit der Nummer 0 Wave .

Speichern Sie die Datei und führen Sie die Szene in Unity aus. Das Wave-Label zeigt 1 korrekt an.


Für einen Spieler beginnt alles mit Welle 1.

Wellen: Erschaffe Haufen von Feinden


Es mag offensichtlich erscheinen, aber um mit einer Horde anzugreifen, müssen mehr Feinde geschaffen werden - obwohl wir nicht wissen, wie das geht. Außerdem sollten wir die nächste Welle erst erzeugen, wenn die aktuelle zerstört ist.

Das heißt, das Spiel sollte in der Lage sein, die Anwesenheit von Feinden in der Szene zu erkennen, und Tags sind eine gute Möglichkeit, Spielobjekte hier zu identifizieren.

Feindliche Markierung


Wählen Sie im Projektbrowser das Enemy- Fertighaus aus. Klicken Sie oben im Inspektor auf die Dropdown-Liste Tag und wählen Sie Tag hinzufügen .


Erstellen Sie ein Tag namens Enemy .


Wählen Sie den vorgefertigten Feind . Legen Sie im Inspektor das Enemy- Tag dafür fest.

Wellen von Feinden definieren


Jetzt müssen wir die Welle der Feinde einstellen. Öffnen Sie SpawnEnemy.cs in der IDE und fügen Sie vor SpawnEnemy die folgende Klassenimplementierung SpawnEnemy :

 [System.Serializable] public class Wave { public GameObject enemyPrefab; public float spawnInterval = 2; public int maxEnemies = 20; } 

Wave enthält enemyPrefab - die Basis zum Erstellen von Instanzen aller Feinde in dieser Welle, spawnInterval - Zeit zwischen Feinden in der Welle in Sekunden und maxEnemies - die Anzahl der in dieser Welle erstellten Feinde.

Die Klasse ist serialisierbar , dh wir können ihre Werte im Inspektor ändern.

Fügen Sie der SpawnEnemy Klasse die folgenden Variablen SpawnEnemy :

 public Wave[] waves; public int timeBetweenWaves = 5; private GameManagerBehavior gameManager; private float lastSpawnTime; private int enemiesSpawned = 0; 

Hier legen wir die Variablen für das Laichen von Feinden fest, was sehr ähnlich ist, wie wir Feinde zwischen Punkten auf der Route bewegt haben.

Wir setzen die Wellen einzelner Feinde in waves und verfolgen die Anzahl der erstellten Feinde und die Zeit, zu der sie in den enemiesSpawned und lastSpawnTime .

Nach all diesen Kills brauchen die Spieler Zeit zum Atmen, also setzen Sie timeBetweenWaves auf 5 Sekunden.

Ersetzen Sie den Inhalt von Start() folgenden Code.

 lastSpawnTime = Time.time; gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>(); 

Hier weisen wir lastSpawnTime Wert der aktuellen Zeit zu, lastSpawnTime Zeit, zu der das Skript nach dem Laden der Szene gestartet wurde. Dann bekommen wir das bereits bekannte GameManagerBehavior .

Fügen Sie Update() den folgenden Code hinzu:

 // 1 int currentWave = gameManager.Wave; if (currentWave < waves.Length) { // 2 float timeInterval = Time.time - lastSpawnTime; float spawnInterval = waves[currentWave].spawnInterval; if (((enemiesSpawned == 0 && timeInterval > timeBetweenWaves) || timeInterval > spawnInterval) && enemiesSpawned < waves[currentWave].maxEnemies) { // 3 lastSpawnTime = Time.time; GameObject newEnemy = (GameObject) Instantiate(waves[currentWave].enemyPrefab); newEnemy.GetComponent<MoveEnemy>().waypoints = waypoints; enemiesSpawned++; } // 4 if (enemiesSpawned == waves[currentWave].maxEnemies && GameObject.FindGameObjectWithTag("Enemy") == null) { gameManager.Wave++; gameManager.Gold = Mathf.RoundToInt(gameManager.Gold * 1.1f); enemiesSpawned = 0; lastSpawnTime = Time.time; } // 5 } else { gameManager.gameOver = true; GameObject gameOverText = GameObject.FindGameObjectWithTag ("GameWon"); gameOverText.GetComponent<Animator>().SetBool("gameOver", true); } 

Lassen Sie es uns Schritt für Schritt analysieren:

  1. Wir erhalten den Index der aktuellen Welle und prüfen, ob es die letzte ist.
  2. Wenn ja, berechnen wir die Zeit, die nach dem vorherigen Spawn des Feindes vergangen ist, und prüfen, ob es Zeit ist, einen Feind zu erschaffen. Hier berücksichtigen wir zwei Fälle. Wenn dies der erste Feind in der Welle ist, prüfen wir, ob timeInterval als timeBetweenWaves . Andernfalls prüfen wir, ob timeInterval als spawnInterval Wellen ist. In jedem Fall überprüfen wir, ob wir nicht alle Feinde in dieser Welle erschaffen haben.
  3. Wenn nötig, enemyPrefab den Feind und erstelle eine Instanz von enemyPrefab . Erhöhen Sie auch den Wert von enemiesSpawned .
  4. Überprüfen Sie die Anzahl der Feinde auf dem Bildschirm. Wenn sie nicht da sind und dies der letzte Feind in der Welle war, dann erschaffen wir die nächste Welle. Ebenfalls am Ende der Welle geben wir dem Spieler 10 Prozent des gesamten verbleibenden Goldes.
  5. Nach dem Sieg über die letzte Welle wird hier eine Animation des Sieges im Spiel abgespielt.

Spawn-Intervalle einstellen


Speichern Sie die Datei und kehren Sie zu Unity zurück. Wählen Sie das Straßenobjekt in der Hierarchie aus . Setzen Sie im Inspektor die Größe des Waves- Objekts auf 4 .

Wählen Sie zunächst ein Enemy- Objekt für alle vier Elemente als Enemy Prefab aus . Konfigurieren Sie die Felder Spawn Interval und Max Enemies wie folgt:

  • Element 0 : Spawn-Intervall: 2,5 , Max. Gegner: 5
  • Element 1 : Spawn-Intervall: 2 , Max. Gegner: 10
  • Element 2 : Spawn-Intervall: 2 , Max. Gegner: 15
  • Element 3 : Spawn-Intervall: 1 , Max. Gegner: 5

Das fertige Schema sollte folgendermaßen aussehen:


Natürlich können Sie mit diesen Werten experimentieren, um die Komplexität zu erhöhen oder zu verringern.

Starte das Spiel. Ja! Käfer haben die Reise zu Ihrem Keks begonnen!

Fehler

Zusätzliche Aufgabe: Füge verschiedene Arten von Feinden hinzu


Kein Turmverteidigungsspiel kann als vollständig mit nur einem Feindtyp betrachtet werden. Glücklicherweise gibt es auch Enemy2 im Prefabs- Ordner.

Wählen Sie im Inspektor Prefabs \ Enemy2 aus und fügen Sie das MoveEnemy- Skript hinzu. Stellen Sie die Geschwindigkeit auf 3 und das Enemy- Tag ein . Jetzt können Sie diesen schnellen Feind verwenden, damit sich der Spieler nicht entspannt!

Player Life Update


Obwohl Horden von Feinden den Cookie angreifen, erleidet der Spieler keinen Schaden. Aber bald werden wir es beheben. Der Spieler muss leiden, wenn er dem Feind erlaubt, sich anzuschleichen.

Öffnen Sie die GameManagerBehavior.cs in der IDE und fügen Sie die folgenden zwei Variablen hinzu:

 public Text healthLabel; public GameObject[] healthIndicator; 

Wir verwenden healthLabel um auf den Lebenswert des Spielers zuzugreifen, und healthIndicator um auf die fünf kleinen grünen Monster zuzugreifen, die Kekse kauen - sie symbolisieren einfach die Gesundheit des Spielers. Es ist lustiger als ein Standard-Gesundheitsindikator.

Gesundheitsmanagement


GameManagerBehavior nun eine Eigenschaft hinzu, die die Gesundheit des Spielers in GameManagerBehavior :

 private int health; public int Health { get { return health; } set { // 1 if (value < health) { Camera.main.GetComponent<CameraShake>().Shake(); } // 2 health = value; healthLabel.text = "HEALTH: " + health; // 3 if (health <= 0 && !gameOver) { gameOver = true; GameObject gameOverText = GameObject.FindGameObjectWithTag("GameOver"); gameOverText.GetComponent<Animator>().SetBool("gameOver", true); } // 4 for (int i = 0; i < healthIndicator.Length; i++) { if (i < Health) { healthIndicator[i].SetActive(true); } else { healthIndicator[i].SetActive(false); } } } } 

So verwalten wir die Gesundheit des Spielers. Und wieder befindet sich der Hauptteil des Codes im Setter:

  1. Wenn wir die Gesundheit des Spielers verringern, verwenden wir die CameraShake Komponente, um einen schönen Schütteleffekt zu erzielen. Dieses Skript ist im herunterladbaren Projekt enthalten und wird hier nicht berücksichtigt.
  2. Wir aktualisieren die private Variable und das Health-Label in der oberen linken Ecke des Bildschirms.
  3. Wenn der Gesundheitszustand auf 0 gesunken ist und das Ende des Spiels noch nicht erreicht ist, gameOver auf true und starten Sie die gameOver Animation.
  4. Wir entfernen eines der Monster aus den Keksen. Wenn wir sie nur ausschalten, kann dieser Teil einfacher geschrieben werden, aber hier unterstützen wir die Wiedereingliederung, falls die Gesundheit hinzugefügt wird.

Wir initialisieren Health in Start() :

 Health = 5; 

Wir setzen Health auf 5 wenn die Szene abgespielt wird.

Nachdem wir dies alles getan haben, können wir jetzt den Zustand des Players aktualisieren, wenn der Fehler im Cookie auftritt. Speichern Sie die Datei und wechseln Sie zur IDE zum Skript MoveEnemy.cs .

Gesundheitsveränderung


Um Ihre Gesundheit zu ändern, suchen Sie den Kommentar in Update() mit den Worten // TODO: und durch diesen Code ersetzen:

 GameManagerBehavior gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>(); gameManager.Health -= 1; 

Also bekommen wir das GameManagerBehavior und subtrahieren die Einheit von ihrer Health .

Speichern Sie die Datei und kehren Sie zu Unity zurück.

Wählen Sie einen GameManager in der Hierarchie aus und wählen Sie HealthLabel als Health Label aus .

Erweitern Sie das Cookie- Objekt in der Hierarchie und ziehen Sie die fünf untergeordneten HealthIndicators in das Health Indicator- Array des GameManager. Die Health-Indikatoren sind kleine grüne Monster, die Cookies essen.

Führen Sie die Szene aus und warten Sie, bis die Fehler den Cookie erreichen. Tu nichts, bis du verlierst.

Cookie Angriff

Monster Rache


Monster an Ort und Stelle? Ja Angreifen Feinde? Ja, und sie sehen bedrohlich aus! Es ist Zeit, diese Tiere zu beantworten!

Dazu benötigen wir Folgendes:

  • Spur der Gesundheit, damit der Spieler weiß, welche Feinde stark und welche schwach sind
  • Feinde in Reichweite eines Monsters erkennen
  • Eine Entscheidung treffen - auf welchen Feind geschossen werden soll
  • Ein paar Muscheln

Feindliche Gesundheitsleiste


Um das Gesundheitsband zu implementieren, verwenden wir zwei Bilder - eines für den dunklen Hintergrund und das zweite (der grüne Balken ist etwas kleiner) skalieren wir entsprechend der Gesundheit des Feindes.

Ziehen Sie vom Projektbrowser in die Szene Prefabs \ Enemy .

Ziehen Sie dann in der Hierarchie Images \ Objects \ HealthBarBackground auf Enemy , um es als untergeordnetes Element hinzuzufügen.

Setzen Sie im Inspektor die Position des HealthBarBackground auf (0, 1, -4) .

Wählen Sie dann im Projektbrowser Images \ Objects \ HealthBar aus und stellen Sie sicher, dass der Pivot links ist . Fügen Sie es dann als untergeordnetes Element des Feindes in die Hierarchie ein und setzen Sie seinen Positionswert (-0,63, 1, -5) . Setzen Sie für X Scale den Wert auf 125 .

Fügen Sie dem HealthBar -Spielobjekt ein neues C # -Skript mit dem Namen HealthBar hinzu . Später werden wir es so ändern, dass es die Länge des Gesundheitsbalkens ändert.

Stellen Sie nach Auswahl eines feindlichen Objekts in der Hierarchie sicher, dass seine Position (20, 0, 0) ist .

Klicken Sie oben im Inspektor auf Übernehmen, um alle Änderungen als Teil des Fertighauses zu speichern. Löschen Sie abschließend das Enemy- Objekt in der Hierarchie .


Wiederholen Sie nun alle diese Schritte, um eine Integritätsleiste für Prefabs \ Enemy2 hinzuzufügen .

Ändern Sie die Länge der Gesundheitsleiste


Öffnen Sie die IDE HealthBar.cs und fügen Sie die folgenden Variablen hinzu:

 public float maxHealth = 100; public float currentHealth = 100; private float originalScale; 

In maxHealth die maximale Gesundheit des Feindes gespeichert und in currentHealth die verbleibende Gesundheit. Schließlich ist in originalScale die anfängliche Größe der Integritätsleiste.

Speichern Sie das originalScale Objekt in Start() :

 originalScale = gameObject.transform.localScale.x; 

Wir speichern den x Wert der localScale Eigenschaft.

Legen Sie die Skalierung der Integritätsleiste fest, indem Sie Update() den folgenden Code hinzufügen:

 Vector3 tmpScale = gameObject.transform.localScale; tmpScale.x = currentHealth / maxHealth * originalScale; gameObject.transform.localScale = tmpScale; 

Wir können localScale in eine temporäre Variable kopieren, da wir ihren x- Wert nicht separat ändern können. Dann berechnen wir die neue x- Skala basierend auf dem aktuellen localScale des Käfers und weisen den Wert localScale erneut einer temporären Variablen zu.

Speichern Sie die Datei und starten Sie das Spiel in Unity. Über Feinden sehen Sie Gesundheitsstreifen.


Erweitern Sie während des Spiels eines der Enemy (Clone) -Objekte in der Hierarchie und wählen Sie die untergeordnete HealthBar aus . Ändern Sie den aktuellen Gesundheitswert und sehen Sie, wie sich der Gesundheitsbalken ändert.


Erkennung von Feinden in Reichweite


Jetzt müssen unsere Monster herausfinden, auf welche Feinde sie zielen sollen. Aber bevor Sie diese Gelegenheit nutzen, müssen Sie Monster und Feind vorbereiten.

Wählen Sie Project Browser Prefabs \ Monster und fügen Sie im Inspector die Circle Collider 2D- Komponente hinzu.

Setze den Radius- Parameter des Colliders auf 2,5 - dies zeigt den Angriffsradius der Monster an.

Aktivieren Sie das Kontrollkästchen Ist Auslöser , damit Objekte diesen Bereich passieren, anstatt mit ihm zu kollidieren.

Stellen Sie schließlich oben im Inspektor die Ebene des Monsters so ein, dass Raycast ignoriert wird . Klicken Sie im Dialogfeld auf Ja, untergeordnete Elemente ändern . Wenn Raycast ignorieren nicht ausgewählt ist, reagiert der Collider auf Mausklickereignisse. Dies wird ein Problem sein, da Monster Ereignisse blockieren, die für Openspot-Objekte unter ihnen bestimmt sind.


Um sicherzustellen, dass der Feind im Triggerbereich erkannt wird, müssen wir einen Collider und einen starren Körper hinzufügen, da Unity nur dann Triggerereignisse sendet, wenn ein starrer Körper an einem der Collider befestigt ist.

Wählen Sie im Projektbrowser Prefabs \ Enemy aus . Fügen Sie die Rigidbody 2D- Komponente hinzu und wählen Sie Kinematik für Körpertyp . Dies bedeutet, dass der Körper nicht von der Physik beeinflusst wird.

Fügen Sie Circle Collider 2D mit einem Radius von 1 hinzu . Wiederholen Sie diese Schritte für Prefabs \ Enemy 2 .

Die Auslöser sind so konfiguriert, dass Monster verstehen, dass sich Feinde innerhalb ihres Aktionsradius befinden.

Wir müssen noch eines vorbereiten: ein Skript, das Monstern sagt, wenn der Feind zerstört wird, damit sie keine Ausnahme auslösen, während sie weiter schießen.

Erstellen Sie ein neues C # -Skript mit dem Namen EnemyDestructionDelegate und fügen Sie es den Prefabs Enemy und Enemy2 hinzu .

Öffnen Sie EnemyDestructionDelegate.cs in der IDE und fügen Sie die folgende Delegierungsdeklaration hinzu:

 public delegate void EnemyDelegate (GameObject enemy); public EnemyDelegate enemyDelegate; 

Hier erstellen wir einen delegate , dh einen Container für eine Funktion, die als Variable übergeben werden kann.

Hinweis : Delegaten werden verwendet, wenn ein Spielobjekt andere Spielobjekte aktiv über Änderungen informieren muss. Weitere Informationen zu Delegierten finden Sie in der Unity-Dokumentation .

Fügen Sie die folgende Methode hinzu:

 void OnDestroy() { if (enemyDelegate != null) { enemyDelegate(gameObject); } } 

Wenn ein Spielobjekt zerstört wird, ruft Unity diese Methode automatisch auf und überprüft den Delegaten auf null . In unserem Fall rufen wir es mit gameObject als Parameter auf. Auf diese Weise können alle als Delegierte registrierten Befragten wissen, dass der Feind zerstört wurde.

Speichern Sie die Datei und kehren Sie zu Unity zurück.

Wir geben Monstern eine Lizenz zum Töten


Und jetzt können Monster Feinde im Radius ihrer Aktion erkennen. Fügen Sie dem Monster- Fertighaus ein neues C # -Skript hinzu und nennen Sie es ShootEnemies .

Öffnen Sie ShootEnemies.cs in der IDE und fügen Sie using Konstrukts Folgendes hinzu using um auf Generics zuzugreifen.

 using System.Collections.Generic; 

Fügen Sie eine Variable hinzu, um alle Feinde in Reichweite zu verfolgen:

 public List<GameObject> enemiesInRange; 

In enemiesInRange speichern wir alle Feinde in Reichweite.

Initialisieren Sie das Feld in Start() .

 enemiesInRange = new List<GameObject>(); 

Ganz am Anfang befinden sich keine Feinde im Aktionsradius, daher erstellen wir eine leere Liste.

enemiesInRange die Liste der enemiesInRange ! Fügen Sie dem Skript den folgenden Code hinzu:

 // 1 void OnEnemyDestroy(GameObject enemy) { enemiesInRange.Remove (enemy); } void OnTriggerEnter2D (Collider2D other) { // 2 if (other.gameObject.tag.Equals("Enemy")) { enemiesInRange.Add(other.gameObject); EnemyDestructionDelegate del = other.gameObject.GetComponent<EnemyDestructionDelegate>(); del.enemyDelegate += OnEnemyDestroy; } } // 3 void OnTriggerExit2D (Collider2D other) { if (other.gameObject.tag.Equals("Enemy")) { enemiesInRange.Remove(other.gameObject); EnemyDestructionDelegate del = other.gameObject.GetComponent<EnemyDestructionDelegate>(); del.enemyDelegate -= OnEnemyDestroy; } } 

  1. In OnEnemyDestroy entfernen wir den Feind aus feindlicher enemiesInRange . Wenn ein Feind auf einen Abzug um ein Monster tritt, wird OnTriggerEnter2D .
  2. Dann fügen wir den Feind zur enemiesInRange Liste hinzu und fügen das EnemyDestructionDelegate Ereignis OnEnemyDestroy . Wir garantieren also, dass bei Zerstörung des Feindes OnEnemyDestroy aufgerufen wird. Wir wollen nicht, dass Monster Munition für tote Feinde ausgeben, oder?
  3. In OnTriggerExit2D entfernen wir den Feind von der Liste und OnTriggerExit2D die Registrierung des Delegaten auf. Jetzt wissen wir, welche Feinde sich in Reichweite befinden.

Speichern Sie die Datei und starten Sie das Spiel in Unity. Um sicherzustellen, dass alles funktioniert, positioniere das Monster, wähle es aus und folge den Änderungen in der enemiesInRange Rangliste im enemiesInRange .

Zielauswahl


Monster wissen jetzt, welcher Feind sich in Reichweite befindet. Aber was werden sie tun, wenn sich mehrere Feinde im Radius befinden?

Natürlich werden sie denjenigen angreifen, der der Leber am nächsten liegt!

Öffne das IDE-Skript MoveEnemy.cs und füge eine neue Methode hinzu, mit der dieses Monster berechnet wird:

 public float DistanceToGoal() { float distance = 0; distance += Vector2.Distance( gameObject.transform.position, waypoints [currentWaypoint + 1].transform.position); for (int i = currentWaypoint + 1; i < waypoints.Length - 1; i++) { Vector3 startPosition = waypoints [i].transform.position; Vector3 endPosition = waypoints [i + 1].transform.position; distance += Vector2.Distance(startPosition, endPosition); } return distance; } 

Der Code berechnet die Weglänge, die der Feind noch nicht zurückgelegt hat. Dazu wird Distance , die als Abstand zwischen zwei Instanzen von Vector3 .

Wir werden diese Methode später verwenden, um herauszufinden, welches Ziel angegriffen werden soll. Obwohl unsere Monster nicht bewaffnet und hilflos sind, werden wir es zuerst tun.

Speichern Sie die Datei und kehren Sie zu Unity zurück, um mit dem Einrichten Ihrer Shells zu beginnen.

Geben wir den Monstern Muscheln. Viele Muscheln!


Ziehen Sie aus dem Projektbrowser in die Szene Images / Objects / Bullet1 . Setzen Sie die Position auf z auf -2 - die Positionen auf x und y sind nicht wichtig, da wir sie jedes Mal setzen, wenn wir während der Programmausführung eine neue Instanz des Projektils erstellen.

Fügen Sie ein neues C # -Skript mit dem Namen BulletBehavior hinzu und fügen Sie dann in der IDE die folgenden Variablen hinzu:

 public float speed = 10; public int damage; public GameObject target; public Vector3 startPosition; public Vector3 targetPosition; private float distance; private float startTime; private GameManagerBehavior gameManager; 

speed bestimmt die Geschwindigkeit der Projektile; Die damage aus dem Namen.

target , startPosition und targetPosition bestimmen die Richtung des Projektils.

distance und startTime verfolgen die aktuelle Position des Projektils. gameManager belohnt den Spieler, wenn er den Feind tötet.

Weisen Sie die Werte dieser Variablen in Start() :

 startTime = Time.time; distance = Vector2.Distance (startPosition, targetPosition); GameObject gm = GameObject.Find("GameManager"); gameManager = gm.GetComponent<GameManagerBehavior>(); 

startTimeWir stellen den Wert der aktuellen Zeit ein und berechnen den Abstand zwischen Start- und Zielposition. Auch bekommen wir wie immer GameManagerBehavior.

Fügen Sie den Update()folgenden Code hinzu, um die Bewegung des Projektils zu steuern :

 // 1 float timeInterval = Time.time - startTime; gameObject.transform.position = Vector3.Lerp(startPosition, targetPosition, timeInterval * speed / distance); // 2 if (gameObject.transform.position.Equals(targetPosition)) { if (target != null) { // 3 Transform healthBarTransform = target.transform.Find("HealthBar"); HealthBar healthBar = healthBarTransform.gameObject.GetComponent<HealthBar>(); healthBar.currentHealth -= Mathf.Max(damage, 0); // 4 if (healthBar.currentHealth <= 0) { Destroy(target); AudioSource audioSource = target.GetComponent<AudioSource>(); AudioSource.PlayClipAtPoint(audioSource.clip, transform.position); gameManager.Gold += 50; } } Destroy(gameObject); } 

  1. Wir berechnen die neue Position des Projektils unter Verwendung der Vector3.LerpInterpolation zwischen der Start- und Endposition.
  2. Wenn das Projektil erreicht targetPosition, prüfen wir, ob es noch vorhanden ist target.
  3. Wir erhalten die Komponente des HealthBarZiels und reduzieren seine Gesundheit um die Größe des damageProjektils.
  4. Wenn die Gesundheit des Feindes auf Null reduziert wird, zerstören wir sie, reproduzieren den Soundeffekt und belohnen den Spieler für seine Genauigkeit.

Speichern Sie die Datei und kehren Sie zu Unity zurück.

Wir machen große Muscheln


Wäre es nicht großartig, wenn das Monster mehr Granaten auf hohem Niveau abschießen würde? Glücklicherweise ist dies einfach zu implementieren.

Ziehen Sie das Bullet1 -Spielobjekt aus der Hierarchie auf die Registerkarte Projekt , um ein Projektil-Fertighaus zu erstellen. Entfernen Sie das Originalobjekt aus der Szene - wir werden es nicht mehr benötigen.

Duplizieren Sie das Bullet1- Fertighaus zweimal . Nennen Sie die Kopien von Bullet2 und Bullet3 .

Wählen Sie Bullet2 . Setzen Sie im Inspektor das Sprite- Feld der Sprite- Renderer- Komponente auf Images / Objects / Bullet2. Also werden wir Bullet2 etwas mehr als Bullet1 machen.

Wiederholen Sie den Vorgang, um das Sprite des Bullet3- Fertighauses in Images / Objects / Bullet3 zu ändern .

Weiter im Bullet Behaviour werden wir die Höhe des durch Granaten verursachten Schadens anpassen.

Wählen Sie das vorgefertigte Bullet1 auf der Registerkarte Projekt aus . Der Inspektor Sie sehen Einschuss Verhalten (Script) , die zugeordnet werden können Damage Wert 10 für Bullet1 , 15 für Bullet2 und 20 für Bullet3 - oder andere Werte, die Sie mögen.

Hinweis : Ich habe die Werte so geändert, dass bei höheren Werten der Schadenspreis höher wird. Dies verhindert, dass der Spieler durch das Upgrade Monster an den besten Punkten aktualisieren kann.


Fertige Muscheln - Größe nimmt mit dem Level zu

Ändern der Muschelstufe


Weisen Sie verschiedenen Monsterstufen unterschiedliche Granaten zu, damit stärkere Monster Feinde schneller zerstören.

Öffnen Sie MonsterData.cs in der IDE und fügen Sie die MonsterLevelfolgenden Variablen hinzu:

 public GameObject bullet; public float fireRate; 

Also haben wir das Fertighaus des Projektils und die Häufigkeit des Feuers für jede Stufe von Monstern festgelegt. Speichern Sie die Datei und kehren Sie zu Unity zurück, um das Monster-Setup abzuschließen.

Wählen Sie das Monster- Fertighaus im Projektbrowser aus . Der Inspektor erweitert Level in der Komponente Monster Daten (Script) . Stellen Sie die Feuerrate jedes Gegenstands auf 1 ein . Setzen Sie dann den Bullet- Parameter von Element 0, 1 und 2 auf Bullet1 , Bullet2 und Bullet3 .

Monsterlevel sollten wie folgt eingestellt werden:


Granaten töten Feinde? Ja! Lass uns das Feuer eröffnen!

Offenes Feuer


Öffnen Sie ShootEnemies.cs in der IDE und fügen Sie die folgenden Variablen hinzu:

 private float lastShotTime; private MonsterData monsterData; 

Wie der Name schon sagt, verfolgen diese Variablen die Zeit des letzten Monsterschusses sowie die Struktur MonsterData, die Informationen über die Art der Monsterschalen, die Häufigkeit des Feuers usw. enthält.

Legen Sie die Werte dieser Felder fest in Start():

 lastShotTime = Time.time; monsterData = gameObject.GetComponentInChildren<MonsterData>(); 

Hier weisen wir den lastShotTimeWert der aktuellen Zeit zu und erhalten Zugriff auf die Komponente MonsterDatadieses Objekts.

Fügen Sie die folgende Methode hinzu, um die Aufnahme zu implementieren:

 void Shoot(Collider2D target) { GameObject bulletPrefab = monsterData.CurrentLevel.bullet; // 1 Vector3 startPosition = gameObject.transform.position; Vector3 targetPosition = target.transform.position; startPosition.z = bulletPrefab.transform.position.z; targetPosition.z = bulletPrefab.transform.position.z; // 2 GameObject newBullet = (GameObject)Instantiate (bulletPrefab); newBullet.transform.position = startPosition; BulletBehavior bulletComp = newBullet.GetComponent<BulletBehavior>(); bulletComp.target = target.gameObject; bulletComp.startPosition = startPosition; bulletComp.targetPosition = targetPosition; // 3 Animator animator = monsterData.CurrentLevel.visualization.GetComponent<Animator>(); animator.SetTrigger("fireShot"); AudioSource audioSource = gameObject.GetComponent<AudioSource>(); audioSource.PlayOneShot(audioSource.clip); } 

  1. Wir erhalten die Start- und Zielpositionen der Kugel. Stellen Sie die Position z gleich z ein bulletPrefab. Zuvor haben wir die Fertighausposition des Projektils in z so eingestellt, dass das Projektil unter dem schießenden Monster, aber über den Feinden erscheint.
  2. Wir erstellen eine Instanz einer neuen Shell mit der bulletPrefabentsprechenden MonsterLevel. Zuweisen startPositionund targetPositionProjektil.
  3. Wir machen das Spiel interessanter: Wenn das Monster schießt, starten Sie die Animation des Schießens und spielen Sie den Klang des Lasers.

Alles zusammenfügen


Es ist Zeit, alles zusammenzusetzen. Definiere das Ziel und lass das Monster es betrachten.


Das Skript ShootEnemies.cs fügen Sie den Update()folgenden Code:

 GameObject target = null; // 1 float minimalEnemyDistance = float.MaxValue; foreach (GameObject enemy in enemiesInRange) { float distanceToGoal = enemy.GetComponent<MoveEnemy>().DistanceToGoal(); if (distanceToGoal < minimalEnemyDistance) { target = enemy; minimalEnemyDistance = distanceToGoal; } } // 2 if (target != null) { if (Time.time - lastShotTime > monsterData.CurrentLevel.fireRate) { Shoot(target.GetComponent<Collider2D>()); lastShotTime = Time.time; } // 3 Vector3 direction = gameObject.transform.position - target.transform.position; gameObject.transform.rotation = Quaternion.AngleAxis( Mathf.Atan2 (direction.y, direction.x) * 180 / Mathf.PI, new Vector3 (0, 0, 1)); } 

Betrachten Sie diesen Code Schritt für Schritt.

  1. Bestimmen Sie den Zweck des Monsters. Wir beginnen mit der maximal möglichen Entfernung in minimalEnemyDistance. Wir gehen in einem Zyklus aller Feinde in Reichweite umher und machen den Feind zu einem neuen Ziel, wenn seine Entfernung zum Cookie geringer ist als die derzeit kleinste.
  2. Wir rufen an, Shootwenn die verstrichene Zeit größer als die Schussfrequenz des Monsters ist, und stellen den lastShotTimeWert der aktuellen Zeit ein.
  3. Wir berechnen den Drehwinkel zwischen dem Monster und seinem Ziel. Wir drehen das Monster in diesen Winkel. Jetzt wird er immer auf das Ziel schauen.

Speichern Sie die Datei und starten Sie das Spiel in Unity. Monster werden verzweifelt anfangen, Cookies zu schützen. Wir sind endlich fertig!

Wohin als nächstes gehen


Das fertige Projekt kann hier heruntergeladen werden .

Wir haben in diesem Tutorial großartige Arbeit geleistet und jetzt haben wir ein großartiges Spiel.

Hier einige Ideen für die Weiterentwicklung des Projekts:

  • Weitere Arten von Feinden und Monstern
  • Verschiedene Wege der Feinde
  • Verschiedene Spielstufen

Jeder dieser Aspekte erfordert nur minimale Änderungen und kann das Spiel unterhaltsamer machen. Wenn Sie ein neues Spiel basierend auf diesem Tutorial erstellen, werde ich es gerne spielen. Bitte teilen Sie einen Link dazu.

Interessante Gedanken zur Erstellung eines Tower Defense-Hit-Spiels finden Sie in diesem Interview .

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


All Articles