Creați Space Invaders cu Swift și Sprite Kit implementarea gameplay-ului

Ce veți crea

În partea anterioară a acestei serii, am implementat piedicile pentru clasele principale ale jocului. În acest tutorial, vom primi invadatorii în mișcare, gloanțe de ardere atât pentru invadatori și jucător, și să pună în aplicare detectarea coliziunii. Să începem.

1. Mutarea Invadatorilor

Vom folosi scena Actualizați pentru a muta invadatorii. Ori de câte ori doriți să mutați ceva manual, faceți clic pe Actualizați metoda este în general în cazul în care doriți să faceți acest lucru.

Înainte de a face acest lucru, totuși, trebuie să actualizăm rightBounds proprietate. A fost inițial stabilită 0, pentru că trebuie să folosim scena mărimea pentru a seta variabila. Nu am reușit să facem acest lucru în afara metodelor clasei, astfel încât vom actualiza această proprietate în didMoveToView (_ :) metodă.

suprascrie func didMoveToView (vizualizare: SKView) backgroundColor = SKColor.blackColor () rightBounds = auto.size.width - 30 setupInvaders () setupPlayer ()

Apoi, implementați moveInvaders mai jos setupPlayer metoda pe care ați creat-o în tutorialul anterior.

func moveInvaders () var changeDirection = false enumerateChildNodesWithName ("invader") nod, opriți în let invader = nod ca! SKSpriteNode permite invaderHalfWidth = invader.size.width / 2 invader.position.x - = CGFloat (auto.invaderSpeed) dacă (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  

Vom declara o variabilă, schimba directia, pentru a urmări când invadatorii trebuie să schimbe direcția, să se miște în stânga sau să meargă la dreapta. Apoi folosim enumerateChildNodesWithName (usingBlock :) , care caută copiii unui nod și cheamă închiderea o dată pentru fiecare nod de potrivire pe care îl găsește cu numele de potrivire "invadator". Închiderea acceptă doi parametri, nodul este nodul care se potrivește cu Nume și Stop este un pointer la o variabilă booleană pentru a termina enumerarea. Nu vom folosi Stop aici, dar este bine să știți pentru ce este folosit.

Am aruncat nodul la un SKSpriteNode exemplu care invadator este o subclasă de, obține jumătate din lățimea sa invaderHalfWidth, și să își actualizeze poziția. Atunci verificăm dacă este poziţie este în limitele, leftBounds și rightBounds, și, dacă nu, am stabilit schimba directia la Adevărat.

Dacă schimba directia este Adevărat, ne negăm invaderSpeed, care va schimba direcția în care se deplasează invadatorul. Apoi enumerăm prin invadatori și actualizăm poziția lor y. În cele din urmă, am stabilit schimba directia înapoi la fals.

moveInvaders metoda este numită în Actualizați(_:) metodă.

suprascrie func actual (actualTime: CFTimeInterval) moveInvaders ()

Dacă testați aplicația acum, ar trebui să vedeți invadatorii să se deplaseze la stânga, la dreapta și apoi în jos dacă ating limitele pe care le-am setat pe ambele părți.

2. Bullets Invader Fire

Pasul 1: fireBullet

Atât de des vrem ca unul dintre invadatori să tragă un glonț. În starea actuală, invadatorii din rândul de jos sunt pregătiți să tragă un glonț, pentru că se află în invadersWhoCanFire mulțime.

Atunci când un invadator este lovit de un glonț al jucătorului, atunci rândul invadatorului un rând în sus și în aceeași coloană va fi adăugat la invadersWhoCanFire array, în timp ce invadatorul care a fost lovit va fi eliminat. În acest fel, numai invadatorul de jos din fiecare coloană poate declanșa gloanțe.

Adaugă fireBullet metoda pentru a InvaderBullet clasă în InvaderBullet.swift.

func fireBullet (scena: SKScene) lăsați bullet = InvaderBullet (imageName: "laser", bulletSound: nil) bullet.position.x = auto.position.x bullet.position.y = auto.position.y - self.size. înălțime / 2 scene.addChild (bullet) permite moveBulletAction = SKAction.moveTo (CGPoint (x: self.position.x, y: 0 - bullet.size.height), duration: 2.0) letBLletAction = SKAction.removeFromParent .runAction (efectul SKAction.sequence ([moveBulletAction, removeBulletAction]) 

În fireBullet metoda, instanțiăm un InvaderBullet exemplu, trecând "Cu laser" pentru imageName, și pentru că nu vrem să jucăm un sunet, trecem zero pentru bulletSound. Am stabilit-o poziţie să fie la fel ca invadatorul, cu o mică dispunere în poziția y și să o adăugăm la scenă.

Creăm două SKAction instanțe, moveBulletAction și removeBulletAction. moveBulletAction acțiunea mută glonțul la un anumit punct pe o anumită durată în timp ce removeBulletAction acțiunea o elimină din scenă. Invocând secvenţă(_:) pe aceste acțiuni, acestea vor rula succesiv. De aceea am menționat waitForDuration atunci când joci un sunet în partea anterioară a acestei serii. Dacă creați un SKAction obiect invocând playSoundFileNamed (_: waitForCompletion :) și stabilit waitForCompletion la Adevărat, atunci durata acelei acțiuni ar fi atâta timp cât sunetul se joacă, altfel ar trece imediat la următoarea acțiune din secvență.

Pasul 2: invokeInvaderFire

Adaugă invokeInvaderFire sub celelalte metode pe care le-ați creat GameScence.swift.

func invakeInvaderFire () let FireBullet = SKAction.runBlock () self.fireInvaderBullet () să aștepteToFireInvaderBullet = SKAction.waitForDuration (1.5) let invaderFire = SKAction.sequence ([fireBullet, waitToFireInvaderBullet]) repeatForeverAction = SKAction.repeatActionForever ) runAction (repeatForeverAction)

runBlock (_ :) metodă a SKAction clasa creează un SKAction instanță și invocă imediat închiderea transmisă runBlock (_ :) metodă. În închidere, invocăm fireInvaderBullet metodă. Pentru că invocăm această metodă într-o închidere, trebuie să o folosim de sine să-l suni.

Apoi creăm un SKAction instanță numită waitToFireInvaderBullet invocând waitForDuration (_ :), trecând în numărul de secunde de așteptat înainte de a porni. Apoi, vom crea un SKAction instanță, invaderFire, invocând secvenţă(_:) metodă. Această metodă acceptă o colecție de acțiuni invocate de către invaderFire acțiune. Vrem ca această secvență să se repete pentru totdeauna, așa că vom crea o acțiune numită repeatForeverAction, treci în SKAction obiecte de repetat și invocați runAction, trecerea în repeatForeverAction acțiune. Metoda runAction este declarată în SKNode clasă.

Pasul 3: fireInvaderBullet

Adaugă fireInvaderBullet mai jos invokeInvaderFire metoda introdusă în pasul anterior.

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

În această metodă, numim ceea ce pare a fi o metodă numită randomElement care va returna un element aleatoriu din invadersWhoCanFire array, și apoi apela fireBullet metodă. Nu există, din păcate, nicio construcție randomElement metoda pe mulțime structura. Cu toate acestea, putem crea un mulțime pentru a oferi această funcționalitate.

Pasul 4: Implementați randomElement

Mergi la Fişier > Nou > Fişier… și alegeți Swift File. Facem ceva diferit decât înainte, deci asigurați-vă că alegeți Swift File si nu Cocoa Touch Class. presa Următor → și numele fișierului Utilități. Adăugați următoarele la Utilities.swift.

(int = arc4random_uniform (UInt32 (self.count))) întoarce auto [index]

Extinde mulțime pentru a avea o metodă numită randomElement.  arc4random_uniform funcția returnează un număr între 0 și orice treceți. Deoarece Swift nu convertește implicit tipuri de numerotare, trebuie să facem conversia noi înșine. În cele din urmă, vom returna elementul matricei la index index.

Acest exemplu ilustrează cât de ușor este să adăugați funcționalitate structurii și clasei. Puteți citi mai multe despre crearea de extensii în Limba de programare Swift.

Pasul 5: Aruncarea bulletului

Cu toate astea din cale, acum putem trage gloanțele. Adăugați următoarele la didMoveToView (_ :) metodă.

 suprascrie func didMoveToView (vizualizare: SKView) ... setupPlayer () invokeInvaderFire ()

Dacă testați aplicația acum, în fiecare secundă ar trebui să vedeți unul dintre invadatorii din rândul inferior să tragă un glonț.

3. Gloanțe pentru jucători

Pasul 1: fireBullet (scena :)

Adăugați următoarea proprietate la Jucător clasă în Player.swift.

Player clasa: SKSpriteNode private var canFire = true

Vrem să limităm de câte ori jucătorul poate trage un glonț. canFire proprietatea va fi utilizată pentru a reglementa acest lucru. Apoi, adăugați următoarele la fireBullet (scena :) metodă în Jucător clasă.

func fireBullet (scena: SKScene) if (! canFire) return altceva canFire = false let bullet = PlayerBullet (imageName: "laser", bulletSound: "laser.mp3") bullet.position.x = x bullet.position.y = auto.position.y + auto.size.height / 2 scena.addChild (bullet) lasa moveBulletAction = SKAction.moveTo (CGPoint (x: self.position.x, y: scene.size.height + bullet.size.height), duration: 1.0) lasă removeBulletAction = SKAction.removeFromParent () bullet.runAction (SKAction.sequence ([moveBulletAction, removeBulletAction])) waitToEnableFire = SKAction.waitForDuration (0.5) runAction (waitToEnableFire, self.canFire = true) 

În primul rând, asigurați-vă că playerul este capabil să declanșeze dacă verificați dacă canFire este setat sa Adevărat. Dacă nu, vom reveni imediat din metodă.

Dacă jucătorul poate trage, am setat canFire la fals așa că nu pot trage imediat un alt glonț. Apoi instanțiăm a PlayerBullet exemplu, trecând "Cu laser" pentru imageNamed parametru. Pentru că vrem să se joace un sunet când jucătorul face un glonț, intrăm "Laser.mp3" pentru bulletSound parametru.

Apoi am setat poziția bulletului și l-am adăugat pe ecran. Următoarele câteva linii sunt identice cu cele de la Invadator„sfireBullet metodă prin faptul că mutăm glonțul și îl scoatem de pe scenă. Apoi, vom crea un SKAction instanță, waitToEnableFire, invocând waitForDuration (_ :) clasă. În cele din urmă, invocăm runAction, trecerea waitToEnableFire, și la finalizarea set canFire înapoi la Adevărat.

Pasul 2: Aruncarea bulletului jucătorului

Ori de câte ori utilizatorul atinge ecranul, vrem să tragem un glonț. Acest lucru este la fel de simplu ca și chemarea fireBullet pe jucător obiect în touchesBegan (_: withEvent :) metodă a GameScene clasă.

 suprascrie funcțiileBegan (atinge: Set, cu eveniment Eveniment: UIEvent) player.fireBullet (self) 

Dacă încercați acum aplicația, ar trebui să puteți declanșa un glonț când atingeți ecranul. De asemenea, ar trebui să auziți sunetul laser de fiecare dată când este declanșat un glonț.

4. Categorii de coliziune

Pentru a detecta când nodurile se ciocnesc sau fac contact între ele, vom folosi motorul fizic încorporat al lui Sprite Kit. Cu toate acestea, comportamentul implicit al motorului fizicii este că totul se ciocnește cu totul atunci când acestea au un corp fizic adăugat la ele. Avem nevoie de o modalitate de a separa ceea ce dorim să interacționăm unul cu celălalt și putem face acest lucru prin crearea de categorii la care apar corpuri fizice specifice.

Definiți aceste categorii utilizând o mască de biți care utilizează un număr întreg de 32 de biți cu 32 de steaguri individuale care pot fi activate sau deconectate. Acest lucru înseamnă, de asemenea, că puteți avea maxim 32 de categorii pentru jocul dvs. Acest lucru nu ar trebui să prezinte o problemă pentru majoritatea jocurilor, dar este ceva de reținut.

Adăugați următoarea definiție a structurii la GameScene clasa, sub invaderNum declarație în GameScene.swift.

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

Folosim o structură, CollsionCategories, pentru a crea categorii pentru Invadator, Jucător, InvaderBullet, și PlayerBullet clase. Folosim deplasarea biților pentru a transforma biții.

5. Jucător și InvaderBullet Coliziune

Pasul 1: Configurarea InvaderBullet pentru Coliziune

Adăugați următorul bloc de coduri la init (imageName: bulletSound :) metoda în InvaderBullet.swift.

 (imagineName: String, bulletSound: String?) super.init (imageName: imageName, bulletSound: bulletSound) self.physicsBody = SKPhysicsBody (textură: self.texture, size: self.size) self.physicsBody? .dynamic = adevarata auto.physicsBody? .usesPreciseCollisionDetection = adevarata auto.physicsBody? .categoryBitMask = CollisionCategories.InvaderBullet self.physicsBody? .contactTestBitMask = CollisionCategories.Player self.physicsBody? .collisionBitMask = 0x0

Există mai multe moduri de a crea un corp de fizică. În acest exemplu, folosim init (textura: dimensiune :) inițializator, care va face detectarea coliziunii să utilizeze forma texturii pe care o parcurgem. Există mai multe alte inițializatoare disponibile, pe care le puteți vedea în referința de clasă SKPhysicsBody.

Am fi putut folosi cu ușurință init (rectangleOfSize :) inițializator, deoarece gloanțele sunt de formă dreptunghiulară. Într-un joc atât de mic, nu contează. Cu toate acestea, trebuie să știți că utilizarea init (textura: dimensiune :) metoda poate fi computațional costisitoare, deoarece trebuie să calculeze forma exactă a texturii. Dacă aveți obiecte cu formă dreptunghiulară sau circulară, atunci ar trebui să utilizați acele tipuri de inițializatoare dacă performanța jocului devine o problemă.

Pentru ca detecția coliziunilor să funcționeze, cel puțin unul dintre corpurile pe care le testează trebuie să fie marcat ca fiind dinamic. Prin setarea usesPreciseCollisionDetection proprietate la Adevărat, Sprite Kit utilizează o detectare mai precisă a coliziunii. Setați această proprietate la Adevărat pe corpuri mici, cu mișcări rapide, precum gloanțele noastre.

Fiecare organism va aparține unei categorii și veți defini acest lucru prin setarea acestuia categoryBitMask. Din moment ce acesta este InvaderBullet clasa, am pus-o la CollisionCategories.InvaderBullet.

Pentru a afla când acest organism a luat legătura cu un alt organism care vă interesează, setați contactBitMask. Aici vrem să știm când InvaderBullet a intrat în contact cu jucătorul, așa că vom folosi CollisionCategories.Player. Deoarece o coliziune nu ar trebui să declanșeze forțe fizice, am stabilit collisionBitMask la 0x0.

Pasul 2: Configurarea Jucător pentru Collision

Adăugați următoarele la init metoda în Player.swift.

 suprascrie init () permite textura = SKTexture (imageNamed: "player1") super.init (textură: textură, culoare: SKColor.clearColor (), dimensiune: texture.size ()) self.physicsBody = SKPhysicsBody textură, mărime: auto.size) self.physicsBody? .dynamic = adevărat self.physicsBody? .usesPreciseCollisionDetection = false self.physicsBody? .categoryBitMask = CollisionCategories.Player self.physicsBody? .contactTestBitMask = CollisionCategories.InvaderBullet | CollisionCategories.Invader auto.physicsBody? .CollisionBitMask = 0x0 animate ()

O mare parte din acest lucru ar trebui să fie familiar de la pasul anterior, așa că nu o voi rehash aici. Există două diferențe de observat totuși. Una este asta usesPreciseCollsionDetection a fost setat la fals, care este implicit. Este important să realizăm că doar unul dintre organismele de contact are nevoie de această proprietate Adevărat (care a fost glontul). Cealaltă diferență este că, de asemenea, vrem să știm când jucătorul contactează un invadator. Puteți avea mai multe contactBitMask categorie prin separarea lor cu bitul sau (|) operator. În afară de asta, ar trebui să observați că este doar practic opus InvaderBullet.

6. Invadator și PlayerBullet Coliziune

Pasul 1: Configurarea Invadator pentru Coliziune

Adăugați următoarele la init metoda în Invader.swift.

 suprascrie init () permite textura = SKTexture (imageNamed: "invader1") super.init (textură: textură, culoare: SKColor.clearColor (), size: texture.size = SKPhysicsBody (textură: auto.texture, dimensiune: self.size) self.physicsBody? .Dynamic = adevărat self.physicsBody? .UsesPreciseCollisionDetection = false auto.physicsBody? .CategoryBitMask = CollisionCategories.Invader self.physicsBody? .ContactTestBitMask = CollisionCategories. PlayerBullet | CollisionCategories.Player self.physicsBody? .CollisionBitMask = 0x0

Toate acestea ar avea sens dacă ați urmat. Am creat physicsBody, categoryBitMask, și contactBitMask.

Pasul 2: Configurarea PlayerBullet pentru Coliziune

Adăugați următoarele la init (imageName: bulletSound :) în PlayerBullet.swift. Din nou, implementarea ar trebui să fie cunoscută până acum.

 (imagineName: String, bulletSound: String?) super.init (imageName: imageName, bulletSound: bulletSound) self.physicsBody = SKPhysicsBody (textură: self.texture, size: self.size) self.physicsBody? .dynamic = adevarul auto.physicsBody? .usesPreciseCollisionDetection = adevarata auto.physicsBody? .categoryBitMask = CollisionCategories.PlayerBullet self.physicsBody? .contactTestBitMask = CollisionCategories.Invader self.physicsBody? .collisionBitMask = 0x0

7. Configurarea fizicii pentru GameScene

Pasul 1: Configurarea lumii fizicii

Trebuie să înființăm GameScene clasa pentru a pune în aplicare SKPhysicsContactDelegate astfel încât să putem răspunde atunci când două corpuri se ciocnesc. Adăugați următoarele pentru a face GameScene clasa este conformă cu SKPhysicsContactDelegate protocol.

clasa de joc Scenă: SKScene, SKPhysicsContactDelegate 

Apoi, trebuie să stabilim câteva proprietăți pe scenă physicsWorld. Introduceți următoarele în partea de sus a paginii didMoveToView (_ :) metoda în GameScene.swift.

suprascrie func didMoveToView (vizualizare: SKView) self.physicsWorld.gravity = CGVectorMake (0, 0) self.physicsWorld.contactDelegate = auto ...

Am setat gravitatie proprietatea physicsWorld la 0 astfel încât niciunul dintre corpurile de fizică din scenă nu este afectat de gravitate. Puteți face acest lucru și pe bază de corp, în loc de a stabili întreaga lume să nu aibă nici o gravitate prin setarea affectedByGravity proprietate. Am setat și contactDelegate proprietatea lumii fizicii de sine, GameScene instanță.

Pasul 2: Implementarea SKPhysicsContactDelegate Protocol

Pentru a se conforma GameScene clasa la SKPhysicsContactDelegate protocol, trebuie să punem în aplicare didBeginContact (_ :) metodă. Această metodă este chemată atunci când două corpuri fac contact. Punerea în aplicare a directivei didBeginContact (_ :) metoda arată astfel.

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")  

Mai întâi declarăm două variabile firstBody și secondBody. Când două obiecte fac contact, nu știm care este corpul. Aceasta înseamnă că mai întâi trebuie să facem niște controale pentru a ne asigura firstBody este cea cu cea mai mică categoryBitMask.

Apoi, vom trece prin fiecare scenariu posibil folosind bitwise & operator și categoriile de coliziune pe care le-am definit mai devreme pentru a verifica ce face contactul. Conectăm rezultatul la consola pentru a ne asigura că totul funcționează așa cum ar trebui. Dacă testați aplicația, toate contactele ar trebui să funcționeze corect.

Concluzie

Acesta a fost un tutorial destul de lung, dar acum avem invadatorii în mișcare, gloanțe fiind trase de la jucător și invadatori, și de detectare a contactelor care lucrează prin utilizarea măști de contact bit. Suntem pe punctul de a ajunge la jocul final. În următoarea și ultima parte a acestei serii, vom avea un joc complet.

Cod