SpriteKit von Grund auf Fortgeschrittene Techniken und Optimierungen

Einführung

In diesem Tutorial, dem fünften und letzten Teil der SpriteKit From Scratch-Serie, werden einige fortgeschrittene Techniken beschrieben, mit denen Sie Ihre auf SpriteKit basierenden Spiele optimieren können, um die Leistung und das Benutzererlebnis zu verbessern.

Für dieses Lernprogramm müssen Sie Xcode 7.3 oder höher ausführen. Dazu gehören Swift 2.2 und die iOS 9.3, tvOS 9.2 und OS X 10.11.4 SDKs. Um zu folgen, können Sie entweder das Projekt verwenden, das Sie im vorherigen Tutorial erstellt haben, oder eine neue Kopie von GitHub herunterladen.

Die für das Spiel in dieser Serie verwendeten Grafiken finden Sie auf GraphicRiver. GraphicRiver ist eine großartige Quelle, um Grafiken und Grafiken für Ihre Spiele zu finden.

1. Textur-Atlanten

Um die Speicherauslastung Ihres Spiels zu optimieren, bietet SpriteKit die Funktionalität von Texturatlasen in Form von SKTextureAtlas Klasse. Diese Atlanten kombinieren die von Ihnen angegebenen Texturen effektiv zu einer einzigen großen Textur, die weniger Speicherplatz beansprucht als die einzelnen Texturen. 

Glücklicherweise kann Xcode sehr leicht Texturatlanten für Sie erstellen. Dies geschieht in denselben Asset-Katalogen, die auch für andere Bilder und Ressourcen in Ihren Spielen verwendet werden. Öffnen Sie Ihr Projekt und navigieren Sie zu Assets.xcassets Asset-Katalog. Klicken Sie unten in der linken Seitenleiste auf + und wählen Sie die Neuer Sprite-Atlas Möglichkeit.

Dem Asset-Katalog wird daher ein neuer Ordner hinzugefügt. Klicken Sie einmal auf den Ordner, um ihn auszuwählen, und klicken Sie erneut, um ihn umzubenennen. Nennen Sie es Hindernisse. Dann ziehen Sie die Hindernis 1 und Hindernis 2 Ressourcen in diesen Ordner. Sie können das Leerzeichen auch löschen Sprite Asset, das Xcode generiert, wenn Sie möchten, aber das ist nicht erforderlich. Wenn Sie fertig sind, werden Sie erweitert Hindernisse Texturatlas sollte so aussehen:

Es ist jetzt an der Zeit, den Texturatlas im Code zu verwenden. Öffnen MainScene.swift und fügen Sie die folgende Eigenschaft hinzu MainScene Klasse. Wir initialisieren einen Texturatlas mit dem Namen, den wir in unseren Bestandskatalog eingegeben haben.

Lassen Sie obstaclesAtlas = SKTextureAtlas (benannt: "Hindernisse")

Obwohl nicht erforderlich, können Sie die Daten eines Texturatlas vor der Verwendung in den Speicher laden. Dadurch kann Ihr Spiel jegliche Verzögerung beseitigen, die beim Laden des Texturatlas und beim Abrufen der ersten Textur auftreten könnte. Das Laden eines Texturatlas erfolgt mit einer einzigen Methode. Sie können auch einen benutzerdefinierten Codeblock ausführen, nachdem das Laden abgeschlossen ist.

In dem MainScene Klasse, fügen Sie den folgenden Code am Ende der didMoveToView (_ :) Methode:

func didMoveToView überschreiben (view: SKView) … obstaclesAtlas.preloadWithCompletionHandler // Nach dem Laden des Texturatlas etwas tun

Um eine Textur aus einem Texturatlas abzurufen, verwenden Sie die textureNamed (_ :) Methode mit dem Namen, den Sie im Objektkatalog als Parameter angegeben haben. Lass uns das aktualisieren spawnObstacle (_ :) Methode in der MainScene Klasse, um den Texturatlas zu verwenden, den wir vor einem Moment erstellt haben. Wir holen die Textur aus dem Texturatlas und erstellen daraus einen Sprite-Knoten.

func spawnObstacle (timer: NSTimer) if player.hidden timer.invalidate () return let spriteGenerator = GKShuffledDistribution (niedrigsterWert: 1, höchsterWert: 2) let texture = obstaclesAtlas.textureNamed ("Hindernis \ (spriteGenerator)") = SKSpriteNode (Textur: Textur) obstacle.xScale = 0,3 obstacle.yScale = 0,3 lass physicsBody = SKPhysicsBody (circleOfRadius: 15) physicsBody.contactTestBitMask = 0x00000001 physicsBody.pinned = true physicsBody.allowsRotation = false hindernachdrainacheladehauchaduradornadhadornadupadup.adaptive.dotation = false . width / 2,0, difference = CGFloat (85.0) var x: CGFloat = 0 let laneGenerator = GKShuffledDistribution (niedrigsterWert: 1, höchsterWert: 3) switch laneGenerator.nextInt () Fall 1: x = Zentrum - Differenzfall 2: x = center case 3: x = center + difference default: fatalError ("Anzahl außerhalb von [1, 3] generiert") obstacle.position = CGPoint (x: x, y: (player.position.y + 800)) addChild ( obstacle) obstacle.lightingBitMask = 0xFFFFFFFF obstacle.shadowCastBitMask = 0xFFFF FFFF

Wenn Ihr Spiel On-Demand-Ressourcen (ODR) nutzt, können Sie problemlos einen oder mehrere Tags für jeden Texturatlas angeben. Wenn Sie mit den ODR-APIs erfolgreich auf die korrekten Ressource-Tags zugreifen, können Sie Ihren Texturatlas wie in der verwenden spawnObstacle (_ :) Methode. Weitere Informationen zu On-Demand-Ressourcen finden Sie in einem anderen Tutorial von mir.

2. Speichern und Laden von Szenen

SpriteKit bietet Ihnen auch die Möglichkeit, Szenen auf einfache Weise zu speichern und von dort zu speichern. Auf diese Weise können Spieler Ihr Spiel beenden, es zu einem späteren Zeitpunkt erneut starten und immer noch den gleichen Punkt in Ihrem Spiel wie zuvor erreichen.

Das Speichern und Laden Ihres Spiels erfolgt über die NSCoding Protokoll, das die SKScene Klasse entspricht bereits. Durch die Implementierung der von SpriteKit für dieses Protokoll erforderlichen Methoden können automatisch alle Details in Ihrer Szene sehr einfach gespeichert und geladen werden. Wenn Sie möchten, können Sie diese Methoden auch überschreiben, um einige benutzerdefinierte Daten zusammen mit Ihrer Szene zu speichern.

Da unser Spiel sehr einfach ist, verwenden wir ein einfaches Bool Wert, um anzuzeigen, ob das Auto abgestürzt ist. Hier erfahren Sie, wie Sie benutzerdefinierte Daten speichern und laden, die an eine Szene gebunden sind. Fügen Sie die folgenden zwei Methoden des hinzu NSCoding Protokoll an die MainScene Klasse.

// MARK: - Erforderliches NSCoding-Protokoll (coder aDecoder: NSCoder) super.init (coder: aDecoder) setze carHasCrashed = aDecoder.decodeBoolForKey ("carCrashed") print ("car crashed: \ (carHasCrashed)") überschreiben func encodeWithCoder (aCoder: NSCoder) super.encodeWithCoder (aCoder) let carHasCrashed = player.hidden aCoder.encodeBool (carHasCrashed, forKey: "carCrashed")

Wenn Sie mit dem nicht vertraut sind NSCoding Protokoll, das encodeWithCoder (_ :) Die Methode übernimmt das Speichern Ihrer Szene und des Initialisierers mit einer einzigen NSCoder Parameter übernimmt das Laden.

Fügen Sie als Nächstes die folgende Methode hinzu MainScene Klasse. Das saveScene () Methode erstellt ein NSData Darstellung der Szene mit der NSKeyedArchiver Klasse. Um es einfach zu halten, speichern wir die Daten in NSUserDefaults.

func saveScene () let sceneData = NSKeyedArchiver.archivedDataWithRootObject (self) NSUserDefaults.standardUserDefaults (). setObject (sceneData, forKey: "currentScene")

Als nächstes ersetzen Sie die Implementierung von didBeginContactMethod (_ :) in dem MainScene Klasse mit folgendem:

func didBeginContact (Kontakt: SKPhysicsContact) if contact.bodyA.node == Spieler || contact.bodyB.node == player wenn let explosionPath = NSBundle.mainBundle (). pathForResource ("Explosion", ofType: "sks"), let smokePath = NSBundle.mainBundle (). pathForResource ("Smoke", ofType: " sks "), let explosion = NSKeyedUnarchiver.unarchiveObjectWithFile (explosionPath) als? SKEmitterNode, rauch = NSKeyedUnarchiver.unarchiveObjectWithFile (smokePath) als? SKEmitterNode player.removeAllActions () player.hidden = true player.physicsBody? .CategoryBitMask = 0 Kamera? .RemoveAllActions () explosion.position = player.position = smoke.position = player.position addChild (smoke) addChild (explosion) saveScene ( )

Die erste Änderung, die an dieser Methode vorgenommen wurde, ist die Bearbeitung des Playerknotens categoryBitMask anstatt es ganz aus der Szene zu entfernen. Dadurch wird sichergestellt, dass der Wiedergabeknoten beim erneuten Laden der Szene immer noch vorhanden ist, auch wenn er nicht sichtbar ist, aber doppelte Kollisionen nicht erkannt werden. Die andere Änderung ist der Aufruf von saveScene () Methode, die wir zuvor definiert haben, nachdem die benutzerdefinierte Explosionslogik ausgeführt wurde.

Endlich offen ViewController.swift und ersetzen Sie die viewDidLoad () Methode mit folgender Implementierung:

func viewDidLoad () überschreiben super.viewDidLoad () let skView = SKView (frame: view.frame) var Szene: MainScene? wenn letsSceneData = NSUserDefaults.standardUserDefaults (). objectForKey ("currentScene") als? NSData, let savedScene = NSKeyedUnarchiver.unarchiveObjectWithData (savedSceneData) als? MainScene. Scene = savedScene, sonst wenn let url = NSBundle.mainBundle (). URLForResource ("MainScene", mit Erweiterung: "sks"), let newSceneData = NSData (contentsOfURL: url), let newScene = NSKeyedUnarchiver.unarchiveOnfaultaina ? MainScene scene = newScene skView.presentScene (scene) view.insertSubview (skView, atIndex: 0) let left = LeftLane (Spieler: Szene! .Player) let Mitte = MiddleLane (Player: Szene! .Player) Rechts = RightLane (Spieler: Szene! .Player) stateMachine = LaneStateMachine (Zustände: [links, Mitte, rechts]) stateMachine? .enterState (MiddleLane)

Beim Laden der Szene prüfen wir zunächst, ob im Standard Daten gespeichert sind NSUserDefaults. Wenn ja, rufen wir diese Daten ab und erstellen die MainScene Objekt mit der NSKeyedUnarchiver Klasse. Wenn nicht, erhalten wir die URL für die in Xcode erstellte Szenendatei und laden die Daten auf ähnliche Weise.

Starten Sie Ihre App und stoßen Sie mit Ihrem Auto auf ein Hindernis. Zu diesem Zeitpunkt sehen Sie keinen Unterschied. Führen Sie Ihre App erneut aus und Sie sollten sehen, dass Ihre Szene genau so wiederhergestellt wurde, als Sie gerade das Auto abgestürzt hatten.

3. Die Animationsschleife

Vor jedem Frame Ihres Spiels führt SpriteKit eine Reihe von Prozessen in einer bestimmten Reihenfolge aus. Diese Gruppe von Prozessen wird als bezeichnet Animationsschleife. Diese Prozesse berücksichtigen die Aktionen, physikalischen Eigenschaften und Einschränkungen, die Sie Ihrer Szene hinzugefügt haben.

Wenn Sie aus irgendeinem Grund benutzerdefinierten Code zwischen diesen Prozessen ausführen müssen, können Sie entweder bestimmte Methoden in Ihrem System überschreiben SKScene eine Unterklasse oder geben Sie einen Delegaten an, der der entspricht SKSceneDelegate Protokoll. Wenn Sie Ihrer Szene einen Delegaten zuweisen, werden die Klassenimplementierungen der folgenden Methoden nicht aufgerufen.

Die Animationsschleifenprozesse sind wie folgt:

Schritt 1

Die Szene nennt sich ihre aktualisieren(_:) Methode. Diese Methode hat eine einzige NSTimeInterval Parameter, der Ihnen die aktuelle Systemzeit angibt. Dieses Zeitintervall kann hilfreich sein, da Sie damit die Zeit berechnen können, die für das Rendern Ihres vorherigen Frames benötigt wurde.

Wenn der Wert größer als 1/60 Sekunde ist, läuft Ihr Spiel nicht mit den glatten 60 Frames pro Sekunde (FPS), die SpriteKit anstrebt. Dies bedeutet, dass Sie möglicherweise einige Aspekte Ihrer Szene ändern müssen (z. B. Partikel, Anzahl der Knoten), um die Komplexität zu reduzieren.

Schritt 2

Die Szene führt und berechnet die Aktionen, die Sie zu Ihren Knoten hinzugefügt haben, und positioniert sie entsprechend.

Schritt 3

Die Szene nennt sich ihre didEvaluateActions () Methode. Hier können Sie eine benutzerdefinierte Logik ausführen, bevor SpriteKit die Animationsschleife fortsetzt.

Schritt 4

Die Szene führt ihre Physik-Simulationen durch und ändert Ihre Szene entsprechend.

Schritt 5

Die Szene nennt sich ihre didSimulatePhysics () Methode, die Sie mit der überschreiben können didEvaluateActions () Methode.

Schritt 6

Die Szene wendet die Einschränkungen an, die Sie Ihren Knoten hinzugefügt haben.

Schritt 7

Die Szene nennt sich ihre didApplyConstraints () Methode, die Sie überschreiben können.

Schritt 8

Die Szene nennt sich ihre didFinishUpdate () Methode, die Sie auch überschreiben können. Dies ist die letzte Methode, in der Sie Ihre Szene ändern können, bevor das Erscheinungsbild für diesen Frame abgeschlossen ist.

Schritt 9

Schließlich rendert die Szene ihren Inhalt und aktualisiert den Inhalt SKView entsprechend.

Es ist wichtig zu beachten, dass, wenn Sie a verwenden SKSceneDelegate Objekt und nicht eine benutzerdefinierte Unterklasse, erhält jede Methode einen zusätzlichen Parameter und ändert ihren Namen geringfügig. Der zusätzliche Parameter ist ein SKScene object, mit dem Sie bestimmen können, zu welcher Szene die Methode ausgeführt wird. Die durch die SKSceneDelegate Protokoll werden wie folgt benannt:

  • Update (_: forScene :)
  • didEvaluateActionsForScene (_ :)
  • didSimulatePhysicsForScene (_ :)
  • didApplyConstraintsForScene (_ :)
  • didFinishUpdateForScene (_ :)

Selbst wenn Sie diese Methoden nicht verwenden, um Änderungen an Ihrer Szene vorzunehmen, können sie dennoch für das Debugging sehr nützlich sein. Wenn Ihr Spiel ständig schwankt und die Framerate zu einem bestimmten Zeitpunkt in Ihrem Spiel sinkt, können Sie jede Kombination der oben genannten Methoden überschreiben und das Zeitintervall zwischen den aufgerufenen Methoden ermitteln. Auf diese Weise können Sie genau feststellen, ob gerade Ihre Aktionen, Physik, Einschränkungen oder Grafiken zu komplex sind, als dass Ihr Spiel mit 60 FPS laufen könnte.

4. Best Practices für die Leistung

Batch-Zeichnung

Beim Rendern Ihrer Szene wird SpriteKit standardmäßig durch die Knoten in Ihrer Szene geführt Kinder Array und zieht sie in derselben Reihenfolge auf den Bildschirm, wie sie im Array sind. Dieser Vorgang wird auch für alle untergeordneten Knoten eines bestimmten Knotens wiederholt und wiederholt.

Einzelnes Auflisten durch untergeordnete Knoten bedeutet, dass SpriteKit für jeden Knoten einen Draw-Aufruf ausführt. Während für einfache Szenen diese Wiedergabemethode die Leistung nicht wesentlich beeinträchtigt, wird Ihre Szene mit der Zunahme der Knotenpunkte sehr ineffizient.

Um das Rendern effizienter zu gestalten, können Sie die Knoten in Ihrer Szene in verschiedenen Ebenen organisieren. Dies geschieht durch die zPosition Eigentum der SKNode Klasse. Je höher ein Knoten ist zPosition Das heißt, je näher der Bildschirm ist, desto größer wird der Abstand zu anderen Knoten in Ihrer Szene. Ebenso der Knoten mit dem niedrigsten zPosition in einer Szene erscheint ganz hinten und kann von jedem anderen Knoten überlappt werden.

Nachdem Sie Knoten in Ebenen organisiert haben, können Sie eine festlegen SKView Objekt ist ignoreSiblingOrder Eigentum an wahr. Dies führt dazu, dass SpriteKit die zPosition Werte, um eine Szene zu rendern und nicht die Reihenfolge der Kinder Array. Dieser Prozess ist weitaus effizienter als jeder Knoten mit demselben zPosition werden zu einem einzelnen Draw-Aufruf zusammengefasst, anstatt für jeden Knoten einen zu haben.

Es ist wichtig zu beachten, dass die zPosition Der Wert eines Knotens kann bei Bedarf negativ sein. Die Knoten in Ihrer Szene werden immer noch in aufsteigender Reihenfolge gerendert zPosition.

Vermeiden Sie benutzerdefinierte Animationen

Beide SKAction und SKConstraint Klassen enthalten eine Vielzahl von Regeln, die Sie einer Szene hinzufügen können, um Animationen zu erstellen. Als Teil des SpriteKit-Frameworks sind sie so viel wie möglich optimiert und passen auch perfekt in die Animationsschleife von SpriteKit.

Die vielfältigen Aktionen und Einschränkungen, die Ihnen zur Verfügung gestellt werden, ermöglichen nahezu jede mögliche Animation, die Sie sich wünschen. Aus diesen Gründen wird empfohlen, dass Sie immer Aktionen und Einschränkungen in Ihren Szenen verwenden, um Animationen zu erstellen, anstatt eine benutzerdefinierte Logik an anderer Stelle in Ihrem Code auszuführen.

In einigen Fällen, insbesondere wenn Sie eine relativ große Gruppe von Knoten animieren müssen, können physische Kraftfelder sogar das gewünschte Ergebnis erzeugen. Kraftfelder sind noch effizienter, da sie zusammen mit den übrigen Physik-Simulationen von SpriteKit berechnet werden.

Bit Masken

Ihre Szenen können noch weiter optimiert werden, indem Sie nur die entsprechenden Bitmasken für Knoten in Ihrer Szene verwenden. Bitmasken sind nicht nur für die Erkennung von Physikkollisionen entscheidend, sie bestimmen auch, wie regelmäßige Physiksimulationen und Beleuchtung die Knoten in einer Szene beeinflussen.

Für jedes Knotenpaar in einer Szene, unabhängig davon, ob sie jemals kollidieren werden oder nicht, überwacht SpriteKit, wo sie sich relativ zueinander befinden. Das bedeutet, dass SpriteKit, wenn die Standardmasken mit allen aktivierten Bits beibehalten werden, den Standort jedes Knotens in Ihrer Szene im Vergleich zu jedem anderen Knoten verfolgt. Sie können die Physik-Simulationen von SpriteKit erheblich vereinfachen, indem Sie entsprechende Bitmasken definieren, sodass nur die Beziehungen zwischen Knoten, die möglicherweise kollidieren können, verfolgt werden.

Ebenso wirkt sich ein Licht in SpriteKit nur auf einen Knoten aus, wenn es sich um einen logischen Knoten handelt UND ihrer Kategorie-Bitmasken ist ein Wert ungleich Null. Durch das Bearbeiten dieser Kategorien, sodass nur die wichtigsten Knoten in Ihrer Szene von einem bestimmten Licht beeinflusst werden, können Sie die Komplexität einer Szene erheblich reduzieren.

Fazit

Sie sollten nun wissen, wie Sie Ihre SpriteKit-Spiele mit fortschrittlicheren Techniken wie Texturatlanten, Stapelzeichnen und optimierten Bitmasken weiter optimieren können. Sie sollten auch mit dem Speichern und Laden von Szenen vertraut sein, um Ihren Spielern ein besseres Gesamterlebnis zu bieten.

In dieser ganzen Serie haben wir uns mit zahlreichen Features und Funktionen des SpriteKit-Frameworks in iOS, tvOS und OS X auseinandergesetzt. Es gibt noch mehr fortgeschrittene Themen, die über den Umfang dieser Serie hinausgehen, beispielsweise benutzerdefinierte OpenGL ES- und Metal-Shader als Physikfelder und Gelenke.

Wenn Sie mehr über diese Themen erfahren möchten, empfiehlt es sich, mit der SpriteKit Framework Reference zu beginnen und sich mit den entsprechenden Klassen vertraut zu machen.

Wie immer sollten Sie Ihre Kommentare und Rückmeldungen in den nachstehenden Kommentaren hinterlassen.