En el tutorial de hoy, enseñaré como hacer un cuerpo Gelatinoso (Soft Body) bajo Cocos2D y chipmunk.
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"
// // 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; } @endSin 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; @endEn 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)
IMPORTANTE! La textura que selecciones DEBE ser POTENCIA de 2 (64, 128, 256, 512, 1024)
Excelente tutoria, muchas gracias!!! una pregunta, tienes pensado hacer algún screencast?
ResponderEliminarPues 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...
ResponderEliminarRondo.m : Errors in line 53 and 75
ResponderEliminarGracias, parece que había un error con las etiquetas de menor y mayor. Todo solucionado
ResponderEliminarYour tutorial is amazing. Thanks.
ResponderEliminarI 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.
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.
ResponderEliminarAbout 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
Thanks for your advice.
ResponderEliminarcan you share your project solution? :)
ResponderEliminarBastante bueno el tutorial y lo demas = , gracias por aportar con conocimientos !!
ResponderEliminar^^