Build Missile Command with Sprite Kit User Interaction

Nel precedente tutorial, abbiamo posto le basi del nostro gioco Missile Command creando il progetto, impostando la scena single-player e aggiungendo l'interazione dell'utente. In questo tutorial, espanderai l'esperienza di gioco aggiungendo una modalità multigiocatore oltre a fisica, collisioni ed esplosioni.


Anteprima finale

Dai uno sguardo al prossimo screenshot per avere un'idea di cosa stiamo mirando.



Pick Up Where We Left Off

Se non lo hai già fatto, ti consigliamo vivamente di completare il tutorial precedente per assicurarci che possiamo basarci sulle fondamenta che abbiamo posto nel primo tutorial. In questo tutorial, eseguiamo lo zoom su un numero di argomenti, come fisica, collisioni, esplosioni e aggiunta di una modalità multi-player.


1. Abilitazione fisica

Il framework Sprite Kit include un motore fisico che simula oggetti fisici. Il motore fisico della struttura di Sprite Kit opera attraverso il SKPhysicsContactDelegate protocollo. Per abilitare il motore fisico nel nostro gioco, abbiamo bisogno di modificare il La mia scena classe. Inizia aggiornando il file di intestazione come mostrato sotto per dire al compilatore il SKScene classe conforme al SKPhysicsContactDelegate protocollo.

#importare  @interface MyScene: SKScene  @fine

Il SKPhysicsContactDelegate il protocollo ci consente di rilevare se due oggetti si sono scontrati l'uno con l'altro. Il La mia scena istanza deve implementare il SKPhysicsContactDelegate protocollo se vuole essere avvisato di collisioni tra oggetti. Un oggetto che implementa il protocollo viene avvisato ogni volta che una collisione inizia e finisce.

Dato che avremo a che fare con esplosioni, missili e mostri, definiremo una categoria per ogni tipo di oggetto fisico. Aggiungi il seguente snippet di codice al file di intestazione del file La mia scena classe.

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

Prima di poter iniziare ad esplorare il motore fisico del framework Sprite Kit, abbiamo bisogno di impostare il gravità proprietà del mondo della fisica e della sua contactDelegate. Aggiorna il initWithSize: metodo come mostrato di seguito.

- (id) initWithSize: (CGSize) size if (self = [super initWithSize: size]) self.backgroundColor = [SKColor colorWithRed: (198.0 / 255.0) verde: (220.0 / 255.0) blu: (54.0 / 255.0) alfa : 1,0]; // ... // // Configure Physics World self.physicsWorld.gravity = CGVectorMake (0, 0); self.physicsWorld.contactDelegate = self;  return self; 

Nel nostro gioco, il motore fisico viene utilizzato per creare tre tipi di corpi fisici, proiettili, missili e mostri. Quando si lavora con il framework Sprite Kit, si utilizzano volumi dinamici e statici per simulare oggetti fisici. Un volume per un gruppo di oggetti è un volume che contiene ciascun oggetto del gruppo. I volumi dinamici e statici sono un elemento importante per migliorare le prestazioni del motore fisico, soprattutto quando si lavora con oggetti complessi. Nel nostro gioco, definiremo due tipi di volumi, cerchi con raggio fisso e oggetti personalizzati.

Mentre i cerchi sono disponibili attraverso il SKPhysicsBody classe, oggetto personalizzato richiede un po 'di lavoro extra da parte nostra. Poiché il corpo di un mostro non è circolare, dobbiamo creare un volume personalizzato per questo. Per rendere questo compito un po 'più semplice, useremo un generatore di percorsi corporei fisici. Lo strumento è semplice da usare. Importa gli sprite del tuo progetto e definisci il percorso di chiusura per ogni sprite. Il codice Objective-C per ricreare il percorso è mostrato sotto lo sprite. Ad esempio, dai un'occhiata al seguente sprite.


Lo screenshot successivo mostra lo stesso sprite con una sovrapposizione del percorso generato dal generatore del percorso fisico del corpo.


Se qualche oggetto tocca o si sovrappone al confine fisico di un oggetto, siamo avvisati di questo evento. Nel nostro gioco, gli oggetti che possono toccare i mostri sono i missili in arrivo. Iniziamo usando i percorsi generati per i mostri.

Per creare un corpo fisico, dobbiamo usare a CGMutablePathRef struttura, che rappresenta un percorso mutevole. Lo usiamo per definire il contorno dei mostri nel gioco.

Rivisitare addMonstersBetweenSpace: e crea un percorso mutabile per ciascun tipo di mostro come mostrato di seguito. Ricorda che ci sono due tipi di mostri nel nostro gioco.

- (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];  

Con il percorso pronto per l'uso, dobbiamo aggiornare i mostri physicsBody proprietà così come una serie di altre proprietà. Dai un'occhiata al seguente frammento di codice per chiarimenti.

- (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];  

Il categoryBitMask e contactTestBitMask proprietà del physicsBody l'oggetto è una parte essenziale e potrebbe aver bisogno di spiegazioni. Il categoryBitMask proprietà del physicsBody oggetto definisce a quali categorie appartiene il nodo. Il contactTestBitMask proprietà definisce quali categorie di corpi causano notifiche di intersezione con il nodo. In altre parole, queste proprietà definiscono quali oggetti possono collidere con quali oggetti.

Poiché stiamo configurando i nodi mostri, impostiamo il categoryBitMask a MonsterCategory e contactTestBitMask a MissileCategory. Ciò significa che i mostri possono scontrarsi con i missili e questo ci consente di rilevare quando un mostro viene colpito da un missile.

Dobbiamo anche aggiornare la nostra implementazione di addMissilesFromSky:. Definire il corpo fisico per i missili è molto più facile poiché ogni missile è circolare. Dai un'occhiata all'implementazione aggiornata di seguito.

- (void) addMissilesFromSky: (CGSize) size int numberMissiles = [self getRandomNumberBetween: 0 a: 3]; per (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];  

A questo punto, i mostri e i missili nel nostro gioco dovrebbero avere un corpo fisico che ci consenta di rilevare quando uno di loro si scontrerà l'uno con l'altro.

Sfida: Le sfide per questa sezione sono le seguenti.

  • Leggi e capisci il SKPhysicsBody classe.
  • Crea diversi corpi di fisica per i mostri.

2. Collisioni ed esplosioni

Le collisioni e le esplosioni sono due elementi strettamente associati. Ogni volta che un proiettile sparato da un fiore raggiunge la sua destinazione, il tocco dell'utente, esplode. Quell'esplosione può causare una collisione tra l'esplosione e qualsiasi altro missile nelle vicinanze.

Per creare l'esplosione quando un proiettile raggiunge il suo obiettivo, ne abbiamo bisogno di un altro SKAction esempio. Quello SKAction l'istanza è responsabile di due aspetti del gioco, definisce le proprietà dell'esplosione e il corpo fisico dell'esplosione.

Per definire un'esplosione, dobbiamo concentrarci sull'esplosione SKSpriteNode, suo zPosition, scala, e posizione. Il posizione è la posizione del tocco dell'utente.

Per creare il corpo fisico dell'esplosione, dobbiamo impostare il nodo physicsBody proprietà come abbiamo fatto in precedenza. Non dimenticare di impostare correttamente il categoryBitMask e contactTestBitMask proprietà del corpo fisico. Creiamo l'esplosione dentro touchesBegan: come mostrato di seguito.

- (vuoto) touchBegan: (NSSet *) tocca conEvent: (UIEvent *) event for (UITouch * touch in touch) // ... // 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) durata: durata]; SKAction * remove = [SKAction removeFromParent]; // Explosion SKAction * callExplosion = [SKAction runBlock: ^ SKSpriteNode * explosion = [SKSpriteNode spriteNodeWithImageNamed: @ "explosion"]; explosion.zPosition = 3; explosion.scale = 0.1; explosion.position = CGPointMake (location.x, location.y); explosion.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius: explosion.size.height / 2]; explosion.physicsBody.dynamic = YES; explosion.physicsBody.categoryBitMask = ExplosionCategory; explosion.physicsBody.contactTestBitMask = MissileCategory; explosion.physicsBody.collisionBitMask = 1; SKAction * explosionAction = [SKAction scaleTo: 0.8 duration: 1.5]; [esplosione runAction: [SKAction sequence: @ [explosionAction, remove]]]; [autoinserimento: esplosione]; ]; [bullet runAction: [sequenza SKAction: @ [move, callExplosion, remove]]]; [self addChild: bullet]; 

Nel touchesBegan:, abbiamo aggiornato il proiettileL'azione La nuova azione deve chiamare il callExplosion azione prima che sia rimosso dalla scena. Per fare ciò, abbiamo aggiornato la seguente riga di codice in touchesBegan:.

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

La sequenza dell'azione ora contiene callExplosion come mostrato di seguito.

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

Costruisci il progetto ed esegui l'applicazione per vedere il risultato del nostro lavoro. Come puoi vedere, dobbiamo ancora rilevare le collisioni tra le esplosioni e i missili in arrivo. Questo è dove il SKPhysicsContactDelegate il protocollo entra in gioco.

C'è un metodo delegato che è di particolare interesse per noi, il didBeginContact: metodo. Questo metodo ci dirà quando è in atto una collisione tra un'esplosione e un missile. Il didBeginContact: il metodo accetta un argomento, un'istanza di SKPhysicsContact classe, che ci dice tutto ciò che dobbiamo sapere sulla collisione. Lasciami spiegare come funziona.

Un SKPhysicsContact l'istanza ha un bodyâ e a bodyB proprietà. Ogni corpo indica un corpo fisico coinvolto nella collisione. quando didBeginContact: viene invocato, dobbiamo rilevare quale tipo di collisione abbiamo a che fare. Può essere (1) una collisione tra un'esplosione e un missile o (2) una collisione tra un missile e un mostro. Rileviamo il tipo di collisione controllando il categoryBitmask proprietà dei corpi di fisica del SKPhysicsContact esempio.

Scoprire quale tipo di collisione abbiamo a che fare è abbastanza facile grazie a categoryBitmask proprietà. Se bodyâ o bodyB ha un categoryBitmask di tipo ExplosionCategory, allora sappiamo che è una collisione tra un'esplosione e un missile. Dai uno sguardo allo snippet di codice qui sotto per chiarimenti.

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

Se abbiamo incontrato una collisione tra un'esplosione e un missile, allora prendiamo il nodo associato al corpo fisico del missile. Dobbiamo anche assegnare un'azione al nodo, che verrà eseguita quando il proiettile colpisce il missile. Il compito dell'azione è rimuovere il missile dalla scena. Nota che non rimuoveremo immediatamente l'esplosione dalla scena in quanto potrebbe essere in grado di distruggere altri missili nelle sue vicinanze.

Quando un missile viene distrutto, incrementiamo il missileExploded variabile di istanza e aggiorna l'etichetta che mostra il numero di missili che il giocatore ha distrutto finora. Se il giocatore ha distrutto venti missili, vince la partita.

- (void) didBeginContact: (SKPhysicsContact *) contact if ((contact.bodyA.categoryBitMask & ExplosionCategory)! = 0 || (contact.bodyB.categoryBitMask & ExplosionCategory)! = 0) // Collisione tra Explosion e Missile SKNode * missile = (contact.bodyA.categoryBitMask & ExplosionCategory)? contact.bodyB.node: contact.bodyA.node; [missile runAction: [SKAction removeFromParent]]; // l'esplosione continua, perché può uccidere più di un missile NSLog (@ "Missile distrutto"); // Aggiorna missile Exploded missileExploded ++; [labelMissilesExploded setText: [NSString stringWithFormat: @ "Missili Exploded:% d", missileExploded]]; if (missileExploded == 20) SKLabelNode * ganhou = [SKLabelNode labelNodeWithFontNamed: @ "Hiragino-Kaku-Gothic-ProN"]; ganhou.text = @ "Vinci!"; ganhou.fontSize = 60; ganhou.position = CGPointMake (sizeGlobal.width / 2, sizeGlobal.height / 2); ganhou.zPosition = 3; [self addChild: ganhou];  else // Collisione tra missile e mostro

Se abbiamo a che fare con una collisione tra un missile e un mostro, rimuoviamo il missile e il nodo mostro dalla scena aggiungendo un'azione [SKAction removeFromParent] alla lista di azioni eseguite dal nodo. Aumentiamo anche il monstersDead variabile di istanza e verificare se è uguale a 6. Se lo è, il giocatore ha perso la partita e viene visualizzato un messaggio che dice che il gioco è finito.

- (void) didBeginContact: (SKPhysicsContact *) contact if ((contact.bodyA.categoryBitMask & ExplosionCategory)! = 0 || (contact.bodyB.categoryBitMask & ExplosionCategory)! = 0) // Collisione tra esplosione e missile // ... // else // Collisione tra missile e mostro SKNode * monster = (contact.bodyA.categoryBitMask & MonsterCategory)? contact.bodyA.node: contact.bodyB.node; SKNode * missile = (contact.bodyA.categoryBitMask & MonsterCategory)? contact.bodyB.node: contact.bodyA.node; [missile runAction: [SKAction removeFromParent]]; [monster runAction: [SKAction removeFromParent]]; NSLog (@ "Monster killed"); monstersDead ++; if (monstersDead == 6) SKLabelNode * perdeu = [SKLabelNode labelNodeWithFontNamed: @ "Hiragino-Kaku-Gothic-ProN"]; perdeu.text = @ "Perdere!"; perdeu.fontSize = 60; perdeu.position = CGPointMake (sizeGlobal.width / 2, sizeGlobal.height / 2); perdeu.zPosition = 3; [self addChild: perdeu]; [auto moveToMenu]; 

Prima di eseguire il gioco sul tuo iPad, dobbiamo implementare il moveToMenu metodo. Questo metodo è invocato quando il giocatore perde il gioco. Nel moveToMenu, il gioco passa nuovamente alla scena del menu in modo che il giocatore possa iniziare una nuova partita. Non dimenticare di aggiungere una dichiarazione di importazione per MenuScene classe.

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

È tempo di costruire il progetto ed eseguire il gioco per vedere il risultato finale.

Sfida: Le sfide per questa sezione sono le seguenti.

  • Cambia le regole del gioco modificando il numero di mostri e proiettili.
  • Rendi il gioco più difficile modificando le dinamiche del gioco. Potresti, ad esempio, aumentare la velocità dei missili una volta che hai usato cinque proiettili.

3. Multi-Player

Nella modalità multigiocatore del gioco, due giocatori possono sfidarsi l'un l'altro in modalità schermo diviso. La modalità multi-giocatore non cambia il gioco stesso. Le principali differenze tra le modalità single-player e multi-player sono elencate di seguito.

  • Abbiamo bisogno di due set di risorse.
  • La posizione e l'orientamento degli asset devono essere aggiornati.
  • Dobbiamo implementare la logica di gioco per il secondo giocatore.
  • Le esplosioni devono essere testate e catturate in base all'esplosione.
  • Solo un giocatore può vincere la partita.

Nella modalità multi-player, il gioco dovrebbe assomigliare allo screenshot qui sotto.


Questa è la sfida finale di questo tutorial. Non è così complicato come potrebbe sembrare. L'obiettivo della sfida è ricreare il comando missile abilitando la modalità multi-giocatore. I file sorgente di questo tutorial contengono due progetti Xcode, uno dei quali (Missile Command Multi-Player) contiene un'implementazione incompleta della modalità multi-player per iniziare con questa sfida. Si noti che il multiscene la classe è incompleta ed è tuo compito terminare la sua implementazione per completare con successo la sfida. Troverai suggerimenti e commenti (/ * Lavora QUI - IL CODICE È MANCANTE * /) per aiutarti con questa sfida.

Non è necessario aggiungere ulteriori metodi o variabili di istanza per completare la sfida. Devi solo concentrarti sull'implementazione della logica per la modalità multi-player.

La prossima schermata mostra lo stato attuale della modalità multi-player.



Conclusione

Abbiamo coperto molto terreno in questa breve serie su Sprite Kit. Ora dovresti essere in grado di creare giochi simili a Missile Command usando il framework Sprite Kit. Se avete domande o commenti, sentitevi liberi di mandarci una linea nei commenti.