[ Erster Teil: Fliesen und den Weg finden ]- Platzierung feindlicher Schöpfungspunkte.
- Das Erscheinen von Feinden und ihre Bewegung über das Feld.
- Gleichmäßige Bewegung mit konstanter Geschwindigkeit.
- Ändere die Größe, Geschwindigkeit und Platzierung von Feinden.
Dies ist der zweite Teil eines Tutorials zu einem einfachen
Tower Defense- Spiel. Es untersucht den Prozess der Schaffung von Feinden und deren Bewegung zum nächsten Endpunkt.
Dieses Tutorial wurde in Unity 2018.3.0f2 erstellt.
Feinde auf dem Weg zum Endpunkt.Punkte der feindlichen Schöpfung (Spawn)
Bevor wir anfangen, Feinde zu erschaffen, müssen wir entscheiden, wo wir sie auf dem Spielfeld platzieren sollen. Dazu erstellen wir Spawnpunkte.
Kachelinhalt
Ein Spawnpunkt ist eine andere Art von
GameTileContentType
.
GameTileContentType
Sie daher einen Eintrag in
GameTileContentType
.
public enum GameTileContentType { Empty, Destination, Wall, SpawnPoint }
Und dann erstellen Sie ein Fertighaus, um es zu visualisieren. Ein Duplikat des Fertighauses des Startpunkts ist für uns durchaus geeignet. Ändern Sie einfach die Art des Inhalts und geben Sie ihm ein anderes Material. Ich habe es orange gemacht.
Spawnpunktkonfiguration.Fügen Sie der Content Factory Unterstützung für Spawnpunkte hinzu und verknüpfen Sie sie mit dem Fertighaus.
[SerializeField] GameTileContent spawnPointPrefab = default; … public GameTileContent Get (GameTileContentType type) { switch (type) { case GameTileContentType.Destination: return Get(destinationPrefab); case GameTileContentType.Empty: return Get(emptyPrefab); case GameTileContentType.Wall: return Get(wallPrefab); case GameTileContentType.SpawnPoint: return Get(spawnPointPrefab); } Debug.Assert(false, "Unsupported type: " + type); return null; }
Fabrik mit Unterstützung für Spawnpunkte.Spawnpunkte aktivieren oder deaktivieren
Die Methode zum Umschalten des
GameBoard
wird wie bei anderen Umschaltmethoden dem
GameBoard
. Spawnpunkte wirken sich jedoch nicht auf die Suche nach dem Pfad aus, sodass wir nach der Änderung nicht nach neuen Pfaden suchen müssen.
public void ToggleSpawnPoint (GameTile tile) { if (tile.Content.Type == GameTileContentType.SpawnPoint) { tile.Content = contentFactory.Get(GameTileContentType.Empty); } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.SpawnPoint); } }
Das Spiel macht nur Sinn, wenn wir Feinde haben und diese Spawnpunkte benötigen. Daher muss das Spielfeld mindestens einen Spawnpunkt enthalten. Wir werden in Zukunft auch Zugriff auf die Spawnpunkte benötigen, wenn wir Feinde hinzufügen. Verwenden wir also die Liste, um alle Kacheln mit diesen Punkten zu verfolgen. Wir werden die Liste aktualisieren, wenn wir den Status des Spawnpunkts ändern und verhindern, dass der letzte Spawnpunkt entfernt wird.
List<GameTile> spawnPoints = new List<GameTile>(); … public void ToggleSpawnPoint (GameTile tile) { if (tile.Content.Type == GameTileContentType.SpawnPoint) { if (spawnPoints.Count > 1) { spawnPoints.Remove(tile); tile.Content = contentFactory.Get(GameTileContentType.Empty); } } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.SpawnPoint); spawnPoints.Add(tile); } }
Die
Initialize
Methode sollte jetzt den Spawnpunkt festlegen, um den anfänglichen korrekten Status des Feldes zu erstellen. Fügen wir einfach die erste Kachel ein, die sich in der unteren linken Ecke befindet.
public void Initialize ( Vector2Int size, GameTileContentFactory contentFactory ) { … ToggleDestination(tiles[tiles.Length / 2]); ToggleSpawnPoint(tiles[0]); }
Wir werden jetzt den Status der
Input.GetKey
, aber wenn Sie die linke Umschalttaste gedrückt halten (der Tastenanschlag wird von der
Input.GetKey
Methode überprüft), wechselt der Status des Endpunkts
void HandleAlternativeTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { if (Input.GetKey(KeyCode.LeftShift)) { board.ToggleDestination(tile); } else { board.ToggleSpawnPoint(tile); } } }
Feld mit Spawnpunkten.Erhalte Zugang zu Spawnpunkten
Das Feld befasst sich mit all seinen Kacheln, aber Feinde sind nicht seine Verantwortung. Wir werden es ermöglichen, über die gemeinsame
GetSpawnPoint
Methode mit einem Indexparameter auf seine
GetSpawnPoint
.
public GameTile GetSpawnPoint (int index) { return spawnPoints[index]; }
Um zu wissen, welche Indizes korrekt sind, werden Informationen zur Anzahl der Spawnpunkte benötigt. Daher werden wir sie mithilfe der allgemeinen Getter-Eigenschaft allgemein machen.
public int SpawnPointCount => spawnPoints.Count;
Feindliche Brut
Das Laichen eines Feindes ähnelt dem Erstellen des Inhalts eines Plättchens. Wir erstellen eine vorgefertigte Instanz durch die Fabrik, die wir dann auf dem Feld platzieren.
Fabriken
Wir werden eine Fabrik für Feinde schaffen, die alles, was sie schafft, auf ihre eigene Bühne stellt. Diese Funktionalität ist in der bereits vorhandenen Factory üblich.
GameObjectFactory
wir den Code dafür in die allgemeine Basisklasse
GameObjectFactory
. Wir benötigen nur eine
CreateGameObjectInstance
Methode mit einem gemeinsamen Prefab-Parameter, der eine Instanz erstellt und zurückgibt und auch die gesamte Szene verwaltet. Wir machen die
protected
Methode, das heißt, sie wird nur der Klasse und allen Typen zur Verfügung stehen, die von ihr erben. Das ist alles, was die Klasse tut, es ist nicht als voll funktionsfähige Fabrik gedacht. Daher markieren wir es als
abstract
, wodurch wir keine Instanzen seiner Objekte erstellen können.
using UnityEngine; using UnityEngine.SceneManagement; public abstract class GameObjectFactory : ScriptableObject { Scene scene; protected T CreateGameObjectInstance<T> (T prefab) where T : MonoBehaviour { if (!scene.isLoaded) { if (Application.isEditor) { scene = SceneManager.GetSceneByName(name); if (!scene.isLoaded) { scene = SceneManager.CreateScene(name); } } else { scene = SceneManager.CreateScene(name); } } T instance = Instantiate(prefab); SceneManager.MoveGameObjectToScene(instance.gameObject, scene); return instance; } }
Ändern Sie die
GameTileContentFactory
so, dass sie diese Art von Factory
GameTileContentFactory
und
CreateGameObjectInstance
in ihrer
Get
Methode verwendet, und entfernen Sie dann den Szenensteuerungscode daraus.
using UnityEngine; [CreateAssetMenu] public class GameTileContentFactory : GameObjectFactory { …
Erstellen Sie anschließend einen neuen
EnemyFactory
Typ, der mithilfe der
Get
Methode und der zugehörigen
Reclaim
Methode eine Instanz eines
Enemy
Fertighauses erstellt.
using UnityEngine; [CreateAssetMenu] public class EnemyFactory : GameObjectFactory { [SerializeField] Enemy prefab = default; public Enemy Get () { Enemy instance = CreateGameObjectInstance(prefab); instance.OriginFactory = this; return instance; } public void Reclaim (Enemy enemy) { Debug.Assert(enemy.OriginFactory == this, "Wrong factory reclaimed!"); Destroy(enemy.gameObject); } }
Der neue
Enemy
Typ musste zunächst nur die ursprüngliche Fabrik im
Enemy
behalten.
using UnityEngine; public class Enemy : MonoBehaviour { EnemyFactory originFactory; public EnemyFactory OriginFactory { get => originFactory; set { Debug.Assert(originFactory == null, "Redefined origin factory!"); originFactory = value; } } }
Fertighaus
Feinde brauchen eine Visualisierung, die alles sein kann - ein Roboter, eine Spinne, ein Geist, etwas Einfacheres, zum Beispiel ein Würfel, den wir verwenden. Im Allgemeinen verfügt der Feind jedoch über ein 3D-Modell beliebiger Komplexität. Um die bequeme Unterstützung zu gewährleisten, verwenden wir das Stammobjekt für die vorgefertigte feindliche Hierarchie, an die nur die
Enemy
Komponente angehängt ist.
Fertige WurzelErstellen wir dieses Objekt als einziges untergeordnetes Element, das die Wurzel des Modells bildet. Es muss Werte für Transformationseinheiten haben.
Die Wurzel des Modells.Die Aufgabe dieser Modellwurzel besteht darin, das 3D-Modell relativ zum lokalen Ursprungspunkt der feindlichen Koordinaten zu positionieren, sodass er es als Referenzpunkt betrachtet, über dem der Feind steht oder hängt. In unserem Fall ist das Modell ein Standardwürfel in halber Größe, dem ich eine dunkelblaue Farbe geben werde. Wir machen es zu einem Kind der Modellwurzel und setzen die Y-Position auf 0,25, so dass es auf dem Boden steht.
WürfelmodellDas Fertighaus des Feindes besteht also aus drei verschachtelten Objekten: der Fertighauswurzel, der Modellwurzel und dem Würfel. Es mag wie eine Büste für einen einfachen Würfel erscheinen, aber mit einem solchen System können Sie jeden Feind bewegen und animieren, ohne sich um seine Eigenschaften sorgen zu müssen.
Die Fertighierarchie des Feindes.Erstellen wir eine feindliche Fabrik und weisen ihr ein Fertighaus zu.
Asset Factory.Feinde auf das Spielfeld bringen
Um Feinde auf das Spielfeld zu bringen, muss das
Game
einen Link zur Fabrik der Feinde erhalten. Da wir viele Feinde benötigen, fügen wir eine Konfigurationsoption hinzu, um die Laichgeschwindigkeit anzupassen, ausgedrückt in der Anzahl der Feinde pro Sekunde. Ein akzeptabler Bereich ist 0,1–10 mit einem Standardwert von 1.
[SerializeField] EnemyFactory enemyFactory = default; [SerializeField, Range(0.1f, 10f)] float spawnSpeed = 1f;
Spiel mit einer feindlichen Fabrik und Laichgeschwindigkeit 4.Wir werden den Fortschritt des Laichens in
Update
verfolgen und ihn um die Geschwindigkeit mal die Deltazeit erhöhen. Wenn der Prggress-Wert 1 überschreitet, dekrementieren wir ihn und spawnen den Feind mit der neuen
SpawnEnemy
Methode. Wir machen so lange weiter, bis der Fortschritt 1 überschreitet, falls die Geschwindigkeit zu hoch und die Frame-Zeit sehr lang ist, so dass nicht mehrere Feinde gleichzeitig erstellt werden.
float spawnProgress; … void Update () { … spawnProgress += spawnSpeed * Time.deltaTime; while (spawnProgress >= 1f) { spawnProgress -= 1f; SpawnEnemy(); } }
Ist es nicht notwendig, den Fortschritt in FixedUpdate zu aktualisieren?Ja, es ist möglich, aber für das Tower Defense-Spiel werden keine so genauen Timings benötigt. Wir werden einfach den Status des Spiels in jedem Frame aktualisieren und dafür sorgen, dass es für jedes Zeitdelta gut genug funktioniert.
Lass
SpawnEnemy
einen zufälligen
SpawnEnemy
vom Spielfeld erhalten und einen Feind in diesem Plättchen erschaffen. Wir werden
Enemy
die
SpawnOn
Methode geben, um sich richtig zu
SpawnOn
.
void SpawnEnemy () { GameTile spawnPoint = board.GetSpawnPoint(Random.Range(0, board.SpawnPointCount)); Enemy enemy = enemyFactory.Get(); enemy.SpawnOn(spawnPoint); }
Im
SpawnOn
muss
SpawnOn
nur seine eigene Position gleich der Mitte der Kachel setzen. Da das Fertigmodell korrekt positioniert ist, befindet sich der feindliche Würfel oben auf diesem Plättchen.
public void SpawnOn (GameTile tile) { transform.localPosition = tile.transform.localPosition; }
Gegner erscheinen an Spawnpunkten.Feinde bewegen
Nachdem der Feind aufgetaucht ist, muss er beginnen, sich auf dem Weg zum nächsten Endpunkt zu bewegen. Um dies zu erreichen, müssen Sie Feinde animieren. Wir beginnen mit einem einfachen sanften Gleiten von Kachel zu Kachel und erschweren dann deren Bewegung.
Sammlung von Feinden
Um den Status von Feinden zu aktualisieren, verwenden wir denselben Ansatz wie in der Tutorials-Reihe "
Objektverwaltung" . Wir fügen
Enemy
allgemeine
GameUpdate
Methode hinzu, die Informationen darüber zurückgibt, ob er lebt, was zu diesem Zeitpunkt immer der Fall sein wird. Lassen Sie ihn vorerst nur entsprechend dem Delta der Zeit vorwärts gehen.
public bool GameUpdate () { transform.localPosition += Vector3.forward * Time.deltaTime; return true; }
Außerdem müssen wir eine Liste lebender Feinde führen und alle aktualisieren, um sie von der Liste der toten Feinde zu entfernen. Wir können den gesamten Code in ein
Game
einfügen, ihn jedoch isolieren und einen
EnemyCollection
Typ erstellen. Dies ist eine serialisierbare Klasse, die von nichts erbt. Wir geben ihm eine allgemeine Methode zum Hinzufügen eines Feindes und eine andere Methode zum Aktualisieren der gesamten Sammlung.
using System.Collections.Generic; [System.Serializable] public class EnemyCollection { List<Enemy> enemies = new List<Enemy>(); public void Add (Enemy enemy) { enemies.Add(enemy); } public void GameUpdate () { for (int i = 0; i < enemies.Count; i++) { if (!enemies[i].GameUpdate()) { int lastIndex = enemies.Count - 1; enemies[i] = enemies[lastIndex]; enemies.RemoveAt(lastIndex); i -= 1; } } } }
Jetzt reicht das
Game
aus, um nur eine solche Sammlung zu erstellen, sie in jedem Frame zu aktualisieren und erstellte Feinde hinzuzufügen. Wir werden die Feinde sofort nach dem möglichen Laichen eines neuen Feindes aktualisieren, so dass die Aktualisierung sofort erfolgt.
EnemyCollection enemies = new EnemyCollection(); … void Update () { … enemies.GameUpdate(); } … void SpawnEnemy () { … enemies.Add(enemy); }
Feinde bewegen sich vorwärts.Bewegung auf dem Weg
Feinde bewegen sich bereits, folgen aber bisher nicht dem Weg. Dazu müssen sie wissen, wohin sie als nächstes gehen müssen. Geben Sie
GameTile
gemeinsame Getter-Eigenschaft, um die nächste Kachel auf dem Pfad
GameTile
.
public GameTile NextTileOnPath => nextOnPath;
Wenn die Feinde die Kachel kennen, von der Sie aussteigen möchten, und die Kachel, in die Sie gelangen müssen, können Sie den Start- und Endpunkt bestimmen, um eine Kachel zu bewegen. Der Feind kann die Position zwischen diesen beiden Punkten interpolieren und ihre Bewegung verfolgen. Nachdem der Umzug abgeschlossen ist, wird dieser Vorgang für die nächste Kachel wiederholt. Die Wege können sich aber jederzeit ändern. Anstatt zu bestimmen, wohin wir uns im Verlauf der Bewegung weiter bewegen sollen, bewegen wir uns einfach weiter entlang der geplanten Route und überprüfen sie, bis wir die nächste Kachel erreichen.
Lassen Sie
Enemy
beide Kacheln verfolgen, damit sie nicht von einer Pfadänderung betroffen sind. Er wird auch Positionen verfolgen, damit wir sie nicht in jedem Frame empfangen müssen, und den Bewegungsprozess verfolgen.
GameTile tileFrom, tileTo; Vector3 positionFrom, positionTo; float progress;
Initialisieren Sie diese Felder in
SpawnOn
. Der erste Punkt ist das Plättchen, von dem aus sich der Feind bewegt, und der Endpunkt ist das nächste Plättchen auf dem Weg. Dies setzt voraus, dass das nächste Plättchen existiert, es sei denn, der Feind wurde am Endpunkt erstellt, was unmöglich sein sollte. Dann zwischenspeichern wir die Positionen der Kacheln und setzen den Fortschritt zurück. Wir müssen hier nicht die Position des Feindes
GameUpdate
, da seine
GameUpdate
Methode im selben Frame aufgerufen wird.
public void SpawnOn (GameTile tile) {
Das Fortschrittsinkrement wird in
GameUpdate
. Fügen wir ein konstantes Zeitdelta hinzu, damit sich die Feinde mit einer Geschwindigkeit von einem Plättchen pro Sekunde bewegen. Wenn der Fortschritt abgeschlossen ist, verschieben wir die Daten so, dass
To
zum Wert
From
wird und das neue
To
die nächste Kachel auf dem Pfad ist. Dann verringern wir den Fortschritt. Wenn die Daten relevant werden, interpolieren wir die Position des Feindes zwischen
From
und
To
. Da der Interpolator ein Fortschritt ist, liegt sein Wert notwendigerweise im Bereich von 0 und 1, sodass wir s
Vector3.LerpUnclamped
.
public bool GameUpdate () { progress += Time.deltaTime; while (progress >= 1f) { tileFrom = tileTo; tileTo = tileTo.NextTileOnPath; positionFrom = positionTo; positionTo = tileTo.transform.localPosition; progress -= 1f; } transform.localPosition = Vector3.LerpUnclamped(positionFrom, positionTo, progress); return true; }
Dies zwingt die Feinde, dem Pfad zu folgen, wirkt jedoch nicht, wenn sie den Endpunkt erreichen. Bevor Sie die Positionen von
From
und
To
ändern, müssen Sie daher die nächste Kachel auf dem Pfad mit
null
. Wenn ja, dann haben wir den Endpunkt erreicht und der Feind hat die Bewegung beendet. Wir führen Reclaim dafür aus und geben
false
.
while (progress >= 1f) { tileFrom = tileTo; tileTo = tileTo.NextTileOnPath; if (tileTo == null) { OriginFactory.Reclaim(this); return false; } positionFrom = positionTo; positionTo = tileTo.transform.localPosition; progress -= 1f; }
Feinde folgen dem kürzesten Weg.Feinde bewegen sich jetzt von der Mitte eines Plättchens zum anderen. Es ist zu bedenken, dass sie ihren Bewegungszustand nur in der Mitte der Kacheln ändern und daher nicht sofort auf Änderungen im Feld reagieren können. Dies bedeutet, dass sich manchmal Feinde durch die gerade gesetzten Wände bewegen. Sobald sie sich auf die Zelle zubewegen, wird sie nichts mehr aufhalten. Deshalb brauchen Mauern auch echte Wege.
Feinde reagieren auf wechselnde Pfade.Bewegung von Kante zu Kante
Die Bewegung zwischen den Kachelmitte und ein scharfer Richtungswechsel sehen für ein abstraktes Spiel, bei dem die Feinde Würfel bewegen, normal aus, aber normalerweise sieht die sanfte Bewegung schöner aus. Der erste Schritt zu seiner Implementierung besteht nicht darin, sich entlang der Zentren zu bewegen, sondern entlang der Kanten der Kacheln.
Der Randpunkt zwischen benachbarten Kacheln kann durch Mitteln ihrer Positionen ermittelt werden. Anstatt es bei jedem Schritt für jeden Feind zu berechnen, berechnen wir es nur, wenn wir den Pfad in
GameTile.GrowPathTo
.
ExitPoint
Sie es mithilfe der
ExitPoint
Eigenschaft zur Verfügung.
public Vector3 ExitPoint { get; private set; } … GameTile GrowPathTo (GameTile neighbor) { … neighbor.ExitPoint = (neighbor.transform.localPosition + transform.localPosition) * 0.5f; return neighbor.Content.Type != GameTileContentType.Wall ? neighbor : null; }
Der einzige Sonderfall ist die letzte Zelle, deren Ausgangspunkt der Mittelpunkt sein wird.
public void BecomeDestination () { distance = 0; nextOnPath = null; ExitPoint = transform.localPosition; }
Ändern Sie
Enemy
so, dass Ausgangspunkte und keine Kachelzentren verwendet werden.
public bool GameUpdate () { progress += Time.deltaTime; while (progress >= 1f) { … positionTo = tileFrom.ExitPoint; progress -= 1f; } transform.localPosition = Vector3.Lerp(positionFrom, positionTo, progress); return true; } public void SpawnOn (GameTile tile) { … positionTo = tileFrom.ExitPoint; progress = 0f; }
Feinde bewegen sich zwischen den Rändern.Ein Nebeneffekt dieser Änderung ist, dass Feinde, die sich aufgrund einer Änderung des Pfades drehen, eine Sekunde lang bewegungslos bleiben.
Beim Drehen hören die Feinde auf.Orientierung
Obwohl sich die Feinde auf den Wegen bewegen, bis sie ihre Ausrichtung ändern. Damit sie in die Bewegungsrichtung schauen können, müssen sie die Richtung des Pfades kennen, dem sie folgen. Wir werden dies auch bei der Suche nach Wegen feststellen, damit dies nicht von den Feinden getan werden muss.
Wir haben vier Richtungen: Nord, Ost, Süd und West. Zählen wir sie auf.
public enum Direction { North, East, South, West }
Dann geben wir die
GameTile
Eigenschaft an, um die Richtung ihres Pfades zu speichern.
public Direction PathDirection { get; private set; }
Fügen Sie
GrowTo
einen Richtungsparameter
GrowTo
, der die Eigenschaft festlegt. Da wir einen Weg von Ende zu Anfang wachsen, ist die Richtung entgegengesetzt zu der Richtung, von der aus wir den Weg wachsen.
public GameTile GrowPathNorth () => GrowPathTo(north, Direction.South); public GameTile GrowPathEast () => GrowPathTo(east, Direction.West); public GameTile GrowPathSouth () => GrowPathTo(south, Direction.North); public GameTile GrowPathWest () => GrowPathTo(west, Direction.East); GameTile GrowPathTo (GameTile neighbor, Direction direction) { … neighbor.PathDirection = direction; return neighbor.Content.Type != GameTileContentType.Wall ? neighbor : null; }
Wir müssen Richtungen in Kurven umwandeln, die als Quaternionen ausgedrückt werden. Es wäre praktisch, wenn wir nur
GetRotation
für die Richtung aufrufen
GetRotation
Lassen Sie uns dies tun, indem Sie eine Erweiterungsmethode erstellen. Fügen Sie die allgemeine statische Methode
DirectionExtensions
, geben Sie ihr ein Array zum Zwischenspeichern der erforderlichen Quaternionen sowie die
GetRotation
Methode, um den entsprechenden Richtungswert zurückzugeben. In diesem Fall ist es sinnvoll, die Erweiterungsklasse in dieselbe Datei wie den Aufzählungstyp zu stellen.
using UnityEngine; public enum Direction { North, East, South, West } public static class DirectionExtensions { static Quaternion[] rotations = { Quaternion.identity, Quaternion.Euler(0f, 90f, 0f), Quaternion.Euler(0f, 180f, 0f), Quaternion.Euler(0f, 270f, 0f) }; public static Quaternion GetRotation (this Direction direction) { return rotations[(int)direction]; } }
Was ist eine Erweiterungsmethode?Eine Erweiterungsmethode ist eine statische Methode innerhalb einer statischen Klasse, die sich wie eine Instanzmethode eines Typs verhält. Dieser Typ kann eine Klasse, eine Schnittstelle, eine Struktur, ein primitiver Wert oder eine Aufzählung sein. Das erste Argument für die Erweiterungsmethode muss das this
. Es definiert den Wert des Typs und der Instanz, mit denen die Methode arbeiten wird. Dieser Ansatz bedeutet, dass eine Erweiterung der Eigenschaften nicht möglich ist.
Können Sie auf diese Weise Methoden zu irgendetwas hinzufügen? Ja, genauso wie Sie jede statische Methode schreiben können, deren Parameter ein beliebiger Typ ist.
Jetzt können wir den
Enemy
beim Laichen und jedes Mal, wenn wir ein neues Plättchen betreten, drehen. Nach dem Aktualisieren der Daten gibt uns die Kachel
From
die Richtung vor.
public bool GameUpdate () { progress += Time.deltaTime; while (progress >= 1f) { … transform.localRotation = tileFrom.PathDirection.GetRotation(); progress -= 1f; } transform.localPosition = Vector3.LerpUnclamped(positionFrom, positionTo, progress); return true; } public void SpawnOn (GameTile tile) { … transform.localRotation = tileFrom.PathDirection.GetRotation(); progress = 0f; }
Richtungswechsel
Anstatt die Richtung sofort zu ändern, ist es besser, Werte zwischen Windungen zu interpolieren, ähnlich wie wir zwischen Positionen interpoliert haben. Um von einer Ausrichtung zur anderen zu gelangen, müssen wir die Richtungsänderung kennen, die durchgeführt werden muss: ohne sich zu drehen, nach rechts zu drehen, nach links zu drehen oder zurück zu drehen. Wir fügen dazu eine Aufzählung hinzu, die wiederum in derselben Datei wie die
Direction
abgelegt werden kann, da sie klein und eng miteinander verbunden sind.
public enum Direction { North, East, South, West } public enum DirectionChange { None, TurnRight, TurnLeft, TurnAround }
Fügen Sie eine weitere Erweiterungsmethode hinzu, diesmal
GetDirectionChangeTo
, die eine Richtungsänderung von der aktuellen Richtung zur nächsten zurückgibt. Wenn die Richtungen übereinstimmen, gibt es keine Verschiebung. Wenn der nächste mehr als der aktuelle ist, biegen Sie rechts ab. Da sich die Anweisungen jedoch wiederholen, ist die gleiche Situation gegeben, wenn die nächste drei weniger als die aktuelle ist. Bei einer Linkskurve ist es dasselbe, nur Addition und Subtraktion wechseln die Plätze. Der einzige verbleibende Fall ist ein Zurück.
public static DirectionChange GetDirectionChangeTo ( this Direction current, Direction next ) { if (current == next) { return DirectionChange.None; } else if (current + 1 == next || current - 3 == next) { return DirectionChange.TurnRight; } else if (current - 1 == next || current + 3 == next) { return DirectionChange.TurnLeft; } return DirectionChange.TurnAround; }
Wir drehen nur in einer Dimension, daher reicht uns eine lineare Interpolation der Winkel. Fügen Sie eine weitere Erweiterungsmethode hinzu, mit der der Richtungswinkel in Grad ermittelt wird. public static float GetAngle (this Direction direction) { return (float)direction * 90f; }
Jetzt müssen Sie Enemy
die Richtung, Richtungsänderung und Winkel verfolgen, zwischen denen Sie eine Interpolation durchführen müssen. Direction direction; DirectionChange directionChange; float directionAngleFrom, directionAngleTo;
SpawnOn
Wenn es schwieriger wird, verschieben wir den Code für die Statusvorbereitung auf eine andere Methode. Wir werden den Anfangszustand des Feindes als Einführungszustand bezeichnen, also werden wir ihn nennen PrepareIntro
. In diesem Zustand bewegt sich der Feind von der Mitte zum Rand seines ursprünglichen Plättchens, sodass keine Richtungsänderung erfolgt. Die Winkel From
und To
die gleichen. public void SpawnOn (GameTile tile) { Debug.Assert(tile.NextTileOnPath != null, "Nowhere to go!", this); tileFrom = tile; tileTo = tile.NextTileOnPath;
In dieser Phase erstellen wir so etwas wie eine kleine Zustandsmaschine. GameUpdate
Verschieben Sie den Statuscode zur Vereinfachung auf eine neue Methode PrepareNextState
. Wir werden nur die Änderungen der Kacheln belassen From
und To
, weil wir sie hier verwenden, um zu überprüfen, ob der Feind den Pfad beendet hat. public bool GameUpdate () { progress += Time.deltaTime; while (progress >= 1f) { …
Wenn Sie in einen neuen Zustand wechseln, müssen Sie immer die Position ändern, eine Richtungsänderung suchen, die aktuelle Richtung aktualisieren und den Winkel To
auf verschieben From
. Wir machen keine Wende mehr. void PrepareNextState () { positionFrom = positionTo; positionTo = tileFrom.ExitPoint; directionChange = direction.GetDirectionChangeTo(tileFrom.PathDirection); direction = tileFrom.PathDirection; directionAngleFrom = directionAngleTo; }
Andere Aktionen hängen von einer Richtungsänderung ab. Fügen wir für jede Option eine Methode hinzu. Wenn wir uns vorwärts bewegen, To
stimmt der Winkel mit der Richtung des Pfades der aktuellen Zelle überein. Außerdem müssen wir die Rotation so einstellen, dass der Feind geradeaus schaut. void PrepareForward () { transform.localRotation = direction.GetRotation(); directionAngleTo = direction.GetAngle(); }
Im Falle einer Wende drehen wir nicht sofort. Wir müssen in einen anderen Winkel interpolieren: 90 ° mehr, um nach rechts zu drehen, 90 ° weniger, um nach links zu drehen, und 180 ° mehr, um zurückzudrehen. Um zu vermeiden, dass aufgrund einer Änderung der Winkelwerte von 359 ° auf 0 ° in die falsche Richtung gedreht wird, sollte der Winkel To
relativ zur aktuellen Richtung angegeben werden. Wir müssen uns keine Sorgen machen, dass der Winkel kleiner als 0 ° oder größer als 360 ° wird, da wir Quaternion.Euler
damit umgehen können. void PrepareTurnRight () { directionAngleTo = directionAngleFrom + 90f; } void PrepareTurnLeft () { directionAngleTo = directionAngleFrom - 90f; } void PrepareTurnAround () { directionAngleTo = directionAngleFrom + 180f; }
Am Ende können PrepareNextState
wir die switch
Richtung ändern, um zu entscheiden, welche der vier Methoden aufgerufen werden soll. void PrepareNextState () { … switch (directionChange) { case DirectionChange.None: PrepareForward(); break; case DirectionChange.TurnRight: PrepareTurnRight(); break; case DirectionChange.TurnLeft: PrepareTurnLeft(); break; default: PrepareTurnAround(); break; } }
Am Ende müssen GameUpdate
wir überprüfen, ob sich die Richtung geändert hat. Wenn ja, interpolieren Sie zwischen den beiden Ecken und stellen Sie die Drehung ein. public bool GameUpdate () { … transform.localPosition = Vector3.LerpUnclamped(positionFrom, positionTo, progress); if (directionChange != DirectionChange.None) { float angle = Mathf.LerpUnclamped( directionAngleFrom, directionAngleTo, progress ); transform.localRotation = Quaternion.Euler(0f, angle, 0f); } return true; }
Feinde drehen sich.Kurvenbewegung
Wir können die Bewegung verbessern, indem wir Feinde beim Drehen entlang einer Kurve bewegen. Anstatt von Rand zu Rand zu gehen, lassen Sie sie einen viertel Kreis laufen. Der Mittelpunkt dieses Kreises liegt in einer Ecke, die den Kacheln gemeinsam ist, From
und To
an derselben Kante, entlang der der Feind die Kachel betreten hat From
.Eine Viertelkreisdrehung nach rechts.Wir können dies realisieren, indem wir den Feind mithilfe der Trigonometrie in einem Bogen bewegen und gleichzeitig drehen. Dies kann jedoch vereinfacht werden, indem nur die Rotation verwendet wird und der lokale Ursprung der feindlichen Koordinaten vorübergehend in den Mittelpunkt des Kreises verschoben wird. Dazu müssen wir die Position des feindlichen Modells ändern, damit wir einen Enemy
Link zu diesem Modell geben, auf den über das Konfigurationsfeld zugegriffen werden kann. [SerializeField] Transform model = default;
Feind in Bezug auf das Modell.In Vorbereitung auf das Vorwärts- oder Rückwärtsbewegen sollte sich das Modell zur Standardposition bewegen, zum lokalen Ursprung der feindlichen Koordinaten. Andernfalls muss das Modell um die Hälfte der Maßeinheit verschoben werden - den Radius des Rotationskreises, weit vom Wendepunkt entfernt. void PrepareForward () { transform.localRotation = direction.GetRotation(); directionAngleTo = direction.GetAngle(); model.localPosition = Vector3.zero; } void PrepareTurnRight () { directionAngleTo = directionAngleFrom + 90f; model.localPosition = new Vector3(-0.5f, 0f); } void PrepareTurnLeft () { directionAngleTo = directionAngleFrom - 90f; model.localPosition = new Vector3(0.5f, 0f); } void PrepareTurnAround () { directionAngleTo = directionAngleFrom + 180f; model.localPosition = Vector3.zero; }
Jetzt muss der Feind selbst zum Wendepunkt gebracht werden. Dazu muss es auch um die halbe Maßeinheit verschoben werden, der genaue Versatz hängt jedoch von der Richtung ab. Fügen wir dem Direction
eine zusätzliche Erweiterungsmethode hinzu GetHalfVector
. static Vector3[] halfVectors = { Vector3.forward * 0.5f, Vector3.right * 0.5f, Vector3.back * 0.5f, Vector3.left * 0.5f }; … public static Vector3 GetHalfVector (this Direction direction) { return halfVectors[(int)direction]; }
Fügen Sie den entsprechenden Vektor hinzu, wenn Sie nach rechts oder links drehen. void PrepareTurnRight () { directionAngleTo = directionAngleFrom + 90f; model.localPosition = new Vector3(-0.5f, 0f); transform.localPosition = positionFrom + direction.GetHalfVector(); } void PrepareTurnLeft () { directionAngleTo = directionAngleFrom - 90f; model.localPosition = new Vector3(0.5f, 0f); transform.localPosition = positionFrom + direction.GetHalfVector(); }
Und beim Zurückdrehen sollte die Position der übliche Ausgangspunkt sein. void PrepareTurnAround () { directionAngleTo = directionAngleFrom + 180f; model.localPosition = Vector3.zero; transform.localPosition = positionFrom; }
Außerdem können wir bei der Berechnung des Austrittspunkts die GameTile.GrowPathTo
Hälfte des Vektors verwenden, sodass wir keinen Zugriff auf die beiden Positionen der Kacheln benötigen. neighbor.ExitPoint = neighbor.transform.localPosition + direction.GetHalfVector();
Wenn wir jetzt die Richtung ändern, müssen wir die Position nicht mehr interpolieren Enemy.GameUpdate
, da die Drehung an der Bewegung beteiligt ist. public bool GameUpdate () { … if (directionChange == DirectionChange.None) { transform.localPosition = Vector3.LerpUnclamped(positionFrom, positionTo, progress); }
Feinde biegen sich sanft um Ecken.Konstante Geschwindigkeit
Bis zu diesem Zeitpunkt war die Geschwindigkeit von Feinden immer gleich einem Plättchen pro Sekunde, unabhängig davon, wie sie sich innerhalb des Plättchens bewegen. Die zurückgelegte Strecke hängt jedoch von ihrem Zustand ab, sodass ihre Geschwindigkeit, ausgedrückt in Einheiten pro Sekunde, variiert. Damit diese Geschwindigkeit konstant ist, müssen wir die Geschwindigkeit des Fortschritts je nach Zustand ändern. Fügen Sie daher das Fortschrittsmultiplikatorfeld hinzu und skalieren Sie damit das Delta in GameUpdate
. float progress, progressFactor; … public bool GameUpdate () { progress += Time.deltaTime * progressFactor; … }
Wenn sich der Fortschritt jedoch je nach Status ändert, kann der verbleibende Fortschrittswert nicht direkt für den nächsten Status verwendet werden. Bevor wir uns auf einen neuen Zustand vorbereiten, müssen wir daher den Fortschritt normalisieren und den neuen Multiplikator bereits in einem neuen Zustand anwenden. public bool GameUpdate () { progress += Time.deltaTime * progressFactor; while (progress >= 1f) { …
Das Vorwärtsbewegen erfordert keine Änderungen, daher wird der Faktor 1 verwendet. Wenn der Feind nach rechts oder links dreht, passiert er ein Viertel eines Kreises mit einem Radius von ½, sodass die zurückgelegte Strecke ¼π beträgt. progress
gleich eins geteilt durch diesen Wert. Das Zurückdrehen sollte nicht zu lange dauern. Verdoppeln Sie den Fortschritt, sodass es eine halbe Sekunde dauert. Schließlich deckt die Einführungsbewegung nur die Hälfte der Kachel ab. Um eine konstante Geschwindigkeit aufrechtzuerhalten, muss der Fortschritt ebenfalls verdoppelt werden. void PrepareForward () { … progressFactor = 1f; } void PrepareTurnRight () { … progressFactor = 1f / (Mathf.PI * 0.25f); } void PrepareTurnLeft () { … progressFactor = 1f / (Mathf.PI * 0.25f); } void PrepareTurnAround () { … progressFactor = 2f; } void PrepareIntro () { … progressFactor = 2f; }
Warum ist der Abstand gleich 1/4 * pi?Der Umfang beträgt das 2π-fache des Radius. Wenn Sie nach rechts oder links drehen, wird nur ein Viertel dieser Länge abgedeckt, und der Radius beträgt ½, sodass der Abstand ½π × ½ beträgt.
Endzustand
Da wir einen Einführungszustand haben, fügen wir einen letzten hinzu. Feinde verschwinden derzeit unmittelbar nach Erreichen des Endpunkts, aber wir verschieben ihr Verschwinden, bis sie die Mitte des Endplättchens erreichen. Lassen Sie uns eine Methode dafür erstellen PrepareOutro
, die Vorwärtsbewegung einstellen, aber nur bis zur Mitte der Kachel mit doppeltem Fortschritt, um eine konstante Geschwindigkeit aufrechtzuerhalten. void PrepareOutro () { positionTo = tileFrom.transform.localPosition; directionChange = DirectionChange.None; directionAngleTo = direction.GetAngle(); model.localPosition = Vector3.zero; transform.localRotation = direction.GetRotation(); progressFactor = 2f; }
Um GameUpdate
den Feind nicht zu früh zu zerstören, werden wir die Kachelverschiebung entfernen. Er wird es jetzt tun PrepareNextState
. Daher wird erst nach dem Ende des Endzustands nach null
Rückgaben true
gesucht. public bool GameUpdate () { progress += Time.deltaTime * progressFactor; while (progress >= 1f) {
In beginnen PrepareNextState
wir mit der Verschiebung der Kacheln. Nach dem Festlegen der Position From
, jedoch vor dem Festlegen der Position, To
prüfen wir, ob die Kachel dem To
Wert entspricht null
. Wenn ja, bereiten Sie den Endzustand vor und überspringen Sie den Rest der Methode. void PrepareNextState () { tileFrom = tileTo; tileTo = tileTo.NextTileOnPath; positionFrom = positionTo; if (tileTo == null) { PrepareOutro(); return; } positionTo = tileFrom.ExitPoint; … }
Feinde mit konstanter Geschwindigkeit und Endzustand.Feindliche Variabilität
Wir haben einen Strom von Feinden, und sie sind alle der gleiche Würfel, der sich mit der gleichen Geschwindigkeit bewegt. Das Ergebnis ist eher eine lange Schlange als einzelne Feinde. Machen wir sie anders, indem wir ihre Größe, Verschiebung und Geschwindigkeit zufällig bestimmen.Float-Wertebereich
Wir werden die Parameter der Feinde ändern und ihre Eigenschaften zufällig aus dem Wertebereich auswählen. Die Struktur FloatRange
, die wir im Artikel Objektverwaltung, Formen konfigurieren erstellt haben , ist hier hilfreich. Kopieren Sie sie also. Die einzigen Änderungen waren das Hinzufügen eines Konstruktors mit einem Parameter und das Öffnen des Zugriffs auf Minimum und Maximum mithilfe von Readonly-Eigenschaften, sodass das Intervall unveränderlich war. using UnityEngine; [System.Serializable] public struct FloatRange { [SerializeField] float min, max; public float Min => min; public float Max => max; public float RandomValueInRange { get { return Random.Range(min, max); } } public FloatRange(float value) { min = max = value; } public FloatRange (float min, float max) { this.min = min; this.max = max < min ? min : max; } }
Wir kopieren auch das Attribut, das darauf gesetzt ist, um das Intervall zu begrenzen. using UnityEngine; public class FloatRangeSliderAttribute : PropertyAttribute { public float Min { get; private set; } public float Max { get; private set; } public FloatRangeSliderAttribute (float min, float max) { Min = min; Max = max < min ? min : max; } }
Wir brauchen nur die Visualisierung des Schiebereglers, also kopieren Sie ihn FloatRangeSliderDrawer
in den Editor- Ordner . using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(FloatRangeSliderAttribute))] public class FloatRangeSliderDrawer : PropertyDrawer { public override void OnGUI ( Rect position, SerializedProperty property, GUIContent label ) { int originalIndentLevel = EditorGUI.indentLevel; EditorGUI.BeginProperty(position, label, property); position = EditorGUI.PrefixLabel( position, GUIUtility.GetControlID(FocusType.Passive), label ); EditorGUI.indentLevel = 0; SerializedProperty minProperty = property.FindPropertyRelative("min"); SerializedProperty maxProperty = property.FindPropertyRelative("max"); float minValue = minProperty.floatValue; float maxValue = maxProperty.floatValue; float fieldWidth = position.width / 4f - 4f; float sliderWidth = position.width / 2f; position.width = fieldWidth; minValue = EditorGUI.FloatField(position, minValue); position.x += fieldWidth + 4f; position.width = sliderWidth; FloatRangeSliderAttribute limit = attribute as FloatRangeSliderAttribute; EditorGUI.MinMaxSlider( position, ref minValue, ref maxValue, limit.Min, limit.Max ); position.x += sliderWidth + 4f; position.width = fieldWidth; maxValue = EditorGUI.FloatField(position, maxValue); if (minValue < limit.Min) { minValue = limit.Min; } if (maxValue < minValue) { maxValue = minValue; } else if (maxValue > limit.Max) { maxValue = limit.Max; } minProperty.floatValue = minValue; maxProperty.floatValue = maxValue; EditorGUI.EndProperty(); EditorGUI.indentLevel = originalIndentLevel; } }
Modellmaßstab
Wir werden zunächst die Größe des Feindes ändern. Fügen Sie der EnemyFactory
Option die Skalierungseinstellungen hinzu. Das Skalierungsintervall sollte nicht zu groß sein, aber ausreichen, um kleine und gigantische Arten von Feinden zu erzeugen. Alles innerhalb von 0,5–2 mit einem Standardwert von 1. Wir werden in diesem Intervall eine zufällige Skala auswählen Get
und diese durch eine neue Methode an den Feind weitergeben Initialize
. [SerializeField, FloatRangeSlider(0.5f, 2f)] FloatRange scale = new FloatRange(1f); public Enemy Get () { Enemy instance = CreateGameObjectInstance(prefab); instance.OriginFactory = this; instance.Initialize(scale.RandomValueInRange); return instance; }
Die Methode Enemy.Initialize
legt einfach den Maßstab ihres Modells fest, der für alle Dimensionen gleich ist. public void Initialize (float scale) { model.localScale = new Vector3(scale, scale, scale); }
Der Skalenbereich reicht von 0,5 bis 1,5.Pfadversatz
Um die Gleichmäßigkeit des Feindflusses weiter zu zerstören, können wir ihre relative Position innerhalb der Kacheln ändern. Sie bewegen sich vorwärts, so dass die Verschiebung in diese Richtung nur den Zeitpunkt ihrer Bewegung ändert, was nicht sehr auffällig ist. Deshalb werden wir sie zur Seite verschieben, weg von dem idealen Pfad, der durch die Mitte der Kacheln führt. Fügen Sie dem EnemyFactory
Intervall einen Pfadversatz hinzu und übergeben Sie den zufälligen Versatz an die Methode Initialize
. Der Versatz kann negativ oder positiv sein, jedoch niemals mehr als ½, da dies den Feind auf ein benachbartes Plättchen bewegen würde. Außerdem möchten wir nicht, dass Feinde über die Kacheln hinausgehen, denen sie folgen, sodass das Intervall tatsächlich geringer ist, z. B. 0,4, aber die wahren Grenzen hängen von der Größe des Feindes ab. [SerializeField, FloatRangeSlider(-0.4f, 0.4f)] FloatRange pathOffset = new FloatRange(0f); public Enemy Get () { Enemy instance = CreateGameObjectInstance(prefab); instance.OriginFactory = this; instance.Initialize( scale.RandomValueInRange, pathOffset.RandomValueInRange ); return instance; }
Da sich die Verschiebung des Pfades auf den zurückgelegten Pfad auswirkt, müssen Enemy
Sie ihn verfolgen. float pathOffset; … public void Initialize (float scale, float pathOffset) { model.localScale = new Vector3(scale, scale, scale); this.pathOffset = pathOffset; }
Wenn Sie sich genau gerade bewegen (während der einführenden, letzten oder normalen Vorwärtsbewegung), wenden wir den Versatz einfach direkt auf das Modell an. Das gleiche passiert, wenn Sie umkehren. Mit einer Rechts- oder Linkskurve verschieben wir das Modell bereits, was relativ zur Verschiebung des Pfades wird. void PrepareForward () { transform.localRotation = direction.GetRotation(); directionAngleTo = direction.GetAngle(); model.localPosition = new Vector3(pathOffset, 0f); progressFactor = 1f; } void PrepareTurnRight () { directionAngleTo = directionAngleFrom + 90f; model.localPosition = new Vector3(pathOffset - 0.5f, 0f); transform.localPosition = positionFrom + direction.GetHalfVector(); progressFactor = 1f / (Mathf.PI * 0.25f); } void PrepareTurnLeft () { directionAngleTo = directionAngleFrom - 90f; model.localPosition = new Vector3(pathOffset + 0.5f, 0f); transform.localPosition = positionFrom + direction.GetHalfVector(); progressFactor = 1f / (Mathf.PI * 0.25f); } void PrepareTurnAround () { directionAngleTo = directionAngleFrom + 180f; model.localPosition = new Vector3(pathOffset, 0f); transform.localPosition = positionFrom; progressFactor = 2f; } void PrepareIntro () { … model.localPosition = new Vector3(pathOffset, 0f); transform.localRotation = direction.GetRotation(); progressFactor = 2f; } void PrepareOutro () { … model.localPosition = new Vector3(pathOffset, 0f); transform.localRotation = direction.GetRotation(); progressFactor = 2f; }
Da die Verschiebung des Pfades während der Drehung den Radius ändert, müssen wir den Prozess der Berechnung des Fortschrittsmultiplikators ändern. Der Pfadversatz muss von ½ subtrahiert werden, um den Radius der Kurve nach rechts zu erhalten, und im Fall einer Kurve nach links addiert werden. void PrepareTurnRight () { … progressFactor = 1f / (Mathf.PI * 0.5f * (0.5f - pathOffset)); } void PrepareTurnLeft () { … progressFactor = 1f / (Mathf.PI * 0.5f * (0.5f + pathOffset)); }
Wir erhalten auch den Wenderadius beim Drehen um 180 °. In diesem Fall decken wir den halben Kreis mit einem Radius ab, der dem Versatz des Pfades entspricht, sodass der Abstand das π-fache des Versatzes beträgt. Dies funktioniert jedoch nicht, wenn die Verschiebung Null ist und bei kleinen Verschiebungen die Windungen zu schnell sind. Um sofortige Kurven zu vermeiden, können wir den minimalen Radius erzwingen, um die Geschwindigkeit zu berechnen, z. B. 0,2. void PrepareTurnAround () { directionAngleTo = directionAngleFrom + (pathOffset < 0f ? 180f : -180f); model.localPosition = new Vector3(pathOffset, 0f); transform.localPosition = positionFrom; progressFactor = 1f / (Mathf.PI * Mathf.Max(Mathf.Abs(pathOffset), 0.2f)); }
Der Pfadversatz liegt im Bereich von –0,25–0,25.Beachten Sie, dass Feinde jetzt ihre relative Pfadverschiebung niemals ändern, selbst wenn sie sich drehen. Daher hat die Gesamtlänge des Pfades für jeden Feind seine eigene.Um zu verhindern, dass Feinde benachbarte Kacheln erreichen, muss auch deren maximal mögliche Größe berücksichtigt werden. Ich habe die Größe nur auf einen Maximalwert von 1 begrenzt, sodass der maximal zulässige Versatz für den Würfel 0,25 beträgt. Wenn die maximale Größe 1,5 betrug, sollte die maximale Verschiebung auf 0,125 reduziert werden.Geschwindigkeit
Das Letzte, was wir zufällig auswählen, ist die Geschwindigkeit der Feinde. Wir fügen ein weiteres Intervall hinzu EnemyFactory
und übertragen den Wert auf die erstellte Kopie des Feindes. Machen wir es zum zweiten Argument der Methode Initialize
. Feinde sollten nicht zu langsam oder zu schnell sein, damit das Spiel nicht trivial einfach oder unmöglich schwierig wird. Begrenzen wir das Intervall auf 0,2–5. Die Geschwindigkeit wird in Einheiten pro Sekunde ausgedrückt, was nur Kacheln pro Sekunde entspricht, wenn Sie sich vorwärts bewegen. [SerializeField, FloatRangeSlider(0.2f, 5f)] FloatRange speed = new FloatRange(1f); [SerializeField, FloatRangeSlider(-0.4f, 0.4f)] FloatRange pathOffset = new FloatRange(0f); public Enemy Get () { Enemy instance = CreateGameObjectInstance(prefab); instance.OriginFactory = this; instance.Initialize( scale.RandomValueInRange, speed.RandomValueInRange, pathOffset.RandomValueInRange ); return instance; }
Jetzt muss ich Enemy
verfolgen und beschleunigen. float speed; … public void Initialize (float scale, float speed, float pathOffset) { model.localScale = new Vector3(scale, scale, scale); this.speed = speed; this.pathOffset = pathOffset; }
Wenn wir die Geschwindigkeit nicht explizit eingestellt haben, haben wir einfach immer den Wert 1 verwendet. Jetzt müssen wir nur noch die Abhängigkeit des Fortschrittsmultiplikators von der Geschwindigkeit erstellen. void PrepareForward () { … progressFactor = speed; } void PrepareTurnRight () { … progressFactor = speed / (Mathf.PI * 0.5f * (0.5f - pathOffset)); } void PrepareTurnLeft () { … progressFactor = speed / (Mathf.PI * 0.5f * (0.5f + pathOffset)); } void PrepareTurnAround () { … progressFactor = speed / (Mathf.PI * Mathf.Max(Mathf.Abs(pathOffset), 0.2f)); } void PrepareIntro () { … progressFactor = 2f * speed; } void PrepareOutro () { … progressFactor = 2f * speed; }
Geschwindigkeit im Bereich von 0,75 bis 1,25.Wir haben also einen wunderschönen Strom von Feinden, die sich zum Endpunkt bewegen. Im nächsten Tutorial lernen wir, wie man mit ihnen umgeht. Möchten Sie wissen, wann es veröffentlicht wird? Folgen Sie meiner Seite auf Patreon !RepositoryPDF-Artikel