Box2D个人学习笔记 - HelloWorld

参考:
官方Manual和对应翻译的中文手册(感谢译者)。


下面的所有内容基于版本Box2D_v2.2.0。


Hello Box2d
1. 说明:
首先要说明的是,Box2D源码中有一个HelloWorld的例子和一个Testbed的单元测试的例子。这两个都是很多的学习参考。首先,就是从HelloWorld的例子来理解Box2D。
另外,需要说明的是,渲染本身是不属于Box2D的一部分的,Testbed使用freeglut和GLUI库渲染部分。Box2D本身只是用于进行计算,用于模拟2D。
2. Box2D的单位
要使用Box2d,先得了解Box2d的单位。Box2D使用MKS(m/kg/s,米/千克/秒)作为单位,角度使用弧度。对于很多渲染引擎,单位一般是像素,而Box2D使用米,所以,在将box2D与其它库结合的时候,要注意单位的问题。对于OpenGL,可以使用视口变换将Box2D的world变换到窗口中。尽量在游戏中使用MKS作为单位,只有在渲染的时候进行适当比例的转换为像素,从而使得游戏逻辑清晰。
关于坐标轴,发现好像没有哪里提到Box2D的坐标轴是怎么样的?可能这个不重要,搜了一下也没有发现来说明这个问题的。无论如下,下面将假设坐标轴为:向右是x方向,向上是y方向。


2.  关于包围盒
旧的版本的文档一般会提到“要创建一个世界对象,首先需要定义一个世界的包围盒。Box2D 使用包围盒来加速碰撞检测。尺寸并不关键,但合适的尺寸有助于性能。这个包围盒过大总比过小好。当一个物体达到了包围盒的边界时,它就会被冻结并停止模拟。"。在新的Box2D中,创建世界对象并不需要以包围盒作为参数,不过相信这里关于包围盒的描述含义等在新版本中也都是有的,无论如何,包围盒在新版本的box2d中仍然存在,在后面如果有机会再去研究其使用。无论如何,下面仍然是创建一个包围盒的方法:
b2AABB worldAABB;
worldAABB.lowerBound.Set(-100.0f, -100.0f);
worldAABB.upperBound.Set(100.0f, 100.0f);
包围盒b2AABB是一个结构体,但是提供了成员方法(相当于类了),其定义在文件b2Collision.h中。它只有两个成员,lowerBound和upperBound,都是b2Vec2类型。


3. 创建世界
每个 Box2D 程序都将从一个世界对象(world object)的创建开始。这是一个管理内存,对象和模拟的
中心。b2World的构造函数为:b2World(const b2Vec2& gravity, bool doSleep);
所以,首先需要定义重力矢量和是否休眠。重力矢量,很显然是重力的方向。在上面的坐标轴的假设下,那么假设重力矢量为(0.0f, -10.0f),那么表示重力是沿着y轴负方向的,即向下。是否休眠用于告诉世界(world)当物体停止移动时允许物体休眠,一个休眠中的物体不需要任何模拟。下面是创建世界的代码:
b2Vec2 gravity(0.0f, -10.0f);
bool doSleep = true;
b2World world(gravity, doSleep);

4. 创建地面盒
刚体bodies一般使用下面的步骤创建:
a. 使用位置(position)、阻尼(damping)定义一个body(刚体)
b. 使用世界对象创建刚体
c. 使用形状、摩擦力、恢复等定义一个配件(fixture)
d. 在刚体上创建配件(fixtures)
上面创建的world世界是”空白的“,下面首先来创建一个地面体(ground body),当然,遵循上面的四步。
a. body definition,定义一个物体定义,使用物体定义,我们指定一个初始化的位置。
b. 使用世界对象创建一个刚体,并将上面的物体定义作为参数。世界对象不会保存对物体定义的引用。刚体默认是静态的(static),静态刚体不会和其它静态刚体碰撞而且是不能移动的。
c. 创建一个地面多边形(ground polygon),其实,就是定义形状了。可以利用SetAsBox()方法将一个形状定义为Box shape(盒子形状),其两个参数为x轴和y轴方向的边长的一半。如SetAsBox(1, 2),那么其大小为2×4的一个盒子(长方形)。
d. 创建形状配件(shape fixture)。使用上面b中创建的刚体,创建配件即可。这里使用默认的配件材料属性,所以直接将形状作为参数即可,不需要先创建一个配件定义。
注意:每一个配件都需要有一个父刚体(parent body),包括静态的配件。当然,可以将所有的静态配件关联到一个静态刚体body。另外,静态对象的质量为0,所以密度是没有用的。

类似于上面世界对象不会保存对物体定义的引用,Box2D不会保存对shape的引用,它会将数据复制到一个新的b2Shape对象中。

下面是创建地面盒相关的代码:

// 2. create a ground body
// a. body definition
b2BodyDef groundBodyDef;
groundBodyDef.position.Set(0.0f, -10.0f);
// b. creaet body
b2Body* groundBody = world.CreateBody(&groundBodyDef);
// c. create shape
b2PolygonShape groundBox;
groundBox.SetAsBox(50.0f, 10.0f);   // size of box is 100m*20m
// d. create fixture
groundBody->CreateFixture(&groundBox, 0.0f);    // 0.0f is density, for static body, mass is zero, so density is not used.

5. 创建动态物体
创建动态物体(dynamic body)的方法和创建静态物体类似,主要区别是必须为动态物体设置质量属性。

由于默认的body是静态的,我们可以在刚体定义的时候使用b2BodyType来设置其为动态的。同时,这里,我们创建一个fixture definition配件定义,用于设置其形状、密度、摩擦系数等,然后利用配件定义来创建fixture配件。(上面创建地面盒的时候我们是直接将形状用于创建配件,是类似的,这里也不是必须的,只是动态物体一般都需要定义密度摩擦力等。)下面是创建动态物体的方法:

// 3. create a dynamic body
// a. body definiton && create body
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;          // Set it as dynamic body as default is static.
bodyDef.position.Set(0.0f, 4.0f);
b2Body* body = world.CreateBody(&bodyDef);
// b. create shape
b2PolygonShape dynamicBox;
dynamicBox.SetAsBox(1.0f, 1.0f); 
// c. fixture defition
b2FixtureDef fixtureDef;
fixtureDef.shape = &dynamicBox;
fixtureDef.density = 1.0f;
fixtureDef.friction = 0.3f;
// d. create fixture
body->CreateFixture(&fixtureDef);

6. 模拟Box2D的世界

参考后面的理解。

7. 清理工作

当一个世界对象超出它的作用域,或通过指针将其 delete 时,所有物体和关节的内存都会被释放。

完整的代码:

/*
    filename: 1.cpp
    compile:
        gcc -Iinclude -Llib -lBox2D -Wl,-rpath=`pwd`/lib 1.cpp
*/

#include <Box2D/Box2D.h>
#include <stdio.h>

int main() {
    // 1. create a world
    b2Vec2 gravity(0.0f, -10.0f);
    bool doSleep = true;
    b2World world(gravity, doSleep);

    // 2. create a ground body (static body)
    // a. body definition
    b2BodyDef groundBodyDef;
    groundBodyDef.position.Set(0.0f, -10.0f);
    // b. creaet body
    b2Body* groundBody = world.CreateBody(&groundBodyDef);
    // c. create shape
    b2PolygonShape groundBox;
    groundBox.SetAsBox(50.0f, 10.0f);   // size of box is 100m*20m
    // d. create fixture
    groundBody->CreateFixture(&groundBox, 0.0f);    // 0.0f is density, for static body, mass is zero, so density is not used.

    // 3. create a dynamic body
    // a. body definiton && create body
    b2BodyDef bodyDef;
    bodyDef.type = b2_dynamicBody;          // Set it as dynamic body as default is static.
    bodyDef.position.Set(0.0f, 4.0f);
    b2Body* body = world.CreateBody(&bodyDef);
    // b. create shape
    b2PolygonShape dynamicBox;
    dynamicBox.SetAsBox(1.0f, 1.0f); 
    // c. fixture defition
    b2FixtureDef fixtureDef;
    fixtureDef.shape = &dynamicBox;
    fixtureDef.density = 1.0f;
    fixtureDef.friction = 0.3f;
    // d. create fixture
    body->CreateFixture(&fixtureDef);

    // 4. simulate the world
    float32 timeStep = 1.0f / 60.0f;
    int32 velocityIterations = 6;
    int32 positionIterations = 2;

    for (int32 i = 0; i < 60; ++i)
    {
        world.Step(timeStep, velocityIterations, positionIterations);
        b2Vec2 position = body->GetPosition();
        float32 angle = body->GetAngle();
        printf("%4.2f %4.2f %4.2f\n", position.x, position.y, angle);
    }

    // 5. clean up

    return 0;
}
0.00 4.00 0.00
0.00 3.99 0.00
0.00 3.98 0.00
0.00 3.97 0.00
0.00 3.96 0.00
0.00 3.94 0.00
0.00 3.92 0.00
0.00 3.90 0.00
......


关于模拟Box2D的世界中step()的理解:

模拟Box2D的一个关键函数是:world.Step(timeStep, velocityIterations, positionIterations);

根据上面的例子的运行,很容易知道,这个step会改变动态对象body的位置(上面的例子只有y坐标改变了)。那么,这些数据如何理解?是如何计算出来的呢?要理解这个问题,就分析每一次调用step会影响些什么。

在分析前,首先说明,Step()的velocityIterations和positionIterations参数不会影响body的坐标改变,所以暂且先忽略这两个参数的作用,同时,body的初始坐标是(0.0f, 4.0f)。

PS:理解了很久还是没有理解到底是怎么计算的。。。纠结中。。。下面的内容根据个人理解慢慢更新。。。

1. timeStep理解为时间步或者刷新时间粒度,这个值并不是帧率。在Step中,进行了碰撞的检测和速度位置的更新等。而,velocityIterations(速度更新迭代数)和positionIterations(位置迭代更新数)是表示每一次时间步内,需要进行的计算次数,很显然,计算次数更多,那么更接近真实情况,自然也需要消耗更多的时间,所以迭代次数是性能和质量的一个平衡,不能太大也不能太小。但是,这两个参数并不会影响最终的step的结果(比如上面的,修改这两个参数,并不会影响body的坐标改变)。

2. 对于timeStep时间步的理解,有点像是Box2D的一个“逻辑时间”。每调用一次,表示时间“推进”一个单位。因为Box2D不能用物理时间来进行模拟,所以提供一个时间步表示时间推进,这样也使得Box2D的模拟不会因为真实时间的不同而不同。比如上面的例子,body只有重力,所以是自由落体运动,如果是物理时间,那么显然s=g*t*t/2,这样来更新时间,但是在一个游戏引擎中,t应该是由游戏来控制的,类似于说,游戏说时间前进一步就前进一步。所以Box2D的world提供了Step(),每调用一次Step()表示,当前的world的时间向前推进了一步,所以其是根据时间步来计算位置速度的改变,而不是根据真实的物理时间,所以不会因为不同的机器运行的速度不一样,物理时间不一样导致结果不一样,游戏逻辑的不一样。简单来说,就是Box2D有一个“时钟”,但是它是不会自己走的,每调用一次Step(),相当于我们把“时钟”向前拨动一个单位。个人理解。。。

3. 如果上面2的理解正确。。。那。。好像还是无法理解上面的例子中的数据是如何计算的,只能解释为何上面的例子每次运行结果是一样的以及在不同配置的机器都是一样的,因为它不依赖于物理时间。大概算了一下,发现不管如何修改timeStep,第一次调用Step()之后的结果,倒总是可以符合s=g*t*t的(不除以2),我想,既然这是一个逻辑时间,单位除以不除以2好像不重要,但是,到了第二次调用Step()的结果,好像不符合s=g*t*t,不知道这个时间步对应于相当于多少的物理时间啊。。。下面是timeStep=5.0f/60.0f和10.0f/60.0f的时候的结果:

timeStep=5.0f/60.0f

0.00 3.93 0.00
0.00 3.79 0.00
0.00 3.58 0.00
0.00 3.31 0.00
0.00 2.96 0.00
0.00 2.54 0.00
0.00 2.06 0.00
0.00 1.50 0.00
0.00 1.01 0.00
-0.00 1.01 0.00
0.00 1.01 0.00
0.00 1.01 0.00
0.00 1.01 0.00
。。。。。。
timeStep=10.0f/60.0f
0.00 3.72 0.00
0.00 3.17 0.00
0.00 2.33 0.00
0.00 1.22 0.00
0.00 1.01 0.00
0.00 1.01 0.00
0.00 1.01 0.00
0.00 1.01 0.00
0.00 1.01 0.00
。。。。。。
说明:在调用Step()若干次之后,发现位置不再变化(y=1.01),这是由于,碰到了地面,不会再“下落”了。回顾前面的代码,地面的位置为(0,-10),为一个box,大小为100m*20m。而动态body的初始位置为:(0,4),大小为2*2的box。所以,地面的上边正好为x轴,当物体受到重力,就会"下落",当y=1.01(y=1)时候,其下边就碰到了x轴,即地面,所以不会下落了,这就解释了上面的1.01之后不变了。

当timeStep=5/60,下落的位移为h=4 - 3.93,自由落体的公式为:s=g*t*t/2。如果假设这里的timeStep就是表示物理时间推进的时间,单位为s,那么倒是发现:s=g*t*t能解释得到s=10*(5/60)*(5/60)=0.0694444= h = 4 - 3.93,但是如果同样的理解来计算第二次调用Step()之后的结果,就不对了:s=10*(2*5/60)*(2*5/60)=0.277777,而实际结果中h=4-3.79=0.21。当timeStep=10/60的时候,也能得到类似的结果。这就有点纠结了。。。。对于每一个timeStep,那么如果用物理公式计算,t到底对应于多少呢?(不可能Box2D进行模拟不符合物理规律吧。。。我表示压力很大。。。)



你可能感兴趣的:(游戏,object,单元测试,delete,float,引擎)