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.
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.
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.
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.
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.
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.
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.
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
.
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.
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.
Speler
en InvaderBullet
BotsingInvaderBullet
voor botsingVoeg 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
.
Speler
voor CollsionVoeg 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
.
binnendringer
en PlayerBullet
Botsingbinnendringer
voor botsingVoeg 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
.
PlayerBullet
voor botsingVoeg 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
GameScene
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.
SKPhysicsContactDelegate
ProtocolOm 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.
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.