viernes, 5 de marzo de 2010

IPhone Box2D Tutorial Part II: Starting with Box2D

Con este post continúo el tutorial para crear un primer proyecto Box2D en IPhone. Si no has leído la primera parte la tienes aquí.

En este punto deberíamos tener el proyecto XCode con el código Box2D compilando perfectamente listo para empezar a implementar, pero antes vamos a ver una pequeña introducción al funcionamiento de Box2D y los elementos que maneja:
  •  Cuerpo rígido (body)
    •  Elemento compuesto de una materia tan fuerte que la distancia entre dos partículas del mismo permanece inalterable. A partir de ahora hablaremos de cuerpo para referirnos a cuerpos rígidos.
  • Forma (shape):
    • Cuerpo geométrico bidimensional que está asociado a un cuerpo. Box2D las usa para la detección de colisiones. Tienen propiedades materiales de fricción y elasticidad.
  • Limitación (constraint):
    • Una limitación es una conexión física que elimina grados de libertad de los cuerpos. En 2D los cuerpos tienen 3 grados de libertad. Si clavamos un cuerpo a una pared (como un péndulo), hemos LIMITADO el movimiento de dicho cuerpo de tal forma que solo podemos rotar el cuerpo alrededor del anclaje, hemos eliminado por tanto 2 grados de libertad.
  • Limitación de contacto (contact constraint):
    • Limitación especial diseñada para prevenir la penetración de cuerpos rígidos y simular así la fricción y elasticidad. Este tipo de limitaciones nunca se crean, Box2D las crea automáticamente.
  • Unión, enlace (joint):
    • Es un tipo especial de limitación que se emplea para mantener dos o más cuerpos unidos. Box2D soporta diferentes tipos de uniones. Las uniones pueden tener límites o guías.
  • Límite de unión (joint limit):
    • Restringe el rango de movimiento de una unión, por ejemplo, el codo de un humano sólo permite un determinado rango de movimiento.
  • Guía de unión (joint motor) :
    • Conduce el movimiento de los cuerpos conectados de acuerdo a los grados de libertad de la unión, por ejemplo, podemos usar una guía para conducir la rotación de un codo humano.
  • Mundo (world):
    • Es una colección de cuerpos, formas y limitaciónes que interactúan. 


Ahora que ya tenemos una idea básica de los elementos con los que contamos, podemos empezar a implementar nuestro primer proyecto de simulación. Para ello lo primero que necesitamos es crear un mundo al que iremos añadiendo cuerpos y formas. Este mundo será un atributo de nuestra clase MyFirstBox2DViewController por tanto lo añadimos en la declaración de la interface:

#import
#import "Box2D.h"

@interface MyFirstBox2DViewController : UIViewController {
    b2World *world;
}

@end
Vamos ahora con la inicialización del mundo, en el archivo .m, escribimos las siguientes lineas:
b2AABB worldAABB;
worldAABB.lowerBound.Set (-100.0f, -100.0f);
worldAABB.upperBound.Set (100.0f, 100.0f);
b2Vec2 gravity (0.0f, 10.0f);
bool doSleep = true;
    
world = new b2World (worldAABB, gravity, doSleep);
 Con esto creamos una región que contendrá el mundo (la estructura b2AABB), representa el "bounding box"  del mundo. Box2D utiliza las "Bounding Box" para acelerar la detección de colisiones. La región del mundo debe siempre ser más grande que la región donde los cuerpos se van a alojar, ya que si algún cuerpo alcanza los límites de la región del mundo quedará congelado y su simulación se detiene.
En este punto hay que resaltar que Box2D no utiliza, en principio, los píxeles como unidad fundamental de medida, sino que utiliza el metro, el kilogramo para las medidas de peso y el segundo como unidad de tiempo. Más adelante veremos cómo establecer una relación entre las unidades con las que trabaja Box2D y los píxeles con los que trabajamos en Cocoa.

Aparte del Bounding Box del mundo, también definimos el vector de la gravedad. En este punto cabe decir que el sistema de coordenadas que usa Box2D tiene el origen en la esquina superior izquierda, creciendo hacia la derecha el eje X y hacia abajo el eje Y, por tanto, iniciamos el mundo para un proyecto con orientación Portrait.

El último parámetro es un valor booleano que indica que permitimos que los cuerpos "duerman" cuando se paran. Un cuerpo dormido no necesita simulación, por tanto, liberamos a la CPU de todos los cálculos asociados al desplazamiento de dicho cuerpo. Los cuerpos dormidos se despiertan cuando algún cuerpo móvil choca contra ellos o cuando explícitamente lo despertemos. Finalmente, creamos el mundo a partir de los parámetros anteriormente definidos.

Ahora que ya tenemos nuestro mundo, vamos a añadir un cuerpo.  
//Create ball body and shape
b2BodyDef ballBodyDef;
ballBodyDef.position.Set (80.0f / RATIO, 0.0f / RATIO);
b2Body *body = world->CreateBody (&ballBodyDef);
Con esto definimos un cuerpo rígido (estructura b2BodyDef) y asignamos su posición en el punto (80, 0). Dividimos estos valores entre una constante RATIO que determina la relación entre las unidades de medida de Box2D y nuestros píxeles, para este ejemplo vamos a considerar que vale 30. De aquí en adelante, esta constante va a estar presente en la mayoría de las interacciones con el motor Box2D. Finalmente indicamos a nuestro mundo que añada un nuevo cuerpo a partir de la definición creada.
A este cuerpo le añadiremos una forma (shape) que, como hemos visto anteriormente sirva para determinar las colisiones que este cuerpo tiene con el entorno.
b2CircleDef ballShapeDef;
ballShapeDef.radius = 20.0f / RATIO;
ballShapeDef.density = 1.0f;
ballShapeDef.friction = 0.3f;
ballShapeDef.restitution = 0.5f;
body->CreateShape (&ballShapeDef);
body->SetMassFromShapes();
Con estas líneas definimos una forma circular (estructura b2CircleDef) a la que asignamos sus propiedades geométricas y físicas:
  • Radio (radius): El radio de la circunferencia, nuevamente dividido por la constante de conversión de medida
  • Densidad (density): Indica la densidad del cuerpo (en kg/m^2). Por defecto la densidad es 0. Box2D considera que un cuerpo con densidad es 0 es estático, por tanto no será simulado.
  • Coeficiente de fricción (friction). Entre 0 y 1.
  • Coeficiente de elasticidad (restitution). Entre 0 y 1.
Añadimos esta forma a nuestro cuerpo previamente creado y le notificamos que calcule la masa que tendrá a partir de las formas añadidas, en este ejemplo sólo hemos añadido una forma al cuerpo, pero podríamos añadirle más.

En este punto tenemos nuestro mundo creado con una bola situada en el punto (80, 0), pero si ejecutáramos el código no veríamos nada, ya que no hemos asociado ninguna figura a nuestra bola. El motor de físicas Box2D es simplemente eso, un motor de físicas, que nada sabe, a priori, del renderizado del mundo que computa. Para renderizar la bola en pantalla vamos a emplear una bola que puedes descargar aquí. Y que debes añadir a los recursos de tu proyecto. Usaremos capas (CALayer) para renderizar los elementos de nuestro mundo. Así definimos una capa a la que colocamos en el mismo punto en el que hemos colocado la bola en el mundo y le asignamos como contenido la imagen recientemente añadida.
UIImage *bImg = [UIImage imageNamed: @"ball.png"];
CALayer *ballLayer = [CALayer layer];
ballLayer.anchorPoint = CGPointMake(0.5, 0.5);
ballLayer.position = CGPointMake(80, 0);
ballLayer.contents = (id) bImg.CGImage;
ballLayer.bounds = CGRectMake(0, 0, bImg.size.width, bImg.size.height);
[self.view.layer addSublayer: ballLayer];
Sólo recordar que para poder usar layers, necesitamos añadir el framework CoreGraphics a nuestro proyecto.
En este punto, Box2D nos facilita asociar cualquier objeto a un cuerpo al que supuestamente representa, por tanto, volvemos a la definición del cuerpo para añadir el objeto, con lo que el código quedaría así:
b2BodyDef ballBodyDef;
ballBodyDef.position.Set (80.0f / RATIO, 0.0f / RATIO);
ballBodyDef.userData = ballLayer;    
b2Body *body = world->CreateBody (&ballBodyDef);
En este punto tenemos que determinar la frecuencia de refresco de la imagen y del mundo para poder representar la animación, una frecuencia recomendada es 60 frames por segundo, por tanto lanzamos un timer con esta frecuencia que invoque una función en la que actualizaremos la posición de la capa según los datos que nos da Box2D.
La función de refresco tiene esta pinta:
- (void) step {
    world->Step (1.0f / 60.0f, 10, 8);
    [CATransaction begin];
    [CATransaction setAnimationDuration: 0.00];
    [CATransaction setDisableActions: YES];
    for (b2Body *b = world->GetBodyList(); b; b=b->GetNext()) {
        if (b->GetUserData() != NULL) {
            CALayer *ball = (CALayer *)b->GetUserData();
            ball.position = CGPointMake(b->GetPosition().x * RATIO, b->GetPosition().y * RATIO);
            float angle = b->GetAngle();
            if (angle != 0.0f) {
                ball.transform = CATransform3DMakeAffineTransform ( CGAffineTransformMakeRotation(angle));   
            }
           
        }
    }
    [CATransaction commit];
}
En esta función indicamos al mapa que se actualice y para cada uno de los cuerpos actualizamos su posición a partir de los datos que nos da Box2D. Sólo recordar que CATransaction sólo está disponible desde la versión 3.0 del firmware en adelante, por tanto es necesario, como mínimo usar esta versión. Si ejecutamos este código en este momento, veremos como la bola aparece en el punto indicado y va cayendo hasta el infinito. Por tanto, vamos a poner suelo a nuestro mundo.

El suelo va a ser un elemento estático que definiremos a partir de esta imagen. Para representarla necesitaremos otra CALayer para representarla y definiremos otro cuerpo al que añadiremos otra forma de forma parecida a como hicimos antes con la bola.

UIImage *groundImg = [UIImage imageNamed: @"floor.png"];
CALayer *groundLayer = [CALayer layer];
groundLayer.anchorPoint = CGPointMake(0.5, 0.5);
groundLayer.position = CGPointMake(160, 465);
groundLayer.contents = (id) groundImg.CGImage;
groundLayer.bounds = CGRectMake(0, 0, groundImg.size.width, groundImg.size.height
[self.view.layer addSublayer: groundLayer];
    
CGSize screenSize = [[UIScreen mainScreen] bounds].size;
b2BodyDef groundBodyDef;
groundBodyDef.position.Set (screenSize.width / RATIO / 2, (screenSize.height - groundImg.size.height / 2) / RATIO);
b2Body *groundBody = world->CreateBody (&groundBodyDef);
b2PolygonDef groundShapeDef;
groundShapeDef.SetAsBox (groundImg.size.width / RATIO / 2, groundImg.size.height / RATIO / 2);
groundBody->CreateShape(&groundShapeDef);

En este caso cabe destacar que a la forma no le asignamos propiedades físicas ya que va a ser un cuerpo completamente estático y por tanto no necesita simulación.

Si ejecutamos el proyecto veremos que la bola cae y rebota contra el suelo hasta quedar parada.

Con esto termina la segunda parte del tutorial. Espero que ayude a iniciarse en el uso del motor de físicas Box2D sin necesidad de usar Cocos2D.

Si quieren descargar el proyecto, pueden aquí.

Un Saludo

4 comentarios:

  1. Hola Carlos, un tutorial estupendo.

    Me da un error:

    Detected an attempt to call a symbol in system libraries that is not present on the iPhone:
    finite called from function _Z9b2IsValidf in image MyFirstBox2D.
    If you are encountering this problem running a simulator binary within gdb, make sure you 'set start-with-shell off' first.

    Espero que puedas ayudarme.

    Un saludo
    Gracias

    ResponderEliminar
  2. Hola, la verdad que no, ese error no lo había visto antes. Si puedes decirme si te ocurre ejecutando para simulador o device y para qué versión del firmware quizás podamos sacar algo más.

    Un Saludo

    ResponderEliminar
  3. Hola Carlos,

    Una pregunta, con vistas a colgar aplicaciones en el AppleStore realizadas con Box2D, este motor de simulacion es aceptado por Apple?

    Muchas gracias.

    ResponderEliminar
  4. Hola, sí, Apple si acepta las aplicaciones que se hagan utilizando este motor.

    Un Saludo

    ResponderEliminar