SpriteKit From Scratch tecniche avanzate e ottimizzazioni

introduzione

In questa esercitazione, la quinta e ultima puntata della serie SpriteKit From Scratch, esaminiamo alcune tecniche avanzate che è possibile utilizzare per ottimizzare i giochi basati su SpriteKit per migliorare le prestazioni e l'esperienza utente.

Questo tutorial richiede l'esecuzione di Xcode 7.3 o versioni successive, che include Swift 2.2 e iOS 9.3, tvOS 9.2 e OS X 10.11.4 SDK. Per seguire, puoi utilizzare il progetto che hai creato nel precedente tutorial o scaricare una nuova copia da GitHub.

La grafica utilizzata per il gioco di questa serie può essere trovata su GraphicRiver. GraphicRiver è un'ottima fonte per trovare grafica e grafica per i tuoi giochi.

1. Atlanti di trama

Al fine di ottimizzare l'utilizzo della memoria del tuo gioco, SpriteKit fornisce la funzionalità di atlanti di texture sotto forma di SKTextureAtlas classe. Questi atlanti combinano efficacemente le trame che specifichi in una singola, grande trama che occupa meno memoria delle singole trame per conto proprio. 

Fortunatamente, Xcode può creare facilmente atlanti di texture per te. Questo viene fatto negli stessi cataloghi delle risorse utilizzati per altre immagini e risorse nei tuoi giochi. Apri il tuo progetto e naviga verso Assets.xcassets catalogo delle risorse. Nella parte inferiore della barra laterale sinistra, fai clic su + pulsante e selezionare il New Sprite Atlas opzione.

Di conseguenza, una nuova cartella viene aggiunta al catalogo delle risorse. Fare clic una volta sulla cartella per selezionarla e fare nuovamente clic per rinominarla. Nominalo ostacoli. Quindi, trascina il Ostacolo 1Ostacolo 2 risorse in questa cartella. Puoi anche cancellare lo spazio vuoto folletto asset che Xcode genera se lo desideri, ma non è richiesto. Al termine, il tuo espanso ostacoli atlante di texture dovrebbe assomigliare a questo:

È giunto il momento di usare l'atlante di texture nel codice. Aperto MainScene.swift e aggiungere la seguente proprietà al MainScene classe. Inizializziamo un atlante di texture usando il nome che abbiamo inserito nel nostro catalogo delle risorse.

let obstaclesAtlas = SKTextureAtlas (denominato: "Ostacoli")

Sebbene non sia necessario, è possibile pre-caricare i dati di un atlante di texture in memoria prima di utilizzarli. Ciò consente al gioco di eliminare qualsiasi ritardo che potrebbe verificarsi quando si carica l'atlante di trama e si recupera la prima trama da esso. Il precaricamento di un atlante di texture viene eseguito con un singolo metodo e puoi anche eseguire un blocco di codice personalizzato una volta completato il caricamento.

Nel MainScene classe, aggiungere il seguente codice alla fine del didMoveToView (_ :) metodo:

override func didMoveToView (visualizza: SKView) ... obstaclesAtlas.preloadWithCompletionHandler // Fai qualcosa una volta che l'atlante texture è stato caricato

Per recuperare una texture da un atlante di texture, si usa il textureNamed (_ :) metodo con il nome specificato nel catalogo delle risorse come parametro. Aggiorniamo il spawnObstacle (_ :) metodo nel MainScene classe per usare l'atlante di texture che abbiamo creato un momento fa. Prendiamo la texture dall'atlante di texture e la usiamo per creare un nodo sprite.

func spawnObstacle (timer: NSTimer) if player.hidden timer.invalidate () return let spriteGenerator = GKShuffledDistribution (lowestValue: 1, highestValue: 2) let texture = obstaclesAtlas.textureNamed ("Obstacle \ (spriteGenerator)") lascia ostacoli = SKSpriteNode (texture: texture) obstacle.xScale = 0.3 obstacle.yScale = 0.3 let physicsBody = SKPhysicsBody (circleOfRadius: 15) physicsBody.contactTestBitMask = 0x00000001 physicsBody.pinned = true physicsBody.allowsRotation = false obstacle.physicsBody = physicsBody let center = dimensione .width / 2.0, difference = CGFloat (85.0) var x: CGFloat = 0 let laneGenerator = GKShuffledDistribution (lowestValue: 1, highestValue: 3) switch laneGenerator.nextInt () caso 1: x = centro - differenza caso 2: x = center case 3: x = center + difference default: fatalError ("Numero al di fuori di [1, 3] generato") obstacle.position = CGPoint (x: x, y: (player.position.y + 800)) addChild ( ostacolo) obstacle.lightingBitMask = 0xFFFFFFFF obstacle.shadowCastBitMask = 0xFFFF FFFF

Nota che, se il tuo gioco sfrutta le risorse on-demand (ODR), puoi facilmente specificare uno o più tag per ciascun atlante di trama. Dopo aver effettuato correttamente l'accesso ai tag di risorse corretti con le API ODR, puoi utilizzare l'atlante di texture proprio come abbiamo fatto noi spawnObstacle (_ :) metodo. Puoi leggere di più sulle risorse on-demand in un altro mio tutorial.

2. Salvataggio e caricamento di scene

SpriteKit ti offre anche la possibilità di salvare e caricare facilmente scene da e per archiviazione persistente. Ciò consente ai giocatori di abbandonare il gioco, farlo rilanciare in un secondo momento e continuare a giocare nello stesso punto del gioco come prima.

Il salvataggio e il caricamento del tuo gioco sono gestiti da NSCoding protocollo, che il SKScene classe già conforme a. L'implementazione di SpriteKit dei metodi richiesti da questo protocollo consente automaticamente di salvare e caricare molto facilmente tutti i dettagli della scena. Se lo desideri, puoi anche ignorare questi metodi per salvare alcuni dati personalizzati insieme alla scena.

Perché il nostro gioco è molto semplice, useremo un semplice Bool valore per indicare se l'auto si è schiantata. Questo ti mostra come salvare e caricare dati personalizzati che sono legati a una scena. Aggiungere i seguenti due metodi di NSCoding protocollo al MainScene classe.

// MARK: - NSCoding Protocol richiesto init? (Coder aDecoder: NSCoder) super.init (coder: aDecoder) let carHasCrashed = aDecoder.decodeBoolForKey ("carCrashed") print ("car crashed: \ (carHasCrashed)") override func encodeWithCoder (aCoder: NSCoder) super.encodeWithCoder (aCoder) lascia carHasCrashed = player.hidden aCoder.encodeBool (carHasCrashed, forKey: "carCrashed")

Se non hai familiarità con il NSCoding protocollo, il encodeWithCoder (_ :) metodo gestisce il salvataggio della scena e l'inizializzatore con un singolo NSCoder parametro gestisce il caricamento.

Quindi, aggiungere il seguente metodo al MainScene classe. Il saveScene () il metodo crea un NSData rappresentazione della scena, usando il NSKeyedArchiver classe. Per semplificare le cose, memorizziamo i dati NSUserDefaults.

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

Quindi, sostituire l'implementazione di didBeginContactMethod (_ :) nel MainScene classe con il seguente:

func didBeginContact (contatto: SKPhysicsContact) if contact.bodyA.node == player || contact.bodyB.node == player if let explosionPath = NSBundle.mainBundle (). pathForResource ("Explosion", ofType: "sks"), lascia che smokePath = NSBundle.mainBundle (). pathForResource ("Smoke", ofType: " sks "), let explosion = NSKeyedUnarchiver.unarchiveObjectWithFile (explosionPath) as? SKEmitterNode, lascia smoke = NSKeyedUnarchiver.unarchiveObjectWithFile (smokePath) come? SKEmitterNode player.removeAllActions () player.hidden = true player.physicsBody? .CategoryBitMask = 0 camera? .RemoveAllActions () explosion.position = player.position smoke.position = player.position addChild (smoke) addChild (explosion) saveScene ( )

La prima modifica apportata a questo metodo è la modifica dei nodi del player categoryBitMask piuttosto che rimuoverlo completamente dalla scena. Ciò garantisce che, ricaricando la scena, il nodo giocatore sia ancora lì, anche se non è visibile, ma che le collisioni duplicate non vengono rilevate. L'altro cambiamento fatto è chiamare il saveScene () metodo che abbiamo definito in precedenza una volta che è stata eseguita la logica di esplosione personalizzata.

Finalmente, apri ViewController.swift e sostituire il viewDidLoad () metodo con la seguente implementazione:

override func viewDidLoad () super.viewDidLoad () lascia skView = SKView (frame: view.frame) var scena: MainScene? se let savedSceneData = NSUserDefaults.standardUserDefaults (). objectForKey ("currentScene") come? NSData, lasciare savedScene = NSKeyedUnarchiver.unarchiveObjectWithData (savedSceneData) come? MainScene scene = savedScene else if let url = NSBundle.mainBundle (). URLForResource ("MainScene", withExtension: "sks"), lascia newSceneData = NSData (contentsOfURL: url), lascia newScene = NSKeyedUnarchiver.unarchiveObjectWithData (newSceneData) come ? MainScene scene = newScene skView.presentScene (scena) view.insertSubview (skView, atIndex: 0) lascia left = LeftLane (player: scene! .Player) lascia middle = MiddleLane (player: scene! .Player) lascia right = RightLane (player: scene! .player) stateMachine = LaneStateMachine (stati: [left, middle, right]) stateMachine? .enterState (MiddleLane)

Quando si carica la scena, prima controlliamo per vedere se ci sono dati salvati nello standard NSUserDefaults. Se è così, recuperiamo questi dati e ricreamo il MainScene oggetto usando il NSKeyedUnarchiver classe. In caso contrario, otteniamo l'URL per il file di scena che abbiamo creato in Xcode e cariciamo i dati da esso in un modo simile.

Esegui la tua app e incontra un ostacolo con la tua auto. In questa fase, non vedi una differenza. Esegui la tua app di nuovo, però, e dovresti vedere che la tua scena è stata ripristinata esattamente come era quando hai appena fatto cadere la macchina.

3. Il ciclo dell'animazione

Prima di ogni frame del tuo gioco, SpriteKit esegue una serie di processi in un ordine particolare. Questo gruppo di processi è indicato come il ciclo di animazione. Questi processi tengono conto delle azioni, delle proprietà fisiche e dei vincoli che sono stati aggiunti alla scena.

Se, per qualsiasi motivo, è necessario eseguire un codice personalizzato tra uno di questi processi, è possibile ignorare alcuni metodi specifici nel proprio SKScene sottoclasse o specificare un delegato conforme al file SKSceneDelegate protocollo. Si noti che, se si assegna un delegato alla scena, le implementazioni della classe dei seguenti metodi non vengono invocate.

I processi del ciclo di animazione sono i seguenti:

Passo 1

La scena la chiama aggiornare(_:) metodo. Questo metodo ha un singolo NSTimeInterval parametro, che ti dà l'ora del sistema corrente. Questo intervallo di tempo può essere utile in quanto consente di calcolare il tempo impiegato per il rendering del fotogramma precedente.

Se il valore è maggiore di 1/60 di secondo, il gioco non è in esecuzione a 60 frame al secondo lisci (FPS) a cui SpriteKit mira. Ciò significa che potrebbe essere necessario modificare alcuni aspetti della scena (ad esempio particelle, numero di nodi) per ridurne la complessità.

Passo 2

La scena viene eseguita e calcola le azioni che hai aggiunto ai tuoi nodi e li posiziona di conseguenza.

Passaggio 3

La scena la chiama didEvaluateActions () metodo. Qui è dove è possibile eseguire qualsiasi logica personalizzata prima che SpriteKit continui con il ciclo dell'animazione.

Passaggio 4

La scena esegue le sue simulazioni fisiche e modifica la scena di conseguenza.

Passaggio 5

La scena la chiama didSimulatePhysics () metodo, che puoi sovrascrivere con didEvaluateActions () metodo.

Passaggio 6

La scena applica i vincoli che hai aggiunto ai tuoi nodi.

Passaggio 7

La scena la chiama didApplyConstraints () metodo, che è disponibile per l'override.

Passaggio 8

La scena la chiama didFinishUpdate () metodo, che puoi anche sovrascrivere. Questo è il metodo finale in cui puoi cambiare la scena prima che il suo aspetto per quel fotogramma sia finalizzato.

Passaggio 9

Infine, la scena esegue il rendering dei suoi contenuti e ne aggiorna il contenuto SKView di conseguenza.

È importante notare che, se si utilizza a SKSceneDelegate oggetto piuttosto che una sottoclasse personalizzata, ogni metodo ottiene un parametro aggiuntivo e cambia leggermente il suo nome. Il parametro extra è un SKScene oggetto, che consente di determinare a quale scena viene eseguito il metodo in relazione a. I metodi definiti dal SKSceneDelegate il protocollo è denominato come segue:

  • aggiornamento (_: FORscene :)
  • didEvaluateActionsForScene (_ :)
  • didSimulatePhysicsForScene (_ :)
  • didApplyConstraintsForScene (_ :)
  • didFinishUpdateForScene (_ :)

Anche se non si utilizzano questi metodi per apportare modifiche alla scena, possono comunque essere molto utili per il debug. Se il gioco è costantemente in ritardo e la frequenza fotogrammi diminuisce in un determinato momento del gioco, è possibile ignorare qualsiasi combinazione dei metodi sopra riportati e trovare l'intervallo di tempo tra ogni chiamata. Ciò consente di individuare con precisione se sono specificamente le azioni, la fisica, i vincoli o la grafica che sono troppo complessi perché il gioco possa girare a 60 FPS.

4. Best Practices delle prestazioni

Disegno in serie

Durante il rendering della scena, SpriteKit, per impostazione predefinita, scorre attraverso i nodi nella scena bambini array e li disegna sullo schermo nello stesso ordine in cui si trovano nell'array. Questo processo viene anche ripetuto e ripetuto per tutti i nodi figlio che un particolare nodo potrebbe avere.

L'enumerazione individuale dei nodi figlio indica che SpriteKit esegue una chiamata di estrazione per ciascun nodo. Mentre per le scene semplici questo metodo di rendering non ha un impatto significativo sulle prestazioni, poiché la scena acquisisce più nodi questo processo diventa molto inefficiente.

Per rendere il rendering più efficiente, è possibile organizzare i nodi della scena in livelli distinti. Questo è fatto attraverso il zPosition proprietà del SKNode classe. Più alto è un nodo zPosition è, il "più vicino" è allo schermo, il che significa che è reso sopra altri nodi nella scena. Allo stesso modo, il nodo con il più basso zPosition in una scena appare proprio in "dietro" e può essere sovrapposto da qualsiasi altro nodo.

Dopo aver organizzato i nodi in livelli, puoi impostare un SKView oggetto di ignoreSiblingOrder proprietà a vero. Ciò si traduce in SpriteKit utilizzando il zPosition valori per rendere una scena piuttosto che l'ordine del bambini array. Questo processo è molto più efficiente di qualsiasi nodo con lo stesso zPosition vengono raggruppati insieme in una singola chiamata di estrazione anziché avere uno per ciascun nodo.

È importante notare che il zPosition il valore di un nodo può essere negativo se necessario. I nodi nella scena vengono ancora visualizzati in ordine crescente zPosition.

Evita animazioni personalizzate

Sia il SKAction e SKConstraint le classi contengono un gran numero di regole che puoi aggiungere a una scena per creare animazioni. Essendo parte del framework SpriteKit, sono ottimizzati il ​​più possibile e si integrano perfettamente con il ciclo di animazione di SpriteKit.

L'ampia gamma di azioni e vincoli a tua disposizione consente praticamente qualsiasi animazione possibile. Per questi motivi, si consiglia di utilizzare sempre azioni e vincoli nelle scene per creare animazioni piuttosto che eseguire una logica personalizzata altrove nel codice.

In alcuni casi, specialmente se è necessario animare un gruppo di nodi ragionevolmente ampio, i campi di forza fisica possono anche essere in grado di produrre il risultato desiderato. I campi di forza sono ancora più efficienti in quanto vengono calcolati insieme al resto delle simulazioni di fisica di SpriteKit.

Maschere bit

Le tue scene possono essere ottimizzate ancora di più usando solo le maschere di bit appropriate per i nodi nella scena. Oltre a essere cruciali per il rilevamento delle collisioni fisiche, le maschere di bit determinano anche il modo in cui le simulazioni fisiche e l'illuminazione regolari influenzano i nodi in una scena.

Per ogni coppia di nodi in una scena, indipendentemente dal fatto che si scontrino o meno, SpriteKit monitora dove sono relativi l'uno all'altro. Ciò significa che, se viene lasciata con le maschere di default con tutti i bit abilitati, SpriteKit tiene traccia di dove ogni nodo si trova nella scena rispetto ad ogni altro nodo. È possibile semplificare notevolmente le simulazioni fisiche di SpriteKit definendo le maschere di bit appropriate in modo che vengano monitorate solo le relazioni tra i nodi potenzialmente in conflitto..

Allo stesso modo, una luce in SpriteKit interessa solo un nodo se la logica E delle loro maschere di bit di categoria è un valore diverso da zero. Modificando queste categorie, in modo che solo i nodi più importanti nella tua scena siano influenzati da una luce particolare, puoi ridurre notevolmente la complessità di una scena.

Conclusione

Ora dovresti sapere come puoi ottimizzare ulteriormente i tuoi giochi SpriteKit usando tecniche più avanzate, come atlanti di trama, disegno in lotti e maschere di bit ottimizzate. Dovresti anche stare tranquillo con il salvataggio e il caricamento delle scene per dare ai tuoi giocatori un'esperienza complessiva migliore.

Nel corso di questa serie, abbiamo esaminato molte delle caratteristiche e delle funzionalità del framework SpriteKit in iOS, tvOS e OS X. Ci sono argomenti ancora più avanzati che vanno oltre lo scopo di questa serie, come gli OpenGL ES e gli shader Metal personalizzati. come campi di fisica e articolazioni.

Se vuoi saperne di più su questi argomenti, ti consiglio di iniziare con SpriteKit Framework Reference e consultare le relative classi.

Come sempre, assicurati di lasciare i tuoi commenti e feedback nei commenti qui sotto.