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.
Ta en titt på neste skjermbilde for å få en ide om hva vi sikter på.
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.
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.
#importeretypedef 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.
SKPhysicsBody
klasse.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 kule
s 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.
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.
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.
Neste skjermbilde viser deg gjeldende tilstand for multispillermodus.
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.