SpriteKit From Scratch Avancerade tekniker och optimeringar

Introduktion

I den här handledningen, den femte och sista delen av SpriteKit From Scratch-serien, tittar vi på några avancerade tekniker som du kan använda för att optimera dina SpriteKit-baserade spel för att förbättra prestanda och användarupplevelse.

Denna handledning kräver att du kör Xcode 7.3 eller senare, som inkluderar Swift 2.2 och iOS 9.3, tvOS 9.2 och OS X 10.11.4 SDK. För att följa med kan du antingen använda det projekt du skapade i föregående handledning eller ladda ner en ny kopia från GitHub.

Grafiken som används för spelet i denna serie finns på GraphicRiver. GraphicRiver är en bra källa för att hitta konstverk och grafik för dina spel.

1. Texturatlaser

För att optimera minnesanvändningen i ditt spel, tillhandahåller SpriteKit funktionaliteten av texturatlaser i form av SKTextureAtlas klass. Dessa atlaser kombinerar effektivt de texturer du anger i en enda stor textur som tar upp mindre minne än de enskilda texturerna på egen hand. 

Lyckligtvis kan Xcode skapa texturatlaser mycket enkelt för dig. Detta görs i samma tillgångskataloger som används för andra bilder och resurser i dina spel. Öppna ditt projekt och navigera till Assets.xcassets tillgångskatalog. Klicka på knappen längst ner i vänster sidofält + knappen och välj Ny Sprite Atlas alternativ.

Som ett resultat läggs en ny mapp till tillgångskatalogen. Klicka på mappen en gång för att markera den och klicka igen för att byta namn på den. Namnge det hinder. Dra sedan Hindring 1 och Hinder 2 resurser i den här mappen. Du kan även ta bort det tomma Sprite tillgång som Xcode genererar om du vill, men det är inte nödvändigt. När du är klar fyller du ut hinder texturatlas bör se så här ut:

Det är dags att använda texturen atlas i kod. Öppna MainScene.swift och lägg till följande egendom till MainScene klass. Vi initierar en texturatlas med namnet vi anlände i vår tillgångskatalog.

låt hinderAtlas = SKTextureAtlas (namngiven: "Hindringar")

Medan det inte behövs kan du förinstala data från en texturatlas till minnet innan den används. Detta gör det möjligt för ditt spel att eliminera eventuell fördröjning som kan uppstå när du laddar texturlaset och hämtar den första konsistensen från den. Förladdning av en texturatlas görs med en enda metod och du kan även köra ett anpassat kodblock när laddningen har slutförts.

MainScene klass, lägg till följande kod i slutet av didMoveToView (_ :) metod:

åsidosätta func didMoveToView (visa: SKView) ... hinderAtlas.preloadWithCompletionHandler // Gör något när texturlaset har laddats

För att hämta en textur från en texturatlas använder du textureNamed (_ :) metod med det namn du angav i tillgångskatalogen som en parameter. Låt oss uppdatera spawnObstacle (_ :) metod i MainScene klass för att använda texturatlaset som vi skapade för en stund sedan. Vi hämtar texturen från texturatlasen och använder den för att skapa en sprite nod.

func spawnObstacle (timer: NSTimer) om player.hidden timer.invalidate () return låt spriteGenerator = GKShuffledDistribution (lowestValue: 1, highestValue: 2) låt textur = hinderAtlas.textureNamed ("Obstacle \ (spriteGenerator)") = SKSpriteNode (textur: textur) obstacle.xScale = 0.3 obstacle.yScale = 0.3 låt 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 låtGenerator = GKShuffledDistribution (lowestValue: 1, highestValue: 3) byt laneGenerator.nextInt () case 1: x = center - difference case 2: x = center case 3: x = center + difference default: fatalError ("Antal utanför [1, 3] genererade") obstacle.position = CGPoint (x: x, y: (player.position.y + 800)) addChild hinder) obstacle.lightingBitMask = 0xFFFFFFFF obstacle.shadowCastBitMask = 0xFFFF F F F F 

Observera att om ditt spel utnyttjar On Demand Resources (ODR), kan du enkelt ange en eller flera taggar för varje texturatlas. När du väl har fått tillgång till rätt källkod med ODR API, kan du sedan använda din texturatlas som vi gjorde i spawnObstacle (_ :) metod. Du kan läsa mer om On-Demand Resources i en annan handledning av mig.

2. Spara och ladda scener

SpriteKit ger dig också möjlighet att enkelt spara och ladda scener till och från bestående lagring. Detta gör det möjligt för spelare att avsluta ditt spel, få det omstartade vid en senare tidpunkt och fortfarande vara upp till samma punkt i ditt spel som tidigare.

Sparar och laddar ditt spel hanteras av NSCoding protokoll, som SKScene klassen överensstämmer redan SpriteKits genomförande av metoderna enligt detta protokoll tillåter automatiskt att alla detaljer i din scen sparas och laddas väldigt enkelt. Om du vill kan du också åsidosätta dessa metoder för att spara några anpassade data tillsammans med din scen.

Eftersom vårt spel är väldigt grundläggande, kommer vi att använda en enkel Bool värde för att ange huruvida bilen har kraschat. Detta visar hur du sparar och laddar anpassade data som är knutna till en scen. Lägg till följande två metoder för NSCoding protokoll till MainScene klass.

// MARK: - NSCoding-protokoll krävs init? (Kodare aDecoder: NSCoder) super.init (kodare: aDecoder) låt carHasCrashed = aDecoder.decodeBoolForKey ("carCrashed") skriva ut ("bil kraschade: \ (carHasCrashed)") func encodeWithCoder (aCoder: NSCoder) super.encodeWithCoder (aCoder) låter carHasCrashed = player.hidden aCoder.encodeBool (carHasCrashed, förKey: "carCrashed")

Om du inte är bekant med NSCoding protokollet, encodeWithCoder (_ :) Metoden hanterar sparandet av din scen och initieraren med en enda NSCoder parameter hanterar laddningen.

Lägg sedan till följande metod i MainScene klass. De saveScene () Metoden skapar en NSData representation av scenen, med hjälp av NSKeyedArchiver klass. För att hålla sakerna enkla lagrar vi data i NSUserDefaults.

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

Nästa, ersätt genomförandet av didBeginContactMethod (_ :)MainScene klass med följande:

func didBeginContact (kontakt: SKPhysicsContact) om contact.bodyA.node == player || contact.bodyB.node == spelare if let explosionPath = NSBundle.mainBundle (). pathForResource ("Explosion", ofType: "sks"), låt smokePath = NSBundle.mainBundle (). pathForResource ("Smoke", ofType: " sks "), låt explosion = NSKeyedUnarchiver.unarchiveObjectWithFile (explosionPath) som? SKEmitterNode, låt rök = NSKeyedUnarchiver.unarchiveObjectWithFile (smokePath) som? SKEmitterNode player.removeAllActions () player.hidden = sant player.physicsBody? .CategoryBitMask = 0 kamera? .RemoveAllActions () explosion.position = player.position smoke.position = player.position addChild (smoke) addChild (explosion) saveScene )

Den första ändringen som gjorts till denna metod är att redigera spelarens nod categoryBitMask snarare än att ta bort den helt från scenen. Detta säkerställer att spelarnoden fortfarande är kvar, trots att den inte är synlig, men att dubbla kollisioner inte detekteras. Den andra ändringen som görs är att ringa saveScene () Metod vi definierade tidigare när den egna explosionslogiken har körts.

Slutligen, öppna ViewController.swift och ersätt viewDidLoad () metod med följande genomförande:

åsidosätta func viewDidLoad () super.viewDidLoad () låt skView = SKView (frame: view.frame) var scen: MainScene? om låt savedSceneData = NSUserDefaults.standardUserDefaults (). objectForKey ("currentScene") som? NSData, låt savedScene = NSKeyedUnarchiver.unarchiveObjectWithData (savedSceneData) som? MainScene scene = savedScene annars om låt url = NSBundle.mainBundle (). URLForResource ("MainScene", medExtension: "sks"), låt newSceneData = NSData (contentOfURL: url), låt newScene = NSKeyedUnarchiver.unarchiveObjectWithData (newSceneData) ? MainScene scene = newScene skView.presentScene (scene) view.insertSubview (skView, atIndex: 0) låt vänster = LeftLane (spelare: scen! .Player) låt mitt = MiddleLane (spelare: scen! .Player) låt höger = RightLane (spelare: scen! .player) stateMachine = LaneStateMachine (stater: [vänster, mitten, höger]) stateMachine? .enterState (MiddleLane)

När du läser in scenen kontrollerar vi först för att se om det finns sparade data i standarden NSUserDefaults. Om så är fallet hämtar vi dessa data och återskapar MainScene objekt med hjälp av NSKeyedUnarchiver klass. Om inte, får vi URL-adressen för scenfilen vi skapade i Xcode och laddar data från den på ett liknande sätt.

Kör din app och kör i ett hinder med din bil. I detta skede ser du ingen skillnad. Kör din app igen, och du bör se att din scen har återställts till exakt hur det var när du bara kraschade bilen.

3. Animation Loop

Innan varje ram i ditt spel gör SpriteKit en serie processer i en viss ordning. Denna grupp av processer kallas för animationsslinga. Dessa processer står för de åtgärder, fysikegenskaper och begränsningar som du har lagt till i din scen.

Om du av någon anledning behöver köra anpassad kod mellan någon av dessa processer kan du antingen åsidosätta vissa specifika metoder i din SKScene underklass eller ange en delegat som överensstämmer med SKSceneDelegate protokoll. Observera att om du tilldelar en delegat till din scen, åberopas inte klassens implementeringar av följande metoder.

Animationsprocesserna är följande:

Steg 1

Scenen kallar sin uppdatering(_:) metod. Denna metod har en singel NSTimeInterval parameter, vilket ger dig den aktuella systemtiden. Det här tidsintervallet kan vara användbart eftersom det kan du beräkna den tid det tog för din tidigare ram att göra.

Om värdet är större än 1/60: e sekund körs inte ditt spel med de smidiga 60 bilder per sekund (FPS) som SpriteKit syftar till. Det innebär att du kanske behöver ändra vissa aspekter av din scen (till exempel partiklar, antal noder) för att minska dess komplexitet.

Steg 2

Scenen kör och beräknar de åtgärder du har lagt till dina noder och positionerar dem i enlighet därmed.

Steg 3

Scenen kallar sin didEvaluateActions () metod. Det är här du kan utföra någon anpassad logik innan SpriteKit fortsätter med animationsslingan.

Steg 4

Scenen utför sina fysiksimuleringar och ändrar din scen i enlighet därmed.

Steg 5

Scenen kallar sin didSimulatePhysics () metod som du kan åsidosätta med didEvaluateActions () metod.

Steg 6

Scenen gäller de begränsningar som du har lagt till dina noder.

Steg 7

Scenen kallar sin didApplyConstraints () metod, som är tillgänglig för dig att åsidosätta.

Steg 8

Scenen kallar sin didFinishUpdate () metod, som du också kan åsidosätta. Det här är den sista metoden som du kan ändra på din scen innan dess utseende för den ramen är färdigställd.

Steg 9

Slutligen gör scenen innehållet och uppdaterar dess innehåll SKView följaktligen.

Det är viktigt att notera att om du använder en SKSceneDelegate objektet istället för ett anpassat underklass får varje metod en extra parameter och ändrar namnet något. Den extra parametern är en SKScene objekt, som låter dig bestämma vilken scen metoden körs i förhållande till. De metoder som definieras av SKSceneDelegate protokollet heter enligt följande:

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

Även om du inte använder dessa metoder för att göra några förändringar i din scen, kan de fortfarande vara mycket användbara för debugging. Om ditt spel konsekvent slår och bildfrekvensen sjunker vid ett visst ögonblick i ditt spel kan du åsidosätta någon kombination av ovanstående metoder och hitta tidsintervallet mellan var och en som kallas. Detta gör att du kan exakt hitta om det är specifikt dina handlingar, fysik, begränsningar eller grafik som är för komplexa för att ditt spel ska springa vid 60 FPS.

4. Utför bästa praxis

Batchteckning

När du gör din scen går SpriteKit som standard genom nodarna i din scen barn array och drar dem till skärmen i samma ordning som de är i arrayen. Denna process upprepas också och slingas för några barnnoder som en viss nod kan ha.

Att räkna upp genom barnnoder innebär att SpriteKit kör ett röstsamtal för varje nod. Medan för enkla scener påverkar den här metoden för återgivning inte betydande prestanda, eftersom din scen blir mer noder blir processen mycket ineffektiv.

För att göra effektiviteten mer effektiv kan du organisera noderna i din scen i olika lager. Detta görs genom zPosition egenskapen hos SKNode klass. Ju högre en nod är zPosition är "närmare" det till skärmen, vilket innebär att det görs på toppen av andra noder i din scen. På samma sätt är noden med den lägsta zPosition i en scen visas mycket "tillbaka" och kan överlappas av någon annan nod.

Efter att ha organiserat noder i lager kan du ställa in en SKView objekt ignoreSiblingOrder egendom till Sann. Detta resulterar i att SpriteKit använder zPosition värden för att göra en scen snarare än ordningen för barn array. Denna process är mycket mer effektiv som alla noder med samma zPosition är sammanslagna i ett enda röstsamtal snarare än att ha en för varje nod.

Det är viktigt att notera att zPosition Värdet på en nod kan vara negativt om det behövs. Noderna i din scen gör fortfarande för att öka zPosition.

Undvik anpassade animeringar

Både SKAction och SKConstraint klasser innehåller ett stort antal regler som du kan lägga till i en scen för att skapa animeringar. Att vara en del av SpriteKit-ramverket optimeras de så mycket de kan vara och passar perfekt med SpriteKits animationsslinga.

Det breda utbudet av åtgärder och begränsningar som ges till dig tillåter nästan alla möjliga animeringar du kan önska. Av dessa skäl rekommenderas att du alltid använder åtgärder och begränsningar i dina scener för att skapa animeringar istället för att utföra någon anpassad logik någon annanstans i din kod.

I vissa fall, speciellt om du behöver animera en ganska stor grupp av noder, kan fysikstyrka även till och med kunna producera det resultat du vill ha. Kraftfält är ännu effektivare eftersom de beräknas tillsammans med resten av SpriteKits fysiksimuleringar.

Bitmasker

Dina scener kan optimeras ännu mer genom att bara använda lämpliga bitmaskar för noder i din scen. Förutom att vara avgörande för kollisionsdetektering av fysiken bestämmer bitmaskar också hur regelbundna fysik simuleringar och belysning påverkar noderna i en scen.

För några par noder i en scen, oavsett om de någonsin kommer att kollidera, övervakar SpriteKit var de är i förhållande till varandra. Detta betyder att, om de lämnas med standardmaskerna med alla bitar aktiverade, håller SpriteKit reda på var varje nod ligger i din scen jämfört med varje annan nod. Du kan i stor utsträckning förenkla SpriteKits fysiksimuleringar genom att definiera lämpliga bitmasker så att endast relationerna mellan noder som potentiellt kan kollidera spåras.

På samma sätt påverkar ett ljus i SpriteKit bara en nod om den logiska OCH av deras kategori bitmaskar är ett icke-nollvärde. Genom att redigera dessa kategorier, så att endast de viktigaste noderna i din scen påverkas av ett visst ljus, kan du avsevärt minska scenens komplexitet.

Slutsats

Du borde nu veta hur du kan optimera dina SpriteKit-spel ytterligare genom att använda mer avancerade tekniker, såsom texturatlaser, batchteckning och optimerade bitmaskar. Du bör också vara bekväm med att spara och ladda scener för att ge dina spelare en bättre övergripande upplevelse.

I hela denna serie har vi tittat på många funktioner och funktionalitet i SpriteKit-ramen i iOS, tvOS och OS X. Det finns ännu mer avancerade ämnen utanför ramen för denna serie, så är även vanliga OpenGL ES och Metal shaders som fysik fält och leder.

Om du vill lära dig mer om dessa ämnen rekommenderar jag att du börjar med SpriteKit Framework Reference och läser om relevanta klasser.

Som alltid, var noga med att lämna dina kommentarer och feedback i kommentarerna nedan.