Imrazor's Blog

Stay Hungry, Stay Foolish

Cocos2d中的box2d

一、准备工作
引入box2d包,在需要使用box2d的文件中加入box2d的头文件;由于box2d是c++编写的,所以要把引入box2d的所有文件后缀名都改为.mm

二、box2d中的一些重要参数
1、gravity,重力加速度,同现实世界中的g,向量
2、shape,形状,形状是有大小的
3、density,密度
4、friction,摩擦力
5、restitution,恢复,此参数用于碰撞,如果两个物体有不同的restitution,box2d总是选择比较大的restitution进行计算
6、meter,距离单位,灵活定义你的meter,当对象为0.1至10meters的时候,box2d可以很好的处理它们,

三、box2d之hello world
让我们先创建一个box2d项目。创建好之后运行,每当我们点击屏幕时,会落下一个小方块:

Alt text

我们来详细看下生成的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
-(id) init
{
  // always call "super" init
  // Apple recommends to re-assign "self" with the "super" return value
  if( (self=[super init])) {
      
      // enable touches
      self.isTouchEnabled = YES;
      
      // enable accelerometer
      self.isAccelerometerEnabled = YES;
      
      CGSize screenSize = [CCDirector sharedDirector].winSize;
      CCLOG(@"Screen width %0.2f screen height %0.2f",screenSize.width,screenSize.height);
      
      // Define the gravity vector.
      b2Vec2 gravity;
      gravity.Set(0.0f, -10.0f);
      
      // Do we want to let bodies sleep?
      // This will speed up the physics simulation
      bool doSleep = true;
      
      // Construct a world object, which will hold and simulate the rigid bodies.
      world = new b2World(gravity, doSleep);
      
      world->SetContinuousPhysics(true);
      
      // Debug Draw functions
      m_debugDraw = new GLESDebugDraw( PTM_RATIO );
      world->SetDebugDraw(m_debugDraw);
      
      uint32 flags = 0;
      flags += b2DebugDraw::e_shapeBit;
//       flags += b2DebugDraw::e_jointBit;
//       flags += b2DebugDraw::e_aabbBit;
//       flags += b2DebugDraw::e_pairBit;
//       flags += b2DebugDraw::e_centerOfMassBit;
      m_debugDraw->SetFlags(flags);      
      
      
      // Define the ground body.
      b2BodyDef groundBodyDef;
      groundBodyDef.position.Set(0, 0); // bottom-left corner
      
      // Call the body factory which allocates memory for the ground body
      // from a pool and creates the ground box shape (also from a pool).
      // The body is also added to the world.
      b2Body* groundBody = world->CreateBody(&groundBodyDef);
      
      // Define the ground box shape.
      b2PolygonShape groundBox;      
      
      // bottom
      groundBox.SetAsEdge(b2Vec2(0,0), b2Vec2(screenSize.width/PTM_RATIO,0));
      groundBody->CreateFixture(&groundBox,0);
      
      // top
      groundBox.SetAsEdge(b2Vec2(0,screenSize.height/PTM_RATIO), b2Vec2(screenSize.width/PTM_RATIO,screenSize.height/PTM_RATIO));
      groundBody->CreateFixture(&groundBox,0);
      
      // left
      groundBox.SetAsEdge(b2Vec2(0,screenSize.height/PTM_RATIO), b2Vec2(0,0));
      groundBody->CreateFixture(&groundBox,0);
      
      // right
      groundBox.SetAsEdge(b2Vec2(screenSize.width/PTM_RATIO,screenSize.height/PTM_RATIO), b2Vec2(screenSize.width/PTM_RATIO,0));
      groundBody->CreateFixture(&groundBox,0);
      
      
      //Set up sprite
      
      CCSpriteBatchNode *batch = [CCSpriteBatchNode batchNodeWithFile:@"blocks.png" capacity:150];
      [self addChild:batch z:0 tag:kTagBatchNode];
      
      [self addNewSpriteWithCoords:ccp(screenSize.width/2, screenSize.height/2)];
      
      CCLabelTTF *label = [CCLabelTTF labelWithString:@"Tap screen" fontName:@"Marker Felt" fontSize:32];
      [self addChild:label z:0];
      [label setColor:ccc3(0,0,255)];
      label.position = ccp( screenSize.width/2, screenSize.height-50);
      
      [self schedule: @selector(tick:)];
  }
  return self;
}

在init方法中,首先创建了重力加速度,加速度是一个向量,-10是因为加速度朝向y轴负方向。之后创建world以及word的四个边缘,防止物体跑出屏幕。再然后是创建精和一个label,然后schedule tick方法

创建body的步骤:
Alt text

创建fixture的步骤:
Alt text

下面,让我们来看看如何创建一个box2d世界中的物体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
-(void) addNewSpriteWithCoords:(CGPoint)p
{
  CCLOG(@"Add sprite %0.2f x %02.f",p.x,p.y);
  CCSpriteBatchNode *batch = (CCSpriteBatchNode*) [self getChildByTag:kTagBatchNode];
  
  //We have a 64x64 sprite sheet with 4 different 32x32 images.  The following code is
  //just randomly picking one of the images
  int idx = (CCRANDOM_0_1() > .5 ? 0:1);
  int idy = (CCRANDOM_0_1() > .5 ? 0:1);
  CCSprite *sprite = [CCSprite spriteWithBatchNode:batch rect:CGRectMake(32 * idx,32 * idy,32,32)];
  [batch addChild:sprite];
  
  sprite.position = ccp( p.x, p.y);
  
  // Define the dynamic body.
  //Set up a 1m squared box in the physics world
  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);
  
  // Define another box shape for our dynamic body.
  b2PolygonShape dynamicBox;
  dynamicBox.SetAsBox(.5f, .5f);//These are mid points for our 1m box
  
  // Define the dynamic body fixture.
  b2FixtureDef fixtureDef;
  fixtureDef.shape = &dynamicBox;    
  fixtureDef.density = 1.0f;
  fixtureDef.friction = 0.3f;
  body->CreateFixture(&fixtureDef);
}

可以看到,代码中的步骤跟我们上面的步骤一样,唯一不同的就是多了一个userData,这个属性用来绑定精灵,否则你会看到精灵在你点击的地方不动,而一个粉色的方块掉了下去。fixture是相对于body的位置来的

接下来是tick方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-(void) tick: (ccTime) dt
{
  //It is recommended that a fixed time step is used with Box2D for stability
  //of the simulation, however, we are using a variable time step here.
  //You need to make an informed choice, the following URL is useful
  //http://gafferongames.com/game-physics/fix-your-timestep/
  
  int32 velocityIterations = 8;
  int32 positionIterations = 1;
  
  // Instruct the world to perform a single step of simulation. It is
  // generally best to keep the time step and iterations fixed.
  world->Step(dt, velocityIterations, positionIterations);

  
  //Iterate over the bodies in the physics world
  for (b2Body* b = world->GetBodyList(); b; b = b->GetNext())
  {
      if (b->GetUserData() != NULL) {
          //Synchronize the AtlasSprites position and rotation with the corresponding body
          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());
      }    
  }
}

Step方法中,velocityIterations和positionIterations这两个参数越大,box2d就能进行更好的模拟,但是性能就会下降,这两个参数你应该自己把握以适合你的游戏。
后面的for循环是为了让你的sprite与box2d中的对象同步,如果注释掉这段代码,你会发现粉方块掉下去了,你的sprite没掉下去。

四、box2d中拖动一个物体
拖移物体需要用到box2d中的b2MouseJoint,首先我们在touchbegan方法中为我们点击到的物体创建b2MouseJoint对象,那么问题来了,我们如何获取点击到的物体呢?

box2d为我们提供了相关方法,即AABB(axis-aligned bounding box),原理为:首先,我们点击位置的四边加上了1-point的边,这样我们点击的位置变成了一个小方块;之后,以某个形状的物体做个长方形,看看我们点击的位置是否在这个长方形里面。如图:

Alt text

如果我们点击的位置在这个长方形里面,那么调用callback对象,这个callback对象是我们自己写的,之后可以通过TestPoint看看点击的位置是否的确在AABB找出的物体上,如果在,那么我们就获取到了点击的物体。上图可以发现其实我们并没有点到那个黑色的物体上,所以TestPoint会返回false。如果同时点到了多个物体,只返回第一个被发现的。

下面是touch began代码,我们可以看到b2MouseJoint如何被创建的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    for( UITouch *touch in touches ) {
        CGPoint location = [touch locationInView:touch.view];
        location = [[CCDirector sharedDirector] convertToGL:location];
        location = [self convertToNodeSpace:location];
        b2Vec2 b2Location = b2Vec2(location.x / PTM_RATIO, location.y / PTM_RATIO);

        b2AABB aabb;
        b2Vec2 delta = b2Vec2(1.0 / PTM_RATIO, 1.0 / PTM_RATIO);
        aabb.upperBound = b2Location + delta;
        aabb.lowerBound = b2Location - delta;

        SimpleQueryCallback callback(b2Location);
        world->QueryAABB(&callback, aabb);

        if (callback.fixtureFound) {
            b2Body *body = callback.fixtureFound->GetBody();
            CCSprite *sprite = (CCSprite *)body->GetUserData();
            if (sprite == nil) {
                return;
            }
            b2MouseJointDef mouseJointDef;
            mouseJointDef.bodyA = groundBody;
            mouseJointDef.bodyB = body;
            mouseJointDef.target = b2Location;
            mouseJointDef.maxForce = 1000 * body->GetMass();
            mouseJointDef.collideConnected = true;

            mouseJoint = (b2MouseJoint *) world->CreateJoint(&mouseJointDef);
            body->SetAwake(true);
        } else {
            [self addNewSpriteWithCoords: location];
        }
    }
}

其中bodyA一般会指定固定的body,bodyB指定你想移动的body,target为你要移动到哪里,maxForce越大,拖动速度越快,collideConnected为true时bodyA和bodyB会相撞。

SimpleQueryCallback是我们自己写的一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SimpleQueryCallback : public b2QueryCallback
{
public:
    b2Vec2 pointToTest;
    b2Fixture * fixtureFound;

    SimpleQueryCallback(const b2Vec2& point) {
        pointToTest = point;
        fixtureFound = NULL;
    }

    bool ReportFixture(b2Fixture* fixture) {
        b2Body* body = fixture->GetBody();
        if (body->GetType() == b2_dynamicBody) {
            if (fixture->TestPoint(pointToTest)) {
                fixtureFound = fixture;
                return false;
            }
        }
        return true;
    }
};

然后是move和touch ended的代码,move中setTarget即会使物体移到新位置,别忘了在touch ended中销毁这个mouseJoint:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    for( UITouch *touch in touches ) {
        CGPoint location = [touch locationInView:touch.view];
        location = [[CCDirector sharedDirector] convertToGL:location];
        location = [self convertToNodeSpace:location];
        b2Vec2 b2Location = b2Vec2(location.x / PTM_RATIO, location.y / PTM_RATIO);

        if (mouseJoint) {
            mouseJoint->SetTarget(b2Location);
        }
    }
}

- (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];
      
//       [self addNewSpriteWithCoords: location];
        if (mouseJoint) {
            world->DestroyJoint(mouseJoint);
            mouseJoint = NULL;
        }
  }
}

五、box2d中的sensor
sensor可以用在我们的人物来到特定的位置触发一些事件,它的创建跟其他刚体的创建一样,只是将b2FixtureDef的isSensor设为true

sensor可以这么创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)createSensor
{
    CGSize winSize = [[CCDirector sharedDirector] winSize];
    CGSize sensorSize = CGSizeMake(100, 50);
    b2BodyDef bodyDef;
    bodyDef.type = b2_staticBody;
    bodyDef.position = b2Vec2((winSize.width-sensorSize.width/2)/PTM_RATIO,
                              (sensorSize.height/2)/PTM_RATIO);
    sensorBody = world->CreateBody(&bodyDef);

    b2PolygonShape shape;
    shape.SetAsBox(sensorSize.width / PTM_RATIO, sensorSize.height / PTM_RATIO);

    b2FixtureDef fixtureDef;
    fixtureDef.shape = &shape;
    fixtureDef.isSensor = true;
    sensorBody->CreateFixture(&fixtureDef);
}

运行后,绿色区域就是我们的sensor: Alt text

我们可以看到sensor是可以让刚体穿过的。下面在update里加入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
b2ContactEdge* edge = sensorBody->GetContactList();
    while (edge)
    {
        b2Contact* contact = edge->contact;
        b2Fixture* fixtureB = contact->GetFixtureB();
        b2Body *bodyB = fixtureB->GetBody();

        if (bodyB != sensorBody && bodyB != groundBody) {
            CCSprite *sprite = (CCSprite *)bodyB->GetUserData();
            [sprite removeFromParentAndCleanup:YES];

            world->DestroyBody(bodyB);
            bodyB = NULL;

            break;
        }
        edge = edge->next;
    }

这时候,我们在sensor上方点击创建出来的物体,掉到sensor上立马就消失了。注意,这里的检测会有一个问题,假如某个物体移动很快,update方法前在sensor这边,第二次update有可能穿出sensor而未被检测到。

六、创建一个不规则的shape
box2d中你可以为一个shape设置多个vertex,这样你就可能生成一个自定义的shape,比如:

Alt text

这样,一个自定义的shape就创建好了。创建自定义shape有几点需要注意:
1、顶点的位置是相对于body中心的
2、顶点需要顺时针定义
3、最大顶点数量不能超过8,数量越大,越费内存,性能也越差
4、顶点无法定义一个凹面体(concave)

下图显示了什么是concave和convex:

Alt text

有一个小工具用于获取顶点的值,叫vertex helper,下载地址:https://github.com/jfahrenkrug/VertexHelper
打开小工具后,首先把你的图片拖到vertex helper中,点击edit mode,type选择box2d,style选择initialization,用鼠标在图上点击即可:

Alt text

Comments