SpriteKit from Scratch geavanceerde technieken en optimalisaties

Invoering

In deze zelfstudie, het vijfde en laatste deel van de SpriteKit From Scratch-serie, bekijken we enkele geavanceerde technieken die u kunt gebruiken om uw SpriteKit-spellen te optimaliseren om de prestaties en gebruikerservaring te verbeteren.

Voor deze zelfstudie moet Xcode 7.3 of hoger worden uitgevoerd, waaronder Swift 2.2 en de SDC's iOS 9.3, tvOS 9.2 en OS X 10.11.4. Om mee te gaan, kun je het project dat je in de vorige tutorial hebt gemaakt gebruiken of een nieuwe kopie van GitHub downloaden.

De afbeeldingen die voor de game in deze serie worden gebruikt, zijn te vinden op GraphicRiver. GraphicRiver is een geweldige bron voor het vinden van illustraties en afbeeldingen voor uw games.

1. Textuuratlassen

Om het geheugengebruik van je spel te optimaliseren, biedt SpriteKit de functionaliteit van textuuratlassen in de vorm van de SKTextureAtlas klasse. Deze atlassen combineren de texturen die u specificeert effectief in een enkele, grote textuur die minder geheugen in beslag neemt dan de afzonderlijke texturen.. 

Gelukkig kan Xcode voor u gemakkelijk textuuratlassen maken. Dit gebeurt in dezelfde activacatalogi die worden gebruikt voor andere afbeeldingen en bronnen in uw games. Open uw project en ga naar Assets.xcassets activacatalogus. Klik onder aan de linkerzijbalk op de + knop en selecteer de Nieuwe Sprite-Atlas keuze.

Als gevolg hiervan wordt een nieuwe map toegevoegd aan de activacatalogus. Klik eenmaal op de map om deze te selecteren en klik nogmaals om de naam te wijzigen. Noem maar op obstakels. Sleep vervolgens de Obstakel 1 en Obstakel 2 bronnen in deze map. U kunt ook de blanco verwijderen sprite activa die Xcode genereert als u dat wilt, maar dat is niet verplicht. Wanneer je klaar bent, wordt je uitgebreid obstakels structuuratlas zou er als volgt uit moeten zien:

Het is nu tijd om de textuuratlas in code te gebruiken. Open MainScene.swift en voeg de volgende eigenschap toe aan de MainScene klasse. We initialiseren een textuuratlas met behulp van de naam die we in onze activacatalogus hebben ingevoerd.

laat obstakels toeAtlas = SKTextureAtlas (genaamd: "Obstakels")

Hoewel niet vereist, kunt u de gegevens van een structuuratlas in het geheugen laden voordat deze wordt gebruikt. Hierdoor kan je spel elke vertraging elimineren die kan optreden bij het laden van de textuuratlas en het ophalen van de eerste textuur ervan. Het vooraf laden van een structuuratlas gebeurt met een enkele methode en u kunt ook een aangepast codeblok uitvoeren nadat het laden is voltooid.

In de MainScene klasse, voeg de volgende code toe aan het einde van de didMoveToView (_ :) methode:

override func didMoveToView (weergave: SKView) ... obstaclesAtlas.preloadWithCompletionHandler // Doe iets als textuuratlas is geladen

Als u een textuur uit een structuuratlas wilt ophalen, gebruikt u de textureNamed (_ :) methode met de naam die u hebt opgegeven in de activacatalogus als parameter. Laten we het bijwerken spawnObstacle (_ :) methode in de MainScene klasse om de textuuratlas te gebruiken die we zojuist hebben gemaakt. We halen de textuur uit de textuuratlas en gebruiken deze om een ​​sprite-knooppunt te maken.

func spawnObstacle (timer: NSTimer) if player.hidden timer.invalidate () return laat spriteGenerator = GKShuffledDistribution (lowestValue: 1, highestValue: 2) laat texture = obstakelsAtlas.textureNamed ("Obstacle \ (spriteGenerator)") laat obstakel = 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 = size .width / 2.0, difference = CGFloat (85.0) var x: CGFloat = 0 let laneGenerator = GKShuffledDistribution (lowestValue: 1, highestValue: 3) switch laneGenerator.nextInt () case 1: x = center - difference case 2: x = centrale case 3: x = center + difference default: fatalError ("Nummer buiten [1, 3] gegenereerd") obstacle.position = CGPoint (x: x, y: (player.position.y + 800)) addChild ( obstakel) obstacle.lightingBitMask = 0xFFFFFFFF obstacle.shadowCastBitMask = 0xFFFF FFFF

Merk op dat als uw spel gebruik maakt van On Demand Resources (ODR), u eenvoudig een of meer tags voor elke structuuratlas kunt opgeven. Zodra u met de ODR-API's de juiste brontag (s) hebt geopend, kunt u uw textuuratlas gebruiken, net zoals we deden in de spawnObstacle (_ :) methode. U kunt meer lezen over on-demand-bronnen in een andere zelfstudie van mij.

2. Scènes opslaan en laden

SpriteKit biedt je ook de mogelijkheid om scènes gemakkelijk op te slaan en te laden van en naar permanente opslag. Hiermee kunnen spelers je game afsluiten, op een later tijdstip opnieuw laten opstarten en nog steeds hetzelfde punt in je game gebruiken als voorheen.

Het opslaan en laden van je spel wordt afgehandeld door de NSCoding protocol, dat de SKScene klas voldoet al aan. SpriteKit's implementatie van de methoden vereist door dit protocol zorgt er automatisch voor dat alle details in uw scène heel gemakkelijk kunnen worden opgeslagen en geladen. Als u wilt, kunt u deze methoden ook overschrijven om bepaalde aangepaste gegevens samen met uw scène op te slaan.

Omdat onze game erg basic is, gaan we een simpele gebruiken Bool waarde om aan te geven of de auto is gecrasht. Hier ziet u hoe u aangepaste gegevens kunt opslaan en laden die aan een scène zijn gekoppeld. Voeg de volgende twee methoden van de NSCoding protocol bij de MainScene klasse.

// MARK: - NSCoding Protocol vereist init? (Coder aDecoder: NSCoder) super.init (coder: aDecoder) laat carHasCrashed = aDecoder.decodeBoolForKey ("carCrashed") afdrukken ("auto gecrasht: \ (carHasCrashed)") overschrijven func encodeWithCoder (aCoder: NSCoder) super.encodeWithCoder (aCoder) laat carHasCrashed = player.hidden aCoder.encodeBool (carHasCrashed, forKey: "carCrashed")

Als u niet bekend bent met de NSCoding protocol, de encodeWithCoder (_ :) methode verwerkt de opslag van uw scène en de initialisator met een enkele NSCoder parameter behandelt het laden.

Voeg vervolgens de volgende methode toe aan de MainScene klasse. De saveScene () methode creëert een NSData weergave van de scène, met behulp van de NSKeyedArchiver klasse. Om de zaken eenvoudig te houden, slaan we de gegevens op NSUserDefaults.

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

Vervang vervolgens de implementatie van didBeginContactMethod (_ :) in de MainScene les met het volgende:

func didBeginContact (contact: SKPhysicsContact) if contact.bodyA.node == speler || contact.bodyB.node == speler if let explosPath = NSBundle.mainBundle (). pathForResource ("Explosion", ofType: "sks"), laat smokePath = NSBundle.mainBundle (). pathForResource ("Smoke", ofType: " sks "), laat explosion = NSKeyedUnarchiver.unarchiveObjectWithFile (explosionPath) als? SKEmitterNode, laat roken = NSKeyedUnarchiver.unarchiveObjectWithFile (smokePath) as? 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 ( )

De eerste wijziging in deze methode is het bewerken van de spelersknooppunten categoryBitMask in plaats van het volledig uit de scene te verwijderen. Dit zorgt ervoor dat bij het opnieuw laden van de scène het spelersknooppunt er nog steeds is, ook al is het niet zichtbaar, maar worden dubbele botsingen niet gedetecteerd. De andere gemaakte wijziging is het oproepen van de saveScene () methode die we eerder hebben gedefinieerd nadat de aangepaste explosielogica is uitgevoerd.

Eindelijk, open ViewController.swift en vervang de viewDidLoad () methode met de volgende implementatie:

override func viewDidLoad () super.viewDidLoad () laat skView = SKView (frame: view.frame) var scene: MainScene? if let savedSceneData = NSUserDefaults.standardUserDefaults (). objectForKey ("currentScene") als? NSData, laat savedScene = NSKeyedUnarchiver.unarchiveObjectWithData (savedSceneData) als? MainScene scene = savedScene else if let url = NSBundle.mainBundle (). URLForResource ("MainScene", metExtension: "sks"), laat newSceneData = NSData (contentsOfURL: url), laat newScene = NSKeyedUnarchiver.unarchiveObjectWithData (newSceneData) als ? MainScene scene = newScene skView.presentScene (scène) view.insertSubview (skView, atIndex: 0) laat left = LeftLane (speler: scene! .Player) laat middle = MiddleLane (speler: scene! .Player) laat right = RightLane (speler: scene! .player) stateMachine = LaneStateMachine (toestanden: [links, midden, rechts]) state Machine? .enterState (MiddleLane)

Bij het laden van de scène controleren we eerst of er opgeslagen gegevens in de standaard zijn NSUserDefaults. Als dat het geval is, halen we deze gegevens op en maken we de MainScene object met behulp van de NSKeyedUnarchiver klasse. Zo niet, dan krijgen we de URL voor het scènebestand dat we in Xcode hebben gemaakt en laden we de gegevens op dezelfde manier.

Run je app en ren met je auto tegen een obstakel aan. In dit stadium zie je geen verschil. Voer uw app opnieuw uit, en u zult zien dat uw scène is hersteld tot precies zoals het was toen u net de auto had gecrasht.

3. De animatielus

Voordat elk frame van je game wordt weergegeven, voert SpriteKit een reeks processen uit in een bepaalde volgorde. Deze groep processen wordt de animatielus. Deze processen zijn verantwoordelijk voor de acties, fysische eigenschappen en beperkingen die u aan uw scène hebt toegevoegd.

Als u, om welke reden dan ook, aangepaste code moet uitvoeren tussen al deze processen, kunt u bepaalde specifieke methoden in uw SKScene subklasse of geef een afgevaardigde op die voldoet aan de SKSceneDelegate protocol. Merk op dat, als u een deelnemer aan uw scène toewijst, de implementaties van de klasse van de volgende methoden niet worden aangeroepen.

De animatielusprocessen zijn als volgt:

Stap 1

De scène noemt het bijwerken(_:) methode. Deze methode heeft een single NSTimeInterval parameter, die u de huidige systeemtijd geeft. Dit tijdsinterval kan handig zijn omdat u hiermee kunt berekenen hoe lang het duurt voordat uw vorige frame is weergegeven.

Als de waarde groter is dan 1/60 seconde, wordt je game niet uitgevoerd met de vloeiende 60 frames per seconde (FPS) waar SpriteKit naar streeft. Dit betekent dat u mogelijk sommige aspecten van uw scène (bijvoorbeeld deeltjes, aantal knooppunten) moet wijzigen om de complexiteit ervan te verminderen.

Stap 2

De scène voert en berekent de acties die u aan uw knooppunten hebt toegevoegd en positioneert ze dienovereenkomstig.

Stap 3

De scène noemt het didEvaluateActions () methode. Hier kunt u aangepaste logica uitvoeren voordat SpriteKit doorgaat met de animatielus.

Stap 4

De scène voert zijn fysica-simulaties uit en wijzigt uw scène dienovereenkomstig.

Stap 5

De scène noemt het didSimulatePhysics () methode, die u kunt negeren met de didEvaluateActions () methode.

Stap 6

De scène past de beperkingen toe die u aan uw knooppunten hebt toegevoegd.

Stap 7

De scène noemt het didApplyConstraints () methode, die beschikbaar is voor u om te negeren.

Stap 8

De scène noemt het didFinishUpdate () methode, die u ook kunt negeren. Dit is de laatste methode waarmee je je scène kunt wijzigen voordat het uiterlijk voor dat frame is voltooid.

Stap 9

Ten slotte wordt de inhoud van de scène weergegeven en wordt de inhoud bijgewerkt SKView overeenkomstig.

Het is belangrijk om op te merken dat, als u een SKSceneDelegate object in plaats van een aangepaste subklasse, krijgt elke methode een extra parameter en verandert de naam enigszins. De extra parameter is een SKScene object, waarmee u kunt bepalen met welke scène de methode wordt uitgevoerd. De methoden gedefinieerd door de SKSceneDelegate protocol worden als volgt genoemd:

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

Zelfs als u deze methoden niet gebruikt om wijzigingen in uw scène aan te brengen, kunnen ze nog steeds erg nuttig zijn voor foutopsporing. Als je spel consequent vertraagt ​​en de framesnelheid daalt op een bepaald moment in je spel, kun je elke combinatie van de bovenstaande methoden overschrijven en het tijdsinterval vinden tussen elk spel dat wordt gebeld. Hiermee kun je nauwkeurig bepalen of het specifiek je acties, fysica, beperkingen of grafische afbeeldingen zijn die te complex zijn om door je spel te laten werken met 60 FPS.

4. Praktische tips voor prestaties

Batch-tekening

Bij het renderen van je scène, loopt SpriteKit standaard door de knooppunten in je scènes kinderen array en trekt ze naar het scherm in dezelfde volgorde als waarin ze zich in de array bevinden. Dit proces wordt ook herhaald en herhaald voor elk kindknooppunt dat een bepaald knooppunt zou kunnen hebben.

Het opsommen via individuele nodes betekent dat SpriteKit een tekenoproep voor elk knooppunt uitvoert. Hoewel deze methode voor eenvoudige scènes geen significante invloed heeft op de prestaties, omdat uw scène meer knooppunten krijgt, wordt dit proces zeer inefficiënt.

Als u de weergave efficiënter wilt maken, kunt u de knooppunten in uw scène indelen in verschillende lagen. Dit gebeurt via de zPosition eigendom van de SKNode klasse. Hoe hoger een knooppunt zPosition is, "dichterbij" het is op het scherm, wat betekent dat het wordt weergegeven bovenop andere knooppunten in uw scène. Evenzo is de knoop met de laagste zPosition in een scène verschijnt helemaal achteraan en kan overlapt worden door een ander knooppunt.

Nadat u knooppunten in lagen hebt georganiseerd, kunt u een instellen SKView voorwerpen ignoreSiblingOrder eigendom aan waar. Dit resulteert in SpriteKit met behulp van de zPosition waarden om een ​​scène weer te geven in plaats van de volgorde van de kinderen matrix. Dit proces is veel efficiënter als alle knooppunten met hetzelfde zPosition zijn samen gebundeld in een enkele tekenoproep in plaats van een voor elke knoop.

Het is belangrijk op te merken dat de zPosition de waarde van een knoop kan, indien nodig, negatief zijn. De knooppunten in uw scène worden nog steeds weergegeven in volgorde van toenamen zPosition.

Vermijd aangepaste animaties

Beide SKAction en SKConstraint klassen bevatten een groot aantal regels die u aan een scène kunt toevoegen om animaties te maken. Als onderdeel van het SpriteKit-framework zijn ze zo veel mogelijk geoptimaliseerd en passen ze ook perfect in de animatielus van SpriteKit.

Het brede scala aan acties en beperkingen die u krijgt, maakt bijna elke mogelijke animatie mogelijk. Om deze redenen wordt aanbevolen dat u altijd acties en beperkingen in uw scènes gebruikt om animaties te maken in plaats van aangepaste logica elders in uw code uit te voeren.

In sommige gevallen, met name als u een redelijk grote groep knooppunten moet animeren, kunnen natuurkundig geforceerde velden mogelijk zelfs het gewenste resultaat produceren. Force-velden zijn nog efficiënter omdat ze worden berekend naast de rest van de fysica-simulaties van SpriteKit.

Bitmaskers

Uw scènes kunnen zelfs nog verder worden geoptimaliseerd door alleen de juiste bitmaskers voor knooppunten in uw scène te gebruiken. Naast dat ze cruciaal zijn voor botsingdetectie van natuurkunde, bepalen bitmaskers ook hoe regelmatige fysische simulaties en verlichting de knooppunten in een scène beïnvloeden.

Voor elk paar knooppunten in een scène, ongeacht of ze ooit zullen botsen, bewaakt SpriteKit waar ze ten opzichte van elkaar zijn. Dit betekent dat, als de standaardmaskers met alle bits ingeschakeld blijven, SpriteKit bijhoudt waar elk knooppunt zich in uw scène bevindt in vergelijking met elk ander knooppunt. U kunt de fysica-simulaties van SpriteKit aanzienlijk vereenvoudigen door geschikte bitmaskers te definiëren, zodat alleen de relaties tussen knooppunten die mogelijk kunnen botsen, worden bijgehouden.

Evenzo beïnvloedt een licht in SpriteKit alleen een knooppunt als het logisch is EN van hun categoriebitmaskers is een niet-nulwaarde. Door deze categorieën te bewerken, zodat alleen de belangrijkste knooppunten in uw scène worden beïnvloed door een bepaald licht, kunt u de complexiteit van een scène aanzienlijk verminderen.

Conclusie

U moet nu weten hoe u uw SpriteKit-spellen verder kunt optimaliseren door meer geavanceerde technieken te gebruiken, zoals textuuratlassen, batchtekening en geoptimaliseerde bitmaskers. Je moet ook comfortabel zijn met het opslaan en laden van scènes om je spelers een betere algehele ervaring te geven.

Gedurende deze reeks hebben we veel van de functies en functionaliteit van het SpriteKit-framework in iOS, tvOS en OS X doorgenomen. Er zijn zelfs meer geavanceerde onderwerpen buiten het bereik van deze serie, zoals aangepaste OpenGL ES en Metal shaders. als natuurkundige velden en gewrichten.

Als u meer wilt weten over deze onderwerpen, raad ik aan te beginnen met de SpriteKit Framework Reference en de relevante klassen te lezen.

Laat zoals altijd uw opmerkingen en feedback achter in de opmerkingen hieronder.