Projectile Physics Engines Building a Game World

In What's in een Projectile Physics Engine bespraken we de theorie en essentiële elementen van physics engines die kunnen worden gebruikt om projectieleffecten in games als Angry Birds te simuleren. Nu zullen we die kennis met een echt voorbeeld verbinden. In deze zelfstudie zal ik de code voor een eenvoudige op fysica gebaseerde game die ik heb geschreven, opsplitsen, zodat je precies kunt zien hoe het werkt.

Voor geïnteresseerden gebruikt de voorbeeldcode die in deze zelfstudie wordt geboden de Sprite Kit-API die wordt aangeboden voor native iOS-games. Deze API gebruikt een Box2D met Objectief-C als fysica-simulatiemachine, maar de concepten en hun toepassing kunnen in elke 2D-physics-engine of -wereld worden gebruikt.

Een spelwereld bouwen

Hier is het voorbeeldspel in actie:

Het algemene concept van het spel heeft de volgende vorm:

  1. Een structuur van platforms met fysische lichamen wordt toegevoegd aan het niveau, het bouwen van een toren.
  2. Een of meer objectieve objecten worden in de toren geplaatst, elk met een fysisch lichaam eraan toegewezen.
  3. Een schietmechanisme schiet een projectiel lichaam met een tijdelijke impuls; wanneer het lichaam van het projectiel botst met de lichamen van de platforms, neemt de simulatie het over en berekent het de resultaten voor ons.
  4. Als een projectiel of een platform het doel raakt, verdwijnt het van de scène en wint de speler! Deze botsing wordt gedetecteerd met behulp van de fysische lichamen, zodat de simulatie zijn realisme behoudt op het moment van de botsing.

Ons eerste gebruik van de natuurkunde zal zijn om een ​​randlichaam rond het frame van ons scherm te creëren. Het volgende is toegevoegd aan een initialisatieprogramma of -(Void) loadLevel methode:

// maak een edge-lus physics-body voor het scherm, eigenlijk creërend een "bounds" self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect: self.frame];

Hierdoor blijven al onze objecten binnen het kader, zodat de zwaartekracht ons hele spel niet van het scherm haalt!

Objecten toevoegen

Laten we eens kijken naar het toevoegen van enkele sprites met physics-functionaliteit aan onze scène. Eerst zullen we de code bekijken voor het toevoegen van drie soorten platforms. In deze simulatie gebruiken we vierkante, rechthoekige en driehoekige platforms om mee te werken.

-(void) createPlatformStructures: (NSArray *) platforms for (NSDictionary * -platform in platforms) // Grab Info From Dictionay en maak variabelen op inttype = [platform [@ "platformType"] intValue]; CGPoint-positie = CGPointFromString (platform [@ "platformPosition"]); SKSpriteNode * platSprite; platSprite.zPosition = 10; // Logica om niveau te vullen op basis van het platformtype if (type == 1) // Square platSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "SquarePlatform"]; // create sprite platSprite.position = positie; // position sprite platSprite.name = @ "Vierkant"; CGRect physicsBodyRect = platSprite.frame; // bouw een rechthoekvariabele op basis van de grootte platSprite.physicsBody = [SKPhysics Body bodyWithRectangleOfSize: physicsBodyRect.size]; // build physics body platSprite.physicsBody.categoryBitMask = otherMask; // wijs een categoriemasker toe aan het fysieke lichaam platSprite.physicsBody.contactTestBitMask = objectiveMask; // maak een contacttestmasker voor physics body contact callbacks platSprite.physicsBody.usesPreciseCollisionDetection = YES;  else if (type == 2) // Rectangle platSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "RectanglePlatform"]; // create sprite platSprite.position = positie; // position sprite platSprite.name = @ "Rectangle"; CGRect physicsBodyRect = platSprite.frame; // bouw een rechthoekvariabele op basis van de grootte platSprite.physicsBody = [SKPhysics Body bodyWithRectangleOfSize: physicsBodyRect.size]; // build physics body platSprite.physicsBody.categoryBitMask = otherMask; // wijs een categoriemasker toe aan het fysieke lichaam platSprite.physicsBody.contactTestBitMask = objectiveMask; // maak een contacttestmasker voor physics body contact callbacks platSprite.physicsBody.usesPreciseCollisionDetection = YES;  else if (type == 3) // Triangle platSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "TrianglePlatform"]; // create sprite platSprite.position = positie; // position sprite platSprite.name = @ "Triangle"; // Maak een mutable pad in de vorm van een driehoek, met behulp van de sprite-grenzen als een leidraad 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, nihil, 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; // wijs een categoriemasker toe aan het fysieke lichaam platSprite.physicsBody.contactTestBitMask = objectiveMask; // maak een contacttestmasker voor physics body contact callbacks platSprite.physicsBody.usesPreciseCollisionDetection = YES; CGPathRelease (physicsPath); // laat het pad vrij nu we ermee klaar zijn [self addChild: platSprite];  

We zullen zien wat alle eigendomsverklaringen betekenen in een beetje. Richt je nu op de creatie van elk lichaam. De vierkante en de rechthoekige platforms creëren elk hun lichaam in een eenregelige verklaring, waarbij het begrenzingsvak van de sprite wordt gebruikt als de lichaamsgrootte. Het lichaam van het driehoeksplatform vereist het tekenen van een pad; dit gebruikt ook het begrenzende kader van de sprite, maar berekent een driehoek op de hoeken en halverwege punten van het frame.

Het object object, een ster, is op dezelfde manier gemaakt, maar we zullen een circulair natuurkundig lichaam gebruiken.

-(void) addObjectives: (NSArray *) -doelen for (NSDictionary * doelstelling in doelstellingen) // Pak de positie-informatie uit het woordenboek van de plist CGPoint position = CGPointFromString (objective [@ "objectivePosition"]); // maak een sprite gebaseerd op de informatie uit het bovenstaande woordenboek SKSpriteNode * objSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "star"]; objSprite.position = positie; objSprite.name = @ "doelstelling"; // Ken een fysica-instantie en fysische eigenschappen toe aan de 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; // voeg het kind toe aan de scène [self addChild: objSprite]; // Maak een actie om het doel interessanter te maken. SKAction * turn = [SKAction rotateByAngle: 1 duration: 1]; SKAction * repeat = [SKAction repeatActionForever: turn]; [objSprite runAction: herhalen]; 

Klaar, Instellen, Vuren!

Het kanon zelf heeft geen lichamen nodig, omdat het geen botsingsdetectie nodig heeft. We zullen het gewoon gebruiken als een startpunt voor ons projectiel. 

Hier is de methode voor het maken van een projectiel:

-(void) addProjectile // Maak een sprite op basis van onze afbeelding, geef het een positie en naam projectile = [SKSpriteNode spriteNodeWithImageNamed: @ "ball"]; projectile.position = cannon.position; projectile.zPosition = 20; projectile.name = @ "Projectile"; // Wijs een physics-body toe aan het sprite-projectiel.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius: projectile.size.width / 2]; // Ken eigenschappen toe aan het fysische lichaam (deze bestaan ​​allemaal en hebben standaardwaarden bij het creëren van het lichaam) projectiel.fysicaBody.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; // Voeg de sprite toe aan de scène, met de physics body bijgevoegd [self addChild: projectile]; 

Hier zien we een vollediger verklaring van sommige eigenschappen die aan een fysisch lichaam kunnen worden toegewezen. Probeer later als u later met het voorbeeldproject speelt teruggave, wrijving, en dichtheid van het projectiel om te zien welke effecten ze hebben op de algehele gameplay. (U kunt definities voor elke eigenschap vinden in What's in a Projectile Physics Engine?)

De volgende stap is om de code te maken om deze bal daadwerkelijk op het doel te schieten. Hiervoor zullen we een impuls geven aan een projectiel op basis van een aanraakgebeurtenis:

-(ongeldig) raaktBegan: (NSSet *) raakt aan metEvent: (UIEvent *) -gebeurtenis / * Wordt aangeroepen wanneer een aanraking begint * / for (UITouch * touch in aanraking) CGPoint location = [touch locationInNode: self]; NSLog (@ "Touched x:% f, y:% f", location.x, location.y); // Controleer of er al een projectiel in de scène aanwezig is (! IsThereAProjectile) // Indien niet, voeg het toeThereAProjectile = YES; [self addProjectile]; // Maak een vector om te gebruiken als een 2D-force-waarde projectileForce = CGVectorMake (18, 18); for (SKSpriteNode * node in self.children) if ([node.name isEqualToString: @ "Projectile"]) // Pas een impuls toe op het projectiel en haal tijdelijk de zwaartekracht en wrijving over [knooppunt.filosofie toepassenImpulse: projectileForce]; 

Een andere leuke wijziging aan het project zou kunnen zijn om met de impulsvectorwaarde te spelen. Krachten - en dus impulsen - worden toegepast met behulp van vectoren, die de grootte en richting geven aan elke krachtwaarde.

Nu hebben we onze structuur en ons doel en kunnen we op hen schieten, maar hoe kunnen we zien of we een hit hebben gescoord?

Ramkoers

Eerst een snel paar definities:

  • EEN contact wordt gebruikt wanneer twee lichamen elkaar raken.
  • EEN botsing wordt gebruikt om te voorkomen dat twee lichamen elkaar kruisen.

Contact opnemen met de luisteraar

Tot nu toe heeft de physics engine contacten en botsingen voor ons behandeld. Wat als we iets speciaals wilden doen wanneer twee specifieke objecten elkaar raken? Om te beginnen moeten we onze game laten weten dat we naar het contact willen luisteren. We zullen een afgevaardigde en een verklaring gebruiken om dit te bereiken. 

We voegen de volgende code toe aan de bovenkant van het bestand:

@interface MyScene () @einde

... en voeg deze verklaring toe aan de initialisatie:

self.physicsWorld.contactDelegate = self

Dit stelt ons in staat om de methode stub hieronder te gebruiken om te luisteren naar contact:

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

Voordat we deze methode kunnen gebruiken, moeten we echter bespreken categorieën.

Categorieën

We kunnen toewijzen categorieën aan onze verschillende fysische lichamen, als eigendom, om ze in groepen te sorteren. 

Sprite Kit gebruikt in het bijzonder bit-wise categorieën, wat betekent dat we beperkt zijn tot 32 categorieën in een bepaalde scène. Ik vind het leuk om mijn categorieën te definiëren met behulp van een statische constante verklaring als deze:

// Creëer natuurkunde Categorie Bit-Mask's statische const uint32_t objectiveMask = 1 << 0; static const uint32_t otherMask = 1 << 1;

Let op het gebruik van bitsgewijze operatoren in de declaratie (een bespreking van bitwise operatoren en bitvariabelen valt buiten het bestek van deze tutorial; weet alleen dat het in feite gewoon getallen zijn die zijn opgeslagen in een zeer snel benaderde variabele, en die u kunt hebben 32 maximum).

We kennen de categorieën toe met behulp van de volgende eigenschappen:

platSprite.physicsBody.categoryBitMask = otherMask; // wijs een categoriemasker toe aan het fysieke lichaam platSprite.physicsBody.contactTestBitMask = objectiveMask; // maak een contacttestmasker voor contactback-callbacks van fysieke instanties

Als we hetzelfde doen voor de andere variabelen in het project, laten we nu onze contact listener methode stub van eerder voltooien, en ook deze discussie!

-(void) didBeginContact: (SKPhysicsContact *) contact // dit is de contact listener methode, we geven het de contactopdrachten waar we om geven en voeren vervolgens acties uit op basis van de botsing uint32_t collision = (contact.bodyA.categoryBitMask | contact.bodyB .categoryBitMask); // definieer een botsing tussen twee categoriemaskers als (botsing == (otherMask | objectiveMask)) // omgaan met de botsing van het bovenstaande if-statement, u kunt meer if / else-instructies voor meer categorieën maken als (! isGameReseting) NSLog (@"Jij wint!"); isGameReseting = YES; // Stel een beetje actie / animatie in voor wanneer een doel wordt geraakt SKAction * scaleUp = [SKAction scaleTo: 1.25 duration: 0.5]; SKAction * tint = [SKAction colorizeWithColor: [UIColor redColor] colorBlendFactor: 1 duur: 0,5]; SKAction * blowUp = [SKAction group: @ [scaleUp, tint]]; SKAction * scaleDown = [SKAction scaleTo: 0.2 duration: 0.75]; SKAction * fadeOut = [SKAction fadeAlphaTo: 0 duur: 0,75]; SKAction * blowDown = [SKAction-groep: @ [scaleDown, fadeOut]]; SKAction * remove = [SKAction removeFromParent]; SKAction * sequence = [SKAction sequence: @ [blowUp, blowDown, remove]]; // Zoek uit welk van de contactlichamen een doel is door de naam ervan te controleren en voer de actie daarop uit als ([contact.bodyA.node.name isEqualToString: @ "objective"]) [contact.bodyA.node runAction :volgorde];  else if ([contact.bodyB.node.name isEqualToString: @ "objective"]) [contact.bodyB.node runAction: sequence];  // na een paar seconden herstart het niveau [self performSelector: @selector (gameOver) withObject: nil afterDelay: 3.0f]; 

Conclusie

Ik hoop dat je deze tutorial leuk vond! We hebben alles geleerd over 2D-fysica en hoe ze kunnen worden toegepast op 2D-projectielen. Ik hoop dat je nu een beter begrip hebt van wat je kunt doen om de fysica in je eigen games te gebruiken, en hoe natuurkunde kan leiden tot een nieuwe en leuke gameplay. Laat me in onderstaande opmerkingen weten wat je ervan vindt en als je iets gebruikt dat je hier vandaag hebt geleerd om zelf projecten te maken, hoor ik het graag. 

Een opmerking over het voorbeeldproject

Ik heb een werkend voorbeeld van de code in dit project opgenomen als een GitHub-repo. De volledig becommentarieerde broncode is er voor iedereen om te gebruiken. 

Sommige minder belangrijke delen van het werkproject die geen verband houden met de natuurkunde, werden niet besproken in deze zelfstudie. Het project is bijvoorbeeld gebouwd om uitbreidbaar te zijn, dus de code staat het laden van meerdere niveaus toe met behulp van een eigenschappenlijstbestand om verschillende platformarrangementen en meerdere te raken doelen te creëren. De game-over sectie en de code om objecten en timers te verwijderen, werden ook niet besproken, maar zijn volledig becommentarieerd en beschikbaar in de projectbestanden..

Enkele ideeën voor functies die u zou kunnen toevoegen om het project uit te breiden:

  1. Verschillende soorten munitie
  2. Beweegbare en schaalbare richtingsrichting en magnitude
  3. Meer soorten en maten van platforms
  4. Terrein
  5. Animatie en geluidseffecten 

Veel plezier! Bedankt voor het lezen!