I Vad som ingår i en projektil fysikmotor, täckte vi teorin och väsentliga delar av fysikmotorer som kan användas för att simulera projektilverkningar i spel som Angry Birds. Nu ska vi cementera den kunskapen med ett riktigt exempel. I denna handledning bryter jag ner koden för ett enkelt fysikbaserat spel som jag har skrivit, så att du kan se exakt hur det fungerar.
För de som är intresserade använder exempelkoden i hela denna handledning Sprite Kit API som tillhandahålls för inhemska iOS-spel. Detta API använder en Objective-C-insvept Box2D som fysik simuleringsmotor, men koncepten och deras tillämpning kan användas i vilken 2D-fysikmotor eller -värld som helst.
Här är provspelet i aktion:
Det övergripande konceptet av spelet tar följande formulär:
Vår första användning av fysik är att skapa en kantslingkropp runt skärmens ram. Följande läggs till i en initialiserare eller -(Void) loadLevel
metod:
// skapa en kant-loop fysik kropp för skärmen, i grund och botten skapa en "gränser" self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect: self.frame];
Detta kommer att hålla alla våra föremål inom ramen, så att tyngdkraften inte kommer att dra hela vårt spel på skärmen!
Låt oss titta på att lägga till några fysikaktiverade sprites till vår scen. Först ska vi titta på koden för att lägga till tre typer av plattformar. Vi använder fyrkantiga, rektangulära och triangulära plattformar för att arbeta med i denna simulering.
-(void) createPlatformStructures: (NSArray *) plattformar för (NSDictionary * plattform i plattformar) // Ta information från Dictionay och förbered variabler int type = [plattform [@ "platformType"] intValue]; CGPoint position = CGPointFromString (plattform [@ "platformPosition"]); SKSpriteNode * platSprite; platSprite.zPosition = 10; // Logic för att fylla i nivå baserat på plattformstypen om (typ == 1) // Square platSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "SquarePlatform"]; // skapa sprite platSprite.position = position; // position sprite platSprite.name = @ "Square"; CGRect physicsBodyRect = platSprite.frame; // bygga en rektangel variabel baserat på storlek platSprite.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize: physicsBodyRect.size]; // bygga fysik kropp platSprite.physicsBody.categoryBitMask = otherMask; // Tilldela en kategori mask till fysik kroppen platSprite.physicsBody.contactTestBitMask = objectiveMask; // skapa en kontakt testmask för fysik kroppskontakt callbacks platSprite.physicsBody.usesPreciseCollisionDetection = YES; annars om (typ == 2) // Rektangel platSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "RectanglePlatform"]; // skapa sprite platSprite.position = position; // position sprite platSprite.name = @ "Rectangle"; CGRect physicsBodyRect = platSprite.frame; // bygga en rektangel variabel baserat på storlek platSprite.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize: physicsBodyRect.size]; // bygga fysik kropp platSprite.physicsBody.categoryBitMask = otherMask; // Tilldela en kategori mask till fysik kroppen platSprite.physicsBody.contactTestBitMask = objectiveMask; // skapa en kontakt testmask för fysik kroppskontakt callbacks platSprite.physicsBody.usesPreciseCollisionDetection = YES; annars om (typ == 3) // Triangle platSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "TrianglePlatform"]; // skapa sprite platSprite.position = position; // position sprite platSprite.name = @ "Triangle"; // Skapa en muterbar väg i form av en triangel, med hjälp av sprite-gränserna som en riktlinje CGMutablePathRef physicsPath = CGPathCreateMutable (); CGPathMoveToPoint (fysikPath, nil, -platSprite.size.width / 2, -platSprite.size.height / 2); CGPathAddLineToPoint (fysikPath, nil, platSprite.size.width / 2, -platSprite.size.height / 2); CGPathAddLineToPoint (fysikPath, nil, 0, platSprite.size.height / 2); CGPathAddLineToPoint (fysikPath, nil, -platSprite.size.width / 2, -platSprite.size.height / 2); platSprite.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath: physicsPath]; // bygga fysik kropp platSprite.physicsBody.categoryBitMask = otherMask; // Tilldela en kategori mask till fysik kroppen platSprite.physicsBody.contactTestBitMask = objectiveMask; // skapa en kontakt testmask för fysik kroppskontakt callbacks platSprite.physicsBody.usesPreciseCollisionDetection = YES; CGPathRelease (physicsPath); // släpp vägen nu när vi är färdiga med det [self addChild: platSprite];
Vi förstår vad alla egendomsdeklarationer betyder lite. För närvarande fokusera på skapandet av varje kropp. Torget och de rektangulära plattformarna skapar var och en sina kroppar i en enlinjedeklaration, med hjälp av spriteens avgränsningsbox som kroppsstorlek. Triangelplattformens kropp kräver att man ritar en väg; detta använder också spriteens gränsvärde, men beräknar en triangel i hörnen och halvvägs i ramen.
Objektet, en stjärna, skapas på samma sätt, men vi kommer att använda en cirkulär fysik kropp.
-(void) addObjectives: (NSArray *) mål for (NSDictionary * mål i mål) // Ta fram positionsinformationen från ordlistan som tillhandahålls från plist CGPoint position = CGPointFromString (objektiv [@ "objectivePosition"]); // skapa en sprite baserad på info från ordlistan ovanför SKSpriteNode * objSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "star"]; objSprite.position = position; objSprite.name = @ "objective"; // Tilldela en fysik kropp och fysikaliska egenskaper till sprite objSprite.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius: objSprite.size.width / 2]; objSprite.physicsBody.categoryBitMask = objectiveMask; objSprite.physicsBody.contactTestBitMask = otherMask; objSprite.physicsBody.usesPreciseCollisionDetection = JA; objSprite.physicsBody.affectedByGravity = NEJ; objSprite.physicsBody.allowsRotation = NEJ; // lägg barnet till scenen [self addChild: objSprite]; // Skapa en åtgärd för att göra målet mer intressant SKAction * turn = [SKAction rotateByAngle: 1 duration: 1]; SKAction * repeat = [SKAction repeatActionForever: sväng]; [objSprite runAction: repeat];
Kanonen i sig behöver inte några fasta kroppar, eftersom det inte behöver någon kollisionsdetektering. Vi kommer helt enkelt använda den som utgångspunkt för vår projektil.
Här är metoden för att skapa en projektil:
-(void) addProjectile // Skapa ett sprite baserat på vår bild, ge den en position och namnprojektil = [SKSpriteNode spriteNodeWithImageNamed: @ "ball"]; projectile.position = cannon.position; projectile.zPosition = 20; projectile.name = @ "Projectile"; // Tilldela en fysik kropp till sprite projectile.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius: projectile.size.width / 2]; // Tilldela egenskaper till fysikkroppen (dessa finns alla och har standardvärden vid kroppens uppbyggnad) projectile.physicsBody.restitution = 0.5; projectile.physicsBody.density = 5; projectile.physicsBody.friction = 1; projectile.physicsBody.dynamic = YES; projectile.physicsBody.allowsRotation = JA; projectile.physicsBody.categoryBitMask = otherMask; projectile.physicsBody.contactTestBitMask = objectiveMask; projectile.physicsBody.usesPreciseCollisionDetection = YES; // Lägg sprite till scenen, med den fysiska kroppen bifogad [self addChild: projectile];
Här ser vi en mer fullständig deklaration av vissa egenskaper som kan hänföras till en fysik kropp. När du spelar med provprojektet senare, försök att ändra återbetalning
, friktion
, och densitet
av projektilen för att se vilka effekter de har på den övergripande gameplayen. (Du kan hitta definitioner för varje egendom i Vad finns i en projektil fysikmotor?)
Nästa steg är att skapa koden för att faktiskt skjuta den här bollen på målet. För detta kommer vi att tillämpa en impuls till en projektil baserat på en touch-händelse:
-(void) touchesBegan: (NSSet *) berörs medEvent: (UIEvent *) händelse / * Kallas när en touch börjar * / för (UITouch * Rör vid beröring) CGPoint location = [touch locationInNode: self]; NSLog (@ "Touched x:% f, y:% f", location.x, location.y); // Kontrollera om det redan finns en projektil i scenen om (! IsThereAProjectile) // Om inte, lägg till det ärThereAProjectile = YES; [self addProjectile]; // Skapa en vektor som ska användas som 2D-kraftvärde projectileForce = CGVectorMake (18, 18); för (SKSpriteNode * nod i self.children) if ([node.name isEqualToString: @ "Projectile"]) // Applicera en impuls till projektilen, temporärt övervinna tyngdkraften och friktionen [node.physicsBody applyImpulse: projectileForce];
En annan rolig förändring av projektet kan vara att spela med impulsvektorvärdet. Krafter - och därför impulser - appliceras med hjälp av vektorer, vilket ger magnitud och riktning till något kraftvärde.
Nu har vi vår struktur och vårt mål, och vi kan skjuta på dem, men hur ser vi om vi gjorde en träff?
Först ett snabbt par definitioner:
Hittills har fysikmotorn hanterat kontakter och kollisioner för oss. Vad händer om vi ville göra något speciellt när två speciella föremål berör? Till att börja med måste vi berätta för vårt spel att vi vill lyssna på kontakten. Vi kommer att använda en delegat och en deklaration för att uppnå detta.
Vi lägger till följande kod till toppen av filen:
@interface MyScene ()@slutet
... och lägg till detta uttalande till initieraren:
self.physicsWorld.contactDelegate = self
Detta gör det möjligt för oss att använda metodstuben som beskrivs nedan för att lyssna på kontakt:
-(void) didBeginContact: (SKPhysicsContact *) kontakt // code
Innan vi kan använda den här metoden måste vi dock diskutera kategorier.
Vi kan tilldela kategorier till våra olika fysikorgan, som en egendom, för att sortera dem i grupper.
Sprite Kit använder speciellt bitvisa kategorier, vilket innebär att vi är begränsade till 32 kategorier i en viss scen. Jag gillar att definiera mina kategorier med en statisk konstant deklaration så här:
// Skapa fysik Kategori Bit-Mask statisk const uint32_t objectiveMask = 1 << 0; static const uint32_t otherMask = 1 << 1;
Notera användningen av bitvisa operatörer i deklarationen (en diskussion om bitvisa operatörer och bitvariabler ligger utanför omfattningen av denna handledning, bara vet att de i huvudsak bara är nummer som lagras i en mycket snabbt tillgänglig variabel, och att du kan ha 32 maximal).
Vi tilldelar kategorierna med följande egenskaper:
platSprite.physicsBody.categoryBitMask = otherMask; // Tilldela en kategori mask till fysik kroppen platSprite.physicsBody.contactTestBitMask = objectiveMask; // skapa en kontakt testmask för fysik kroppskontakt callbacks
Om vi gör detsamma för de andra variablerna i projektet, låt oss nu nu slutföra vår kontaktlarmsmetoden från tidigare, och även denna diskussion!
-(void) didBeginContact: (SKPhysicsContact *) kontakt // det här är kontaktlytningsmetoden, vi ger det de kontaktuppgifter vi bryr oss om och utför sedan åtgärder baserade på kollisionen uint32_t collision = (contact.bodyA.categoryBitMask | contact.bodyB .categoryBitMask); // definiera en kollision mellan två kategori masker om (kollision == (otherMask | objectiveMask)) // hantera kollisionen från ovanstående om uttalande, kan du skapa mer om / annat uttalanden för fler kategorier om (! isGameReseting) NSLog (@"Du vinner!"); isGameReseting = YES; // Ställ in en liten åtgärd / animering för när ett mål träffas SKAction * scaleUp = [SKAction scaleTo: 1,25 duration: 0.5]; SKAction * tint = [SKAction colorizeWithColor: [UIColor redColor] colorBlendFactor: 1 varaktighet: 0,5]; SKAction * blowUp = [SKAction group: @ [scaleUp, tint]]; SKAction * scaleDown = [SKAction scaleTo: 0.2 duration: 0.75]; SKAction * fadeOut = [SKAction fadeAlphaTo: 0 duration: 0.75]; SKAction * blowDown = [SKAction group: @ [scaleDown, fadeOut]]; SKAction * remove = [SKAction removeFromParent]; SKAction * sequence = [SKAction sekvens: @ [blowUp, blowDown, remove]]; // Ta reda på vilken kontaktkommandon som är ett mål genom att kontrollera namnet och sedan köra åtgärden på det om ([contact.bodyA.node.name isEqualToString: @ "objective"]) contact.bodyA.node runAction :sekvens]; annars om ([contact.bodyB.node.name isEqualToString: @ "objective"]) [contact.bodyB.node runAction: sequence]; // efter några sekunder, starta om nivån [self performSelector: @selector (gameOver) withObject: nil afterDelay: 3.0f];
Jag hoppas att du har haft den här handledningen! Vi har lärt oss allt om 2D-fysik och hur de kan appliceras på ett 2D-projektilspel. Jag hoppas att du nu har en bättre förståelse för vad du kan göra för att börja använda fysik i dina egna spel, och hur fysiken kan leda till lite nytt och roligt spel. Låt mig veta i kommentarerna nedan vad du tycker, och om du använder något du har lärt dig idag för att skapa egna projekt, skulle jag gärna höra om det.
Jag har tagit med ett fungerande exempel på koden i detta projekt som en GitHub repo. Den fullständigt kommenterade källkoden finns för alla att använda.
Några mindre delar av arbetsprojektet som inte var relaterade till fysik diskuterades inte i denna handledning. Projektet är till exempel byggt för att vara utbyggbart, så koden tillåter laddning av flera nivåer med hjälp av en egenskapslistafil för att skapa olika plattformar och flera mål att träffa. Spelet över sektionen, och koden för att ta bort objekt och timers, diskuterades inte, men är fullt kommenterade och tillgängliga inom projektfilerna.
Några idéer för funktioner som du kan lägga till för att expandera på projektet:
Ha så kul! Tack för att du läser!