Les moteurs de la physique des projectiles Construire un monde de jeu

Dans Contenu des moteurs de physique des projectiles, nous avons abordé la théorie et les éléments essentiels des moteurs de physique pouvant être utilisés pour simuler les effets de projectiles dans des jeux comme Angry Birds. Maintenant, nous allons cimenter cette connaissance avec un exemple réel. Dans ce tutoriel, je vais décomposer le code d'un jeu simple basé sur la physique que j'ai écrit, afin que vous puissiez voir exactement comment cela fonctionne..

Pour les personnes intéressées, l'exemple de code fourni tout au long de ce didacticiel utilise l'API Sprite Kit fournie pour les jeux iOS natifs. Cette API utilise un Box2D enveloppé d’Objective-C comme moteur de simulation physique, mais les concepts et leur application peuvent être utilisés dans n’importe quel moteur physique 2D ou monde..

Construire un monde de jeu

Voici l'exemple de jeu en action:

Le concept général du jeu prend la forme suivante:

  1. Une structure de plates-formes avec des corps physiques sont ajoutés au niveau, construisant une tour.
  2. Un ou plusieurs objets objectifs sont placés dans la tour, chacun étant associé à un corps physique..
  3. Un mécanisme de mise à feu tire un corps de projectile avec une impulsion momentanée; lorsque le corps du projectile entre en collision avec le corps des plates-formes, la simulation prend le relais et calcule les résultats pour nous.
  4. Si un projectile ou une plate-forme touche l'objectif, il disparaît de la scène et le joueur gagne! Cette collision est détectée à l'aide des corps physiques, de sorte que la simulation conserve son réalisme au point de collision.

Notre première utilisation de la physique consistera à créer un corps de boucle de contour autour du cadre de notre écran. Ce qui suit est ajouté à un initialiseur ou -(vide) loadLevel méthode:

// crée un corps physique de boucle de contour pour l'écran, créant essentiellement un "bornage" self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect: self.frame];

Cela gardera tous nos objets dans le cadre, afin que la gravité ne tire pas tout notre jeu de l'écran!

Ajout d'objets

Regardons l'ajout de sprites activés par la physique à notre scène. Dans un premier temps, nous examinerons le code permettant d’ajouter trois types de plates-formes. Nous utiliserons des plateformes carrées, rectangulaires et triangulaires dans cette simulation..

-(void) createPlatformStructures: (NSArray *) plates-formes pour (plate-forme NSDictionary * dans les plates-formes) // Récupérer les informations à partir de Dictionay et préparer des variables int type = [platform [@ "platformType"] intValue]; CGPoint position = CGPointFromString (plateforme [@ "plateformePosition"]); SKSpriteNode * platSprite; platSprite.zPosition = 10; // Logique à remplir en fonction du type de plate-forme if (type == 1) // Square platSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "SquarePlatform"]; // crée le sprite platSprite.position = position; // position sprite platSprite.name = @ "Square"; CGRect physicsBodyRect = platSprite.frame; // construit une variable rectangle basée sur la taille platSprite.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize: physicsBodyRect.size]; // construit le corps physique platSprite.physicsBody.categoryBitMask = otherMask; // attribue un masque de catégorie au corps physique platSprite.physicsBody.contactTestBitMask = objectiveMask; // crée un masque de test de contact pour les rappels de contact de corps physique platSprite.physicsBody.usesPreciseCollisionDetection = YES;  else if (type == 2) // Rectangle platSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "RectanglePlatform"]; // crée le sprite platSprite.position = position; // position sprite platSprite.name = @ "Rectangle"; CGRect physicsBodyRect = platSprite.frame; // construit une variable rectangle basée sur la taille platSprite.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize: physicsBodyRect.size]; // construit le corps physique platSprite.physicsBody.categoryBitMask = otherMask; // attribue un masque de catégorie au corps physique platSprite.physicsBody.contactTestBitMask = objectiveMask; // crée un masque de test de contact pour les rappels de contact de corps physique platSprite.physicsBody.usesPreciseCollisionDetection = YES;  else if (type == 3) // Triangle platSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "TrianglePlatform"]; // crée le sprite platSprite.position = position; // position sprite platSprite.name = @ "Triangle"; // Crée un chemin modifiable sous la forme d'un triangle, en utilisant les limites de l'image-objet comme directive 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]; // construit le corps physique platSprite.physicsBody.categoryBitMask = otherMask; // attribue un masque de catégorie au corps physique platSprite.physicsBody.contactTestBitMask = objectiveMask; // crée un masque de test de contact pour les rappels de contact de corps physique platSprite.physicsBody.usesPreciseCollisionDetection = YES; CGPathRelease (physicsPath); // libère le chemin maintenant que nous en avons terminé avec lui [self addChild: platSprite];  

Nous allons arriver à ce que toutes les déclarations de propriétés signifient dans un instant. Pour l'instant, concentrez-vous sur la création de chaque corps. Le carré et les plates-formes rectangulaires créent chacun leur corps dans une déclaration sur une ligne, en utilisant le cadre de sélection du sprite comme taille du corps. Le corps de la plate-forme triangle nécessite de dessiner un chemin; ceci utilise également le cadre de sélection du sprite, mais calcule un triangle aux angles et à mi-hauteur du cadre.

L'objet objectif, une étoile, est créé de la même façon, mais nous allons utiliser un corps de physique circulaire.

-(void) addObjectives: (NSArray *) objectives pour (NSDictionary * objectif dans les objectifs) // Récupère les informations de position dans le dictionnaire fournies par la pliste CGPoint position = CGPointFromString (objectif [@ "objectivePosition"]); // crée une image-objet basée sur les informations du dictionnaire ci-dessus SKSpriteNode * objSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "star"]; objSprite.position = position; objSprite.name = @ "objectif"; // Attribue un corps physique et des propriétés physiques au 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; // ajoute l'enfant à la scène [self addChild: objSprite]; // Crée une action pour rendre l'objectif plus intéressant SKAction * turn = [SKAction rotateByAngle: 1 duration: 1]; SKAction * repeat = [SKAction repeatActionForever: tournez]; [objSprite runAction: repeat]; 

Prêt, Set, Feu!

Le canon lui-même n'a pas besoin de corps, car il n'a pas besoin de détection de collision. Nous allons simplement l'utiliser comme point de départ pour notre projectile. 

Voici la méthode pour créer un projectile:

-(void) addProjectile // Créer un sprite basé sur notre image, lui donner une position et un nom projectile = [SKSpriteNode spriteNodeWithImageNamed: @ "ball"]; projectile.position = cannon.position; projectile.zPosition = 20; projectile.name = @ "Projectile"; // Attribuer un corps physique à l'image-objet projectile.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius: projectile.size.width / 2]; // Attribue des propriétés au corps physique (elles existent toutes et ont des valeurs par défaut lors de la création du corps) 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; // Ajoute le sprite à la scène, avec le corps physique attaché [self addChild: projectile]; 

Nous voyons ici une déclaration plus complète de certaines propriétés assignables à un corps physique. Lors de la lecture ultérieure du projet exemple, essayez de modifier le restitution, friction, et densité du projectile pour voir quels effets ils ont sur le gameplay global. (Vous pouvez trouver des définitions pour chaque propriété dans Que contient un moteur de physique des projectiles?)

La prochaine étape consiste à créer le code pour lancer cette balle sur la cible. Pour cela, nous allons appliquer une impulsion à un projectile basé sur un événement tactile:

-(void) touchesBegan: (NSSet *) touches withEvent: (UIEvent *) événement / * Appelé lorsqu'une touche commence * / pour (UITouch * touche au contact) CGPoint location = [touch locationInNode: self]; NSLog (@ "Touché x:% f, y:% f", emplacement.x, emplacement.y); // Vérifiez s'il y a déjà un projectile dans la scène if (! IsThereAProjectile) // Sinon, ajoutez-le isThereAProjectile = YES; [auto addProjectile]; // Crée un vecteur à utiliser comme valeur de force 2D projectileForce = CGVectorMake (18, 18); for (noeud SKSpriteNode * dans self.children) if ([node.name isEqualToString: @ "Projectile"]) // applique une impulsion au projectile, dépassant temporairement la gravité et les frictions [node.physicsBody applyImpulse: projectileForce]; 

Une autre modification amusante du projet pourrait être de jouer avec la valeur du vecteur impulsion. Les forces - et donc les impulsions - sont appliquées à l'aide de vecteurs, donnant l'ampleur et la direction à toute valeur de force.

Maintenant, nous avons notre structure et notre objectif, et nous pouvons leur tirer dessus, mais comment pouvons-nous voir si nous avons frappé un coup?

Cours de collision

Tout d'abord, une paire rapide de définitions:

  • UNE contact est utilisé lorsque deux corps se touchent.
  • UNE collision est utilisé pour éviter que deux corps ne se croisent.

Contact auditeur

Jusqu'à présent, le moteur physique gère pour nous les contacts et les collisions. Et si nous voulions faire quelque chose de spécial quand deux objets en particulier se touchent? Pour commencer, nous devons dire à notre jeu que nous voulons écouter le contact. Nous allons utiliser un délégué et une déclaration pour accomplir cela. 

Nous ajoutons le code suivant en haut du fichier:

@interface MyScene () @fin

… Et ajoutez cette déclaration à l'initialiseur:

self.physicsWorld.contactDelegate = self

Cela nous permet d’utiliser la méthode décrite ci-dessous pour écouter les contacts:

-(void) didBeginContact: (SKPhysicsContact *) contact // code

Avant de pouvoir utiliser cette méthode, cependant, nous devons discuter catégories.

Les catégories

Nous pouvons assigner catégories à nos divers corps de physique, en tant que propriété, de les trier en groupes. 

Sprite Kit en particulier utilise des catégories au niveau des bits, ce qui signifie que nous sommes limités à 32 catégories dans une scène donnée. J'aime définir mes catégories en utilisant une déclaration de constante statique comme celle-ci:

// Créer la constante statique du masque de bits de la catégorie physique uint32_t objectiveMask = 1 << 0; static const uint32_t otherMask = 1 << 1;

Notez l'utilisation d'opérateurs binaires dans la déclaration (une discussion sur les opérateurs binaires et les variables binaires va au-delà de la portée de ce didacticiel; sachez qu'il s'agit essentiellement de nombres stockés dans une variable à accès très rapide et qu'il est possible d'avoir des valeurs 32 maximum).

Nous assignons les catégories en utilisant les propriétés suivantes:

platSprite.physicsBody.categoryBitMask = otherMask; // attribue un masque de catégorie au corps physique platSprite.physicsBody.contactTestBitMask = objectiveMask; // crée un masque de test de contact pour les rappels de contact physique du corps

En procédant de la même manière pour les autres variables du projet, complétons à présent le stub de notre méthode d'écoute de contact, ainsi que cette discussion.!

-(void) didBeginContact: (SKPhysicsContact *) contact // c'est la méthode d'écoute des contacts, nous lui attribuons les affectations de contacts qui nous intéressent et effectuons ensuite des actions en fonction de la collision uint32_t collision = (contact.bodyA.categoryBitMask | contact.bodyB .categoryBitMask); // définit une collision entre deux masques de catégorie if (collision == (otherMask | objectiveMask)) // gérez la collision à partir de l'instruction if ci-dessus, vous pouvez créer davantage d'instructions if / else pour plusieurs catégories if (! isGameReseting) NSLog (@"Vous gagnez!"); isGameReseting = YES; // Configurez une petite action / animation lorsqu'un objectif est atteint SKAction * scaleUp = [SKAction scaleTo: 1.25 duration: 0.5]; SKAction * teinte = [SKAction colorizeWithColor: [UIColor redColor] colorBlendFactor: 1 durée: 0,5]; SKAction * blowUp = [groupe SKAction: @ [scaleUp, teinte]]; SKAction * scaleDown = [SKAction scaleTo: 0.2 durée: 0.75]; SKAction * fadeOut = [SKAction fadeAlphaTo: 0 durée: 0,75]; SKAction * blowDown = [groupe SKAction: @ [scaleDown, fadeOut]]; SKAction * remove = [SKAction removeFromParent]; SKAction * sequence = [Séquence SKAction: @ [blowUp, blowDown, remove]]; // Déterminez quel corps de contact est un objectif en vérifiant son nom, puis exécutez l'action sur celui-ci si ([contact.bodyA.node.name estEqualToString: @ "objectif"]) [contact.bodyA.node runAction :séquence];  else if ([contact.bodyB.node.name isEqualToString: @ "objectif"]) [contact.bodyB.node runAction: sequence];  // après quelques secondes, redémarrez le niveau [self performSelector: @selector (gameOver) withObject: nil afterDelay: 3.0f]; 

Conclusion

J'espère que vous avez apprécié ce tutoriel! Nous avons tout appris sur la physique 2D et sur la manière dont elle peut être appliquée à un jeu de projectiles 2D. J'espère que vous comprenez maintenant mieux ce que vous pouvez faire pour commencer à utiliser la physique dans vos propres jeux et comment la physique peut conduire à un gameplay nouveau et amusant. Laissez-moi savoir dans les commentaires ci-dessous ce que vous pensez, et si vous utilisez ce que vous avez appris ici aujourd'hui pour créer vos propres projets, j'aimerais en entendre parler. 

Une note sur l'exemple de projet

J'ai inclus un exemple de travail du code fourni dans ce projet en tant que dépôt GitHub. Le code source entièrement commenté est à la disposition de tous. 

Certaines parties mineures du projet de travail non liées à la physique n'ont pas été abordées dans ce tutoriel. Par exemple, le projet est conçu pour être extensible. Par conséquent, le code permet le chargement de plusieurs niveaux à l'aide d'un fichier de liste de propriétés pour créer différentes dispositions de plate-forme et plusieurs objectifs à atteindre. La section game over et le code permettant de supprimer des objets et des minuteries n’ont pas non plus été discutés, mais sont entièrement commentés et disponibles dans les fichiers du projet..

Quelques idées de fonctionnalités que vous pourriez ajouter pour développer le projet:

  1. Différents types de munitions
  2. Direction et magnitude de tir mobiles et adaptables
  3. Plus de types et de tailles de plateformes
  4. Terrain
  5. Animation et effets sonores 

S'amuser! Merci d'avoir lu!