Bygg Missile Command med Sprite Kit Brukerinteraksjon

I den forrige opplæringen lagde vi grunnlaget for vårt Missile Command-spill ved å lage prosjektet, sette opp singleplayer-scenen og legge til brukerinteraksjon. I denne opplæringen vil du utvide spillopplevelsen ved å legge til en multispillermodus, så vel som fysikk, kollisjoner og eksplosjoner.


Endelig forhåndsvisning

Ta en titt på neste skjermbilde for å få en ide om hva vi sikter på.



Plukk opp hvor vi dro av

Hvis du ikke allerede har det, anbefaler vi sterkt at du fullfører den forrige veiledningen for å sikre at vi kan bygge videre på grunnlaget vi la i første opplæringsoppgave. I denne opplæringen zoomer vi inn på en rekke emner, for eksempel fysikk, kollisjoner, eksplosjoner, og legger til en flerspiller modus.


1. Aktivering av fysikk

Sprite Kit-rammeverket inneholder en fysikkmotor som simulerer fysiske objekter. Fysikkmotoren til Sprite Kit-rammen opererer gjennom SKPhysicsContactDelegate protokoll. For å aktivere fysikkmotoren i spillet vårt, må vi endre MyScene klasse. Begynn med å oppdatere toppfilen som vist nedenfor for å fortelle kompilatoren SKScene klassen stemmer overens med SKPhysicsContactDelegate protokollen.

#importere  @interface MyScene: SKScene  @slutt

De SKPhysicsContactDelegate protokollen gjør det mulig for oss å oppdage om to objekter har kollidert med hverandre. De MyScene eksempel må implementere SKPhysicsContactDelegate protokoll hvis den vil bli varslet om kollisjoner mellom objekter. Et objekt som implementerer protokollen blir varslet når en kollisjon begynner og slutter.

Siden vi skal håndtere eksplosjoner, missiler og monstre, definerer vi en kategori for hver type fysisk gjenstand. Legg til følgende kodestykke i headerfilen til MyScene klasse.

#importere  typedef enum: NSUInteger ExplosionCategory = (1 << 0), MissileCategory = (1 << 1), MonsterCategory = (1 << 2)  NodeCategory; @interface MyScene : SKScene  @slutt

Før vi kan begynne å utforske fysikkmotoren til Sprite Kit-rammen, må vi sette inn tyngde egenskapen til fysikkverdenen så vel som dens contactDelegate. Oppdater initWithSize: metode som vist nedenfor.

- (id) initWithSize: (CGSize) størrelse if (selv = [super initWithSize: size]) self.backgroundColor = [SKColor colorWithRed: (198.0 / 255.0) grønn: (220.0 / 255.0) blå: (54.0 / 255.0) alpha : 1,0]; // // // // Konfigurer Fysikk Verden self.physicsWorld.gravity = CGVectorMake (0, 0); self.physicsWorld.contactDelegate = self;  returner selv; 

I vårt spill brukes fysikkmotoren til å lage tre typer fysikklegemer, kuler, missiler og monstre. Når du arbeider med Sprite Kit-rammen, bruker du dynamiske og statiske volumer for å simulere fysiske objekter. Et volum for en gruppe objekter er et volum som inneholder hvert objekt av gruppen. Dynamiske og statiske volumer er et viktig element for å forbedre ytelsen til fysikkmotoren, spesielt når du arbeider med komplekse objekter. I vårt spill definerer vi to typer volumer, sirkler med en fast radius og egendefinerte objekter.

Mens sirkler er tilgjengelige gjennom SKPhysicsBody klasse, tilpasset objekt krever litt ekstra arbeid fra vår side. Fordi kroppen til et monster ikke er sirkulær, må vi opprette et eget volum for det. For å gjøre denne oppgaven litt enklere, bruker vi en fysikk kroppsstien generator. Verktøyet er greit å bruke. Importer prosjektets sprites og definer omsluttingsbanen for hvert sprite. Objektiv-C-koden for å gjenopprette banen er vist under sprite. For eksempel, ta en titt på følgende sprite.


Det neste skjermbildet viser det samme sprite med et overlegg av banen generert av fysikkens kroppsstien generator.


Hvis et objekt berører eller overlapper et objekts fysikkgrense, blir vi varslet om denne hendelsen. I vårt spill kan objektene som berører monstrene være innkommende missiler. La oss begynne med å bruke genererte baner for monstrene.

For å skape en fysikk kropp, må vi bruke en CGMutablePathRef struktur, som representerer en mutable bane. Vi bruker den til å definere konturene til monstrene i spillet.

revidere addMonstersBetweenSpace: og opprett en mutable bane for hver monstertype som vist nedenfor. Husk at det er to typer monstre i spillet vårt.

- (void) addMonstersBetweenSpace: (int) spaceOrder for (int i = 0; i< 3; i++)  int giveDistanceToMonsters = 60 * i -60; int randomMonster = [self getRandomNumberBetween:0 to:1]; SKSpriteNode *monster; CGMutablePathRef path = CGPathCreateMutable(); if (randomMonster == 0)  monster = [SKSpriteNode spriteNodeWithImageNamed:@"protectCreature4"]; CGFloat offsetX = monster.frame.size.width * monster.anchorPoint.x; CGFloat offsetY = monster.frame.size.height * monster.anchorPoint.y; CGPathMoveToPoint(path, NULL, 10 - offsetX, 1 - offsetY); CGPathAddLineToPoint(path, NULL, 42 - offsetX, 0 - offsetY); CGPathAddLineToPoint(path, NULL, 49 - offsetX, 13 - offsetY); CGPathAddLineToPoint(path, NULL, 51 - offsetX, 29 - offsetY); CGPathAddLineToPoint(path, NULL, 50 - offsetX, 42 - offsetY); CGPathAddLineToPoint(path, NULL, 42 - offsetX, 59 - offsetY); CGPathAddLineToPoint(path, NULL, 29 - offsetX, 67 - offsetY); CGPathAddLineToPoint(path, NULL, 19 - offsetX, 67 - offsetY); CGPathAddLineToPoint(path, NULL, 5 - offsetX, 53 - offsetY); CGPathAddLineToPoint(path, NULL, 0 - offsetX, 34 - offsetY); CGPathAddLineToPoint(path, NULL, 1 - offsetX, 15 - offsetY); CGPathCloseSubpath(path);  else  monster = [SKSpriteNode spriteNodeWithImageNamed:@"protectCreature2"]; CGFloat offsetX = monster.frame.size.width * monster.anchorPoint.x; CGFloat offsetY = monster.frame.size.height * monster.anchorPoint.y; CGPathMoveToPoint(path, NULL, 0 - offsetX, 1 - offsetY); CGPathAddLineToPoint(path, NULL, 47 - offsetX, 1 - offsetY); CGPathAddLineToPoint(path, NULL, 47 - offsetX, 24 - offsetY); CGPathAddLineToPoint(path, NULL, 40 - offsetX, 43 - offsetY); CGPathAddLineToPoint(path, NULL, 28 - offsetX, 53 - offsetY); CGPathAddLineToPoint(path, NULL, 19 - offsetX, 53 - offsetY); CGPathAddLineToPoint(path, NULL, 8 - offsetX, 44 - offsetY); CGPathAddLineToPoint(path, NULL, 1 - offsetX, 26 - offsetY); CGPathCloseSubpath(path);  monster.zPosition = 2; monster.position = CGPointMake(position * spaceOrder - giveDistanceToMonsters, monster.size.height / 2); [self addChild:monster];  

Med banen klar til bruk, må vi oppdatere monsterets physicsBody eiendom samt en rekke andre eiendommer. Ta en titt på følgende kodestykke for avklaring.

- (void) addMonstersBetweenSpace: (int) spaceOrder for (int i = 0; i< 3; i++)  //… // monster.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:path]; monster.physicsBody.dynamic = YES; monster.physicsBody.categoryBitMask = MonsterCategory; monster.physicsBody.contactTestBitMask = MissileCategory; monster.physicsBody.collisionBitMask = 1; monster.zPosition = 2; monster.position = CGPointMake(position * spaceOrder - giveDistanceToMonsters, monster.size.height / 2); [self addChild:monster];  

De categoryBitMask og contactTestBitMask egenskaper av physicsBody objekt er en viktig del og kan trenge litt forklaring. De categoryBitMask eiendom av physicsBody objekt definerer i hvilke kategorier knuten tilhører. De contactTestBitMask eiendom definerer hvilke kategorier av organer som forårsaker skjæringsvarsler med noden. Med andre ord definerer disse egenskapene hvilke objekter som kan kollidere med hvilke objekter.

Fordi vi konfigurerer monster noder, setter vi inn categoryBitMask til MonsterCategory og contactTestBitMask til MissileCategory. Dette betyr at monstre kan kollidere med missiler, og dette gjør at vi kan oppdage når et monster rammes av et missil.

Vi må også oppdatere vår implementering av addMissilesFromSky:. Definisjon av fysikklegemet for missiler er mye lettere siden hvert missil er sirkulært. Ta en titt på den oppdaterte gjennomføringen nedenfor.

- (void) addMissilesFromSky: (CGSize) størrelse int numberMissiles = [self getRandomNumberBetween: 0 to: 3]; for (int i = 0; i < numberMissiles; i++)  SKSpriteNode *missile; missile = [SKSpriteNode spriteNodeWithImageNamed:@"enemyMissile"]; missile.scale = 0.6; missile.zPosition = 1; int startPoint = [self getRandomNumberBetween:0 to:size.width]; missile.position = CGPointMake(startPoint, size.height); missile.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:missile.size.height/2]; missile.physicsBody.dynamic = NO; missile.physicsBody.categoryBitMask = MissileCategory; missile.physicsBody.contactTestBitMask = ExplosionCategory | MonsterCategory; missile.physicsBody.collisionBitMask = 1; int endPoint = [self getRandomNumberBetween:0 to:size.width]; SKAction *move =[SKAction moveTo:CGPointMake(endPoint, 0) duration:15]; SKAction *remove = [SKAction removeFromParent]; [missile runAction:[SKAction sequence:@[move,remove]]]; [self addChild:missile];  

På dette tidspunktet bør monstre og missiler i vårt spill ha en fysikk kropp som gjør det mulig for oss å oppdage når noen av dem kolliderer med hverandre.

Utfordring: Utfordringene for denne delen er som følger.

  • Les og forstå SKPhysicsBody klasse.
  • Lag forskjellige fysikklegemer for monstrene.

2. Kollisjoner og eksplosjoner

Kollisjoner og eksplosjoner er to elementer som er nært forbundet. Hver gang en kule som er skutt av en blomst når målet, blir brukerens berøring det eksploderer. Den eksplosjonen kan forårsake kollisjon mellom eksplosjonen og eventuelle missiler i nærheten.

For å skape eksplosjonen når en kule når sitt mål, trenger vi en annen SKAction forekomst. At SKAction eksempel er ansvarlig for to aspekter av spillet, definere eksplosjonens egenskaper og eksplosjonens fysikk kropp.

For å definere en eksplosjon må vi fokusere på eksplosjonen SKSpriteNode, det er zPosition, skala, og stilling. De stilling er plasseringen av brukerens berøring.

For å skape eksplosjonens fysikk kropp, må vi sette noden physicsBody eiendom som vi gjorde tidligere. Ikke glem å sette riktig categoryBitMask og contactTestBitMask egenskaper av fysikk kroppen. Vi lager eksplosjonen i touchesBegan: som vist under.

- (ugyldig) berørerBegan: (NSSet *) berører medEvent: (UIEvent *) hendelse for (UITouch * berøre) // ... // SKSpriteNode * bullet = [SKSpriteNode spriteNodeWithImageNamed: @ "flowerBullet"]; bullet.zPosition = 1; bullet.scale = 0,6; bullet.position = CGPointMake (bulletBeginning, 110); bullet.color = [SKColor redColor]; bullet.colorBlendFactor = 0.5; float duration = (2 * location.y) /sizeGlobal.width; SKAction * move = [SKAction moveTo: CGPointMake (location.x, location.y) varighet: varighet]; SKAction * remove = [SKAction removeFromParent]; // Eksplosjon SKAction * callExplosion = [SKAction runBlock: ^ SKSpriteNode * eksplosjon = [SKSpriteNode spriteNodeWithImageNamed: @ "eksplosjon"]; eksplosjon.zPosisjon = 3; eksplosjon.skala = 0,1; explosion.position = CGPointMake (location.x, location.y); explosion.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius: explosion.size.height / 2]; eksplosjon.physicsBody.dynamic = YES; explosion.physicsBody.categoryBitMask = ExplosionCategory; explosion.physicsBody.contactTestBitMask = MissileCategory; eksplosjon.physicsBody.collisionBitMask = 1; SKAction * explosionAction = [SKAction skalaTo: 0,8 varighet: 1,5]; [eksplosjon runAction: [SKAction sekvens: @ [eksplosjonAction, remove]]]; [self addChild: eksplosjon]; ]; [bullet runAction: [SKAction sekvens: @ [move, callExplosion, remove]]]; [self addChild: bullet]; 

I touchesBegan:, Vi har oppdatert kules handling. Den nye handlingen må ringe til callExplosion handling før den er fjernet fra scenen. For å oppnå dette har vi oppdatert følgende linje med kode i touchesBegan:.

[bullet runAction: [SKAction sekvens: @ [move, remove]]];

Sekvensen av handlingen inneholder nå callExplosion som vist under.

[bullet runAction: [SKAction sekvens: @ [move, callExplosion, remove]]];

Bygg prosjektet og kjør programmet for å se resultatet av vårt arbeid. Som du kan se, må vi likevel oppdage kollisjoner mellom eksplosjonene og de innkommende missilene. Det er her SKPhysicsContactDelegate protokollen kommer inn i spill.

Det er en delegatemetode som er av spesiell interesse for oss, didBeginContact: metode. Denne metoden vil fortelle oss når en kollisjon mellom en eksplosjon og et missil finner sted. De didBeginContact: Metoden tar ett argument, en forekomst av SKPhysicsContact klassen, som forteller oss alt vi trenger å vite om kollisjonen. La meg forklare hvordan dette virker.

en SKPhysicsContact forekomsten har a bodyâ og a bodyB eiendom. Hver kropp peker på en fysikk kropp som er involvert i kollisjonen. Når didBeginContact: er påkalt, må vi oppdage hvilken type kollisjon vi har å gjøre med. Det kan være (1) en kollisjon mellom en eksplosjon og et missil eller (2) en kollisjon mellom et missil og et monster. Vi oppdager kollisjonstypen ved å inspisere categoryBitmask eiendom av fysikk organene av SKPhysicsContact forekomst.

Finn ut hvilken type kollisjon vi har å gjøre med, er ganske enkelt takket være categoryBitmask eiendom. Hvis bodyâ eller bodyB har en categoryBitmask av type ExplosionCategory, da vet vi at det er en kollisjon mellom en eksplosjon og et missil. Ta en titt på kodestykket nedenfor for avklaring.

- (void) didBeginContact: (SKPhysicsContact *) kontakt if ((contact.bodyA.categoryBitMask & ExplosionCategory)! = 0 || (contact.bodyB.categoryBitMask & ExplosionCategory)! = 0) NSLog (@ "EXPLOSION HIT");  ellers NSLog (@ "MONSTER HIT"); 

Hvis vi har møtt en kollisjon mellom en eksplosjon og en missil, så tar vi tak i knutepunktet som er knyttet til missilens fysikk kropp. Vi må også tilordne en handling til noden, som vil bli utført når kulen rammer missilet. Oppgaven av handlingen er å fjerne missilen fra scenen. Vær oppmerksom på at vi ikke umiddelbart fjerner eksplosjonen fra scenen, da det kan ødelegge andre missiler i nærheten.

Når et missil ødelegges, øker vi missileExploded instansvariabel og oppdater etiketten som viser antall missiler som spilleren har ødelagt hittil. Hvis spilleren har ødelagt tyv missiler, vinner de spillet.

- (void) didBeginContact: (SKPhysicsContact *) kontakt if ((contact.bodyA.categoryBitMask & ExplosionCategory)! = 0 || (contact.bodyB.categoryBitMask & ExplosionCategory)! = 0) // Kollisjon mellom eksplosjon og missil SKNode * missil = (contact.bodyA.categoryBitMask & ExplosionCategory)? contact.bodyB.node: contact.bodyA.node; [missil runAction: [SKAction removeFromParent]]; // eksplosjonen fortsetter, fordi kan drepe mer enn en missil NSLog (@ "Missile ødelagt"); // Oppdater Missile Exploded MissileExploded ++; [labelMissilesExploded setText: [NSString stringWithFormat: @ "Missiler eksplodert:% d", missileExploded]]; hvis (missileExploded == 20) SKLabelNode * ganhou = [SKLabelNode labelNodeWithFontNamed: @ "Hiragino-Kaku-Gothic-ProN"]; ganhou.text = @ "Du vinner!"; ganhou.fontSize = 60; ganhou.position = CGPointMake (sizeGlobal.width / 2, sizeGlobal.height / 2); ganhou.zPosition = 3; [self addChild: ganhou];  annet // Kollisjon mellom rakett og monster

Hvis vi arbeider med en kollisjon mellom et rakett og et monster, fjerner vi rakett- og monsterknutepunktet fra scenen ved å legge til en handling [SKAction removeFromParent] til listen over handlinger utført av noden. Vi øker også monstersDead instansvariabel og sjekk om den er lik 6. Hvis det er, har spilleren mistet spillet og vi viser en melding som forteller at spillet er over.

- (void) didBeginContact: (SKPhysicsContact *) kontakt if ((contact.bodyA.categoryBitMask & ExplosionCategory)! = 0 || (contact.bodyB.categoryBitMask & ExplosionCategory)! = 0) // Kollisjon mellom eksplosjon og missil // ... // else // Kollisjon mellom rakett og monster SKNode * monster = (contact.bodyA.categoryBitMask & MonsterCategory)? contact.bodyA.node: contact.bodyB.node; SKNode * missile = (contact.bodyA.categoryBitMask & MonsterCategory)? contact.bodyB.node: contact.bodyA.node; [missil runAction: [SKAction removeFromParent]]; [monster runAction: [SKAction removeFromParent]]; NSLog (@ "Monster drept"); monstersDead ++; hvis (monstersDead == 6) SKLabelNode * perdeu = [SKLabelNode labelNodeWithFontNamed: @ "Hiragino-Kaku-Gothic-ProN"]; perdeu.text = @ "Du mister!"; perdeu.fontSize = 60; perdeu.position = CGPointMake (sizeGlobal.width / 2, sizeGlobal.height / 2); perdeu.zPosition = 3; [self addChild: perdeu]; [self moveToMenu]; 

Før du kjører spillet på iPad, må vi implementere moveToMenu metode. Denne metoden er påkalt når spilleren taper spillet. I moveToMenu, Spillet går over til menyscenen, slik at spilleren kan starte et nytt spill. Ikke glem å legge til en importerklæring for MenuScene klasse.

- (void) moveToMenu SKTransition * overgang = [SKTransition fadeWithDuration: 2]; MenuScene * myscene = [[Menyscene allokere] initWithSize: CGSizeMake (CGRectGetMaxX (self.frame), CGRectGetMaxY (self.frame))]; [self.scene.view presentScene: myscene overgang: overgang]; 
#import "MyScene.h" #import "MenuScene.h" @interface MyScene () // ... // @end

Det er på tide å bygge prosjektet og kjøre spillet for å se det endelige resultatet.

Utfordring: Utfordringene for denne delen er som følger.

  • Endre reglene i spillet ved å endre antall monstre og kuler.
  • Gjør spillet mer utfordrende ved å endre spillets dynamikk. Du kan for eksempel øke hastigheten til missilene når du har brukt fem kuler.

3. Multi-Player

I spillets multispillermodus kan to spillere utfordre hverandre via en delt skjermmodus. Multispillermodusen endrer ikke spillet selv. De viktigste forskjellene mellom singleplayer og multi-player modusene er oppført nedenfor.

  • Vi trenger to sett med eiendeler.
  • Plasseringen og orienteringen av eiendelene må oppdateres.
  • Vi må implementere spilllogikk for den andre spilleren.
  • Eksplosjonene må testes og fanges på eksplosjonsbasis.
  • Bare en spiller kan vinne spillet.

I multispillermodus skal spillet se ut som skjermbildet nedenfor.


Dette er den siste utfordringen i denne opplæringen. Det er ikke så komplisert som det kan virke. Målet med utfordringen er å gjenopprette Missile Command ved å aktivere multi-player modus. Kildefilene i denne opplæringen inneholder to Xcode-prosjekter, hvorav en (Missile Command Multi-Player) inneholder en ufullstendig implementering av multispillermodus for å komme i gang med denne utfordringen. Legg merke til at multiscene klassen er ufullstendig og det er din oppgave å fullføre gjennomføringen for å fullføre utfordringen. Du finner tips og kommentarer (/ * Arbeid HER - KODE ER MISSING * /) for å hjelpe deg med denne utfordringen.

Du trenger ikke å legge til flere metoder eller instansvariabler for å fullføre utfordringen. Du trenger bare å fokusere på å implementere logikken for multispillermodus.

Neste skjermbilde viser deg gjeldende tilstand for multispillermodus.



Konklusjon

Vi har dekket mye bakken i denne korte serien på Sprite Kit. Du bør nå kunne lage spill som ligner på Missile Command ved hjelp av Sprite Kit-rammen. Hvis du har spørsmål eller kommentarer, vær så snill å slippe en linje i kommentarene.