Creëer Space Invaders met Swift en Sprite Kit het implementeren van gameplay

Wat je gaat creëren

In het vorige deel van deze serie hebben we de stubs voor de hoofdklassen van het spel geïmplementeerd. In deze tutorial zullen we de indringers laten bewegen, kogels schieten voor zowel de indringers als de speler en botsdetectie implementeren. Laten we beginnen.

1. De indringers verplaatsen

We zullen de scènes gebruiken bijwerken methode om de indringers te verplaatsen. Wanneer u iets handmatig wilt verplaatsen, kunt u het volgende doen: bijwerken methode is meestal waar je dit zou willen doen.

Voordat we dit echter doen, moeten we het rightBounds eigendom. Het was aanvankelijk ingesteld op 0, omdat we de scènes moeten gebruiken grootte om de variabele in te stellen. We waren niet in staat om dat te doen buiten de methoden van de klasse, dus we zullen deze eigenschap updaten in de didMoveToView (_ :) methode.

override func didMoveToView (weergave: SKView) backgroundColor = SKColor.blackColor () rightBounds = self.size.width - 30 setupInvaders () setupPlayer ()

Implementeer vervolgens de moveInvaders methode onder de setupPlayer methode die u in de vorige zelfstudie hebt gemaakt.

func moveInvaders () var changeDirection = false enumerateChildNodesWithName ("invader") knooppunt, stop in let invader = node as! SKSpriteNode laat invaderHalfWidth = invader.size.width / 2 invader.position.x - = CGFloat (self.invaderSpeed) if (invader.position.x> self.rightBounds - invaderHalfWidth || invader.position.x < self.leftBounds + invaderHalfWidth) changeDirection = true   if(changeDirection == true) self.invaderSpeed *= -1 self.enumerateChildNodesWithName("invader")  node, stop in let invader = node as! SKSpriteNode invader.position.y -= CGFloat(46)  changeDirection = false  

We verklaren een variabele, verander richting, om bij te houden wanneer de indringers van richting moeten veranderen, naar links moeten bewegen of naar rechts moeten bewegen. We gebruiken dan de enumerateChildNodesWithName (usingBlock :) methode, die de kinderen van een knooppunt doorzoekt en eenmaal de sluiting oproept voor elk overeenkomend knooppunt dat wordt gevonden met de overeenkomende naam "Invader". De sluiting accepteert twee parameters, knooppunt is het knooppunt dat overeenkomt met het naam en hou op is een aanwijzer naar een Booleaanse variabele om de opsomming te beëindigen. We zullen niet gebruiken hou op hier, maar het is goed om te weten waarvoor het wordt gebruikt.

We casten knooppunt aan een SKSpriteNode bijvoorbeeld welke binnendringer is een subklasse van, haal de helft van de breedte ervan invaderHalfWidth, en update zijn positie. We controleren dan of het is positie is binnen de grenzen, leftBounds en rightBounds, en zo niet, dan gaan we verander richting naar waar.

Als verander richting is waar, we ontkennen invaderSpeed, die de richting waarin de indringer zich verplaatst zal veranderen. We inventariseren vervolgens de indringers en werken hun y-positie bij. Ten slotte hebben we vastgesteld verander richting terug naar vals.

De moveInvaders methode wordt aangeroepen in de bijwerken(_:) methode.

override func update (currentTime: CFTimeInterval) moveInvaders ()

Als u de toepassing nu test, ziet u dat de indringers naar links, rechts en vervolgens naar beneden gaan als ze de grenzen bereiken die we aan weerszijden hebben ingesteld.

2. Invader Bullets afvuren

Stap 1: fireBullet

Af en toe willen we dat een van de indringers een kogel afvuurt. Zoals het er nu uitziet, zijn de indringers in de onderste rij opgezet om een ​​kogel af te vuren, omdat ze zich in de invadersWhoCanFire rangschikking.

Wanneer een aanvaller wordt geraakt door een speler-opsommingsteken, wordt de indringer één rij omhoog en in dezelfde kolom toegevoegd aan de invadersWhoCanFire array, terwijl de indringer die geraakt is, zal worden verwijderd. Op deze manier kan alleen de onderste indringer van elke kolom kogels afvuren.

Voeg de toe fireBullet methode om de InvaderBullet klasse in InvaderBullet.swift.

func fireBullet (scène: SKScene) let bullet = InvaderBullet (imageName: "laser", bulletSound: nil) bullet.position.x = self.position.x bullet.position.y = self.position.y - self.size. height / 2 scene.addChild (bullet) laat moveBulletAction = SKAction.moveTo (CGPoint (x: self.position.x, y: 0 - bullet.size.height), duration: 2.0) laat removeBulletAction = SKAction.removeFromParent () opsommingsteken .runAction (SKAction.sequence ([moveBulletAction, removeBulletAction])) 

In de fireBullet methode, we instantiëren een InvaderBullet bijvoorbeeld, passeren "laser" voor imageName, en omdat we geen geluid willen horen, komen we binnen nul voor Bulletsound. We hebben zijn positie hetzelfde zijn als die van de indringer, met een kleine afwijking op de y-positie, en voeg het toe aan de scène.

We creëren er twee SKAction instanties, moveBulletAction en removeBulletAction. De moveBulletAction actie verplaatst de kogel naar een bepaald punt gedurende een bepaalde duur, terwijl de removeBulletAction actie verwijdert het van de scène. Door het volgorde(_:) methode voor deze acties, ze zullen achtereenvolgens worden uitgevoerd. Dit is de reden waarom ik de waitForDuration methode bij het afspelen van een geluid in het vorige deel van deze serie. Als u een maakt SKAction object door aan te roepen playSoundFileNamed (_: waitForCompletion :) En instellen waitForCompletion naar waar, dan zou de duur van die actie zo lang zijn als het geluid speelt, anders zou het onmiddellijk overslaan naar de volgende actie in de reeks.

Stap 2: invokeInvaderFire

Voeg de toe invokeInvaderFire methode onder de andere methoden die u hebt gemaakt in GameScence.swift.

func invokeInvaderFire () let fireBullet = SKAction.runBlock () self.fireInvaderBullet () let waitToFireInvaderBullet = SKAction.waitForDuration (1.5) laat invaderFire = SKAction.sequence ([fireBullet, waitToFireInvaderBullet]) laten herhalenForeverAction = SKAction.repeatActionForever (invaderFire ) runAction (repeatForeverAction)

De runBlock (_ :) methode van de SKAction klasse maakt een SKAction bijvoorbeeld en roept onmiddellijk de sluiting door die is doorgegeven aan de runBlock (_ :) methode. In de afsluiting roepen we de fireInvaderBullet methode. Omdat we deze methode gebruiken bij een afsluiting, moeten we gebruiken zelf om het te noemen.

We maken vervolgens een SKAction exemplaar genoemd waitToFireInvaderBullet door aan te roepen waitForDuration (_ :), doorgeven van het aantal seconden dat moet worden gewacht alvorens verder te gaan. Vervolgens maken we een SKAction aanleg, invaderFire, door het volgorde(_:) methode. Deze methode accepteert een verzameling acties die worden aangeroepen door de invaderFire actie. We willen dat deze reeks voor altijd wordt herhaald, dus we maken een actie met de naam repeatForeverAction, pas in de SKAction objecten die moeten worden herhaald en opgeroepen runAction, passeren in de repeatForeverAction actie. De runAction-methode wordt gedeclareerd in de SKNode klasse.

Stap 3: fireInvaderBullet

Voeg de toe fireInvaderBullet methode onder de invokeInvaderFire methode die u in de vorige stap hebt ingevoerd.

 func fireInvaderBullet () let randomInvader = invadersWhoCanFire.randomElement () randomInvader.fireBullet (self) 

In deze methode noemen we wat een methode lijkt te zijn genaamd randomElement dat zou een willekeurig element uit de invadersWhoCanFire array en roep het dan fireBullet methode. Er is helaas geen ingebouwd randomElement methode op de reeks structuur. We kunnen echter een maken reeks uitbreiding om deze functionaliteit te bieden.

Stap 4: Implementeer randomElement

Ga naar het dossier > nieuwe > Het dossier… en kies Swift-bestand. We doen iets anders dan ervoor, dus zorg ervoor dat je kiest Swift-bestand en niet Cocoa Touch Class. druk op volgende en noem het bestand nutsbedrijven. Voeg het volgende toe aan Utilities.swift.

import Foundation-extensie Array func randomElement () -> T let index = Int (arc4random_uniform (UInt32 (self.count))) return self [index]

We verlengen de reeks structuur om een ​​methode genaamd te hebben randomElement. De arc4random_uniform functie retourneert een getal tussen 0 en alles wat u invoert. Omdat Swift niet numeriek typen impliciet converteert, moeten we de conversie zelf uitvoeren. Ten slotte retourneren we het element van de array op index inhoudsopgave.

Dit voorbeeld illustreert hoe gemakkelijk het is om functionaliteit toe te voegen aan de structuur en klassen. U kunt meer lezen over het maken van extensies in de programmeertaal van The Swift.

Stap 5: De kogel afvuren

Met dit alles uit de weg kunnen we nu de kogels afvuren. Voeg het volgende toe aan de didMoveToView (_ :) methode.

 override func didMoveToView (view: SKView) ... setupPlayer () invokeInvaderFire ()

Als je de applicatie nu test, moet je elke seconde of zo een van de indringers uit de onderste rij zien schieten.

3. Vuren Player Bullets

Stap 1: fireBullet (scene :)

Voeg de volgende eigenschap toe aan de Speler klasse in Player.swift.

class Player: SKSpriteNode private var canFire = true

We willen beperken hoe vaak de speler een kogel kan afvuren. De canFire eigendom zal worden gebruikt om dat te regelen. Voeg vervolgens het volgende toe aan de fireBullet (scene :) methode in de Speler klasse.

func fireBullet (scène: SKScene) if (! canFire) return else canFire = false let bullet = PlayerBullet (imageName: "laser", bulletSound: "laser.mp3") bullet.position.x = zelfpositie. x bullet.position.y = self.position.y + self.size.height / 2 scene.addChild (bullet) laat moveBulletAction = SKAction.moveTo (CGPoint (x: self.position.x, y: scene.size.height + bullet.size.height), duration: 1.0) laten removeBulletAction = SKAction.removeFromParent () bullet.runAction (SKAction.sequence ([moveBulletAction, removeBulletAction])) laat waitToEnableFire = SKAction.waitForDuration (0.5) runAction (waitToEnableFire, completion: self.canFire = true) 

We zorgen er eerst voor dat de speler in staat is om te schieten door te controleren of canFire ingesteld op waar. Als dit niet het geval is, keren we onmiddellijk terug van de methode.

Als de speler kan vuren, stellen we in canFire naar vals zodat ze niet meteen een nieuwe kogel kunnen afvuren. We maken vervolgens een a PlayerBullet bijvoorbeeld, passeren "laser" voor de imageNamed parameter. Omdat we een geluid willen laten spelen wanneer de speler een kogel afvuurt, komen we binnen "Laser.mp3" voor de Bulletsound parameter.

We stellen vervolgens de positie van de kogel in en voegen deze toe aan het scherm. De volgende regels zijn hetzelfde als de binnendringer'sfireBullet methode doordat we de kogel verplaatsen en verwijderen van de scène. Vervolgens maken we een SKAction aanleg, waitToEnableFire, door het waitForDuration (_ :) klassemethode. Ten slotte voeren we aan runAction, binnenkomen waitToEnableFire, en bij voltooiing ingesteld canFire terug naar waar.

Stap 2: De kogel van de speler afvuren

Wanneer de gebruiker het scherm aanraakt, willen we een kogel afvuren. Dit is zo simpel als bellen fireBullet op de speler object in de touchesBegan (_: withEvent :) methode van de GameScene klasse.

 override func touchesBegan (touches: Set, withEvent event: UIEvent) player.fireBullet (self) 

Als u de toepassing nu test, zou u in staat moeten zijn een kogel af te vuren wanneer u op het scherm tikt. Je hoort ook het lasergeluid telkens wanneer een kogel wordt afgevuurd.

4. Collision Categories

Om te detecteren wanneer knooppunten botsen of contact maken met elkaar, zullen we de ingebouwde physics-engine van Sprite Kit gebruiken. Het standaardgedrag van de physics engine is echter dat alles botst met alles als er een fysica-instantie aan wordt toegevoegd. We hebben een manier nodig om te scheiden van wat we willen dat met elkaar in wisselwerking staat en we kunnen dit doen door categorieën te creëren waartoe specifieke fysieke lichamen behoren.

U definieert deze categorieën met een bitmasker dat een 32-bits geheel getal gebruikt met 32 ​​afzonderlijke vlaggen die kunnen worden in- of uitgeschakeld. Dit betekent ook dat je maximaal 32 categorieën kunt hebben voor je spel. Dit zou voor de meeste spellen geen probleem moeten zijn, maar het is iets om in gedachten te houden.

Voeg de volgende structuurdefinitie toe aan de GameScene klasse, onder de invaderNum verklaring in GameScene.swift.

struct CollisionCategories static laat Invader: UInt32 = 0x1 << 0 static let Player: UInt32 = 0x1 << 1 static let InvaderBullet: UInt32 = 0x1 << 2 static let PlayerBullet: UInt32 = 0x1 << 3 

We gebruiken een structuur, CollsionCategories, om categorieën voor de te maken binnendringer, Speler, InvaderBullet, en PlayerBullet klassen. We gebruiken bitverschuiving om de bits aan te zetten.

5. Speler en InvaderBullet Botsing

Stap 1: instellen InvaderBullet voor botsing

Voeg het volgende codeblok toe aan de init (imageName: Bulletsound :) methode in InvaderBullet.swift.

 override init (imageName: String, bulletSound: String?) super.init (imageName: imageName, bulletSound: bulletSound) self.physicsBody = SKPhysicsBody (texture: self.texture, size: self.size) self.physicsBody? .dynamic = true self.physicsBody? .usesPreciseCollisionDetection = true self.physicsBody? .categoryBitMask = CollisionCategories.InvaderBullet self.physicsBody? .contactTestBitMask = CollisionCategories.Player self.physicsBody? .collisionBitMask = 0x0

Er zijn verschillende manieren om een ​​natuurkundig lichaam te maken. In dit voorbeeld gebruiken we de init (textuur: maat :) initializer, die ervoor zorgt dat de botsingdetectie de vorm van de textuur gebruikt die we doorgeven. Er zijn verschillende andere initializers beschikbaar, die u kunt zien in de SKPhysicsBody-klasseverwijzing.

We hadden de. Gemakkelijk kunnen gebruiken init (rectangleOfSize :) initialisator, omdat de kogels rechthoekig van vorm zijn. In een spel dat zo klein is maakt het niet uit. Houd er echter rekening mee dat het gebruik van de init (textuur: maat :) methode kan rekenkundig duur zijn omdat het de exacte vorm van de textuur moet berekenen. Als u objecten hebt die rechthoekig of rond van vorm zijn, moet u dat soort initializers gebruiken als de spelprestaties een probleem worden.

Om detectie van botsingen te laten werken, moet minstens één van de lichamen die u test als dynamisch worden gemarkeerd. Door de usesPreciseCollisionDetection eigendom aan waar, Sprite Kit maakt gebruik van een nauwkeuriger botsingsdetectie. Stel deze eigenschap in op waar op kleine, snel bewegende lichamen zoals onze kogels.

Elk lichaam zal tot een categorie behoren en u definieert dit door het in te stellen categoryBitMask. Aangezien dit het is InvaderBullet klas, we hebben het ingesteld CollisionCategories.InvaderBullet.

Om te weten wanneer dit lichaam contact heeft gemaakt met een andere instantie waarin u bent geïnteresseerd, stelt u de contactBitMask. Hier willen we weten wanneer het InvaderBullet heeft contact gemaakt met de speler, zodat we deze gebruiken CollisionCategories.Player. Omdat een botsing geen fysische krachten moet veroorzaken, zetten we in collisionBitMask naar 0x0.

Stap 2: instellen Speler voor Collsion

Voeg het volgende toe aan de in het methode in Player.swift.

 override init () let texture = SKTexture (imageNamed: "player1") super.init (texture: texture, kleur: SKColor.clearColor (), size: texture.size ()) self.physicsBody = SKPhysicsBody (texture: self. texture, size: self.size) self.physicsBody? .dynamic = true self.physicsBody? .usesPreciseCollisionDetection = false self.physicsBody? .categoryBitMask = CollisionCategories.Player self.physicsBody? .contactTestBitMask = CollisionCategories.InvaderBullet | CollisionCategories.Invader self.physicsBody? .CollisionBitMask = 0x0 animé ()

Een groot deel hiervan zou bekend moeten zijn uit de vorige stap, dus ik zal het hier niet herhalen. Er zijn echter twee verschillen om op te merken. De eerste is dat usesPreciseCollsionDetection is ingesteld op vals, wat de standaard is. Het is belangrijk om te beseffen dat slechts één van de contacterende instanties deze eigenschap nodig heeft waar (wat de kogel was). Het andere verschil is dat we ook willen weten wanneer de speler contact maakt met een indringer. Je kunt er meerdere hebben contactBitMask categorie door ze met bitsgewijs te scheiden of (|) operator. Anders dan dat, zou je moeten opmerken dat het gewoon fundamenteel tegenovergesteld is van de InvaderBullet.

6. binnendringer en PlayerBullet Botsing

Stap 1: instellen binnendringer voor botsing

Voeg het volgende toe aan de in het methode in Invader.swift.

 override init () let texture = SKTexture (imageNamed: "invader1") super.init (texture: texture, kleur: SKColor.clearColor (), size: texture.size ()) self.name = "invader" self.physicsBody = SKPhysicsBody (texture: self.texture, size: self.size) self.physicsBody? .Dynamic = true self.physicsBody? .UsesPreciseCollisionDetection = false self.physicsBody? .CategoryBitMask = CollisionCategories.Invader self.physicsBody? .ContactTestBitMask = CollisionCategories. PlayerBullet | CollisionCategories.Player self.physicsBody? .CollisionBitMask = 0x0

Dit zou allemaal logisch moeten zijn als je meegaat. We hebben de physicsBody, categoryBitMask, en contactBitMask.

Stap 2: instellen PlayerBullet voor botsing

Voeg het volgende toe aan de init (imageName: Bulletsound :) in PlayerBullet.swift. Nogmaals, de implementatie zou nu al bekend moeten zijn.

 override init (imageName: String, bulletSound: String?) super.init (imageName: imageName, bulletSound: bulletSound) self.physicsBody = SKPhysicsBody (texture: self.texture, size: self.size) self.physicsBody? .dynamic = true self.physicsBody? .usesPreciseCollisionDetection = true self.physicsBody? .categoryBitMask = CollisionCategories.PlayerBullet self.physicsBody? .contactTestBitMask = CollisionCategories.Invader self.physicsBody? .collisionBitMask = 0x0

7. Fysica instellen voor GameScene

Stap 1: Physics World configureren

We moeten de GameScene klasse om het te implementeren SKPhysicsContactDelegate zodat we kunnen reageren wanneer twee lichamen botsen. Voeg het volgende toe om het te maken GameScene klasse conform de SKPhysicsContactDelegate protocol.

class GameScene: SKScene, SKPhysicsContactDelegate 

Vervolgens moeten we een aantal eigenschappen op de scène instellen Physics World. Voer het volgende in bovenaan de didMoveToView (_ :) methode in GameScene.swift.

override func didMoveToView (weergave: SKView) self.physicsWorld.gravity = CGVectorMake (0, 0) self.physicsWorld.contactDelegate = self ...

We hebben de zwaartekracht eigendom van Physics World naar 0 zodat geen van de fysische lichamen in de scène wordt beïnvloed door de zwaartekracht. Je kunt dit ook per lichaam doen in plaats van dat de hele wereld zwaartekracht krijgt door de affectedByGravity eigendom. We hebben ook de contactDelegate eigendom van de natuurkunde wereld zelf, de GameScene aanleg.

Stap 2: Implementeren SKPhysicsContactDelegate Protocol

Om te voldoen aan de GameScene les naar SKPhysicsContactDelegate protocol, we moeten de didBeginContact (_ :) methode. Deze methode wordt genoemd wanneer twee lichamen contact maken. De implementatie van de didBeginContact (_ :) methode ziet er als volgt uit.

func didBeginContact (contact: SKPhysicsContact) var firstBody: SKPhysicsBody var secondBody: SKPhysicsBody if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask  firstBody = contact.bodyA secondBody = contact.bodyB  else  firstBody = contact.bodyB secondBody = contact.bodyA  if ((firstBody.categoryBitMask & CollisionCategories.Invader != 0) && (secondBody.categoryBitMask & CollisionCategories.PlayerBullet != 0)) NSLog("Invader and Player Bullet Conatact")  if ((firstBody.categoryBitMask & CollisionCategories.Player != 0) && (secondBody.categoryBitMask & CollisionCategories.InvaderBullet != 0))  NSLog("Player and Invader Bullet Contact")  if ((firstBody.categoryBitMask & CollisionCategories.Invader != 0) && (secondBody.categoryBitMask & CollisionCategories.Player != 0))  NSLog("Invader and Player Collision Contact")  

We verklaren eerst twee variabelen firstBody en secondBody. Wanneer twee objecten contact maken, weten we niet welk lichaam dat is. Dit betekent dat we eerst enkele controles moeten uitvoeren om zeker te zijn firstBody is degene met de lagere categoryBitMask.

Vervolgens doorlopen we elk mogelijk scenario met behulp van het bitsgewijze & operator en de botsingscategorieën die we eerder hebben gedefinieerd om te controleren wat contact maakt. We loggen het resultaat naar de console om er zeker van te zijn dat alles naar behoren werkt. Als u de toepassing test, zouden alle contacten correct moeten werken.

Conclusie

Dit was een vrij lange tutorial, maar we hebben nu de indringers in beweging, waarbij kogels worden afgevuurd door zowel de speler als de indringers, en contactdetectie werkt door contactbitmaskers te gebruiken. We zijn aan het begin van het laatste spel. In het volgende en laatste deel van deze serie hebben we een voltooide game.