martes, 20 de septiembre de 2011

ChipMunk, Cocos2D y Cuerpos Gelatinosos (Soft Body)

He estado trabajando en un sustancioso tutorial, en el que he averiguado como hacer un cuerpo blando, gelatinoso, o simplemente del inglés "Soft Body". Viendo desde fuera un video en el que se muestran físicas de este estilo, cualquiera puede pensar que es una "BARBARIDAD" hablando en el sentido de dificultad. Pero nada más lejos... Crear un cuerpo gelatinoso no es tan dificil como lo parece.

En el tutorial de hoy, enseñaré como hacer un cuerpo Gelatinoso (Soft Body) bajo Cocos2D y chipmunk.

¿Cómo empezamos? 
Iniciaremos un proyecto con la plantilla (template) de Chipmunk en Cocos2D. Ahora, descargamos este archivo, que contiene las clases, encargadas de dibujas las formas y cuerpos de Chipmunk (Sólo para debug) http://www.megaupload.com/?d=X03R1WW6
Una vez importados los dos archivos (debugDraw.h y debugDraw.m la clase helloworld layer, la borramos entera, y la dejamos tal que así.
//
//  HelloWorldLayer.m
//  Chipmunk
//
//  Created by Daniel López Sánchez on 10/09/11.
//  Copyright Bluelephant 2011. All rights reserved.
//


// Import the interfaces
#import "HelloWorldLayer.h"

static void eachShape(void *ptr, void* unused)
{
    cpShape *shape = (cpShape*) ptr;
    cpBody *body = shape->body;
    
    CCSprite *sprite = body->data;
    if( sprite ){
         //Sólo actualizo la posición, porque de la rotación, nos encargaremos nosotros "a mano"
        [sprite setPosition: body->p];
    }
}



// HelloWorldLayer implementation
@implementation HelloWorldLayer

+(CCScene *) scene{
    CCScene *scene = [CCScene node];
    HelloWorldLayer *layer = [HelloWorldLayer node];
    [scene addChild: layer];
    return scene;
}



-(void) addNewSpriteFlubX:(float)x y:(float)y{    
    Rondo *rondo = [[Rondo alloc] initWithPosition:ccp(x,y) withWorld:space];
    rondo.tag = 1;
    [self addChild:rondo];
    
}

// on "init" you need to initialize your instance
-(id) init
{
	// always call "super" init
	// Apple recommends to re-assign "self" with the "super" return value
	if( (self=[super init] )) {
		self.isTouchEnabled = YES;
        glClearColor(0.5, 0.5, 0.5, 1);
		CGSize wins = [[CCDirector sharedDirector] winSize];
		cpInitChipmunk();
		
		cpBody *staticBody = cpBodyNew(INFINITY, INFINITY);
		space = cpSpaceNew();
		cpSpaceResizeStaticHash(space, 400.0f, 40);
		cpSpaceResizeActiveHash(space, 100, 600);
		
		space->elasticIterations = space->iterations;
		
		cpShape *shape;
		
		// bottom
		shape = cpSegmentShapeNew(staticBody, ccp(0,0), ccp(wins.width*CC_CONTENT_SCALE_FACTOR(),0), 0.0f);
		shape->e = 1.0f; shape->u = 1.0f;
		cpSpaceAddStaticShape(space, shape);
		
		// top
		shape = cpSegmentShapeNew(staticBody, ccp(0,wins.height*CC_CONTENT_SCALE_FACTOR()), ccp(wins.width*CC_CONTENT_SCALE_FACTOR(),wins.height*CC_CONTENT_SCALE_FACTOR()), 0.0f);
		shape->e = 1.0f; shape->u = 1.0f;
		cpSpaceAddStaticShape(space, shape);
		
		// left
		shape = cpSegmentShapeNew(staticBody, ccp(0,0), ccp(0,wins.height*CC_CONTENT_SCALE_FACTOR()), 0.0f);
		shape->e = 1.0f; shape->u = 1.0f;
		cpSpaceAddStaticShape(space, shape);
		
		// right
		shape = cpSegmentShapeNew(staticBody, ccp(wins.width*CC_CONTENT_SCALE_FACTOR(),0), ccp(wins.width*CC_CONTENT_SCALE_FACTOR(),wins.height*CC_CONTENT_SCALE_FACTOR()), 0.0f);
		shape->e = 1.0f; shape->u = 1.0f;
		cpSpaceAddStaticShape(space, shape);
        
		space->gravity = ccp(0,-900);
		[self schedule: @selector(step:)];
	}
	return self;
}

- (void) dealloc
{
cpSpaceFree(space);
space = NULL;

[super dealloc];
}

-(void) step: (ccTime) delta
{
	int steps = 2;
	CGFloat dt = delta/(CGFloat)steps;
	
	for(int i=0; i<steps; i++){
		cpSpaceStep(space, dt);
	}
	cpSpaceHashEach(space->activeShapes, &eachShape, nil);
	cpSpaceHashEach(space->staticShapes, &eachShape, nil);
}

- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    for( UITouch *touch in touches ) {
        CGPoint location = [touch locationInView: [touch view]];
        location = [[CCDirector sharedDirector] convertToGL: location];
        location = ccpMult(location, CC_CONTENT_SCALE_FACTOR());
        [self addNewSpriteFlubX: location.x y:location.y];
    }
}


- (void)draw {
        drawSpaceOptions options = {
        0, // drawHash
        0, // drawBBs,
        1, // drawShapes
        0, // collisionPointSize
        0, // bodyPointSize,
        0 // lineThickness
    };
    drawSpace(space, &options);
}
@end
¿Qué es lo que he hecho en este código? Simplemente crear un mundo con cuatro paredes (en el método init) las cuales se corresponden con los bordes de la pantalla del dispositivo. Y además he creado un delegado de TouchesEnded, en el cual, creo un nuevo esprite tipo "Rondo"
El sprite es una clase heredada de CCSprite, cuya implementación, vemos aquí:
//
//  Rondo.m
//  DebugDraw
//
//  Created by Daniel López Sánchez on 13/09/11.
//  Copyright 2011 __MyCompanyName__. All rights reserved.
//

#import "Rondo.h"
#define LADOS  14

@implementation Rondo
-(id) init
{
// always call "super" init
// Apple recommends to re-assign "self" with the "super" return value
if( (self=[super init])) {
    }
    return self;
}

-(id) initWithPosition:(CGPoint) p withWorld:(cpSpace*) w{
    // always call "super" init
	// Apple recommends to re-assign "self" with the "super" return value
    //[super init]
	if( (self=[super initWithFile:[[NSBundle mainBundle] pathForResource:@"Ball" ofType:@"png"]])) {
        RADIO = 40*CC_CONTENT_SCALE_FACTOR();
        self.opacity = 0;
        skin = [self texture];
        world = w;
        //self.position = p;
        cpFloat centralMass = 1.0f/LADOS;
        centralBody = cpBodyNew(centralMass, cpMomentForCircle(centralMass, 0, RADIO, cpvzero));
        centralBody->p = p;
        
        cpSpaceAddBody(world, centralBody);
        
        centralBodyShape = cpCircleShapeNew(centralBody, RADIO, cpvzero);
        centralBodyShape -> layers = 1;
        cpSpaceAddShape(world, centralBodyShape);
        centralBodyShape->data = self;

        int i;
        muelles = [[NSMutableArray alloc] initWithCapacity:LADOS];
        edgeMass = 1.0/LADOS;
        edgeDistance = 2.0*RADIO*cpfsin(M_PI/(cpFloat)LADOS);
        _edgeRadius = edgeDistance/2.0;
        
		cpFloat coeficienteEstrujamiento = 0.1;
		cpFloat fuerzaMuelle = 60;
		cpFloat retrocesoMuelle = 0.75;
        
        for (i=0; i<LADOS; i++) {       
            cpVect dir = cpvforangle((cpFloat)i/(cpFloat)LADOS*2.0*M_PI);
			cpVect offset = cpvmult(dir, RADIO);
            
            cpBody* body = cpBodyNew(edgeMass, INFINITY);
            body->p = cpvadd(centralBody->p, offset);
            cpSpaceAddBody(world, body);
            
            cpShape *shape = cpCircleShapeNew(body, _edgeRadius, cpvzero);
            shape -> layers = 2;
            shape -> u = 1;
            shape -> e = 0.5;
            
            cpSpaceAddShape(world, shape);

            cpConstraint *jointDef = cpSlideJointNew(centralBody,body, offset, cpvzero, 0, RADIO*coeficienteEstrujamiento);
            cpSpaceAddConstraint(world,jointDef);
            
            cpVect springOffset = cpvmult(dir, RADIO + _edgeRadius + 4);
            cpConstraint *dampedDef = cpDampedSpringNew(centralBody, body, springOffset,cpvzero, 10, fuerzaMuelle,retrocesoMuelle);
            cpSpaceAddConstraint(world,dampedDef);
            
            m_perimeterBodies[i] = body;
            
            cpBodyApplyImpulse(body, cpv(40,40),cpv(0,0));
        }
        
        for (i=0;i<LADOS;i++){
 
            cpBody *muelle = m_perimeterBodies[i];
            cpBody *muelleB = m_perimeterBodies[(i+1)%LADOS];

            
            cpConstraint *jointDef = cpSlideJointNew(muelle,muelleB, cpvzero, cpvzero, 0, edgeDistance);
            cpSpaceAddConstraint(world,jointDef);
            
        }
        
        m_perimeterBodies[LADOS] = m_perimeterBodies[0];
        m_perimeterBodies[LADOS+1] = m_perimeterBodies[1];
        
        cpBodyApplyImpulse(centralBody, cpv(0,200),cpv(0,40));
    }
    return self;
}
@end
Sin olvidar ambas cabeceras, helloworldlayer.h y Rondo.h
//
//  Rondo.h
//  DebugDraw
//
//  Created by Daniel López Sánchez on 13/09/11.
//  Copyright 2011 __MyCompanyName__. All rights reserved.
//

#import "cocos2d.h"
#import  "chipmunk.h"

@interface Rondo : CCSprite {
    CCTexture2D *skin;
    float PTM_RATIO;
    cpSpace* world;
    cpBody *centralBody;
    cpShape *centralBodyShape;
    NSMutableArray *muelles;
    cpBody* m_perimeterBodies[16];
    float RADIO;
    cpFloat edgeMass;
    cpFloat edgeDistance;
    cpFloat _edgeRadius;
}
-(id) initWithPosition:(CGPoint) p withWorld:(cpSpace*) w;
@end
//
//  HelloWorldLayer.h
//  Chipmunk
//
//  Created by Daniel López Sánchez on 10/09/11.
//  Copyright __MyCompanyName__ 2011. All rights reserved.
//


// When you import this file, you import all the cocos2d classes
#import "cocos2d.h"
#import "drawSpace.h"
#import "Rondo.h"

// Importing Chipmunk headers
#import "chipmunk.h"

// HelloWorldLayer
@interface HelloWorldLayer : CCLayer
{
cpSpace *space;
}

// returns a CCScene that contains the HelloWorldLayer as the only child
+(CCScene *) scene;
-(void) step: (ccTime) dt;

@end
En este punto, si ejecutamos la aplicación, veremos esto:


¿Entendeis lo que sucede? He creado un cuerpo, formado por varios elementos, entre ellos existen muelles y juntas. Los muelles van desde el centro de la forma, hasta los extremos, y para mantener la forma circular, las uniones unen los extremos entre sí, dos a dos. Desde ya, parece tener cierto "peso" y cierta deformación. Pero el efecto se amplifica si le pegamos una textura, dándole como coordenadas los propios extremos del objeto.

¿Cómo pegar una textura a mano?
Bien, todos los métodos que poseen una representación gráfica, tienen un método Draw (Si no la tiene la clase en sí, la tiene algún padre). Así que partiendo de esta base, podemos "sobreescribir" el método padre, por el nuestro. Y tenemos que añadir el siguietne método a la clase Rondo.

-(float) anguloBase{

    cpBody *muelle = m_perimeterBodies[0];
    
    CGPoint e = ccp(muelle->p.x,muelle->p.y);
    CGPoint c = ccp(centralBody->p.x,centralBody->p.y);
    
    return ccpAngleSigned(e, c);
    
}
-( void )draw {
    CGPoint segmentPos[ LADOS + 2 ];
    CGPoint texturePos[ LADOS + 2 ];
    CGPoint textureCenter;
    float angle, baseAngle;
    
    segmentPos[ 0 ] = CGPointZero;
    for ( int count = 0; count < LADOS; count ++ ) {
        //Multiplica por un factor de escala, para pegar la textura, acorde con la forma (esto se hace mejor a mano), así que para mi ejemplo, el valor 1.3f va muy bien.
        segmentPos[ count + 1 ] = ccpMult( ccpSub( m_perimeterBodies[ count ]->p, centralBody->p ), 1.3f );
    }
    segmentPos[ LADOS + 1 ] = segmentPos[ 1 ];
    
    // Indicamos los puntos de la textura
    for ( int count = 0; count < ( LADOS + 2 ); count ++ ){
            segmentPos[count] = ccpAdd(segmentPos[count], ccp((RADIO-_edgeRadius+1)*2,(RADIO-_edgeRadius+1)*2));
    }
    
    // Dibujamos la textura, en los extremos de la forma.
    // Angulo base, nos devuelve el ángulo de la forma, referente a dos puntos. El extremo número 0  y el cuerpo central.
    baseAngle = [self anguloBase];
    texturePos[ 0 ] = CGPointZero;
    
    for ( int count = 0; count < LADOS; count ++ ) {
        angle	 = baseAngle + ( 2 * M_PI / LADOS * count );
        texturePos[ count + 1 ].x	= sinf( angle );
        texturePos[ count + 1 ].y	= cosf( angle );
    }
    texturePos[ LADOS + 1 ] = texturePos[ 1 ];
    textureCenter = CGPointMake( 0.5f, 0.5f );
    for ( int count = 0; count < ( LADOS + 2 ); count ++ )
        texturePos[ count ] = ccpAdd( ccpMult( texturePos[ count ], 0.5f ), textureCenter );
    
//Rutinas OPENGL para dibujar la textura.
    glColor4ub( 255, 255, 255, 255 );
    glEnable( GL_TEXTURE_2D );
    glBindTexture( GL_TEXTURE_2D, [skin name] ); 
    glDisableClientState( GL_COLOR_ARRAY ),
    glTexCoordPointer( 2, GL_FLOAT, 0, texturePos );
    glVertexPointer( 2, GL_FLOAT, 0, segmentPos );
    glDrawArrays( GL_TRIANGLE_FAN, 0, LADOS + 2 );
    glEnableClientState( GL_COLOR_ARRAY ) ;
}


Y terminado esto, lo que obtendremos es lo enseñado en el primer video.
¡A disfrutar toqueteando los valores de las variables en los muelles!

IMPORTANTE! La textura que selecciones DEBE ser POTENCIA de 2 (64, 128, 256, 512, 1024)

11 comentarios:

  1. Excelente tutoria, muchas gracias!!! una pregunta, tienes pensado hacer algún screencast?

    ResponderEliminar
  2. Pues de momento no lo sé, estoy en una oficina y no debería molestar a otros compañeros. Pero quién sabe... Quizás en un futuro...

    ResponderEliminar
  3. Rondo.m : Errors in line 53 and 75

    ResponderEliminar
  4. Gracias, parece que había un error con las etiquetas de menor y mayor. Todo solucionado

    ResponderEliminar
  5. Your tutorial is amazing. Thanks.
    I have two issues.
    (1)The ball images do not follow their bodies and all are placed at (0,0). I think it is resulting from the line "centralBody->data = self;", so I delete it and add the line "centralBodyShape->data = self;". Then everything is fine. Also, if there are more than 3 balls in my 2G iphone, balls will be crazy. Any suggestion?
    (2)I use your given drawSpace and got the message "OpenGL error 0x0501 in -[EAGLView swapBuffers]". I am a openGL beginner, so I don't know how to fix it. Please help.
    I am so happy to play with softbody.
    Again. Thank you very much.

    ResponderEliminar
  6. Just you should play trying different joints to avoid the strange behavior. I gave you the basics, try to master them :P Even you can try instead of balls shapes in the extremes, make a chain, like a tank "gear". Another tip, to improve your FPS try to reduce the sides of the shape, I've seen it with 9 sides, and it was still working awesome.

    About OpenGL, I can't guess why are you getting that error.

    I don't have so much time, and I am making many mistakes, I will correct that line you'r talking about. Maybe iPhone 2G are old enough? I've tried in iPad 2, iPhone 4 and iPod 3G and it was good.

    Regards

    ResponderEliminar
  7. can you share your project solution? :)

    ResponderEliminar
  8. Bastante bueno el tutorial y lo demas = , gracias por aportar con conocimientos !!
    ^^

    ResponderEliminar
  9. Hola Daniel, tienes twitter? el mío es @juaxix

    Bueno, he creado el proyecto, todo ok, he añadido una textura de 128x128 píxels (2^7), con el balón de fútbol en PNG con fondo transparente, pero al lanzar un balón el método *Rondo -> initWithPosition* no la coloca en su sitio en el centro de cada bola, he quitado el comentario de la línea

    self.position = p;

    y aparece donde se toca la pantalla pero luego no se queda "pegada", por así decirlo, en el centro de cada circunferencia,así que he añadido en Rondo->draw:
    //---
    self.position = centralBody->p;
    //---
    que sería lo mismo que :
    //--
    self.position = centralBodyShape->body->p;
    //--

    pero cuando creo unas cuantas seguidas tocando la pantalla, se le va un poco la olla y cuando ya hay varias se vuelve totalmente loco xD

    algún consejo para que en la pantalla de un iPad2 funcione bien?

    ResponderEliminar
  10. Me recuerda mucho a esto:
    http://dashiellgough.wordpress.com/2011/10/29/cocos2d-box2d-textured-soft-body/

    ResponderEliminar