En Qué hay en un motor de física de proyectiles, cubrimos la teoría y los elementos esenciales de los motores de física que se pueden usar para simular efectos de proyectiles en juegos como Angry Birds. Ahora, cimentaremos ese conocimiento con un ejemplo real. En este tutorial, desglosaré el código de un juego simple basado en la física que he escrito, para que puedas ver exactamente cómo funciona..
Para aquellos interesados, el código de ejemplo que se proporciona en este tutorial usa la API del Sprite Kit que se proporciona para los juegos iOS nativos. Esta API utiliza un Box2D envuelto con Objective-C como motor de simulación física, pero los conceptos y su aplicación se pueden usar en cualquier motor o mundo de física 2D..
Aquí está el juego de muestra en acción:
El concepto general del juego toma la siguiente forma:
Nuestro primer uso de la física será crear un cuerpo de bucle de borde alrededor del marco de nuestra pantalla. Lo siguiente se agrega a un inicializador o -(vacío) loadLevel
método:
// crear un cuerpo físico de bucle de borde para la pantalla, básicamente crear un "límites" self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect: self.frame];
Esto mantendrá todos nuestros objetos dentro del marco, para que la gravedad no saque todo nuestro juego de la pantalla!
Veamos cómo agregar algunos sprites habilitados para la física a nuestra escena. Primero, veremos el código para agregar tres tipos de plataformas. Usaremos plataformas cuadradas, rectangulares y triangulares para trabajar en esta simulación..
-(void) createPlatformStructures: (NSArray *) plataformas para (NSDictionary * plataforma en plataformas) // Agarra la información de Dictionay y prepara las variables int type = [platform [@ "platformType"] intValue]; CGPoint position = CGPointFromString (platform [@ "platformPosition"]); SKSpriteNode * platSprite; platSprite.zPosition = 10; // Lógica para completar el nivel según el tipo de plataforma if (type == 1) // Square platSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "SquarePlatform"]; // crear sprite platSprite.position = posición; // position sprite platSprite.name = @ "Square"; CGRect physicsBodyRect = platSprite.frame; // construir una variable de rectángulo basada en el tamaño platSprite.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize: physicsBodyRect.size]; // construir el cuerpo de física platSprite.physicsBody.categoryBitMask = otherMask; // asigna una máscara de categoría al cuerpo de física platSprite.physicsBody.contactTestBitMask = objectiveMask; // crear una máscara de prueba de contacto para devoluciones de llamada de contacto del cuerpo de física platSprite.physicsBody.usesPreciseCollisionDetection = YES; else if (tipo == 2) // Rectangle platSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "RectanglePlatform"]; // crear sprite platSprite.position = posición; // posicionar sprite platSprite.name = @ "Rectangle"; CGRect physicsBodyRect = platSprite.frame; // construir una variable de rectángulo basada en el tamaño platSprite.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize: physicsBodyRect.size]; // construir el cuerpo de física platSprite.physicsBody.categoryBitMask = otherMask; // asigna una máscara de categoría al cuerpo de física platSprite.physicsBody.contactTestBitMask = objectiveMask; // crear una máscara de prueba de contacto para devoluciones de llamada de contacto del cuerpo de física platSprite.physicsBody.usesPreciseCollisionDetection = YES; else if (tipo == 3) // Triangle platSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "TrianglePlatform"]; // crear sprite platSprite.position = posición; // posicionar sprite platSprite.name = @ "Triangle"; // Crear una ruta mutable en la forma de un triángulo, usando los límites de sprites como una guía 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]; // construir el cuerpo de física platSprite.physicsBody.categoryBitMask = otherMask; // asigna una máscara de categoría al cuerpo de física platSprite.physicsBody.contactTestBitMask = objectiveMask; // crear una máscara de prueba de contacto para devoluciones de llamada de contacto del cuerpo de física platSprite.physicsBody.usesPreciseCollisionDetection = YES; CGPathRelease (physicsPath); // libera el camino ahora que hemos terminado con él [self addChild: platSprite];
Vamos a llegar a lo que todas las declaraciones de propiedad significan en un momento. Por ahora, enfócate en la creación de cada cuerpo. El cuadrado y las plataformas rectangulares crean cada uno sus cuerpos en una declaración de una línea, utilizando el cuadro delimitador del sprite como el tamaño del cuerpo. El cuerpo de la plataforma triangular requiere trazar un camino; Esto también utiliza el cuadro delimitador del sprite, pero calcula un triángulo en las esquinas y en los puntos intermedios del marco..
El objeto objetivo, una estrella, se crea de manera similar, pero usaremos un cuerpo de física circular.
-(void) addObjectives: (NSArray *) objetivos para (NSDictionary * objetivo en los objetivos) // Coge la información de posición del diccionario proporcionado desde la posición de CGPoint = CGPointFromString (objetivo [@ "objectoPosición")); // crear un sprite basado en la información del diccionario anterior SKSpriteNode * objSprite = [SKSpriteNode spriteNodeWithImageNamed: @ "star"]; objSprite.position = posición; objSprite.name = @ "object"; // Asigne un cuerpo físico y propiedades físicas al 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; // agrega el niño a la escena [self addChild: objSprite]; // Crear una acción para hacer que el objetivo sea más interesante SKAction * turn = [SKAction rotateByAngle: 1 duration: 1]; SKAction * repeat = [SKAction repeatActionForever: turn]; [objSprite runAction: repetir];
El cañón en sí no necesita ningún cuerpo conectado, ya que no necesita detección de colisiones. Simplemente lo utilizaremos como punto de partida para nuestro proyectil..
Aquí está el método para crear un proyectil:
-(void) addProjectile // Crea un sprite basado en nuestra imagen, dale una posición y nombre proyectile = [SKSpriteNode spriteNodeWithImageNamed: @ "ball"]; proyectil.posicion = canon.posicion; projectile.zPosition = 20; projectile.name = @ "Projectile"; // Asignar un cuerpo de física al sprite projectile.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius: projectile.size.width / 2]; // Asignar propiedades al cuerpo de física (todos estos existen y tienen valores predeterminados al crear el cuerpo) 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; // Agrega el sprite a la escena, con el cuerpo de física adjunto [self addChild: proyectile];
Aquí vemos una declaración más completa de algunas propiedades asignables a un cuerpo de física. Cuando juegue con el proyecto de muestra más tarde, intente alterar el restitución
, fricción
, y densidad
del proyectil para ver qué efectos tienen en el juego en general. (Puede encontrar definiciones para cada propiedad en ¿Qué hay en un motor de física de proyectiles?)
El siguiente paso es crear el código para disparar esta bola al objetivo. Para esto, aplicaremos un impulso a un proyectil basado en un evento táctil:
-(void) touchesBegan: (NSSet *) toca withEvent: (UIEvent *) evento / * Llamado cuando comienza un toque * / para (UITouch * touch in touches) CGPoint location = [touch locationInNode: self]; NSLog (@ "Tocado x:% f, y:% f", ubicación.x, ubicación.y); // Verifique si ya hay un proyectil en la escena si (! IsThereAProjectile) // Si no, agréguelo isThereAProjectile = YES; [auto addProjectile]; // Crear un vector para usar como un valor de fuerza 2D proyectileForce = CGVectorMake (18, 18); para (SKSpriteNode * node en self.children) if ([node.name isEqualToString: @ "Projectile"]) // Aplique un impulso al proyectil, superando la gravedad y la fricción temporalmente [node.physicsBody applyImpulse: projectileForce];
Otra alteración divertida del proyecto podría ser jugar con el valor del vector de impulso. Las fuerzas, y por lo tanto los impulsos, se aplican mediante vectores, dando magnitud y dirección a cualquier valor de fuerza.
Ahora tenemos nuestra estructura y nuestro objetivo, y podemos dispararles, pero ¿cómo podemos ver si marcamos un golpe??
Primero, un par de definiciones rápidas:
Hasta ahora, el motor de física ha estado manejando contactos y colisiones para nosotros. ¿Y si quisiéramos hacer algo especial cuando se tocan dos objetos particulares? Para empezar, necesitamos decirle a nuestro juego que queremos escuchar al contacto. Usaremos un delegado y una declaración para lograr esto..
Añadimos el siguiente código a la parte superior del archivo:
@interface MyScene ()@fin
... y agregue esta declaración al inicializador:
self.physicsWorld.contactDelegate = self
Esto nos permite utilizar el código auxiliar que se muestra a continuación para escuchar el contacto:
-(void) didBeginContact: (SKPhysicsContact *) contact // code
Antes de que podamos usar este método, sin embargo, tenemos que discutir categorías.
Podemos asignar categorías a nuestros diversos cuerpos de física, como propiedad, para clasificarlos en grupos.
Sprite Kit, en particular, utiliza categorías de bits, lo que significa que estamos limitados a 32 categorías en cualquier escena dada. Me gusta definir mis categorías usando una declaración de constante estática como esta:
// Crear la categoría física Física de la máscara de bits uint32_t objectMask = 1 << 0; static const uint32_t otherMask = 1 << 1;
Tenga en cuenta el uso de operadores de bit a bit en la declaración (una discusión sobre los operadores de bit a bit y las variables de bit está fuera del alcance de este tutorial; sepa que son esencialmente solo números almacenados en una variable de acceso muy rápido, y que puede tener 32 máximo).
Asignamos las categorías utilizando las siguientes propiedades:
platSprite.physicsBody.categoryBitMask = otherMask; // asigna una máscara de categoría al cuerpo de física platSprite.physicsBody.contactTestBitMask = objectiveMask; // crear una máscara de prueba de contacto para devoluciones de llamada de cuerpo físico
Haciendo lo mismo para las otras variables en el proyecto, ahora completemos nuestro apéndice de método de escucha de contacto de antes, y también esta discusión!
-(void) didBeginContact: (SKPhysicsContact *) contact // este es el método de escucha de contacto, le asignamos las asignaciones de contacto que nos interesan y luego realizamos acciones basadas en la colisión uint32_t collision = (contact.bodyA.categoryBitMask | contact.bodyB .categoryBitMask); // define una colisión entre dos máscaras de categoría si (colisión == (otraMáscara | objectMask)) // maneja la colisión de la instrucción if anterior, puedes crear más sentencias if / else para más categorías si (! isGameReseting) NSLog (@"¡Tú ganas!"); isGameReseting = YES; // Configurar un poco de acción / animación para cuando se alcanza un objetivo SKAction * scaleUp = [SKAction scale Para: 1.25 duración: 0.5]; SKAction * tint = [SKAction colorizeWithColor: [UIColor redColor] colorBlendFactor: 1 duración: 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 sequence: @ [blowUp, blowDown, remove]]; // Averigüe cuál de los cuerpos de contacto es un objetivo al verificar su nombre, y luego ejecute la acción si ([contact.bodyA.node.name isEqualToString: @ "object"]) [contact.bodyA.node runAction :secuencia]; else if ([contact.bodyB.node.name isEqualToString: @ "object"]) [contact.bodyB.node runAction: sequence]; // después de unos segundos, reinicia el nivel [self performSelector: @selector (gameOver) conObject: nil afterDelay: 3.0f];
Espero que hayas disfrutado este tutorial! Hemos aprendido todo sobre la física 2D y cómo se pueden aplicar a un juego de proyectiles 2D. Espero que ahora entiendas mejor lo que puedes hacer para comenzar a usar la física en tus propios juegos, y cómo la física puede llevarte a un juego nuevo y divertido. Hazme saber en los comentarios a continuación lo que piensas, y si utilizas algo que hayas aprendido hoy aquí para crear tus propios proyectos, me encantaría saberlo..
He incluido un ejemplo de trabajo del código provisto en este proyecto como un repositorio de GitHub. El código fuente completamente comentado está ahí para que todos lo usen.
Algunas partes menores del proyecto de trabajo no relacionado con la física no se trataron en este tutorial. Por ejemplo, el proyecto está diseñado para ser expandible, por lo que el código permite la carga de múltiples niveles utilizando un archivo de lista de propiedades para crear diferentes acuerdos de plataforma y múltiples objetivos para alcanzar. La sección sobre el juego y el código para eliminar objetos y temporizadores tampoco se discutieron, pero están completamente comentados y disponibles dentro de los archivos del proyecto.
Algunas ideas para características que podría agregar para expandir el proyecto:
¡Que te diviertas! Gracias por leer!