Publicidad

Crear el proyecto en cocos2D

Creación del proyecto usando Box2D para nuestro juego

POR ADMINISTRADOR EL 01/07/2013

La tutorial de este juego es para aquellas personas ya familiarizadas tanto en el lenguaje de programación Objetive-C y el uso de las librería de Cocos2D. Este tutorial tratará todo el desarrollo desde el inicio del proyecto hasta los detalles del acabado del juego utilizando como motor del juego Box2D de Cocos2D para iOS.

Creación del proyecto

Con el entorno de desarrollo Xcode abierto y con las librerías correctamente instaladas de Cocos2D (en este ejemplo utilizaremos la versión 1.0.1 aunque debería de funcionar sin ningún problema en cualquier versión inferior a la 2.X) creamos un nuevo proyecto desde el menú File/New/Proyect. donde se abrirá un diálogo donde nos mostrará los diferentes tipos de proyecto que podemos crear. En nuestro caso seleccionamos iOS cocos2d y el tipo de proyecto cocos2d_box2d. Mira la siguiente imagen:

Ahora indicamos un nombre a nuestro nuevo proyecto. Escribimos gameBox2D en el campo Product Name y en el siguiente campo (Company Identifier) escribimos el nombre de nuestro identificador de desarrollo de iOS, si no tienes uno escribe por ejemplo 'iphonegame'. Mira la siguiente imagen:

Finalmente se abrirá un nuevo diálogo y presionamos el botón 'create' para crear y guardar nuestro proyecto de Box2d. A continuación ejecutamos el proyecto presionando el botón 'Run' (se encuentra en la parte superior izquierda) y tendremos que ver algo parecido a lo siguiente. Mira la siguiente imagen:

Clases de la plantilla de Box2d

El proyecto generado a partir de la plantilla Box2d de Cocos2d crea una serie de archivos muy similar que se hace para Cocos2d pero con algunas diferencias. Aquí el listado de archivos con las distintas clases.
  • AppDelegate.h
  • AppDelegate.m
  • RootViewController.h
  • RootViewController.m
  • HelloWorldLayer.h
  • HelloWorldLayer.m
  • GLES-Render.h
  • GLES-Render.m
  • GameConfig.h
La que nos importa a nosotros y va a ser la clase HelloWorldLayer.h y HelloWorldLayer.m y será con la que trabajaremos el resto de la tutorial.

Modificamos la clase HelloWorldLayer

Si ejecutaste el programa anterior verás que caen una serie de cubos. Al presionar la pantalla se crea un nuevo Sprite y éste por el efecto de la gravedad cae hasta la parte inferior. Ahora lo que haremos será reemplazar el código de la clase HelloWorldLayer.m como se indica a continuación.


// Import the interfaces

#import 'HelloWorldLayer.h'

#define PTM_RATIO 32



// HelloWorldLayer implementation

@implementation HelloWorldLayer



+(CCScene *) scene{

	// 'scene' is an autorelease object.

	CCScene *scene = [CCScene node];

	

	// 'layer' is an autorelease object.

	HelloWorldLayer *layer = [HelloWorldLayer node];

	

	// add layer as a child to scene

	[scene addChild: layer];

	

	// return the scene

	return scene;

}



-(id) init{

    if((self=[super init])) {

        //habilitamos el toque de pantalla y el acelerometro

	self.isTouchEnabled = YES;

	self.isAccelerometerEnabled = YES;

		

        //obtenemos el tamaño de la pantalla del dispositivo

	CGSize screenSize = [CCDirector sharedDirector].winSize;

		

	//definimos el efecto de gravedad para el juego

	b2Vec2 gravity;

	gravity.Set(0.0f, -10.0f);

		

        //creamos el mundo donde insertaremos todos los objetos de nuestro juego

	world = new b2World(gravity, TRUE);

		

	//definimos los limites de la zona de juego. 

	b2BodyDef groundBodyDef;

	groundBodyDef.position.Set(0, 0);// bottom-left corner

        

	b2Body* groundBody = world->CreateBody(&groundBodyDef);

		

	b2PolygonShape groundBox;		

		

	// suelo o límite inferior

	groundBox.SetAsEdge(b2Vec2(0,0), b2Vec2(screenSize.width/PTM_RATIO,0));

	groundBody->CreateFixture(&groundBox,0);

		

	//limite superior

	groundBox.SetAsEdge(b2Vec2(0,screenSize.height/PTM_RATIO), b2Vec2(screenSize.width/PTM_RATIO,screenSize.height/PTM_RATIO));

	groundBody->CreateFixture(&groundBox,0);

		

	//pared izquierda o limite izquierdo

	groundBox.SetAsEdge(b2Vec2(0,screenSize.height/PTM_RATIO), b2Vec2(0,0));

	groundBody->CreateFixture(&groundBox,0);

		

	//pared derecha o limite derecho

		groundBox.SetAsEdge(b2Vec2(screenSize.width/PTM_RATIO,screenSize.height/PTM_RATIO), b2Vec2(screenSize.width/PTM_RATIO,0));

	groundBody->CreateFixture(&groundBox,0);

		

        [self schedule: @selector(tick:)];

    }

    return self;

}



-(void) addNewSpriteWithCoords:(CGPoint)p{

    //cramos un sprite igual que se hace en cocos2d

    CCSprite *sprite = [CCSprite spriteWithFile:@'blocks.png'];

    sprite.position = ccp( p.x, p.y);

    [self addChild:sprite z:1];

	

    //definimos el cuerpo o esqueleto del sprite

    b2BodyDef bodyDef;

    bodyDef.type = b2_dynamicBody;



    bodyDef.position.Set(p.x/PTM_RATIO, p.y/PTM_RATIO);

    bodyDef.userData = sprite;

    b2Body *body = world->CreateBody(&bodyDef);

	

    //definimos la forma del sprite con una forma geométrica

    b2PolygonShape dynamicBox;

    dynamicBox.SetAsBox(sprite.contentSize.width/PTM_RATIO/2, sprite.contentSize.height/PTM_RATIO/2);

	

    //Definimos las propiedades del cuerpo o esqueleto del sprite

    b2FixtureDef fixtureDef;

    fixtureDef.shape = &dynamicBox;	

    fixtureDef.density = 1.0f;

    fixtureDef.friction = 0.3f;

    fixtureDef.restitution = 0.0f;

    body->CreateFixture(&fixtureDef);

}







-(void) tick: (ccTime) dt{

    world->Step(dt, 10, 10);



    //Recorremos todos los sprites que contiene nuestro juego

    for (b2Body* b = world->GetBodyList(); b; b = b->GetNext()){

	if (b->GetUserData() != NULL) {

            //Actualizamos la posición y el ángulo de nuestro sprite en función del cuerpo o esqueleto

	    CCSprite *myActor = (CCSprite*)b->GetUserData();

	    myActor.position = CGPointMake( b->GetPosition().x * PTM_RATIO, b->GetPosition().y * PTM_RATIO);

	    myActor.rotation = -1 * CC_RADIANS_TO_DEGREES(b->GetAngle());

	}	

    }

}



- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{

    //Add a new body/atlas sprite at the touched location

    for( UITouch *touch in touches ) {

	CGPoint location = [touch locationInView: [touch view]];

		

	location = [[CCDirector sharedDirector] convertToGL: location];

	//crea un nuevo sprite en las coordenadas de la pantalla

	[self addNewSpriteWithCoords: location];

    }

}



- (void)accelerometer:(UIAccelerometer*)accelerometer didAccelerate:(UIAcceleration*)acceleration{	

	

}



//liberamos los recursos utilizados

- (void) dealloc{

    delete world;

    world = NULL;

    delete m_debugDraw;

    [super dealloc];

}

@end

Análisis paso a paso del código del juego

Método init()

Como ya se sabe este método es el primero en ejecutarse cuando se instancia un objeto de la clase HelloWorldLayer. Allí vamos.
Las primeras instrucciones del método init() son puramente de cocos2d. Donde se activa la capacidad de responder a los eventos de tocar la pantalla, activar el acelerómetro y obtener el tamaño de la pantalla del dispositivo.


-(id) init{

    if((self=[super init])) {

        //habilitamos el toque de pantalla y el acelerometro

	self.isTouchEnabled = YES;

	self.isAccelerometerEnabled = YES;

		

        //obtenemos el tamaño de la pantalla del dispositivo

	CGSize screenSize = [CCDirector sharedDirector].winSize;

Las siguientes instrucciones son de Box2d y su función es crear el mundo o lienzo donde añadiremos los distintos elementos de nuestro juego. Para crear el mundo o lienzo lo primero es crear un vector con dos valores, este vector se encargará de simular el efecto de la gravedad y necesita un valor para x e y. A continuación creamos el objeto b2World (que es el lienzo) y pasamos el vector de gravedad junto a un valor buleano con valor TRUE. Éste último parámetro fue eliminado de la versión 2.x de Cocos2d.


        //definimos el efecto de gravedad para el juego

	b2Vec2 gravity;

	gravity.Set(0.0f, -10.0f);

		

        //creamos el mundo donde insertaremos todos los objetos de nuestro juego

	world = new b2World(gravity, TRUE);

La siguiente lineas de código se encargan de crear las paredes (derecha e izquierda) el techo y suelo. Limitando así la zona de juego. Con lo cual si lanzáramos una pelota rebotaría por la pantalla de nuestro móvil.


        //definimos los limites de la zona de juego. 

	b2BodyDef groundBodyDef;

	groundBodyDef.position.Set(0, 0);// bottom-left corner

        

	b2Body* groundBody = world->CreateBody(&groundBodyDef);

		

	b2PolygonShape groundBox;		

		

	// suelo o límite inferior

	groundBox.SetAsEdge(b2Vec2(0,0), b2Vec2(screenSize.width/PTM_RATIO,0));

	groundBody->CreateFixture(&groundBox,0);

		

	//limite superior

	groundBox.SetAsEdge(b2Vec2(0,screenSize.height/PTM_RATIO),b2Vec2(screenSize.width/PTM_RATIO

,screenSize.height/PTM_RATIO));

	groundBody->CreateFixture(&groundBox,0);

		

	//pared izquierda o limite izquierdo

	groundBox.SetAsEdge(b2Vec2(0,screenSize.height/PTM_RATIO),b2Vec2(0,0));

	groundBody->CreateFixture(&groundBox,0);

		

	//pared derecha o limite derecho

		groundBox.SetAsEdge(b2Vec2(screenSize.width/PTM_RATIO,screenSize.height/PTM_RATIO)

, b2Vec2(screenSize.width/PTM_RATIO,0));

	groundBody->CreateFixture(&groundBox,0);

		

        [self schedule: @selector(tick:)];

    }

    return self;

}

En el código anterior notaremos varios objetos nuevos. Aquí explicamos cada uno de ellos:

  • b2BodyDef: Definición del cuerpo que contiene todos los datos necesarios para la construcción de un cuerpo rígido. Primero se crea el objeto b2BodyDef y luego se añaden al mismo las distintas partes. En nuestro ejemplo añadiremos las paredes, techo y suelo.
  • b2Body: Es un cuerpo rígido. En nuestro ejemplo creamos un objeto b2Body con la instrucción world->CreateBody(&groundBodyDef); y le pasábamos por parámetro la definición del cuerpo (b2BodyDef). A su vez guardamos la referencia de este objeto en b2Body* groundBody.
  • b2PolygonShape: Es un polígono convexo. Basicamente es la forma del cuerpo o esqueleto que tendrá nuestro sprite. En nuestro caso utilizaremos un rectángulo. Con el método SetAsEdge creamos la forma y nos solicita dos vectores del tipo b2Vec2. El primer parámetro indica la posición x,y y el segundo el ancho y alto. Finalmente el polígono es añadido al cuerpo con la siguiente instrucción: groundBody->CreateFixture(&groundBox,0);

Seguramente te preguntas porque se divide el ancho o alto por el valor definido como PTM_RATIO. La respuesta es simple Box2d trabaja en otra escala diferente con lo cual PTM_RATIO es la relación de conversión entre Box2d y los pixeles de la pantalla del móvil.

La última instrucción del método init() [self schedule: @selector(tick:)]; es para iniciar el bucle del juego para actualizar los datos de los sprites con los cuerpos.


Método addNewSpriteWithCoords

Éste método es para crear y añadir un nuevo sprite al juego. Tenemos que tener presente que un sprite a diferencia de cocos2d pasa a convertirse en la apariencia o sea en el vestuario del objeto b2Body.
Las primeras lineas de código es cocos2d donde creamos un sprite.

-(void) addNewSpriteWithCoords:(CGPoint)p{

    //cramos un sprite igual que se hace en cocos2d

    CCSprite *sprite = [CCSprite spriteWithFile:@'blocks.png'];

    sprite.position = ccp( p.x, p.y);

    [self addChild:sprite z:1];

A continuación se define un nuevo cuerpo con b2BodyDef y asignamos inmediatamente el tipo, este puede ser tipo b2_dynamicBody , b2_staticBody y b2_kinematicBody.

  • b2_dynamicBody: este tipo de cuerpo que se mueve y es afectado por la física del mundo. Un ejemplo de este tipo de cuerpo puede ser una pelota que rebota y colisiona contra otros objetos.
  • b2_staticBody: Es un cuerpo estático que no se mueve y no reacciona a las fuerzas y colisiones.
  • b2_kinematicBody: Es un cuerpo híbrido que no reacciona a las fuerzas y colisiones pero si puede moverse de forma lineal.
Indicamos la posición que tendrá el cuerpo. Para ello debemos dividir el valor x e y por PTM_RATIO. Ahora la parte de asignarle el sprite creado anteriormente. Podemos almacenar la referncia de nuestro sprite de cocos2d en la propiedad userData del cuerpo. Finalmente creamos el cuerpo y lo añadimos al mundo.

    //definimos el cuerpo o esqueleto del sprite

    b2BodyDef bodyDef;

    bodyDef.type = b2_dynamicBody;



    bodyDef.position.Set(p.x/PTM_RATIO, p.y/PTM_RATIO);

    bodyDef.userData = sprite;

    b2Body *body = world->CreateBody(&bodyDef);

La forma del cuerpo: en Box2d debemos crear la figura geométrica que tendrá el cuerpo. Utilizaremos un rectángulo. Ya que la forma del sprite es un cuadrado. Si fuese una pelota usaríamos una esfera y si tuviera una forma irregular como un jarrón tendríamos que utilizar un polígono. Esta última es más completa que las anteriores.


    //definimos la forma del sprite con una forma geométrica

    b2PolygonShape dynamicBox;

    dynamicBox.SetAsBox(sprite.contentSize.width/PTM_RATIO/2, sprite.contentSize.height/

PTM_RATIO/2);

Finalmente asignamos la geometria al cuerpo. Para realizar esto primero debemos crear una nueva definición utilizando b2FixtureDef en su propiedad shape asignamos la geometría creada anteriormente. Luego aparecen tres valores: Density, friction y restitution. Éstos tres valores dan propiedades que afectan a la física del mundo y su interacción con otros cuerpos. Cambia estos valores para ver como afecta al cuerpo. Por último se asigna al cuerpo la geometría y sus propiedades físicas.

	

    //Definimos las propiedades del cuerpo o esqueleto del sprite

    b2FixtureDef fixtureDef;

    fixtureDef.shape = &dynamicBox;	

    fixtureDef.density = 1.0f;

    fixtureDef.friction = 0.3f;

    fixtureDef.restitution = 0.0f;

    body->CreateFixture(&fixtureDef);

}

La siguiente imagen verás de una forma más simple que el sprite (el pez) forma una capa por encima del cuerpo (fondo rectangular negra).


Método tick

El método tick es un bucle que lo utilizaremos para actualizar la posición y rotación de los sprites con respecto al cuerpo. Quizás en este punto te lleve confusión y creas que porque has de actualizar la posición y rotación del sprite dado que habia sido asignado al cuerpo. La cuestión es que Box2d no actualiza de forma automática la posición del sprite asignado. Con lo cual en este método recorremos todos los cuerpos que existen en el juego (world) y actualizamos la posición del sprite con la posición del cuerpo.


-(void) tick: (ccTime) dt{

    world->Step(dt, 10, 10);



    //Recorremos todos los sprites que contiene nuestro juego

    for (b2Body* b = world->GetBodyList(); b; b = b->GetNext()){

	if (b->GetUserData() != NULL) {

            //Actualizamos la posición y el ángulo de nuestro sprite en función del cuerpo

            //o esqueleto

	    CCSprite *myActor = (CCSprite*)b->GetUserData();

	    myActor.position = CGPointMake( b->GetPosition().x * PTM_RATIO, b->GetPosition().y 

* PTM_RATIO);

	    myActor.rotation = -1 * CC_RADIANS_TO_DEGREES(b->GetAngle());

	}	

    }

}

Si ejecutas el programa verás un resultado como el siguiente y que además coincide con el ejemplo de cocos2d. Este código será nuestra base para los próximos juegos que vayamos haciendo.

Descarga de proyecto


Quizás te interese ...