In Was ist in einer Projektil-Physik-Engine enthalten, haben wir die theoretischen Grundlagen und die wesentlichen Elemente von Physik-Engines behandelt, mit denen Projektileffekte in Spielen wie Angry Birds simuliert werden können. Nun, wir werden dieses Wissen an einem echten Beispiel festigen. In diesem Tutorial werde ich den Code für ein einfaches, auf Physik basierendes Spiel, das ich geschrieben habe, auflösen, damit Sie genau sehen können, wie es funktioniert.
Für Interessenten verwendet der in diesem Lernprogramm bereitgestellte Beispielcode die Sprite Kit-API für native iOS-Spiele. Diese API verwendet eine Objective-C-ummantelte Box2D als Physik-Simulations-Engine. Die Konzepte und ihre Anwendung können jedoch in jeder beliebigen 2D-Physics-Engine oder -Oberfläche verwendet werden.
Hier ist das Beispielspiel in Aktion:
Das Gesamtkonzept des Spiels hat folgende Form:
Die erste Anwendung der Physik besteht darin, einen Randschleifenkörper um den Rahmen des Bildschirms zu erstellen. Folgendes wird zu einem Initialisierer oder hinzugefügt -(void) loadLevel
Methode:
// einen Rand-Loop-Physik-Body für den Bildschirm erstellen und im Grunde eine "Begrenzung" erstellen self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect: self.frame];
Dadurch bleiben alle unsere Objekte im Rahmen, sodass die Schwerkraft unser gesamtes Spiel nicht vom Bildschirm zieht!
Schauen wir uns an, wie wir unserer Szene einige physikfähige Sprites hinzufügen können. Zunächst betrachten wir den Code zum Hinzufügen von drei Arten von Plattformen. In dieser Simulation werden quadratische, rechteckige und dreieckige Plattformen verwendet.
-(void) createPlatformStructures: (NSArray *) -Plattformen für (NSDictionary * -Plattform in Plattformen) // Informationen aus Dictionay abrufen und Variablen vorbereiten int type = [platform [@ "platformType"] intValue]; CGPoint-Position = CGPointFromString (Plattform [@ "PlattformPosition"]); SKSpriteNode * platSprite; platSprite.zPosition = 10; // Zu befüllende Logik basierend auf dem Plattformtyp if (type == 1) // Square platSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "SquarePlatform"]; // sprite erstellen platSprite.position = position; // position sprite platSprite.name = @ "Quadrat"; CGRect physicsBodyRect = platSprite.frame; // Eine Rechteckvariable basierend auf der Größe erstellen platSprite.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize: physicsBodyRect.size]; // Physikkörper erstellen platSprite.physicsBody.categoryBitMask = otherMask; // weise dem Physikkörper eine Kategoriemaske zu PlatSprite.physicsBody.contactTestBitMask = ObjectiveMask; // Erstellen einer Kontakttestmaske für physische Körperkontaktrückrufe platSprite.physicsBody.usesPreciseCollisionDetection = YES; else if (type == 2) // Rectangle platSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "RectanglePlatform"]; // sprite erstellen platSprite.position = position; // position sprite platSprite.name = @ "Rechteck"; CGRect physicsBodyRect = platSprite.frame; // Eine Rechteckvariable basierend auf der Größe erstellen platSprite.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize: physicsBodyRect.size]; // Physikkörper erstellen platSprite.physicsBody.categoryBitMask = otherMask; // weise dem Physikkörper eine Kategoriemaske zu PlatSprite.physicsBody.contactTestBitMask = ObjectiveMask; // Erstellen einer Kontakttestmaske für physische Körperkontaktrückrufe platSprite.physicsBody.usesPreciseCollisionDetection = YES; else if (type == 3) // Dreieck platSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "TrianglePlatform"]; // sprite erstellen platSprite.position = position; // position sprite platSprite.name = @ "Dreieck"; // Erstellen Sie einen veränderbaren Pfad in Form eines Dreiecks und verwenden Sie die Sprite-Begrenzungen als Richtlinie. CGMutablePathRef physicsPath = CGPathCreateMutable (); CGPathMoveToPoint (physicsPath, null, -platSprite.size.width / 2, -platSprite.size.height / 2); CGPathAddLineToPoint (physicsPath, null, platSprite.size.width / 2, -platSprite.size.height / 2); CGPathAddLineToPoint (physicsPath, nil, 0, platSprite.size.height / 2); CGPathAddLineToPoint (physicsPath, null, -platSprite.size.width / 2, -platSprite.size.height / 2); platSprite.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath: physicsPath]; // Physikkörper erstellen platSprite.physicsBody.categoryBitMask = otherMask; // weise dem Physikkörper eine Kategoriemaske zu PlatSprite.physicsBody.contactTestBitMask = ObjectiveMask; // Erstellen einer Kontakttestmaske für physische Körperkontaktrückrufe platSprite.physicsBody.usesPreciseCollisionDetection = YES; CGPathRelease (physicsPath); // Geben Sie jetzt den Pfad frei, nachdem wir damit fertig sind. [Self addChild: platSprite];
Wir werden in kurzer Zeit auf das eingehen, was alle Eigentumserklärungen bedeuten. Für den Moment konzentrieren Sie sich auf die Schaffung jedes Körpers. Die quadratischen und die rechteckigen Plattformen erstellen jeweils ihre Körper in einer einzeiligen Deklaration, wobei der Begrenzungsrahmen des Sprites als Körpergröße verwendet wird. Der Körper der Triangel-Plattform muss einen Pfad zeichnen. Dies verwendet auch den Begrenzungsrahmen des Sprites, berechnet jedoch ein Dreieck an den Ecken und in der Mitte des Rahmens.
Das objektive Objekt, ein Stern, wird ähnlich erstellt, aber wir werden einen kreisförmigen Physikkörper verwenden.
-(void) addObjectives: (NSArray *) Objectives for (NSDictionary * Objective in Objectives) // Besorgen Sie sich die Positionsinformationen aus dem Wörterbuch, das von der plist bereitgestellt wird. CGPoint position = CGPointFromString (object [@ "objectPosition"]); // ein Sprite basierend auf den Informationen aus dem Wörterbuch oben erstellen SKSpriteNode * objSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "star"]; objSprite.position = Position; objSprite.name = @ "Ziel"; // Weisen Sie dem Sprite einen physischen Körper und physische Eigenschaften zu. 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; // füge das Kind der Szene hinzu [self addChild: objSprite]; // Eine Aktion erstellen, um das Ziel interessanter zu machen. SKAction * turn = [SKAction rotateByAngle: 1 duration: 1]; SKAction * repeat = [SKAction repeatActionForever: drehen]; [objSprite runAction: repeat];
Die Kanone selbst benötigt keine angebrachten Körper, da keine Kollisionserkennung erforderlich ist. Wir werden es einfach als Ausgangspunkt für unser Projektil verwenden.
Hier ist die Methode zum Erstellen eines Projektils:
-(void) addProjectile // Erzeuge ein Sprite basierend auf unserem Image, gib ihm eine Position und benenne es projectile = [SKSpriteNode spriteNodeWithImageNamed: @ "ball"]; Projektil.Position = Kanon.Position; projectile.zPosition = 20; projectile.name = @ "Projektil"; // Weisen Sie dem Sprite einen Physikkörper zu. Projectile.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius: projectile.size.width / 2]; // Weisen Sie dem physischen Körper Eigenschaften zu (diese sind alle vorhanden und haben beim Erstellen des Körpers Standardwerte). 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; // Das Sprite der Szene hinzufügen, wobei der Physik-Körper angehängt ist [self addChild: projectile];
Hier sehen wir eine umfassendere Deklaration einiger Eigenschaften, die einem Physikkörper zugeordnet werden können. Wenn Sie später mit dem Beispielprojekt spielen, versuchen Sie, das zu ändern Restitution
, Reibung
, und Dichte
des Projektils zu sehen, welche Auswirkungen sie auf das gesamte Gameplay haben. (Definitionen für jede Eigenschaft finden Sie in Was ist in einer Projectile Physics Engine?)
Im nächsten Schritt erstellen Sie den Code, um diesen Ball tatsächlich auf das Ziel zu schießen. Dazu geben wir einem Projektil einen Impuls basierend auf einem Berührungsereignis:
-(void) touchesBegan: (NSSet *) berührt withEvent: (UIEvent *) -Ereignis / * Wird aufgerufen, wenn eine Berührung beginnt * / for (UITouch * touch in touches) CGPoint location = [touch locationInNode: self]; NSLog (@ "Berührt x:% f, y:% f", location.x, location.y); // Überprüfen Sie, ob sich in der Szene bereits ein Projektil befindet, wenn (! IsThereAProjectile) // Wenn nicht, fügen Sie es hinzu. IsThereAProjectile = YES; [self addProjectile]; // Einen Vektor erstellen, der als 2D-Force-Wert verwendet werden soll projectileForce = CGVectorMake (18, 18); for (SKSpriteNode * node in self.children) if ([node.name isEqualToString: @ "Projectile"]) // Einen Impuls auf das Projektil ausüben und die Schwerkraft und Reibung zeitweise überholen [node.physicsBody applyImpulse: projectileForce];
Eine weitere unterhaltsame Änderung des Projekts könnte das Spiel mit dem Impulsvektorwert sein. Kräfte - und damit Impulse - werden unter Verwendung von Vektoren angewendet, um jedem Kraftwert Größe und Richtung zu geben.
Jetzt haben wir unsere Struktur und unser Ziel und wir können auf sie schießen, aber wie sehen wir, ob wir einen Treffer erzielt haben??
Zuerst ein paar kurze Definitionen:
Bisher hat die Physik-Engine für uns Kontakte und Kollisionen bewältigt. Was wäre, wenn wir etwas Besonderes tun wollten, wenn sich zwei bestimmte Objekte berühren? Zunächst müssen wir unserem Spiel mitteilen, dass wir auf den Kontakt hören möchten. Wir werden dazu einen Delegierten und eine Erklärung verwenden.
Wir fügen den folgenden Code am Anfang der Datei hinzu:
@Interface MyScene ()@Ende
… Und füge diese Anweisung dem Initialisierer hinzu:
self.physicsWorld.contactDelegate = self
Dies ermöglicht uns, die unten abgebildete Methode stub zu verwenden, um auf Kontakt zu hören:
-(void) didBeginContact: (SKPhysicsContact *) Kontakt // Code
Bevor wir diese Methode verwenden können, müssen wir jedoch besprechen Kategorien.
Wir können zuordnen Kategorien zu unseren verschiedenen physischen Körpern als Eigenschaft, sie in Gruppen zu sortieren.
Sprite Kit verwendet insbesondere bitweise Kategorien, was bedeutet, dass wir in jeder Szene auf 32 Kategorien beschränkt sind. Ich definiere meine Kategorien gerne mit einer statischen Konstantendeklaration wie folgt:
// Physik-Kategorie erstellen Die statische Konstante von Bit-Mask uint32_t aimMask = 1 << 0; static const uint32_t otherMask = 1 << 1;
Beachten Sie die Verwendung von bitweisen Operatoren in der Deklaration (eine Diskussion über bitweise Operatoren und Bitvariablen geht über den Rahmen dieses Tutorials hinaus; Sie sollten nur wissen, dass es sich im Wesentlichen nur um Zahlen handelt, die in einer Variablen mit sehr schnellem Zugriff gespeichert sind und dass Sie 32 haben können maximal).
Wir ordnen die Kategorien mit folgenden Eigenschaften zu:
platSprite.physicsBody.categoryBitMask = otherMask; // weise dem Physikkörper eine Kategoriemaske zu PlatSprite.physicsBody.contactTestBitMask = ObjectiveMask; // Erstellen einer Kontakttestmaske für physische Körperkontaktrückrufe
Machen wir dasselbe für die anderen Variablen im Projekt. Lassen Sie uns nun den Stub für die Kontaktlistener-Methode von früher und auch diese Diskussion abschließen!
-(void) didBeginContact: (SKPhysicsContact *) Kontakt // Dies ist die Kontaktlistener-Methode. Wir geben die Kontaktzuweisungen an, die uns wichtig sind, und führen dann Aktionen basierend auf der Kollision aus. uint32_t collision = (contact.bodyA.categoryBitMask | contact.bodyB .categoryBitMask); // Definieren einer Kollision zwischen zwei Kategoriemasken if (collision == (otherMask | objectMask)) // Behandeln Sie die Kollision aus der obigen if-Anweisung. Sie können weitere if / else-Anweisungen für weitere Kategorien erstellen, wenn (! isGameReseting) NSLog (@"Du gewinnst!"); isGameReseting = YES; // Eine kleine Aktion / Animation einrichten, wenn ein Ziel getroffen wird SKAction * scaleUp = [SKAction scaleTo: 1.25 duration: 0.5]; SKAction * tint = [SKAction colorizeWithColor: [UIColor redColor] colorBlendFactor: 1 Dauer: 0,5]; SKAction * blowUp = [SKAction-Gruppe: @ [scaleUp, tint]]]; SKAction * scaleDown = [SKAction scaleTo: 0,2 Dauer: 0,75]; SKAction * fadeOut = [SKAction fadeAlphaTo: 0 Dauer: 0,75]; SKAction * blowDown = [SKAction-Gruppe: @ [scaleDown, fadeOut]]; SKAction * remove = [SKAction removeFromParent]; SKAction * -Sequenz = [SKAction-Sequenz: @ [BlowUp, BlowDown, Entfernen]]; // Finden Sie heraus, welcher der Kontaktkörper ein Ziel ist, indem Sie den Namen überprüfen und dann die Aktion darauf ausführen, wenn ([contact.bodyA.node.name isEqualToString: @ "object"])) [contact.bodyA.node runAction :Sequenz]; else if ([contact.bodyB.node.name isEqualToString: @ "Ziel"]) [contact.bodyB.node runAction: sequence]; // Starten Sie nach einigen Sekunden den Level neu [self performSelector: @selector (gameOver) withObject: nil afterDelay: 3.0f];
Ich hoffe, dass Ihnen dieses Tutorial gefallen hat! Wir haben alles über die 2D-Physik und deren Anwendung in einem 2D-Projektilspiel gelernt. Ich hoffe, Sie haben jetzt ein besseres Verständnis dafür, was Sie tun können, um die Physik in Ihren eigenen Spielen einzusetzen, und wie die Physik zu einem neuen und unterhaltsamen Gameplay führen kann. Lassen Sie mich in den Kommentaren wissen, was Sie denken, und wenn Sie etwas, das Sie heute hier gelernt haben, verwenden, um eigene Projekte zu erstellen, würde ich gerne davon erfahren.
Ich habe ein funktionierendes Beispiel für den in diesem Projekt bereitgestellten Code als GitHub-Repo beigefügt. Der vollständig kommentierte Quellcode steht allen zur Verfügung.
Einige kleinere Teile des Arbeitsprojekts, die nichts mit Physik zu tun haben, wurden in diesem Tutorial nicht behandelt. Beispielsweise ist das Projekt erweiterbar, sodass der Code das Laden mehrerer Ebenen mithilfe einer Eigenschaftslistendatei ermöglicht, um unterschiedliche Plattformanordnungen und mehrere zu erreichende Ziele zu erstellen. Der Game Over-Abschnitt und der Code zum Entfernen von Objekten und Timern wurden ebenfalls nicht besprochen, sind jedoch vollständig kommentiert und in den Projektdateien verfügbar.
Einige Ideen für Funktionen, die Sie hinzufügen können, um das Projekt zu erweitern:
Habe Spaß! Danke fürs Lesen!