In What's in a Projectile Physics Engine, abbiamo trattato la teoria e gli elementi essenziali dei motori fisici che possono essere utilizzati per simulare gli effetti del proiettile in giochi come Angry Birds. Ora, cementeremo quella conoscenza con un esempio reale. In questo tutorial, analizzerò il codice per un semplice gioco basato sulla fisica che ho scritto, in modo che tu possa vedere esattamente come funziona.
Per chi fosse interessato, il codice di esempio fornito in questo tutorial utilizza l'API del kit Sprite fornita per i giochi iOS nativi. Questa API utilizza un Box2D di Objective-C come motore di simulazione fisica, ma i concetti e la loro applicazione possono essere utilizzati in qualsiasi motore di fisica 2D o mondo.
Ecco il gioco di esempio in azione:
Il concetto generale del gioco assume la seguente forma:
Il nostro primo utilizzo della fisica sarà quello di creare un corpo ad anello intorno al telaio dello schermo. Quanto segue viene aggiunto a un inizializzatore o -(Void) loadLevel
metodo:
// crea un corpo fisico di loop-edge per lo schermo, creando fondamentalmente un "bounds" self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect: self.frame];
Ciò manterrà tutti i nostri oggetti all'interno della cornice, in modo che la forza di gravità non spinga tutto il gioco fuori dallo schermo!
Diamo un'occhiata all'aggiunta di alcuni sprite abilitati alla fisica alla nostra scena. Innanzitutto, esamineremo il codice per l'aggiunta di tre tipi di piattaforme. Useremo piattaforme quadrate, rettangolari e triangolari con cui lavorare in questa simulazione.
-(void) createPlatformStructures: (NSArray *) piattaforme for (piattaforma NSDictionary * nelle piattaforme) // Grab Info From Dictionay e prepara le variabili int type = [platform [@ "platformType"] intValue]; CGPoint position = CGPointFromString (piattaforma [@ "platformPosition"]); SKSpriteNode * platSprite; platSprite.zPosition = 10; // Logica per popolare il livello in base al tipo di piattaforma if (type == 1) // Square platSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "SquarePlatform"]; // crea sprite platSprite.position = position; // position sprite platSprite.name = @ "Square"; Fisica CGRectBodyRect = platSprite.frame; // crea una variabile rettangolare basata sulla dimensione platSprite.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize: physicsBodyRect.size]; // build physics body platSprite.physicsBody.categoryBitMask = otherMask; // assegna una maschera di categoria al corpo fisico platSprite.physicsBody.contactTestBitMask = objectiveMask; // crea una maschera di test di contatto per i richiami dei contatti fisici del corpo platSprite.physicsBody.usesPreciseCollisionDetection = YES; else if (type == 2) // Rectangle platSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "RectanglePlatform"]; // crea sprite platSprite.position = position; // position sprite platSprite.name = @ "Rectangle"; Fisica CGRectBodyRect = platSprite.frame; // crea una variabile rettangolare basata sulla dimensione platSprite.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize: physicsBodyRect.size]; // build physics body platSprite.physicsBody.categoryBitMask = otherMask; // assegna una maschera di categoria al corpo fisico platSprite.physicsBody.contactTestBitMask = objectiveMask; // crea una maschera di test di contatto per i richiami dei contatti fisici del corpo platSprite.physicsBody.usesPreciseCollisionDetection = YES; else if (type == 3) // Triangle platSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "TrianglePlatform"]; // crea sprite platSprite.position = position; // position sprite platSprite.name = @ "Triangle"; // Crea un percorso mutabile nella forma di un triangolo, utilizzando i limiti dello sprite come linea guida CGMutablePathRef physicsPath = CGPathCreateMutable (); CGPathMoveToPoint (physicsPath, nil, -platSprite.size.width / 2, -platSprite.size.height / 2); CGPathAddLineToPoint (physicsPath, nil, platSprite.size.width / 2, -platSprite.size.height / 2); CGPathAddLineToPoint (physicsPath, nil, 0, platSprite.size.height / 2); CGPathAddLineToPoint (physicsPath, nil, -platSprite.size.width / 2, -platSprite.size.height / 2); platSprite.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath: physicsPath]; // build physics body platSprite.physicsBody.categoryBitMask = otherMask; // assegna una maschera di categoria al corpo fisico platSprite.physicsBody.contactTestBitMask = objectiveMask; // crea una maschera di test di contatto per i richiami dei contatti fisici del corpo platSprite.physicsBody.usesPreciseCollisionDetection = YES; CGPathRelease (physicsPath); // rilascia il percorso ora che abbiamo finito con esso [self addChild: platSprite];
Arriveremo a quello che tutte le dichiarazioni di proprietà significano in un po '. Per ora, concentrati sulla creazione di ciascun corpo. Il quadrato e le piattaforme rettangolari creano ciascuno il proprio corpo in una dichiarazione di una riga, usando la casella di delimitazione dello sprite come dimensione del corpo. Il corpo della piattaforma triangolare richiede di tracciare un percorso; questo usa anche il riquadro di delimitazione dello sprite, ma calcola un triangolo agli angoli e a metà del punto del frame.
L'oggetto obiettivo, una stella, viene creato in modo simile, ma useremo un corpo di fisica circolare.
-(void) addObjectives: (NSArray *) obiettivi for (obiettivo NSDictionary * negli obiettivi) // Cattura le informazioni sulla posizione dal dizionario fornito dal plist CGPoint posizione = CGPointFromString (obiettivo [@ "obiettivoPosition"]); // crea uno sprite basato sulle informazioni dal dizionario sopra SKSpriteNode * objSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "star"]; objSprite.position = position; objSprite.name = @ "obiettivo"; // Assegna un corpo fisico e le proprietà fisiche allo sprite objSprite.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius: objSprite.size.width / 2]; objSprite.physicsBody.categoryBitMask = objectiveMask; objSprite.physicsBody.contactTestBitMask = otherMask; objSprite.physicsBody.usesPreciseCollisionDetection = YES; objSprite.physicsBody.affectedByGravity = NO; objSprite.physicsBody.allowsRotation = NO; // aggiungi il bambino alla scena [self addedChild: objSprite]; // Crea un'azione per rendere l'obiettivo più interessante SKAction * turn = [SKAction rotateByAngle: 1 duration: 1]; SKAction * repeat = [SKAction repeatActionForever: turn]; [objSprite runAction: repeat];
Il cannone non ha bisogno di corpi attaccati, in quanto non ha bisogno del rilevamento delle collisioni. Lo useremo semplicemente come punto di partenza per il nostro proiettile.
Ecco il metodo per creare un proiettile:
-(void) addProjectile // Creare uno sprite basato sulla nostra immagine, assegnargli una posizione e nome projectile = [SKSpriteNode spriteNodeWithImageNamed: @ "ball"]; projectile.position = cannon.position; projectile.zPosition = 20; projectile.name = @ "Projectile"; // Assegna un corpo fisico allo sprite projectile.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius: projectile.size.width / 2]; // Assegna le proprietà al corpo fisico (queste esistono tutte e hanno valori predefiniti alla creazione del corpo) projectile.physicsBody.restitution = 0.5; projectile.physicsBody.density = 5; projectile.physicsBody.friction = 1; projectile.physicsBody.dynamic = YES; projectile.physicsBody.allowsRotation = YES; projectile.physicsBody.categoryBitMask = otherMask; projectile.physicsBody.contactTestBitMask = objectiveMask; projectile.physicsBody.usesPreciseCollisionDetection = YES; // Aggiungi lo sprite alla scena, con il corpo fisico allegato [self addedChild: proiettile];
Qui vediamo una dichiarazione più completa di alcune proprietà assegnabili a un corpo fisico. Quando giochi con il progetto di esempio, prova a modificare il file restituzione
, attrito
, e densità
del proiettile per vedere quali effetti hanno sul gameplay generale. (Puoi trovare le definizioni per ogni proprietà in Cosa c'è in un motore di fisica del proiettile?)
Il prossimo passo è creare il codice per sparare effettivamente questa palla al bersaglio. Per questo, applicheremo un impulso a un proiettile basato su un evento tattile:
-(vuoto) touchBegan: (NSSet *) tocca Evento: (UIEvent *) evento / * Chiamato all'avvio di un tocco * / per (tocco UITouch * in tocco) Posizione CGPoint = [touch locationInNode: self]; NSLog (@ "Toccata x:% f, y:% f", location.x, location.y); // Controlla se c'è già un proiettile nella scena se (! IsThereAProjectile) // In caso contrario, aggiungiloThereAProjectile = YES; [auto-addProjectile]; // Crea un vettore da utilizzare come valore di forza 2D projectileForce = CGVectorMake (18, 18); per (nodo SKSpriteNode * in self.children) if ([node.name isEqualToString: @ "Projectile"]) // Applicare un impulso al proiettile, sorpassando temporaneamente gravità e attrito [node.physicsBody applyImpulse: projectileForce];
Un'altra divertente alterazione del progetto potrebbe essere quella di giocare con il valore del vettore di impulso. Le forze - e quindi gli impulsi - vengono applicate usando vettori, dando grandezza e direzione a qualsiasi valore di forza.
Ora abbiamo la nostra struttura e il nostro obiettivo, e possiamo sparare a loro, ma come possiamo vedere se abbiamo segnato un colpo?
Innanzitutto, una rapida coppia di definizioni:
Finora, il motore fisico ha gestito contatti e collisioni per noi. E se volessimo fare qualcosa di speciale quando toccano due oggetti particolari? Per cominciare, dobbiamo dire al nostro gioco che vogliamo ascoltare il contatto. Useremo un delegato e una dichiarazione per realizzare questo.
Aggiungiamo il seguente codice all'inizio del file:
@interface MyScene ()@fine
... e aggiungi questa dichiarazione all'inizializzatore:
self.physicsWorld.contactDelegate = self
Questo ci consente di utilizzare il metodo stub illustrato di seguito per ascoltare il contatto:
-(void) didBeginContact: (SKPhysicsContact *) contact // code
Prima di poter usare questo metodo, però, dobbiamo discutere categorie.
Possiamo assegnare categorie ai nostri vari corpi di fisica, come proprietà, per dividerli in gruppi.
In particolare, Sprite Kit utilizza categorie bit-saggio, il che significa che siamo limitati a 32 categorie in ogni scena. Mi piace definire le mie categorie usando una dichiarazione di costante statica come questa:
// Crea statica della maschera di Bit-Mask della categoria di fisica fisica uint32_t objectiveMask = 1 << 0; static const uint32_t otherMask = 1 << 1;
Notare l'uso di operatori bit-saggio nella dichiarazione (una discussione su operatori bit a bit e variabili di bit va oltre lo scopo di questo tutorial, basta sapere che sono essenzialmente solo numeri memorizzati in una variabile ad accesso molto rapido e che si può avere 32 massimo).
Assegniamo le categorie usando le seguenti proprietà:
platSprite.physicsBody.categoryBitMask = otherMask; // assegna una maschera di categoria al corpo fisico platSprite.physicsBody.contactTestBitMask = objectiveMask; // crea una maschera di test di contatto per le callback di contatto fisico del corpo
Facendo lo stesso per le altre variabili nel progetto, consentiteci ora di completare il nostro stub del listener di contatti da prima, e anche questa discussione!
-(void) didBeginContact: (SKPhysicsContact *) contact // questo è il metodo del listener di contatti, gli assegniamo le assegnazioni dei contatti che ci interessano e quindi eseguiamo azioni basate sulla collisione uint32_t collision = (contact.bodyA.categoryBitMask | contact.bodyB .categoryBitMask); // definisce una collisione tra due maschere di categorie if (collision == (otherMask | objectiveMask)) // gestisce la collisione dall'istruzione if precedente, puoi creare più if / else istruzioni per più categorie if (! isGameReseting) NSLog (@"Hai vinto!"); isGameReseting = SÌ; // Imposta una piccola azione / animazione per quando viene colpito un obiettivo. SKAction * scaleUp = [SKAction scaleTo: 1.25 duration: 0.5]; SKAction * tint = [SKAction colorizeWithColor: [UIColor redColor] colorBlendFactor: 1 durata: 0.5]; SKAction * blowUp = [gruppo di SKAction: @ [scaleUp, tint]]; SKAction * scaleDown = [SKAction scaleTo: 0.2 duration: 0.75]; SKAction * fadeOut = [SKAction fadeAlphaTo: 0 duration: 0.75]; SKAction * blowDown = [gruppo di SKAction: @ [scaleDown, fadeOut]]; SKAction * remove = [SKAction removeFromParent]; SKAction * sequence = [sequenza di SKAction: @ [blowUp, blowDown, remove]]; // Scopri quale dei corpi dei contatti è un obiettivo controllandone il nome, quindi esegui l'azione su di esso se ([contact.bodyA.node.name isEqualToString: @ "objective"]) [contact.bodyA.node runAction :sequenza]; else if ([contact.bodyB.node.name isEqualToString: @ "obiettivo"]) [contact.bodyB.node runAction: sequence]; // Dopo alcuni secondi, riavvia il livello [self performSelector: @selector (gameOver) withObject: nil afterDelay: 3.0f];
Spero che ti sia piaciuto questo tutorial! Abbiamo imparato tutto sulla fisica 2D e su come possono essere applicati a un gioco proiettile 2D. Spero che ora tu abbia una migliore comprensione di cosa puoi fare per iniziare a utilizzare la fisica nei tuoi giochi e come la fisica può portare a un nuovo e divertente gameplay. Fammi sapere nei commenti qui sotto che cosa pensi, e se usi tutto ciò che hai imparato qui oggi per creare progetti per conto tuo, mi piacerebbe sentirne parlare.
Ho incluso un esempio funzionante del codice fornito in questo progetto come un repo GitHub. Il codice sorgente completamente commentato è lì per tutti da usare.
Alcune parti minori del progetto di lavoro non correlate alla fisica non sono state discusse in questo tutorial. Ad esempio, il progetto è costruito per essere espandibile, quindi il codice consente il caricamento di più livelli utilizzando un file di elenco delle proprietà per creare diverse disposizioni della piattaforma e più obiettivi da colpire. Anche la sezione del gioco e il codice per rimuovere oggetti e timer non sono stati discussi, ma sono completamente commentati e disponibili all'interno dei file di progetto.
Alcune idee per le funzionalità che potresti aggiungere per espandere il progetto:
Divertiti! Grazie per aver letto!