[
Der erste und
zweite Teil des Tutorials]
- Wir platzieren auf dem Feld des Turms.
- Wir zielen mit Hilfe der Physik auf Feinde.
- Wir verfolgen sie, solange es möglich ist.
- Wir schießen sie mit einem Laserstrahl.
Dies ist der dritte Teil einer Reihe von Tutorials zur Erstellung eines einfachen Tower Defense-Genres. Es beschreibt die Schaffung von Türmen, die auf Feinde zielen und auf diese schießen.
Das Tutorial wurde in Unity 2018.3.0f2 erstellt.
Erhitzen wir die Feinde.Turmschöpfung
Mauern verlangsamen nur Feinde und verlängern den Weg, den sie gehen müssen. Das Ziel des Spiels ist es jedoch, die Feinde zu zerstören, bevor sie den Endpunkt erreichen. Dieses Problem wird gelöst, indem Türme auf dem Feld platziert werden, die auf sie schießen.
Kachelinhalt
Türme sind eine andere Art von
GameTileContent
.
GameTileContent
Sie daher einen Eintrag für sie in
GameTileContent
.
public enum GameTileContentType { Empty, Destination, Wall, SpawnPoint, Tower€ }
In diesem Tutorial werden wir nur einen Turmtyp unterstützen, der implementiert werden kann, indem
GameTileContentFactory
einem Link zum Tower-Fertighaus versehen wird, von dem eine Instanz auch über
Get
erstellt werden kann.
[SerializeField] GameTileContent towerPrefab = default; public GameTileContent Get (GameTileContentType type) { switch (type) { … case GameTileContentType.Tower€: return Get(towerPrefab); } … }
Aber die Türme müssen schießen, daher muss ihr Zustand aktualisiert werden und sie benötigen ihren eigenen Code. Erstellen Sie zu diesem Zweck eine
Tower
Klasse, die die
GameTileContent
Klasse erweitert.
using UnityEngine; public class Tower : GameTileContent {}
Sie können dem Tower-Fertighaus eine eigene Komponente hinzufügen, indem Sie den Feldtyp der Fabrik in
Tower
ändern. Da die Klasse immer noch als
GameTileContent
, muss nichts anderes geändert werden.
Tower towerPrefab = default;
Fertighaus
Erstellen Sie ein Fertighaus für den Turm. Sie können beginnen, indem Sie das Wandfertigteil duplizieren und die
GameTileContent
Komponente durch die
Tower
Komponente ersetzen und dann ihren Typ in
Tower ändern. Speichern Sie den Wandwürfel als Basis des Turms, damit der Turm an die Wände passt. Dann legen Sie einen weiteren Würfel darauf. Ich gab ihm eine Skala von 0,5. Lege einen weiteren Würfel darauf, der auf einen Turm hinweist. Dieser Teil zielt und schießt auf Feinde.
Drei Würfel bilden einen Turm.Der Turm dreht sich und da er einen Kollider hat, wird er von einem physischen Motor verfolgt. Wir müssen jedoch nicht so präzise sein, da wir Turmkollider nur zur Auswahl von Zellen verwenden. Dies kann ungefähr erfolgen. Entfernen Sie den Revolverwürfelkollider und ändern Sie den Turmwürfelkollider so, dass er beide Würfel bedeckt.
Collider Würfelturm.Der Turm schießt einen Laserstrahl. Es kann auf viele Arten visualisiert werden, aber wir verwenden nur einen durchscheinenden Würfel, den wir dehnen, um einen Strahl zu bilden. Jeder Turm muss einen eigenen Balken haben, also fügen Sie ihn dem vorgefertigten Turm hinzu. Platzieren Sie es so im Turm, dass es standardmäßig ausgeblendet ist, und geben Sie ihm einen kleineren Maßstab, z. B. 0,2. Machen wir es zu einem Kind der vorgefertigten Wurzel, nicht des Turmwürfels.
Versteckter Würfel eines Laserstrahls.Erstellen Sie ein geeignetes Material für den Laserstrahl. Ich habe gerade das durchscheinende schwarze Standardmaterial verwendet, alle Reflexionen ausgeschaltet und ihm eine rot emittierte Farbe gegeben.
Das Material des Laserstrahls.Stellen Sie sicher, dass der Laserstrahl keinen Kollider hat, und schalten Sie auch den Wurf und den Schatten aus.
Der Laserstrahl interagiert nicht mit Schatten.Nachdem die Erstellung des Turmfertigteils abgeschlossen ist, werden wir es der Fabrik hinzufügen.
Fabrik mit einem Turm.Turmplatzierung
Wir werden Türme mit einer anderen Schaltmethode hinzufügen und entfernen. Sie können
GameBoard.ToggleWall
einfach duplizieren, indem Sie den Methodennamen und den Inhaltstyp ändern.
public void ToggleTower (GameTile tile) { if (tile.Content.Type == GameTileContentType.Tower€) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.Tower€); if (!FindPaths()) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } } }
Game.HandleTouch
in
Game.HandleTouch
die Umschalttaste gedrückt halten, werden eher Türme als Wände
Game.HandleTouch
.
void HandleTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { if (Input.GetKey(KeyCode.LeftShift)) { board.ToggleTower(tile); } else { board.ToggleWall(tile); } } }
Türme auf dem Feld.Pfadblockierung
Bisher können nur Mauern die Suche nach einem Pfad blockieren, sodass sich Feinde durch Türme bewegen.
GameTileContent
wir
GameTileContent
Eigenschaft
GameTileContent
, die angibt, ob der Inhalt den Pfad blockiert. Der Weg ist blockiert, wenn es sich um eine Mauer oder einen Turm handelt.
public bool BlocksPath => Type == GameTileContentType.Wall || Type == GameTileContentType.Tower€;
Verwenden Sie diese Eigenschaft in
GameTile.GrowPathTo
anstatt den Inhaltstyp zu überprüfen.
GameTile GrowPathTo (GameTile neighbor, Direction direction) { … return
Jetzt ist der Weg durch Mauern und Türme blockiert.Ersetzen Sie die Wände
Höchstwahrscheinlich wird der Spieler die Wände oft durch Türme ersetzen. Es wird für ihn unpraktisch sein, zuerst die Mauer zu entfernen, und außerdem können Feinde in diese vorübergehend erscheinende Lücke eindringen. Sie können einen direkten Ersatz implementieren, indem Sie
GameBoard.ToggleTower
zwingen, zu überprüfen, ob sich die Wand derzeit auf der Kachel befindet. Wenn ja, ersetzen Sie es sofort durch einen Turm. In diesem Fall müssen wir nicht nach anderen Wegen suchen, da die Kachel sie immer noch blockiert.
public void ToggleTower (GameTile tile) { if (tile.Content.Type == GameTileContentType.Tower) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else if (tile.Content.Type == GameTileContentType.Empty) { … } else if (tile.Content.Type == GameTileContentType.Wall) { tile.Content = contentFactory.Get(GameTileContentType.Tower); } }
Wir zielen auf Feinde
Ein Turm kann seine Aufgabe nur erfüllen, wenn er einen Feind findet. Nachdem sie den Feind gefunden hat, muss sie entscheiden, auf welchen Teil sie zielen soll.
Zielpunkt
Um Ziele zu erkennen, verwenden wir die Physik-Engine. Wie beim Tower Collider muss der feindliche Collider nicht unbedingt mit seiner Form übereinstimmen. Sie können den einfachsten Collider auswählen, dh eine Kugel. Nachdem wir den Feind entdeckt haben, verwenden wir die Position des Spielobjekts mit dem daran befestigten Collider als Zielpunkt.
Wir können den Collider nicht am Wurzelobjekt des Feindes befestigen, da er nicht immer mit der Position des Modells übereinstimmt und den Turm auf den Boden zielen lässt. Das heißt, Sie müssen den Collider irgendwo auf dem Modell platzieren. Die Physik-Engine gibt uns einen Link zu diesem Objekt, den wir zum Zielen verwenden können, aber wir benötigen weiterhin Zugriff auf die
Enemy
Komponente des Stammobjekts. Um die Aufgabe zu vereinfachen, erstellen
TargetPoint
die
TargetPoint
Komponente. Geben wir ihm eine Eigenschaft für den privaten Auftrag und den öffentlichen Empfang der
Enemy
Komponente und eine weitere Eigenschaft für die Erlangung seiner Position in der Welt.
using UnityEngine; public class TargetPoint : MonoBehaviour { public Enemy Enemy€ { get; private set; } public Vector3 Position => transform.position; }
Geben wir ihm eine
Awake
Methode, die einen Link zu seiner
Enemy
Komponente herstellt. Gehen Sie mit
transform.root
direkt zum
transform.root
. Wenn die
Enemy
Komponente nicht vorhanden ist, haben wir beim Erstellen des Feindes einen Fehler gemacht. Fügen wir also eine Erklärung dazu hinzu.
void Awake () { Enemy€ = transform.root.GetComponent<Enemy>(); Debug.Assert(Enemy€ != null, "Target point without Enemy root!", this); }
Außerdem muss der Collider an dasselbe Spielobjekt angehängt werden, an das
TargetPoint
angehängt ist.
Debug.Assert(Enemy€ != null, "Target point without Enemy root!", this); Debug.Assert( GetComponent<SphereCollider>() != null, "Target point without sphere collider!", this );
Fügen Sie dem vorgefertigten Würfel des Feindes eine Komponente und einen Collider hinzu. Dadurch zielen die Türme auf die Mitte des Würfels. Wir verwenden einen Kugelcollider mit einem Radius von 0,25. Der Würfel hat eine Skala von 0,5, sodass der wahre Radius des Kolliders 0,125 beträgt. Dank dessen muss der Feind den Entfernungskreis des Turms visuell überqueren, und erst nach einiger Zeit wird das eigentliche Ziel erreicht. Die Größe des Kolliders wird auch von der zufälligen Skala des Feindes beeinflusst, sodass seine Größe im Spiel ebenfalls geringfügig variiert.
Ein Feind mit einem Zielpunkt und einem Kollider auf einem Würfel.Feindliche Schicht
Türme kümmern sich nur um Feinde, und sie zielen nicht auf etwas anderes, also werden wir alle Feinde in eine separate Schicht legen. Wir werden Ebene 9 verwenden. Ändern Sie ihren Namen im Fenster "
Ebenen und Tags" in "
Feind". Dies kann über die Option "
Ebenen bearbeiten " im Dropdown-Menü "
Ebenen" in der oberen rechten Ecke des Editors geöffnet werden.
Schicht 9 wird für Feinde verwendet.Diese Schicht wird nur zur Erkennung von Feinden und nicht für physische Interaktionen benötigt. Lassen Sie uns darauf hinweisen, indem Sie sie in der
Layer Collision Matrix deaktivieren, die sich im Bereich
Physik der Projektparameter befindet.
Matrix von Schichtkollisionen.Stellen Sie sicher, dass sich das Spielobjekt des Zielpunkts auf der gewünschten Ebene befindet. Der Rest des Fertighauses des Feindes befindet sich möglicherweise auf anderen Ebenen, aber es ist einfacher, alles zu koordinieren und das gesamte Fertighaus in der
feindlichen Ebene zu platzieren. Wenn Sie die Ebene des Stammobjekts ändern, werden Sie aufgefordert, die Ebene für alle untergeordneten Objekte zu ändern.
Feind auf der rechten Ebene.TargetPoint
wir die Aussage hinzu, dass
TargetPoint
wirklich auf der richtigen Ebene liegt.
void Awake () { … Debug.Assert(gameObject.layer == 9, "Target point on wrong layer!", this); }
Außerdem müssen die Aktionen des Spielers von feindlichen Kollidern ignoriert werden. Dies kann erreicht werden, indem
Physics.Raycast
in
GameBoard.GetTile
Physics.Raycast
GameBoard.GetTile
. Diese Methode hat eine Form, die den Abstand zum Strahl und zur Ebenenmaske als zusätzliche Argumente verwendet. Wir geben ihm standardmäßig den maximalen Abstand und die maximale Ebenenmaske, d. H. 1.
public GameTile GetTile (Ray ray) { if (Physics.Raycast(ray, out RaycastHit hit, float.MaxValue, 1)) { … } return null; }
Sollte die Ebenenmaske nicht 0 sein?Der Standard-Ebenenindex ist Null, aber wir übergeben die Ebenenmaske. Die Maske ändert die einzelnen Bits einer Ganzzahl in 1, wenn die Ebene aktiviert werden muss. In diesem Fall müssen Sie nur das erste Bit setzen, dh das niedrigstwertige, dh 2 0 , was 1 entspricht.
Aktualisieren des Kachelinhalts
Türme können ihre Aufgabe nur ausführen, wenn ihr Status aktualisiert wird. Gleiches gilt für den Inhalt der gesamten Kacheln, obwohl der Rest des Inhalts bisher nichts bewirkt.
GameTileContent
daher
GameTileContent
virtuelle
GameTileContent
Methode
GameUpdate
, die standardmäßig nichts bewirkt.
public virtual void GameUpdate () {}
Lassen Sie
Tower
neu definieren, auch wenn es vorerst einfach in der Konsole anzeigt, dass es nach einem Ziel sucht.
public override void GameUpdate () { Debug.Log("Searching for target..."); }
GameBoard
befasst sich mit Kacheln und deren Inhalten, sodass auch
GameBoard
werden kann, welche Inhalte aktualisiert werden müssen.
GameUpdate
Sie dazu die Liste und die öffentliche
GameUpdate
Methode hinzu, mit der alle
GameUpdate
in der Liste aktualisiert werden.
List<GameTileContent> updatingContent = new List<GameTileContent>(); … public void GameUpdate () { for (int i = 0; i < updatingContent.Count; i++) { updatingContent[i].GameUpdate(); } }
In unserem Tutorial müssen Sie nur die Türme aktualisieren. Ändern Sie
ToggleTower
so, dass bei Bedarf Inhalte
ToggleTower
und entfernt werden. Wenn auch andere Inhalte benötigt werden, brauchen wir einen allgemeineren Ansatz, aber im Moment reicht dies aus.
public void ToggleTower (GameTile tile) { if (tile.Content.Type == GameTileContentType.Tower) { updatingContent.Remove(tile.Content); tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.Tower);
Damit dies funktioniert, reicht es jetzt aus, das Feld in
Game.Update
einfach zu aktualisieren. Wir werden das Feld nach den Feinden aktualisieren. Dank dessen können die Türme genau zielen, wo sich die Feinde befinden. Wenn wir es anders machen würden, würden die Türme zielen, wo sich die Feinde im letzten Frame befanden.
void Update () { … enemies.GameUpdate(); board.GameUpdate(); }
Zielbereich
Türme haben einen begrenzten Zielradius. Machen wir es uns benutzerdefiniert, indem wir der
Tower
Klasse ein Feld hinzufügen. Der Abstand wird von der Mitte des Turmplättchens gemessen, sodass in einem Bereich von 0,5 nur das eigene Plättchen abgedeckt wird. Daher wäre ein angemessener Mindest- und Standardbereich 1,5, der die meisten benachbarten Fliesen abdeckt.
[SerializeField, Range(1.5f, 10.5f)] float targetingRange = 1.5f;
Zielbereich 2.5.Lassen Sie uns die Reichweite mit Gizmo visualisieren. Wir müssen es nicht ständig sehen, daher erstellen wir die
OnDrawGizmosSelected
Methode, die nur für die ausgewählten Objekte aufgerufen wird. Wir zeichnen den gelben Rahmen der Kugel mit einem Radius, der dem Abstand entspricht und relativ zum Turm zentriert ist. Stellen Sie es leicht über den Boden, damit es immer gut sichtbar ist.
void OnDrawGizmosSelected () { Gizmos.color = Color.yellow; Vector3 position = transform.localPosition; position.y += 0.01f; Gizmos.DrawWireSphere(position, targetingRange); }
Gizmo Zielstrecke.Jetzt können wir sehen, welcher der Feinde ein erschwingliches Ziel für jeden der Türme ist. Die Auswahl von Türmen im Szenenfenster ist jedoch unpraktisch, da wir einen der untergeordneten Würfel auswählen und dann zum Stammobjekt des Turms wechseln müssen. Andere Arten von Flieseninhalten leiden ebenfalls unter dem gleichen Problem. Wir können die Auswahl des
GameTileContent
des
GameTileContent
im
GameTileContent
indem
GameTileContent
dem
GameTileContent
das
SelectionBase
Attribut
GameTileContent
.
[SelectionBase] public class GameTileContent : MonoBehaviour { … }
Zielerfassung
Fügen
TargetPoint
der
Tower
Klasse ein
TargetPoint
Feld hinzu, damit sie das erfasste Ziel verfolgen kann. Anschließend
GameUpdate
wir
GameUpdate
, um die neue
AquireTarget
Methode
AquireTarget
, die Informationen darüber zurückgibt, ob das Ziel gefunden wurde. Bei der Erkennung wird eine Meldung in der Konsole angezeigt.
TargetPoint target; public override void GameUpdate () { if (AcquireTarget()) { Debug.Log("Acquired target!"); } }
In
AcquireTarget
wir alle verfügbaren Ziele, indem wir
Physics.OverlapSphere
mit einer
Physics.OverlapSphere
und einem Bereich als Argumente aufrufen. Das Ergebnis ist ein
Collider
Array, das alle
Collider
enthält, die mit der Kugel in Kontakt stehen. Wenn die Länge des Arrays positiv ist, gibt es mindestens einen Zielpunkt, und wir wählen einfach den ersten aus. Nehmen Sie die
TargetPoint
Komponente, die immer vorhanden sein muss, weisen Sie sie dem Zielfeld zu und melden Sie den Erfolg. Andernfalls löschen wir das Ziel und melden den Fehler.
bool AcquireTarget () { Collider[] targets = Physics.OverlapSphere( transform.localPosition, targetingRange ); if (targets.Length > 0) { target = targets[0].GetComponent<TargetPoint>(); Debug.Assert(target != null, "Targeted non-enemy!", targets[0]); return true; } target = null; return false; }
Wir erhalten garantiert die richtigen Zielpunkte, wenn wir Kollider nur auf der Ebene der Feinde berücksichtigen. Dies ist Ebene 9, daher übergeben wir die entsprechende Ebenenmaske.
const int enemyLayerMask = 1 << 9; … bool AcquireTarget () { Collider[] targets = Physics.OverlapSphere( transform.localPosition, targetingRange, enemyLayerMask ); … }
Wie funktioniert diese Bitmaske?Da die feindliche Schicht einen Index von 9 hat, sollte das zehnte Bit der Bitmaske den Wert 1 haben. Dies entspricht einer ganzen Zahl 2 9 , dh 512. Ein solcher Bitmaskendatensatz ist jedoch nicht intuitiv. Wir können auch ein binäres Literal schreiben, zum Beispiel 0b10_0000_0000
, aber dann müssen wir Nullen zählen. In diesem Fall wäre die bequemste Eingabe die Verwendung des Linksverschiebungsoperators <<
, der die Bits nach links verschiebt. das entspricht einer Zahl in der Potenz von zwei.
Sie können das erfasste Ziel visualisieren, indem Sie eine Gizmo-Linie zwischen den Positionen des Turms und des Ziels zeichnen.
void OnDrawGizmosSelected () { … if (target != null) { Gizmos.DrawLine(position, target.Position); } }
Visualisierung von Zielen.Warum nicht Methoden wie OnTriggerEnter verwenden?Der Vorteil der manuellen Überprüfung von Querschnittszielen besteht darin, dass wir dies nur bei Bedarf tun können. Es gibt keinen Grund, nach Zielen zu suchen, wenn der Turm bereits eines hat. Indem wir alle potenziellen Ziele gleichzeitig erreichen, müssen wir nicht für jeden Turm eine Liste potenzieller Ziele verarbeiten, die sich ständig ändert.
Zielsperre
Das zu erfassende Ziel hängt von der Reihenfolge ab, in der sie von der physischen Engine dargestellt werden, dh es ist tatsächlich willkürlich. Daher scheint sich das erfasste Ziel ohne Grund zu ändern. Nachdem der Turm das Ziel erhalten hat, ist es für sie logischer, ihr Ziel zu folgen und nicht zu einem anderen zu wechseln. Fügen Sie eine
TrackTarget
Methode hinzu, die eine solche Verfolgung implementiert und Informationen darüber zurückgibt, ob sie erfolgreich war. Zunächst teilen wir Ihnen nur mit, ob das Ziel erfasst wurde.
bool TrackTarget () { if (target == null) { return false; } return true; }
Wir werden diese Methode in
GameUpdate
und nur wenn false zurückgegeben wird, werden wir
AcquireTarget
. Wenn die Methode true zurückgibt, haben wir ein Ziel. Dies kann erreicht werden, indem beide Methodenaufrufe beim
if
Operator einer
if
Prüfung unterzogen werden. Wenn der erste Operand
true
zurückgibt, wird der zweite nicht geprüft und der Aufruf wird verpasst. Der AND-Operator verhält sich ähnlich.
public override void GameUpdate () { if (TrackTarget() || AcquireTarget()) { Debug.Log("Locked on target!"); } }
Ziele verfolgen.Infolgedessen werden die Türme am Ziel befestigt, bis es den Endpunkt erreicht und zerstört wird. Wenn Sie wiederholt Feinde verwenden, müssen Sie stattdessen die Richtigkeit des Links überprüfen, wie dies bei Links zu Zahlen der Fall ist, die in einer Reihe von Tutorials zur
Objektverwaltung verarbeitet wurden.
Um Ziele nur dann zu verfolgen, wenn sie sich in Reichweite befinden, muss
TrackTarget
die Entfernung zwischen dem Turm und dem Ziel verfolgen. Wenn der Bereichswert überschritten wird, muss das Ziel zurückgesetzt und false zurückgegeben werden. Sie können die
Vector3.Distance
Methode für diese Prüfung verwenden.
bool TrackTarget () { if (target == null) { return false; } Vector3 a = transform.localPosition; Vector3 b = target.Position; if (Vector3.Distance(a, b) > targetingRange) { target = null; return false; } return true; }
Dieser Code berücksichtigt jedoch nicht den Radius des Colliders. Infolgedessen kann der Turm das Ziel verlieren und es dann erneut erfassen, um es im nächsten Frame nicht mehr zu verfolgen, und so weiter. Wir können dies vermeiden, indem wir dem Bereich einen Kolliderradius hinzufügen.
if (Vector3.Distance(a, b) > targetingRange + 0.125f) { … }
Dies gibt uns die richtigen Ergebnisse, aber nur, wenn die Größe des Feindes nicht geändert wird. Da wir jedem Feind eine zufällige Skala geben, müssen wir diese bei der Änderung der Reichweite berücksichtigen. Dazu müssen wir uns die von
Enemy
vorgegebene Skala merken und sie mit der Getter-Eigenschaft öffnen.
public float Scale { get; private set; } … public void Initialize (float scale, float speed, float pathOffset) { Scale = scale; … }
Jetzt können wir den korrekten Bereich in
Tower.TrackTarget
.
if (Vector3.Distance(a, b) > targetingRange + 0.125f * target.Enemy€.Scale) { … }
Wir synchronisieren die Physik
Alles scheint gut zu funktionieren, aber Türme, die auf die Mitte des Feldes zielen können, können Ziele erfassen, die außerhalb der Reichweite liegen sollten. Sie können diese Ziele nicht verfolgen, daher sind sie nur für einen Frame darauf festgelegt.
Falsches Zielen.Dies geschieht, weil der Status der physischen Engine nicht perfekt mit dem Status des Spiels synchronisiert ist. Instanzen aller Feinde entstehen am Ursprung der Welt, der mit dem Mittelpunkt des Feldes zusammenfällt. Dann bewegen wir sie zum Punkt der Schöpfung, aber die Physik-Engine weiß nicht sofort davon.
Sie können die sofortige Synchronisierung aktivieren, die beim Ändern von Objekttransformationen auftritt, indem Sie
Physics.autoSyncTransforms
auf
true
. Standardmäßig ist es jedoch deaktiviert, da es viel effizienter ist, alles miteinander und bei Bedarf zu synchronisieren. In unserem Fall ist eine Synchronisierung nur erforderlich, wenn der Status der Türme aktualisiert wird. Wir können es ausführen, indem
Physics.SyncTransforms
zwischen Feind- und
Game.Update
in
Game.Update
.
void Update () { … enemies.GameUpdate(); Physics.SyncTransforms(); board.GameUpdate(); }
Ignoriere die Höhe
Tatsächlich findet unser Gameplay in 2D statt. Ändern wir daher den
Tower
so, dass beim Zielen und Verfolgen nur die X- und Z-Koordinaten berücksichtigt werden. Die physische Engine arbeitet im 3D-Raum, aber im Wesentlichen können wir
AcquireTarget
in 2D ausführen:
AcquireTarget
die Kugel so, dass sie alle Kollider unabhängig davon abdeckt von ihrer vertikalen Position. Dies kann durch Verwendung einer Kapsel anstelle einer Kugel erfolgen, deren zweiter Punkt mehrere Einheiten über dem Boden liegt (z. B. drei).
bool AcquireTarget () { Vector3 a = transform.localPosition; Vector3 b = a; by += 3f; Collider[] targets = Physics.OverlapCapsule( a, b, targetingRange, enemyLayerMask ); … }
Ist es nicht möglich, eine physische 2D-Engine zu verwenden?, XZ, 2D- XY. , , 2D- . 3D-.
Es ist auch notwendig zu ändern TrackTarget
. Natürlich können wir 2D-Vektoren verwenden und Vector2.Distance
, aber lassen Sie uns die Berechnungen selbst durchführen und stattdessen die Quadrate der Entfernungen vergleichen, wird dies ausreichen. Wir müssen also nicht mehr die Quadratwurzel berechnen. bool TrackTarget () { if (target == null) { return false; } Vector3 a = transform.localPosition; Vector3 b = target.Position; float x = ax - bx; float z = az - bz; float r = targetingRange + 0.125f * target.Enemy€.Scale; if (x * x + z * z > r * r) { target = null; return false; } return true; }
Wie funktionieren diese mathematischen Berechnungen?2D- , . , . , , .
Speicherzuordnung vermeiden
Der Nachteil der Verwendung Physics.OverlapCapsule
besteht darin, dass für jeden Aufruf ein neues Array zugewiesen wird. Dies kann vermieden werden, indem das Array einmal zugewiesen und eine alternative Methode OverlapCapsuleNonAlloc
mit dem Array als zusätzlichem Argument aufgerufen wird. Die Länge des übertragenen Arrays bestimmt die Anzahl der Ergebnisse. Alle potenziellen Ziele außerhalb des Arrays werden verworfen. Trotzdem verwenden wir nur das erste Element, daher reicht uns ein Array mit der Länge 1 aus.Anstelle eines Arrays wird OverlapCapsuleNonAlloc
die Anzahl der aufgetretenen Kollisionen bis zum maximal zulässigen Wert zurückgegeben. Dies ist die Anzahl, die wir anstelle der Länge des Arrays überprüfen. static Collider[] targetsBuffer = new Collider[1]; … bool AcquireTarget () { Vector3 a = transform.localPosition; Vector3 b = a; by += 2f; int hits = Physics.OverlapCapsuleNonAlloc( a, b, targetingRange, targetsBuffer, enemyLayerMask ); if (hits > 0) { target = targetsBuffer[0].GetComponent<TargetPoint>(); Debug.Assert(target != null, "Targeted non-enemy!", targetsBuffer[0]); return true; } target = null; return false; }
Wir schießen auf Feinde
Jetzt, da wir ein echtes Ziel haben, ist es Zeit, es zu schießen. Das Schießen umfasst das Zielen, einen Laserschuss und das Verursachen von Schaden.Revolver zielen
Um den Turm zum Ziel zu lenken, muss die Klasse Tower
eine Verbindung zur Transform
Turmkomponente haben. Fügen Sie dazu ein Konfigurationsfeld hinzu und verbinden Sie es mit dem Tower-Fertighaus. [SerializeField] Transform turret = default;
Der angebrachte Turm.Wenn GameUpdate
es ein echtes Ziel gibt, müssen wir es abschießen. Geben Sie den Aufnahmecode in eine separate Methode ein. Lassen Sie ihn den Turm in Richtung des Ziels drehen und seine Methode Transform.LookAt
mit dem Zielpunkt als Argument aufrufen . public override void GameUpdate () { if (TrackTarget() || AcquireTarget()) {
Nur zielen.Wir schießen einen Laser
Um den Laserstrahl zu positionieren, benötigt die Klasse Tower
auch eine Verknüpfung dazu. [SerializeField] Transform turret = default, laserBeam = default;
Wir haben einen Laserstrahl angeschlossen.Um einen Würfel in einen echten Laserstrahl zu verwandeln, müssen Sie drei Schritte ausführen. Erstens sollte seine Ausrichtung der Ausrichtung des Turmes entsprechen. Dies kann durch Kopieren der Drehung erfolgen. void Shoot () { Vector3 point = target.Position; turret.LookAt(point); laserBeam.localRotation = turret.localRotation; }
Zweitens skalieren wir den Laserstrahl so, dass seine Länge dem Abstand zwischen dem lokalen Ursprungspunkt des Turmes und dem Zielpunkt entspricht. Wir skalieren es entlang der Z-Achse, dh der lokalen Achse, die auf das Ziel gerichtet ist. Um die ursprüngliche XY-Skala beizubehalten, schreiben wir die ursprüngliche Skala auf, wenn wir den Awake-Turm aufwecken. Vector3 laserBeamScale; void Awake () { laserBeamScale = laserBeam.localScale; } … void Shoot () { Vector3 point = target.Position; turret.LookAt(point); laserBeam.localRotation = turret.localRotation; float d = Vector3.Distance(turret.position, point); laserBeamScale.z = d; laserBeam.localScale = laserBeamScale; }
Drittens platzieren wir den Laserstrahl in der Mitte zwischen dem Turm und dem Zielpunkt. laserBeam.localScale = laserBeamScale; laserBeam.localPosition = turret.localPosition + 0.5f * d * laserBeam.forward;
Laserschießen.Ist es nicht möglich, einen Laserstrahl zum Kind eines Turmes zu machen?, , forward. , . .
Dies funktioniert, während der Turm am Ziel befestigt ist. Wenn jedoch kein Ziel vorhanden ist, bleibt der Laser aktiv. Wir können die Laseranzeige ausschalten, indem wir ihre Skala auf GameUpdate
0 setzen. public override void GameUpdate () { if (TrackTarget() || AcquireTarget()) { Shoot(); } else { laserBeam.localScale = Vector3.zero; } }
Leerlauftürme feuern nicht.Feindliche Gesundheit
Bisher berühren unsere Laserstrahlen nur die Feinde und beeinflussen sie nicht mehr. Es muss sichergestellt werden, dass der Laser Feinden Schaden zufügt. Wir wollen Feinde nicht sofort zerstören, also geben wir die Enemy
Eigenschaft der Gesundheit. Sie können einen beliebigen Wert als Gesundheit auswählen, nehmen wir also 100. Für große Feinde ist es jedoch logischer, mehr Gesundheit zu haben. Daher führen wir hierfür einen Koeffizienten ein. float Health { get; set; } … public void Initialize (float scale, float speed, float pathOffset) { … Health = 100f * scale; }
Fügen Sie eine öffentliche Methode hinzu ApplyDamage
, die den Parameter von der Gesundheit abzieht, um Unterstützung für das Verursachen von Schaden hinzuzufügen . Wir gehen davon aus, dass der Schaden nicht negativ ist, und fügen daher eine Erklärung dazu hinzu. public void ApplyDamage (float damage) { Debug.Assert(damage >= 0f, "Negative damage applied."); Health -= damage; }
Wir werden den Feind nicht sofort loswerden, sobald seine Gesundheit Null erreicht. Zu Beginn wird geprüft, ob die Gesundheit erschöpft ist und der Feind zerstört ist GameUpdate
. public bool GameUpdate () { if (Health <= 0f) { OriginFactory.Reclaim(this); return false; } … }
Dank dessen schießen alle Türme im Wesentlichen gleichzeitig und nicht nacheinander, wodurch sie zu anderen Zielen wechseln können, wenn der vorherige Turm den Feind zerstört hat, auf den sie auch abgezielt haben.Schaden pro Sekunde
Jetzt müssen wir bestimmen, wie viel Schaden der Laser anrichten wird. Fügen Sie dazu Tower
das Konfigurationsfeld hinzu. Da der Laserstrahl kontinuierlichen Schaden verursacht, werden wir ihn als Schaden pro Sekunde ausdrücken. Wir Shoot
wenden es auf die Enemy
Zielkomponente mit Multiplikation mit der Deltazeit an. [SerializeField, Range(1f, 100f)] float damagePerSecond = 10f; … void Shoot () { … target.Enemy.ApplyDamage(damagePerSecond * Time.deltaTime); }
Der Schaden jedes Turms beträgt 20 Einheiten pro Sekunde.Zufälliges Zielen
Da wir immer das erste verfügbare Ziel auswählen, hängt das Zielverhalten von der Reihenfolge ab, in der die Physik-Engine sich überschneidende Kollider überprüft. Diese Abhängigkeit ist nicht sehr gut, da wir die Details nicht kennen und sie nicht kontrollieren können. Außerdem wird sie seltsam und inkonsistent aussehen. Oft führt dieses Verhalten zu konzentriertem Feuer, aber dies ist nicht immer der Fall.Anstatt sich ausschließlich auf die Physik-Engine zu verlassen, fügen wir etwas Zufälligkeit hinzu. Dies kann erreicht werden, indem die Anzahl der von Kollidern empfangenen Kreuzungen beispielsweise auf 100 erhöht wird. Möglicherweise reicht dies nicht aus, um alle möglichen Ziele in einem dicht mit Feinden gefüllten Feld zu erreichen, aber dies reicht aus, um das Zielen zu verbessern. static Collider[] targetsBuffer = new Collider[100];
Anstatt das erste potenzielle Ziel auszuwählen, wählen wir jetzt ein zufälliges Element aus dem Array aus. bool AcquireTarget () { … if (hits > 0) { target = targetsBuffer[Random.Range(0, hits)].GetComponent<TargetPoint>(); … } target = null; return false; }
Zufälliges Zielen.Können andere Kriterien für die Auswahl der Ziele verwendet werden?, , . , , . . .
In unserem Tower Defense-Spiel sind also endlich Türme aufgetaucht. Im nächsten Teil wird das Spiel noch mehr seine endgültige Form annehmen.